tools/code_review_tools/log.py (109 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import logging import logging.handlers import os import re import pkg_resources import sentry_sdk import structlog from sentry_sdk.integrations.logging import LoggingIntegration root = logging.getLogger() # Found on https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python # 7-bit C1 ANSI sequences ANSI_ESCAPE = re.compile( r""" \x1B # ESC (?: # 7-bit C1 Fe (except CSI) [@-Z\\-_] | # or [ for CSI, followed by a control sequence \[ [0-?]* # Parameter bytes [ -/]* # Intermediate bytes [@-~] # Final byte ) """, re.VERBOSE, ) class AppNameFilter(logging.Filter): def __init__(self, project_name, channel, *args, **kwargs): self.project_name = project_name self.channel = channel super().__init__(*args, **kwargs) def filter(self, record): record.app_name = f"code-review/{self.channel}/{self.project_name}" return True def setup_papertrail(project_name, channel, PAPERTRAIL_HOST, PAPERTRAIL_PORT): """ Setup papertrail account using taskcluster secrets """ # Setup papertrail papertrail = logging.handlers.SysLogHandler( address=(PAPERTRAIL_HOST, int(PAPERTRAIL_PORT)), ) formatter = logging.Formatter( "%(app_name)s: %(asctime)s %(filename)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) papertrail.setLevel(logging.INFO) papertrail.setFormatter(formatter) # This filter is used to add the 'app_name' value to all logs to be formatted papertrail.addFilter(AppNameFilter(project_name, channel)) root.addHandler(papertrail) def remove_color_codes(event, hint): """ Remove ANSI color codes from a Sentry event before it gets published """ def _remove(content): try: return ANSI_ESCAPE.sub("", content) except Exception as e: # Do not log here, rely on simple print print(f"Failed to remove color code: {e}") return content # Remove from breadcrumb breadcrumbs = event.get("breadcrumbs", {}) for value in breadcrumbs.get("values", []): if "message" in value: value["message"] = _remove(value["message"]) # Remove from log entry logentry = event.get("logentry", {}) if "message" in logentry: logentry["message"] = _remove(logentry["message"]) return event def setup_sentry(name, channel, dsn): """ Setup sentry account using taskcluster secrets """ # Detect environment task_id = os.environ.get("TASK_ID") if task_id is not None: site = "taskcluster" elif "DYNO" in os.environ: site = "heroku" else: site = "unknown" # This integration allows sentry to catch logs from logging and process them # By default, the 'event_level' is set to ERROR, we are defining it to WARNING sentry_logging = LoggingIntegration( level=logging.INFO, # Capture INFO and above as breadcrumbs event_level=logging.WARNING, # Send WARNINGs as events ) # sentry_sdk will automatically retrieve the 'extra' attribute from logs and # add contained values as Additional Data on the dashboard of the Sentry issue sentry_sdk.init( dsn=dsn, integrations=[sentry_logging], server_name=name, environment=channel, release=pkg_resources.get_distribution(f"code-review-{name}").version, before_send=remove_color_codes, ) sentry_sdk.set_tag("site", site) if task_id is not None: # Add a Taskcluster task id when available # It will be shown in a new section called Task on the dashboard sentry_sdk.set_context("task", {"task_id": task_id}) def init_logger( project_name, channel=None, level=logging.INFO, PAPERTRAIL_HOST=None, PAPERTRAIL_PORT=None, SENTRY_DSN=None, ): if not channel: channel = os.environ.get("APP_CHANNEL") # Render extra information from structlog on default logging output formatter = logging.Formatter( "%(asctime)s.%(msecs)06d [%(levelname)-8s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) handler = logging.StreamHandler() handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.addHandler(handler) root_logger.setLevel(level) # Log to papertrail if channel and PAPERTRAIL_HOST and PAPERTRAIL_PORT: setup_papertrail(project_name, channel, PAPERTRAIL_HOST, PAPERTRAIL_PORT) # Log to sentry if channel and SENTRY_DSN: setup_sentry(project_name, channel, SENTRY_DSN) # Setup structlog processors = [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, structlog.dev.ConsoleRenderer(), ] structlog.configure( processors=processors, context_class=structlog.threadlocal.wrap_dict(dict), logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, )