import logging import os import queue import sys import threading from json import dumps from logging.config import dictConfig from logging.handlers import QueueHandler, QueueListener import sentry_sdk import structlog from ddtrace.helpers import get_correlation_ids from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration import delphi_es_utils from delphi_es_utils.settings import ( ENVIRONMENT, ES_LOG_LEVEL, LOG_LEVEL, SENTRY_DSN, SENTRY_ENABLE, SENTRY_PROJECT, ) LOG = structlog.get_logger(__name__) # pylint: disable=unused-argument def thread_info_injection(logger, method_name, event_dict): thread = threading.current_thread() event_dict['thread_no'] = thread.ident event_dict['thread_name'] = thread.name event_dict['process_pid'] = os.getpid() return event_dict # pylint: disable=unused-argument def tracer_injection(logger, log_method, event_dict) -> dict: """Add DataDog tracing fields to our structured log entries via structlog processor interface """ # get correlation ids from current tracer context trace_id, span_id = get_correlation_ids() # add ids to structlog event dictionary # if no trace present, set ids to 0 event_dict['dd.trace_id'] = trace_id or 0 event_dict['dd.span_id'] = span_id or 0 stack = event_dict.pop('stack', None) if stack: event_dict['error'] = event_dict.get('error', {}) event_dict['error']['stack'] = stack return event_dict def configure_logging(cache_logger: bool): """Configure our (global) logging format, handlers, and processors """ _configure_es_logging(ES_LOG_LEVEL) # Logging config before app instantiation dictConfig( { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'default': { 'format': '%(message)s', } }, 'root': { 'level': LOG_LEVEL, 'handlers': [] }, } ) # instantiate queue & attach it to handler for async/non-blocking logging log_queue: queue.Queue = queue.Queue(-1) # no limit on size queue_handler = QueueHandler(log_queue) handler = logging.StreamHandler(stream=sys.stdout) listener = QueueListener(log_queue, handler) root = logging.getLogger() root.addHandler(queue_handler) listener.start() structlog.configure( processors=[ # This performs the initial filtering, so we don't # evaluate e.g. DEBUG when unnecessary structlog.stdlib.filter_by_level, # Adds logger=module_name (e.g __main__) structlog.stdlib.add_logger_name, # Adds level=info, debug, etc. structlog.stdlib.add_log_level, # Performs the % string interpolation as expected structlog.stdlib.PositionalArgumentsFormatter(), # Include the stack when stack_info=True structlog.processors.StackInfoRenderer(), # Include the exception when exc_info=True # e.g log.exception() or log.warning(exc_info=True)'s behavior structlog.processors.format_exc_info, # Decodes the unicode values in any kv pairs structlog.processors.UnicodeDecoder(), # Creates the necessary args, kwargs for log() structlog.stdlib.render_to_log_kwargs, thread_info_injection, tracer_injection, structlog.processors.JSONRenderer(dumps, sort_keys=True), ], context_class=structlog.threadlocal.wrap_dict(dict), logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=cache_logger, ) def _configure_es_logging(level=logging.WARNING): """Prevent all HTTP requests being logged to stdout""" es_logger = logging.getLogger('elasticsearch') es_logger.propagate = False es_logger.setLevel(level) def configure_sentry(): """Enable sentry.io integration""" if SENTRY_ENABLE: # pylint: disable=abstract-class-instantiated sentry_sdk.init( dsn=SENTRY_DSN, environment=ENVIRONMENT, release=f'{SENTRY_PROJECT}@{delphi_es_utils.__version__}', integrations=[ SqlalchemyIntegration(), ] )