예제 #1
0
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")
예제 #2
0
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')
예제 #3
0
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")
예제 #4
0
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