"""Flask Logger. Util to quickly and easily setup logging on any flask application by calling one single method: `setup`. Example:: from flask import Flask from owslogger import flask_logger app = Flask(__name__) flask_logger.setup( app, 'https://url', 'dev', 'logger_name', logging.INFO, 'service_name', '1.0.0') """ import logging import uuid from functools import partial from operator import itemgetter from typing import Optional, Union, cast from ddtrace.trace import tracer from flask import Flask, Response, g, request from owslogger import constants, logger class Ows: """Ows Objects.""" pass def setup( app: Flask, environment: str, logger_name: str, logger_level: Union[str, int], service_name: str, service_version: str, exclude_paths: Optional[list[str]] = None, enable_autolog: bool = True, clear_handlers: bool = True, dsn: Optional[str] = None, autolog_full_path: bool = False, ) -> None: """Set up logging for the flask application. Args: app: the application to add logging to. dsn: the data source name. environment: the application's environment. logger_name: name of the logger. logger_level: logging level of the logger. service_name: the service name. service_version: the service version. exclude_paths: optional list of paths to exclude. enable_autolog: auto-log information that relates to a response. clear_handlers: clear handler list on root logger. autolog_full_path: include full path to the log message. """ current_logger = logger.setup( environment, logger_name, logger_level, service_name, service_version, clear_handlers=clear_handlers, dsn=dsn, ) app.global_correlation_id = partial( # type: ignore global_correlation_id, current_logger, exclude_paths=exclude_paths ) app.global_logger = partial(global_logger, current_logger) # type: ignore app.before_request(app.global_correlation_id) # type: ignore app.before_request(app.global_logger) # type: ignore app.after_request(add_correlation_id_to_response) if enable_autolog: app.after_request( partial( autolog, autolog_full_path=autolog_full_path, exclude_paths=exclude_paths, ) ) def get_ows() -> Ows: """Get the ows object on the global. This is very useful when we want to keep a copy of all the different ows specific attributes set on flask.g. Returns: Ows: the ows namespace """ if not hasattr(g, "ows"): g.ows = Ows() return cast(Ows, g.ows) def global_correlation_id( current_logger: Union[logging.Logger, logging.LoggerAdapter[logging.Logger]], exclude_paths: Optional[list[str]] = None, ) -> None: """Global correlation id. The correlation id is either provided by the request, and if not, it is created by the service and used whenever a call is made to another system. We are using flask.g, since Flask is thread safe. Args: current_logger: the app logger. exclude_paths: list of paths to exclude. """ exclude_paths = exclude_paths or [] if hasattr(g, "correlation_id"): return get_ows().correlation_id = request.headers.get(constants.CORRELATION_ID_HEADER) # type: ignore if not g.ows.correlation_id: g.ows.correlation_id = str(uuid.uuid1()) message = f"Correlation-Id ({g.ows.correlation_id}) created." else: message = f"Correlation-Id ({g.ows.correlation_id}) received." g.correlation_id = g.ows.correlation_id global_logger(current_logger) if request.path not in exclude_paths: g.ows.log.debug(message) def global_logger( current_logger: Union[logging.Logger, logging.LoggerAdapter[logging.Logger]], ) -> None: """Global logger. The global logger is used everywhere through the application. It formats the logs following our standards and it attaches additional information such as the correlation id. Code sample: from flask import g @app.route('/') def homepage(): g.log.info('User has hit the homepage') Args: current_logger (Logger): the application logger. """ global_correlation_id(current_logger) context = {"correlation_id": g.correlation_id} get_ows().log = logger.OwsLoggingAdapter(current_logger, context) # type: ignore g.log = g.ows.log def add_correlation_id_to_response(response: Response) -> Response: """Set correlation id into response header. Args: response: response object to hydrate Returns: Response hydrated with correlation id """ if response: response.headers[constants.CORRELATION_ID_HEADER] = g.ows.correlation_id return response def autolog( response: Response, autolog_full_path: bool = False, exclude_paths: Optional[list[str]] = None, ) -> Response: """Auto log a response. This mechanism allows us to send the result of a request to our different systems. It also includes additional information such as the user id, account type, and response code (if available). Args: response: response object to use. autolog_full_path: include full path to the log message. exclude_paths: paths to exclude from this. Returns: The provided response (non altered). """ exclude_paths = exclude_paths or [] if request.path in exclude_paths: return response resources = g.ows.log.resources resources.update( account_id=request.headers.get(constants.GRASS_ACCOUNT_ID_HEADER), account_type=request.headers.get(constants.GRASS_ACCOUNT_TYPE_HEADER), user_id=request.headers.get(constants.USER_ID_HEADER), ) resources = dict(filter(itemgetter(1), resources.items())) if hasattr(g, "request_context") and g.request_context.context_type: resources.update(vars(g.request_context)) resources.pop("authorization") root_span = tracer.current_root_span() if root_span: for name, value in resources.items(): if value is not None: root_span.set_tag(f"request_context.{name}", value) root_span.set_tag("correlation-id", g.ows.correlation_id) if 299 < response.status_code < 499: level_logger = g.ows.log.warning elif 499 < response.status_code: level_logger = g.ows.log.error else: level_logger = g.ows.log.info extra_message = g.ows.log.extra_message message = constants.AUTOLOG_MESSAGE_WITH_EXTRA if not extra_message: message = constants.AUTOLOG_MESSAGE path_to_log = request.full_path if autolog_full_path else request.path level_logger( message.format( status=response.status_code, verb=request.method, resource=path_to_log, extra=extra_message, ), resources=resources, ) return response