PageRenderTime 62ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/src/sentry/sdk_updates.py

https://github.com/pombredanne/django-sentry
Python | 451 lines | 370 code | 65 blank | 16 comment | 45 complexity | 1a74346e9412d725e2ae467130163156 MD5 | raw file
  1. from __future__ import absolute_import
  2. import logging
  3. from distutils.version import LooseVersion
  4. from django.conf import settings
  5. from django.core.cache import cache
  6. from sentry.net.http import Session
  7. from sentry.utils.safe import get_path
  8. logger = logging.getLogger(__name__)
  9. SDK_INDEX_CACHE_KEY = u"sentry:sdk-versions"
  10. class SdkSetupState(object):
  11. def __init__(self, sdk_name, sdk_version, modules, integrations):
  12. self.sdk_name = sdk_name
  13. self.sdk_version = sdk_version
  14. self.modules = dict(modules or ())
  15. self.integrations = list(integrations or ())
  16. def copy(self):
  17. return type(self)(
  18. sdk_name=self.sdk_name,
  19. sdk_version=self.sdk_version,
  20. modules=self.modules,
  21. integrations=self.integrations,
  22. )
  23. @classmethod
  24. def from_event_json(cls, event_data):
  25. sdk_name = get_path(event_data, "sdk", "name")
  26. if sdk_name:
  27. sdk_name = sdk_name.lower().rsplit(":", 1)[0]
  28. if sdk_name == "sentry-python":
  29. sdk_name = "sentry.python"
  30. return cls(
  31. sdk_name=sdk_name,
  32. sdk_version=get_path(event_data, "sdk", "version"),
  33. modules=get_path(event_data, "modules"),
  34. integrations=get_path(event_data, "sdk", "integrations"),
  35. )
  36. class SdkIndexState(object):
  37. def __init__(self, sdk_versions=None, deprecated_sdks=None, sdk_supported_modules=None):
  38. self.sdk_versions = sdk_versions or get_sdk_versions()
  39. self.deprecated_sdks = deprecated_sdks or settings.DEPRECATED_SDKS
  40. self.sdk_supported_modules = sdk_supported_modules or SDK_SUPPORTED_MODULES
  41. class Suggestion(object):
  42. def to_json(self):
  43. raise NotImplementedError()
  44. def __eq__(self, other):
  45. return self.to_json() == other.to_json()
  46. class EnableIntegrationSuggestion(Suggestion):
  47. def __init__(self, integration_name, integration_url):
  48. self.integration_name = integration_name
  49. self.integration_url = integration_url
  50. def to_json(self):
  51. return {
  52. "type": "enableIntegration",
  53. "integrationName": self.integration_name,
  54. "integrationUrl": self.integration_url,
  55. }
  56. def get_new_state(self, old_state):
  57. if self.integration_name in old_state.integrations:
  58. return old_state
  59. new_state = old_state.copy()
  60. new_state.integrations.append(self.integration_name)
  61. return new_state
  62. class UpdateSDKSuggestion(Suggestion):
  63. def __init__(self, sdk_name, new_sdk_version):
  64. self.sdk_name = sdk_name
  65. self.new_sdk_version = new_sdk_version
  66. def to_json(self):
  67. return {
  68. "type": "updateSdk",
  69. "sdkName": self.sdk_name,
  70. "newSdkVersion": self.new_sdk_version,
  71. "sdkUrl": get_sdk_urls().get(self.sdk_name),
  72. }
  73. def get_new_state(self, old_state):
  74. if self.new_sdk_version is None:
  75. return old_state
  76. try:
  77. has_newer_version = LooseVersion(old_state.sdk_version) < LooseVersion(
  78. self.new_sdk_version
  79. )
  80. except Exception:
  81. has_newer_version = False
  82. if not has_newer_version:
  83. return old_state
  84. new_state = old_state.copy()
  85. new_state.sdk_version = self.new_sdk_version
  86. return new_state
  87. class ChangeSDKSuggestion(Suggestion):
  88. """
  89. :param module_names: Hide this suggestion if any of the given modules is
  90. loaded. This list is used to weed out invalid suggestions when using
  91. multiple SDKs in e.g. .NET.
  92. """
  93. def __init__(self, new_sdk_name, module_names=None):
  94. self.new_sdk_name = new_sdk_name
  95. self.module_names = module_names
  96. def to_json(self):
  97. return {
  98. "type": "changeSdk",
  99. "newSdkName": self.new_sdk_name,
  100. "sdkUrl": get_sdk_urls().get(self.new_sdk_name),
  101. }
  102. def get_new_state(self, old_state):
  103. if old_state.sdk_name == self.new_sdk_name:
  104. return old_state
  105. if any(x in old_state.modules for x in self.module_names or ()):
  106. return old_state
  107. new_state = old_state.copy()
  108. new_state.sdk_name = self.new_sdk_name
  109. return new_state
  110. SDK_SUPPORTED_MODULES = [
  111. {
  112. "sdk_name": "sentry.python",
  113. "sdk_version_added": "0.3.2",
  114. "module_name": "django",
  115. "module_version_min": "1.6.0",
  116. "suggestion": EnableIntegrationSuggestion(
  117. "django", "https://docs.sentry.io/platforms/python/django/"
  118. ),
  119. },
  120. {
  121. "sdk_name": "sentry.python",
  122. "sdk_version_added": "0.3.2",
  123. "module_name": "flask",
  124. "module_version_min": "0.11.0",
  125. "suggestion": EnableIntegrationSuggestion(
  126. "flask", "https://docs.sentry.io/platforms/python/flask/"
  127. ),
  128. },
  129. {
  130. "sdk_name": "sentry.python",
  131. "sdk_version_added": "0.7.9",
  132. "module_name": "bottle",
  133. "module_version_min": "0.12.0",
  134. "suggestion": EnableIntegrationSuggestion(
  135. "bottle", "https://docs.sentry.io/platforms/python/bottle/"
  136. ),
  137. },
  138. {
  139. "sdk_name": "sentry.python",
  140. "sdk_version_added": "0.7.11",
  141. "module_name": "falcon",
  142. "module_version_min": "1.4.0",
  143. "suggestion": EnableIntegrationSuggestion(
  144. "falcon", "https://docs.sentry.io/platforms/python/falcon/"
  145. ),
  146. },
  147. {
  148. "sdk_name": "sentry.python",
  149. "sdk_version_added": "0.3.6",
  150. "module_name": "sanic",
  151. "module_version_min": "0.8.0",
  152. "suggestion": EnableIntegrationSuggestion(
  153. "sanic", "https://docs.sentry.io/platforms/python/sanic/"
  154. ),
  155. },
  156. {
  157. "sdk_name": "sentry.python",
  158. "sdk_version_added": "0.3.2",
  159. "module_name": "celery",
  160. "module_version_min": "3.0.0",
  161. "suggestion": EnableIntegrationSuggestion(
  162. "celery", "https://docs.sentry.io/platforms/python/celery/"
  163. ),
  164. },
  165. # TODO: Detect AWS Lambda for Python
  166. {
  167. "sdk_name": "sentry.python",
  168. "sdk_version_added": "0.5.0",
  169. "module_name": "pyramid",
  170. "module_version_min": "1.3.0",
  171. "suggestion": EnableIntegrationSuggestion(
  172. "pyramid", "https://docs.sentry.io/platforms/python/pyramid/"
  173. ),
  174. },
  175. {
  176. "sdk_name": "sentry.python",
  177. "sdk_version_added": "0.5.1",
  178. "module_name": "rq",
  179. "module_version_min": "0.6",
  180. "suggestion": EnableIntegrationSuggestion(
  181. "rq", "https://docs.sentry.io/platforms/python/rq/"
  182. ),
  183. },
  184. {
  185. "sdk_name": "sentry.python",
  186. "sdk_version_added": "0.6.1",
  187. "module_name": "aiohttp",
  188. "module_version_min": "3.4.0",
  189. "suggestion": EnableIntegrationSuggestion(
  190. "aiohttp", "https://docs.sentry.io/platforms/python/aiohttp/"
  191. ),
  192. },
  193. {
  194. "sdk_name": "sentry.python",
  195. "sdk_version_added": "0.6.3",
  196. "module_name": "tornado",
  197. "module_version_min": "5.0.0",
  198. "suggestion": EnableIntegrationSuggestion(
  199. "tornado", "https://docs.sentry.io/platforms/python/tornado/"
  200. ),
  201. },
  202. {
  203. "sdk_name": "sentry.python",
  204. "sdk_version_added": "0.10.0",
  205. "module_name": "redis",
  206. "module_version_min": "0.0.0",
  207. "suggestion": EnableIntegrationSuggestion(
  208. "redis", "https://docs.sentry.io/platforms/python/redis/"
  209. ),
  210. },
  211. {
  212. "sdk_name": "sentry.python",
  213. "sdk_version_added": "0.11.0",
  214. "module_name": "sqlalchemy",
  215. "module_version_min": "1.2.0",
  216. "suggestion": EnableIntegrationSuggestion(
  217. "sqlalchemy", "https://docs.sentry.io/platforms/python/sqlalchemy/"
  218. ),
  219. },
  220. {
  221. "sdk_name": "sentry.python",
  222. "sdk_version_added": "0.11.0",
  223. "module_name": "apache_beam",
  224. "module_version_min": "2.12.0",
  225. "suggestion": EnableIntegrationSuggestion(
  226. "beam", "https://docs.sentry.io/platforms/python/beam/"
  227. ),
  228. },
  229. {
  230. "sdk_name": "sentry.python",
  231. "sdk_version_added": "0.13.0",
  232. "module_name": "pyspark",
  233. "module_version_min": "2.0.0",
  234. "suggestion": EnableIntegrationSuggestion(
  235. "spark", "https://docs.sentry.io/platforms/python/pyspark/"
  236. ),
  237. },
  238. {
  239. "sdk_name": "sentry.dotnet",
  240. "sdk_version_added": "0.0.0",
  241. "module_name": "Microsoft.AspNetCore.Hosting",
  242. "module_version_min": "2.1.0",
  243. "suggestion": ChangeSDKSuggestion("sentry.dotnet.aspnetcore", ["Sentry.AspNetCore"]),
  244. },
  245. {
  246. "sdk_name": "sentry.dotnet",
  247. "sdk_version_added": "0.0.0",
  248. "module_name": "EntityFramework",
  249. "module_version_min": "6.0.0",
  250. "suggestion": ChangeSDKSuggestion(
  251. "sentry.dotnet.entityframework", ["Sentry.EntityFramework"]
  252. ),
  253. },
  254. {
  255. "sdk_name": "sentry.dotnet",
  256. "sdk_version_added": "0.0.0",
  257. "module_name": "log4net",
  258. "module_version_min": "2.0.8",
  259. "suggestion": ChangeSDKSuggestion("sentry.dotnet.log4net", ["Sentry.Log4Net"]),
  260. },
  261. {
  262. "sdk_name": "sentry.dotnet",
  263. "sdk_version_added": "0.0.0",
  264. "module_name": "Microsoft.Extensions.Logging.Configuration",
  265. "module_version_min": "2.1.0",
  266. "suggestion": ChangeSDKSuggestion(
  267. "sentry.dotnet.extensions.logging",
  268. [
  269. "Sentry.Extensions.Logging",
  270. # If AspNetCore is used, do not show this suggestion at all,
  271. # because the (hopefully visible) suggestion to use the
  272. # AspNetCore SDK is more specific.
  273. "Microsoft.AspNetCore.Hosting",
  274. ],
  275. ),
  276. },
  277. {
  278. "sdk_name": "sentry.dotnet",
  279. "sdk_version_added": "0.0.0",
  280. "module_name": "Serilog",
  281. "module_version_min": "2.7.1",
  282. "suggestion": ChangeSDKSuggestion("sentry.dotnet.serilog", ["Sentry.Serilog"]),
  283. },
  284. {
  285. "sdk_name": "sentry.dotnet",
  286. "sdk_version_added": "0.0.0",
  287. "module_name": "NLog",
  288. "module_version_min": "4.6.0",
  289. "suggestion": ChangeSDKSuggestion("sentry.dotnet.nlog", ["Sentry.NLog"]),
  290. },
  291. ]
  292. def get_sdk_index():
  293. value = cache.get(SDK_INDEX_CACHE_KEY)
  294. if value is not None:
  295. return value
  296. base_url = settings.SENTRY_RELEASE_REGISTRY_BASEURL
  297. if not base_url:
  298. return {}
  299. url = "%s/sdks" % (base_url,)
  300. try:
  301. with Session() as session:
  302. response = session.get(url, timeout=1)
  303. response.raise_for_status()
  304. json = response.json()
  305. except Exception:
  306. logger.exception("Failed to fetch version index from release registry")
  307. json = {}
  308. cache.set(SDK_INDEX_CACHE_KEY, json, 3600)
  309. return json
  310. def get_sdk_versions():
  311. try:
  312. rv = settings.SDK_VERSIONS
  313. rv.update((key, info["version"]) for (key, info) in get_sdk_index().items())
  314. return rv
  315. except Exception:
  316. logger.exception("sentry-release-registry.sdk-versions")
  317. return {}
  318. def get_sdk_urls():
  319. try:
  320. rv = dict(settings.SDK_URLS)
  321. rv.update((key, info["main_docs_url"]) for (key, info) in get_sdk_index().items())
  322. return rv
  323. except Exception:
  324. logger.exception("sentry-release-registry.sdk-urls")
  325. return {}
  326. def _get_suggested_updates_step(setup_state, index_state):
  327. if not setup_state.sdk_name or not setup_state.sdk_version:
  328. return
  329. yield UpdateSDKSuggestion(
  330. setup_state.sdk_name, index_state.sdk_versions.get(setup_state.sdk_name)
  331. )
  332. # If an SDK is both outdated and entirely deprecated, we want to inform
  333. # the user of both. It's unclear if they would want to upgrade the SDK
  334. # or migrate to the new one.
  335. newest_name = settings.DEPRECATED_SDKS.get(setup_state.sdk_name, setup_state.sdk_name)
  336. yield ChangeSDKSuggestion(newest_name)
  337. for support_info in SDK_SUPPORTED_MODULES:
  338. if support_info["sdk_name"] != setup_state.sdk_name and not setup_state.sdk_name.startswith(
  339. support_info["sdk_name"] + "."
  340. ):
  341. continue
  342. if support_info["module_name"] not in setup_state.modules:
  343. continue
  344. try:
  345. if LooseVersion(support_info["sdk_version_added"]) > LooseVersion(
  346. setup_state.sdk_version
  347. ):
  348. continue
  349. except Exception:
  350. continue
  351. try:
  352. if LooseVersion(support_info["module_version_min"]) > LooseVersion(
  353. setup_state.modules[support_info["module_name"]]
  354. ):
  355. # TODO(markus): Maybe we want to suggest people to upgrade their module?
  356. #
  357. # E.g. "please upgrade Django so you can get the Django
  358. # integration"
  359. continue
  360. except Exception:
  361. continue
  362. yield support_info["suggestion"]
  363. def get_suggested_updates(setup_state, index_state=None, parent_suggestions=None):
  364. if index_state is None:
  365. index_state = SdkIndexState()
  366. if parent_suggestions is None:
  367. parent_suggestions = []
  368. suggestions = list(_get_suggested_updates_step(setup_state, index_state))
  369. rv = []
  370. new_setup_states = []
  371. for suggestion in suggestions:
  372. if suggestion in parent_suggestions:
  373. continue
  374. new_setup_state = suggestion.get_new_state(setup_state)
  375. if new_setup_state == setup_state:
  376. continue
  377. rv.append(suggestion)
  378. new_setup_states.append(new_setup_state)
  379. for new_setup_state, suggestion in zip(new_setup_states, rv):
  380. json = suggestion.to_json()
  381. json["enables"] = list(
  382. get_suggested_updates(
  383. new_setup_state, parent_suggestions=parent_suggestions + rv, index_state=index_state
  384. )
  385. )
  386. yield json