Source code for gwcelery.sentry

"""Error telemetry for `Sentry <https://sentry.io>`_."""
from urllib.parse import urlparse, urlunparse
from subprocess import CalledProcessError
import os

from celery.utils.log import get_logger
from safe_netrc import netrc, NetrcParseError
import sentry_sdk
from sentry_sdk.integrations import celery, flask, redis, tornado

from . import _version
from .util import SPHINX

log = get_logger(__name__)

__all__ = ('configure', 'DSN')

DSN = 'https://sentry.io/1425216'
"""Sentry data source name (DSN)."""


def before_send(event, hint):
    """Capture stderr and stdout from CalledProcessError exceptions."""
    if 'exc_info' not in hint:
        return event

    _, e, _ = hint['exc_info']
    if not isinstance(e, CalledProcessError):
        return event

    breadcrumbs = event.get('breadcrumbs', [])
    if len(breadcrumbs) < 1:
        return event
    breadcrumb = breadcrumbs[0]

    for key in ['stderr', 'stdout']:
        value = getattr(e, key)
        if value:
            breadcrumb.setdefault('data', {})[key] = value.decode(
                errors='replace')
    return event


def _read_classad(filename):
    with open(filename) as f:
        for line in f:
            key, _, value = line.partition('=')
            key = key.strip()
            value = value.strip().strip('"')
            yield key, value


def _add_htcondor():
    """Record HTCondor job information in Sentry."""
    try:
        data = dict(_read_classad(os.environ['_CONDOR_JOB_AD']))
    except (KeyError, IOError):
        return
    with sentry_sdk.configure_scope() as scope:
        scope.set_tag('htcondor.cluster_id', '{}.{}'.format(
            data['ClusterId'], data['ProcId']))


[docs]def configure(): """Configure Sentry logging integration for Celery. See the `official instructions for Celery integration <https://docs.sentry.io/platforms/python/celery/>`_. Notes ----- Add the API key username/pasword pair to your netrc file. """ # Catching NetrcParseError confuses sphinx. if SPHINX: # pragma: no cover return # Delayed import from . import app scheme, netloc, *rest = urlparse(DSN) try: auth = netrc().authenticators(netloc) if not auth: raise ValueError('No netrc entry found for {}'.format(netloc)) except (NetrcParseError, OSError, ValueError): log.exception('Disabling Sentry integration because we could not load ' 'the username and password for %s from the netrc file', netloc) return # The "legacy" Sentry DSN requires a "public key" and a "private key", # which are transmitted as the username and password in the URL. # However, as of Sentry 9, then "private key" part is no longer required. username, _, _ = auth dsn = urlunparse( (scheme, '{}@{}'.format(username, netloc), *rest)) version = 'gwcelery-{}'.format(_version.get_versions()['version']) environment = app.conf['sentry_environment'] sentry_sdk.init(dsn, environment=environment, release=version, before_send=before_send, integrations=[celery.CeleryIntegration(), flask.FlaskIntegration(), redis.RedisIntegration(), tornado.TornadoIntegration()]) _add_htcondor()