tools/code_coverage_tools/log.py (109 lines of code) (raw):
# -*- coding: utf-8 -*-
# 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 sys
import structlog
import importlib.metadata
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
root = logging.getLogger()
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-coverage/{self.channel}/{self.project_name}"
return True
class ExtraFormatter(logging.Formatter):
def format(self, record):
log = super().format(record)
extra = {
key: value
for key, value in record.__dict__.items()
if key
not in list(sentry_sdk.integrations.logging.COMMON_RECORD_ATTRS)
+ ["asctime", "app_name"]
}
if len(extra) > 0:
log += " | extra=" + str(extra)
return log
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 = ExtraFormatter(
"%(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 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=importlib.metadata.version(f"code-coverage-{name}"),
)
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})
class RenameAttrsProcessor(structlog.processors.KeyValueRenderer):
"""
Rename event_dict keys that will attempt to overwrite LogRecord common
attributes during structlog.stdlib.render_to_log_kwargs processing
"""
def __call__(self, logger, method_name, event_dict):
to_rename = [
key
for key in event_dict
if key in sentry_sdk.integrations.logging.COMMON_RECORD_ATTRS
]
for key in to_rename:
event_dict[f"{key}_"] = event_dict[key]
event_dict.pop(key)
return event_dict
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")
logging.basicConfig(
format="%(asctime)s.%(msecs)06d [%(levelname)-8s] %(filename)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
stream=sys.stdout,
level=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.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
RenameAttrsProcessor(),
# Transpose the 'event_dict' from structlog into keyword arguments for logging.log
# E.g.: 'event' become 'msg' and, at the end, all remaining values from 'event_dict'
# are added as 'extra'
structlog.stdlib.render_to_log_kwargs,
]
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,
)