audit_silo_decorators.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #!/usr/bin/env sentry exec
  2. from __future__ import annotations
  3. import abc
  4. import json # noqa - I want the `indent` param
  5. import sys
  6. from collections import defaultdict
  7. import django.apps
  8. import django.urls
  9. def audit_silo_limits(format="json"):
  10. """Lists which classes have had silo decorators applied."""
  11. from sentry.runner import configure
  12. configure()
  13. model_table = create_model_table()
  14. endpoint_table = create_endpoint_table()
  15. if format == "json":
  16. json_repr = {
  17. "models": ModelPresentation().as_json_repr(model_table),
  18. "endpoints": EndpointPresentation().as_json_repr(endpoint_table),
  19. }
  20. json.dump(json_repr, sys.stdout, indent=4)
  21. elif format == "markdown":
  22. ModelPresentation().print_markdown(model_table)
  23. EndpointPresentation().print_markdown(endpoint_table)
  24. else:
  25. raise ValueError
  26. def create_model_table():
  27. table = defaultdict(list)
  28. for model_class in django.apps.apps.get_models():
  29. if model_class._meta.app_label != "sentry":
  30. continue
  31. limit = getattr(model_class._meta, "_ModelSiloLimit__silo_limit", None)
  32. key = (limit.modes, limit.read_only) if limit else None
  33. table[key].append(model_class)
  34. return table
  35. def create_endpoint_table():
  36. from sentry.api.base import Endpoint
  37. def is_endpoint(view_function, bindings):
  38. view_class = getattr(view_function, "view_class", None)
  39. return view_class and issubclass(view_class, Endpoint)
  40. def get_endpoint_classes():
  41. url_mappings = list(django.urls.get_resolver().reverse_dict.items())
  42. for (view_function, bindings) in url_mappings:
  43. if is_endpoint(view_function, bindings):
  44. yield view_function.view_class
  45. table = defaultdict(list)
  46. for endpoint_class in get_endpoint_classes():
  47. limit = getattr(endpoint_class, "__silo_limit", None)
  48. key = limit.modes if limit else None
  49. table[key].append(endpoint_class)
  50. return table
  51. class ConsolePresentation(abc.ABC):
  52. @property
  53. @abc.abstractmethod
  54. def table_label(self):
  55. raise NotImplementedError
  56. @abc.abstractmethod
  57. def order(self, group):
  58. raise NotImplementedError
  59. @abc.abstractmethod
  60. def get_group_label(self, key):
  61. raise NotImplementedError
  62. @abc.abstractmethod
  63. def get_key_repr(self, key):
  64. raise NotImplementedError
  65. @staticmethod
  66. def format_mode_set(modes):
  67. if modes is None:
  68. return None
  69. return sorted(str(x) for x in modes)
  70. @staticmethod
  71. def format_value(value):
  72. return f"{value.__module__}.{value.__name__}"
  73. def normalize_table(self, table):
  74. return {
  75. key: sorted({self.format_value(value) for value in group})
  76. for (key, group) in (sorted(table.items(), key=self.order))
  77. }
  78. def as_json_repr(self, table):
  79. table = self.normalize_table(table)
  80. return {
  81. "total_count": sum(len(group) for group in table.values()),
  82. "decorators": [
  83. {
  84. "decorator": self.get_key_repr(group_key),
  85. "count": len(group),
  86. "values": group,
  87. }
  88. for group_key, group in table.items()
  89. ],
  90. }
  91. def print_markdown(self, table):
  92. table = self.normalize_table(table)
  93. total_count = sum(len(group) for group in table.values())
  94. table_header = f"{self.table_label} ({total_count})"
  95. print("\n" + table_header) # noqa
  96. print("=" * len(table_header), end="\n\n") # noqa
  97. for (group_key, group) in table.items():
  98. group_label = self.get_group_label(group_key)
  99. group_header = f"{group_label} ({len(group)})"
  100. print(group_header) # noqa
  101. print("-" * len(group_header), end="\n\n") # noqa
  102. for value in group:
  103. print(" - " + value) # noqa
  104. print() # noqa
  105. class ModelPresentation(ConsolePresentation):
  106. @property
  107. def table_label(self):
  108. return "MODELS"
  109. def order(self, group):
  110. group_key, _model_group = group
  111. if group_key is None:
  112. return ()
  113. write_modes, read_modes = group_key
  114. return (
  115. len(write_modes),
  116. len(read_modes),
  117. self.format_mode_set(write_modes),
  118. self.format_mode_set(read_modes),
  119. )
  120. def get_key_repr(self, key):
  121. if key is None:
  122. return None
  123. write_modes, read_modes = key
  124. return {
  125. "write_modes": self.format_mode_set(write_modes),
  126. "read_modes": self.format_mode_set(read_modes),
  127. }
  128. def get_group_label(self, key):
  129. if key is None:
  130. return "No decorator"
  131. write_modes, read_modes = key
  132. if read_modes:
  133. return (
  134. f"{self.format_mode_set(write_modes)}, read_only={self.format_mode_set(read_modes)}"
  135. )
  136. else:
  137. return self.format_mode_set(write_modes)
  138. class EndpointPresentation(ConsolePresentation):
  139. @property
  140. def table_label(self):
  141. return "VIEWS"
  142. def order(self, group):
  143. mode_set, _endpoint_group = group
  144. return len(mode_set or ()), self.format_mode_set(mode_set)
  145. def get_group_label(self, key):
  146. return self.format_mode_set(key) if key else "No decorator"
  147. def get_key_repr(self, key):
  148. return self.format_mode_set(key)
  149. if __name__ == "__main__":
  150. audit_silo_limits()