class SymbolicatorSession(object): def __init__(self, url=None, sources=None, project_id=None, event_id=None, timeout=None): self.url = url self.project_id = project_id self.event_id = event_id self.sources = sources or [] self.timeout = timeout self.session = None self._query_params = {"timeout": timeout, "scope": project_id} def __enter__(self): self.open() return self def __exit__(self, *args): self.close() def open(self): if self.session is None: self.session = Session() def close(self): if self.session is not None: self.session.close() self.session = None def _ensure_open(self): if not self.session: raise RuntimeError("Session not opened") def _request(self, method, path, **kwargs): self._ensure_open() url = urljoin(self.url, path) # required for load balancing kwargs.setdefault("headers", {})["x-sentry-project-id"] = self.project_id kwargs.setdefault("headers", {})["x-sentry-event-id"] = self.event_id attempts = 0 wait = 0.5 while True: try: response = self.session.request(method, url, **kwargs) metrics.incr( "events.symbolicator.status_code", tags={ "status_code": response.status_code, "project_id": self.project_id }, ) if (method.lower() == "get" and path.startswith("requests/") and response.status_code == 404): # The symbolicator does not know this task. This is # expected to happen when we're currently deploying # symbolicator (which will clear all of its state). Re-send # the symbolication task. return None if response.status_code in (502, 503): raise ServiceUnavailable() if response.ok: json = response.json() else: json = { "status": "failed", "message": "internal server error" } return json except (IOError, RequestException): attempts += 1 # Any server error needs to be treated as a failure. We can # retry a couple of times, but ultimately need to bail out. # # This can happen for any network failure. if attempts > MAX_ATTEMPTS: logger.error("Failed to contact symbolicator", exc_info=True) raise time.sleep(wait) wait *= 2.0 def symbolicate_stacktraces(self, stacktraces, modules, signal=None): json = { "sources": self.sources, "stacktraces": stacktraces, "modules": modules } if signal: json["signal"] = signal return self._request("post", "symbolicate", params=self._query_params, json=json) def upload_minidump(self, minidump): return self._request( method="post", path="minidump", params=self._query_params, data={"sources": json.dumps(self.sources)}, files={"upload_file_minidump": minidump}, ) def upload_applecrashreport(self, report): return self._request( method="post", path="applecrashreport", params=self._query_params, data={"sources": json.dumps(self.sources)}, files={"apple_crash_report": report}, ) def query_task(self, task_id): task_url = "requests/%s" % (task_id, ) return self._request("get", task_url, params=self._query_params) def healthcheck(self): return self._request("get", "healthcheck")
class SymbolicatorSession(object): def __init__(self, url=None, sources=None, project_id=None, event_id=None, timeout=None): self.url = url self.project_id = project_id self.event_id = event_id self.sources = sources or [] self.timeout = timeout self.session = None self._query_params = {'timeout': timeout, 'scope': project_id} def __enter__(self): self.open() return self def __exit__(self, *args): self.close() def open(self): if self.session is None: self.session = Session() def close(self): if self.session is not None: self.session.close() self.session = None def _ensure_open(self): if not self.session: raise RuntimeError('Session not opened') def _request(self, method, path, **kwargs): self._ensure_open() url = urljoin(self.url, path) # required for load balancing kwargs.setdefault('headers', {})['x-sentry-project-id'] = self.project_id kwargs.setdefault('headers', {})['x-sentry-event-id'] = self.event_id attempts = 0 wait = 0.5 while True: try: response = self.session.request(method, url, **kwargs) metrics.incr('events.symbolicator.status_code', tags={ 'status_code': response.status_code, 'project_id': self.project_id, }) if ( method.lower() == 'get' and path.startswith('requests/') and response.status_code == 404 ): # The symbolicator does not know this task. This is # expected to happen when we're currently deploying # symbolicator (which will clear all of its state). Re-send # the symbolication task. return None if response.status_code in (502, 503): raise ServiceUnavailable() if response.ok: json = response.json() else: json = { 'status': 'failed', 'message': 'internal server error', } return json except (IOError, RequestException): attempts += 1 # Any server error needs to be treated as a failure. We can # retry a couple of times, but ultimately need to bail out. # # This can happen for any network failure. if attempts > MAX_ATTEMPTS: logger.error('Failed to contact symbolicator', exc_info=True) raise time.sleep(wait) wait *= 2.0 def symbolicate_stacktraces(self, stacktraces, modules, signal=None): json = { 'sources': self.sources, 'stacktraces': stacktraces, 'modules': modules, } if signal: json['signal'] = signal return self._request('post', 'symbolicate', params=self._query_params, json=json) def upload_minidump(self, minidump): files = { 'upload_file_minidump': minidump } data = { 'sources': json.dumps(self.sources), } return self._request('post', 'minidump', params=self._query_params, data=data, files=files) def query_task(self, task_id): task_url = 'requests/%s' % (task_id, ) return self._request('get', task_url, params=self._query_params) def healthcheck(self): return self._request('get', 'healthcheck')
class SymbolicatorSession(object): def __init__(self, url=None, sources=None, project_id=None, event_id=None, timeout=None, options=None): self.url = url self.project_id = project_id self.event_id = event_id self.sources = sources or [] self.options = options or None self.timeout = timeout self.session = None def __enter__(self): self.open() return self def __exit__(self, *args): self.close() def open(self): if self.session is None: self.session = Session() def close(self): if self.session is not None: self.session.close() self.session = None def _ensure_open(self): if not self.session: raise RuntimeError("Session not opened") def _process_response(self, json): source_names = { source["id"]: source.get("name") for source in self.sources } source_names[INTERNAL_SOURCE_NAME] = "Sentry" for module in json.get("modules") or (): for candidate in module.get("candidates") or (): if candidate.get("source"): candidate["source_name"] = source_names.get( candidate["source"]) return json def _request(self, method, path, **kwargs): self._ensure_open() url = urljoin(self.url, path) # required for load balancing kwargs.setdefault("headers", {})["x-sentry-project-id"] = self.project_id kwargs.setdefault("headers", {})["x-sentry-event-id"] = self.event_id attempts = 0 wait = 0.5 while True: try: with metrics.timer("events.symbolicator.session.request", tags={"attempt": attempts}): response = self.session.request( method, url, timeout=settings.SYMBOLICATOR_POLL_TIMEOUT + 1, **kwargs) metrics.incr( "events.symbolicator.status_code", tags={ "status_code": response.status_code, "project_id": self.project_id }, ) if (method.lower() == "get" and path.startswith("requests/") and response.status_code == 404): # The symbolicator does not know this task. This is # expected to happen when we're currently deploying # symbolicator (which will clear all of its state). Re-send # the symbolication task. return None if response.status_code in (502, 503): raise ServiceUnavailable() if response.ok: json = response.json() else: json = { "status": "failed", "message": "internal server error" } return self._process_response(json) except (IOError, RequestException) as e: metrics.incr( "events.symbolicator.request_error", tags={ "exc": ".".join( [e.__class__.__module__, e.__class__.__name__]), "attempt": attempts, }, ) attempts += 1 # Any server error needs to be treated as a failure. We can # retry a couple of times, but ultimately need to bail out. # # This can happen for any network failure. if attempts > MAX_ATTEMPTS: logger.error("Failed to contact symbolicator", exc_info=True) raise time.sleep(wait) wait *= 2.0 def _create_task(self, path, **kwargs): params = {"timeout": self.timeout, "scope": self.project_id} with metrics.timer("events.symbolicator.create_task", tags={"path": path}): return self._request(method="post", path=path, params=params, **kwargs) def symbolicate_stacktraces(self, stacktraces, modules, signal=None): json = { "sources": self.sources, "options": self.options, "stacktraces": stacktraces, "modules": modules, } if signal: json["signal"] = signal return self._create_task("symbolicate", json=json) def upload_minidump(self, minidump): return self._create_task( path="minidump", data={ "sources": json.dumps(self.sources), "options": json.dumps(self.options) }, files={"upload_file_minidump": minidump}, ) def upload_applecrashreport(self, report): return self._create_task( path="applecrashreport", data={ "sources": json.dumps(self.sources), "options": json.dumps(self.options) }, files={"apple_crash_report": report}, ) def query_task(self, task_id): task_url = "requests/%s" % (task_id, ) params = { "timeout": 0, # Only wait when creating, but not when querying tasks "scope": self.project_id, } with metrics.timer("events.symbolicator.query_task"): return self._request("get", task_url, params=params) def healthcheck(self): return self._request("get", "healthcheck")
class SymbolicatorSession: # used in x-sentry-worker-id http header # to keep it static for celery worker process keep it as class attribute _worker_id = None def __init__(self, url=None, sources=None, project_id=None, event_id=None, timeout=None, options=None): self.url = url self.project_id = project_id self.event_id = event_id self.sources = sources or [] self.options = options or None self.timeout = timeout self.session = None # Build some maps for use in ._process_response() self.reverse_source_aliases = reverse_aliases_map( settings.SENTRY_BUILTIN_SOURCES) self.source_names = { source["id"]: source.get("name", "unknown") for source in self.sources } # Add a name for the special "sentry:project" source. self.source_names[INTERNAL_SOURCE_NAME] = "Sentry" # Add names for aliased sources. for source in settings.SENTRY_BUILTIN_SOURCES.values(): if source.get("type") == "alias": self.source_names[source["id"]] = source.get("name", "unknown") # Remove sources that should be ignored. This leaves a few extra entries in the alias # maps and source names maps, but that's fine. The orphaned entries in the maps will just # never be used. self.sources = filter_ignored_sources(self.sources, self.reverse_source_aliases) def __enter__(self): self.open() return self def __exit__(self, *args): self.close() def open(self): if self.session is None: self.session = Session() def close(self): if self.session is not None: self.session.close() self.session = None def _ensure_open(self): if not self.session: raise RuntimeError("Session not opened") def _process_response(self, json): """Post-processes the JSON response. This modifies the candidates list from Symbolicator responses to undo aliased sources, hide information about unknown sources and add names to sources rather then just have their IDs. """ for module in json.get("modules") or (): for candidate in module.get("candidates") or (): # Reverse internal source aliases from the response. source_id = candidate["source"] original_source_id = self.reverse_source_aliases.get(source_id) if original_source_id is not None: candidate["source"] = original_source_id source_id = original_source_id # Add a "source_name" field to save the UI a lookup. candidate["source_name"] = self.source_names.get( source_id, "unknown") redact_internal_sources(json) return json def _request(self, method, path, **kwargs): self._ensure_open() url = urljoin(self.url, path) # required for load balancing kwargs.setdefault("headers", {})["x-sentry-project-id"] = self.project_id kwargs.setdefault("headers", {})["x-sentry-event-id"] = self.event_id kwargs.setdefault("headers", {})["x-sentry-worker-id"] = self.get_worker_id() attempts = 0 wait = 0.5 while True: try: with metrics.timer("events.symbolicator.session.request", tags={"attempt": attempts}): response = self.session.request( method, url, timeout=settings.SYMBOLICATOR_POLL_TIMEOUT + 1, **kwargs) metrics.incr( "events.symbolicator.status_code", tags={ "status_code": response.status_code, "project_id": self.project_id }, ) if (method.lower() == "get" and path.startswith("requests/") and response.status_code == 404): # The symbolicator does not know this task. This is # expected to happen when we're currently deploying # symbolicator (which will clear all of its state). Re-send # the symbolication task. return None if response.status_code in (502, 503): raise ServiceUnavailable() if response.ok: json = response.json() else: json = { "status": "failed", "message": "internal server error" } return self._process_response(json) except (OSError, RequestException) as e: metrics.incr( "events.symbolicator.request_error", tags={ "exc": ".".join( [e.__class__.__module__, e.__class__.__name__]), "attempt": attempts, }, ) attempts += 1 # Any server error needs to be treated as a failure. We can # retry a couple of times, but ultimately need to bail out. # # This can happen for any network failure. if attempts > MAX_ATTEMPTS: logger.error("Failed to contact symbolicator", exc_info=True) raise time.sleep(wait) wait *= 2.0 def _create_task(self, path, **kwargs): params = {"timeout": self.timeout, "scope": self.project_id} with metrics.timer( "events.symbolicator.create_task", tags={"path": path}, ): return self._request(method="post", path=path, params=params, **kwargs) def symbolicate_stacktraces(self, stacktraces, modules, signal=None): json = { "sources": self.sources, "options": self.options, "stacktraces": stacktraces, "modules": modules, } if signal: json["signal"] = signal return self._create_task("symbolicate", json=json) def upload_minidump(self, minidump): return self._create_task( path="minidump", data={ "sources": json.dumps(self.sources), "options": json.dumps(self.options) }, files={"upload_file_minidump": minidump}, ) def upload_applecrashreport(self, report): return self._create_task( path="applecrashreport", data={ "sources": json.dumps(self.sources), "options": json.dumps(self.options) }, files={"apple_crash_report": report}, ) def query_task(self, task_id): task_url = f"requests/{task_id}" params = { "timeout": 0, # Only wait when creating, but not when querying tasks "scope": self.project_id, } with metrics.timer("events.symbolicator.query_task"): return self._request("get", task_url, params=params) def healthcheck(self): return self._request("get", "healthcheck") @classmethod def get_worker_id(cls): # as class attribute to keep it static for life of process if cls._worker_id is None: # %5000 to reduce cardinality of metrics tagging with worker id cls._worker_id = str(uuid.uuid4().int % 5000) return cls._worker_id