Source code for prozorro_sale.tools.logger

"""
Package to provide a logging with specific output accepted for CBD3.
"""
import logging
import os
from logging import config, LogRecord
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse

import yaml
from aiohttp.abc import AbstractAccessLogger, BaseRequest
from aiohttp.web_log import AccessLogger

from prozorro_sale.tools.context_variables import REQUEST_ID, PROCEDURE_ID, AUCTION_ID
from prozorro_sale.tools.environment import path_discover

CONFIG_PATH = os.getenv("LOGGING_CONFIG_PATH", '/config/logging.yml')

FILTER = None


[docs]class RequestIdFilter(logging.Filter): """Logger filter for request id"""
[docs] def filter(self, record: LogRecord) -> int: request_id = REQUEST_ID.get('unknown') record.x_request_id = request_id return True
[docs]class ProcedureIdFilter(logging.Filter): """Logger filter for procedure id"""
[docs] def filter(self, record: logging.LogRecord) -> bool: if procedure_id := PROCEDURE_ID.get(): record.procedure_id = procedure_id return True
[docs]class AuctionIdFilter(logging.Filter): """Logger filter for procedure id"""
[docs] def filter(self, record: logging.LogRecord) -> bool: if auction_id := AUCTION_ID.get(): record.auction_id = auction_id return True
[docs]def get_filter(): """Get logger filter class""" global FILTER if not FILTER: FILTER = RequestIdFilter() return FILTER
[docs]def get_custom_logger(name): """Get logger custom filter class""" logger = logging.getLogger(name) logger.addFilter(get_filter()) return logger
LOG = get_custom_logger(__name__) FILTER_MAPPING = { 'requestid': RequestIdFilter, 'procedureid': ProcedureIdFilter, 'auctionid': AuctionIdFilter, }
[docs]def configure_logging(config_path=None): """Configure logger from config file. Raises: Exception: Failed to configure logging from file """ log_level = os.getenv('LOGGING_LEVEL', 'INFO') config_path = config_path or CONFIG_PATH config_path = path_discover(config_path) try: with open(config_path, 'r') as c: log_config = yaml.safe_load(c) log_config['logging']['root']['level'] = log_level out_filters = {} for filter_ in log_config['logging'].get('filters', []): if filter_ in FILTER_MAPPING: out_filters.update({ filter_: { '()': FILTER_MAPPING[filter_] } }) log_config['logging']['filters'] = out_filters config.dictConfig(log_config['logging']) return except Exception as ex: LOG.exception(f'Failed to configure logging from file cause of {ex}') log_format = '%(asctime)s | %(name)s | %(levelname)s - %(message)s' logging.basicConfig(level=log_level, format=log_format, datefmt='%Y-%m-%dT%H:%M:%S %z')
[docs]class CustomAccessLogger(AbstractAccessLogger): """Custom Access Logger. """
[docs] @staticmethod def _format_r(request: BaseRequest) -> str: """Format log message output. Args: request (BaseRequest) Returns: str: log message """ if request is None: return '-' path_qs = urlparse(request.path_qs) query = parse_qs(path_qs.query, keep_blank_values=True) if 'acc_token' in query: query['acc_token'] = '#censored#' path_qs = path_qs._replace(query=urlencode(query, True)) return '%s %s HTTP/%s.%s' % (request.method, urlunparse(path_qs), request.version.major, request.version.minor)
[docs] def log(self, request, response, time): """logging method. Args: request (aiohttp.web.Request object) response (aiohttp.web.Response object) time (float) – Time taken to serve the request. """ remote = request.headers.get('X-Forwarded-For', request.remote) request_info = self._format_r(request) refer = request.headers.get('Referer', "-") user_agent = request.headers.get('User-Agent', "-") time = AccessLogger._format_Tf(request, response, time) self.logger.info(f"\"{remote} {request_info} {response.status} " f"{response.body_length} {refer} {user_agent} {time} \"")
[docs]class LogFormatter(logging.Formatter): """Log formatter. """
[docs] def formatMessage(self, record: LogRecord) -> str: """ Format output log message. Args: record (LogRecord) """ record_dict = record.__dict__ origin_fmt = self._style._fmt custom_attrs = [ ('PROCEDURE-ID', 'procedure_id'), ('AUCTION-ID', 'auction_id'), ('X-Request-ID', 'x_request_id'), ] for title, param in custom_attrs: formatted_param = f"%({param})s" if record_dict.get(param) and formatted_param not in origin_fmt: self._style._fmt += f" | {title}: {formatted_param}" styled_mes = super().formatMessage(record) self._style._fmt = origin_fmt return styled_mes