diff --git a/google/api_core/client_logging.py b/google/api_core/client_logging.py index d3cf805a..35938ce7 100644 --- a/google/api_core/client_logging.py +++ b/google/api_core/client_logging.py @@ -1,84 +1,73 @@ import logging import json import re +import os -# NOTE: We can essentially just use `json.dumps` and make structured logging our default format. -class DefaultFormatter(logging.Formatter): - def format(self, record): - # Base format with time, severity, and main message - # base_message = f"{self.formatTime(record)} - {record.levelname} - {record.name} - {record.getMessage()}" - base_message = super().format(record) - - # Add any extra fields below: - extra_info = [] - if hasattr(record, 'client_id'): - extra_info.append(f"client_id={record.client_id}") - if hasattr(record, 'response_id'): - extra_info.append(f"client_id={record.client_id}") - if hasattr(record, 'httpRequest'): - extra_info.append(f"httpRequest={record.httpRequest}") - if hasattr(record, 'json_fields'): - for key, val in record.json_fields.items(): - extra_info.append(f"{key}={val}") - - # Combine base message with extra fields - if extra_info: - base_message += " | " + " - ".join(extra_info) - - return base_message +LOGGING_INITIALIZED = False +# TODO(): Update Request / Response messages. +REQUEST_MESSAGE = "Sending request ..." +RESPONSE_MESSAGE = "Receiving response ..." -# NOTE: Option 1: Allow users to configure log levels. -# def setup_logging(log_level, namespace="google"): - -# # NOTE: A logger with namespace="google" is only configured if all of the below conditions hold true: -# # - A root logger is not configured. -# # - N/A: A logger with namespace="google" is not already configured (This statement is removed.) -# # - GOOGLE_SDK_PYTHON_LOGGING_LEVEL is set. -# if not logging.getLogger().hasHandlers() and log_level: +# TODO(): Update this list to support additional logging fields +_recognized_logging_fields = ["httpRequest", "rpcName", "serviceName"] # Additional fields to be Logged. -# # define a module for our repositories -# logger = logging.getLogger(namespace) -# try: -# logger.setLevel(log_level) -# except ValueError: -# logger.setLevel("WARNING") -# logger.warning(f"Configured log level `{log_level}` is incorrect. Defaulting to WARNING.") +def logger_configured(logger): + return logger.hasHandlers() or logger.level != logging.NOTSET -# # Default settings -# console_handler = logging.StreamHandler() -# formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -# console_handler.setFormatter(formatter) -# logger.addHandler(console_handler) +def initialize_logging(): + global LOGGING_INITIALIZED + if LOGGING_INITIALIZED: + return + scopes = os.getenv("GOOGLE_SDK_PYTHON_LOGGING_SCOPE") + setup_logging(scopes) + LOGGING_INITIALIZED = True +def parse_logging_scopes(scopes): + if not scopes: + return [] + # TODO(): check if the namespace is a valid namespace. + # TODO(): parse a list of namespaces. Current flow expects a single string for now. + namespaces = [scopes] + return namespaces -# NOTE: Option 2: Allow users to configure log systems. -def setup_logging(namespace): - - # Instantiate a base logger unconditionally to avoid propogating logs to the root logger. +def default_settings(logger): + if not logger_configured(logger): + console_handler = logging.StreamHandler() + logger.setLevel("DEBUG") + logger.propagate = False + formatter = StructuredLogFormatter() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + +def setup_logging(scopes): + # disable log propagation at base logger level to the root logger only if a base logger is not already configured via code changes. base_logger = logging.getLogger("google") - base_logger.propagate = False + if not logger_configured(base_logger): + base_logger.propagate = False - # If a namespace is not provided, we don't need to do anything. - if not namespace: - return + # only returns valid logger scopes (namespaces) + # this list has at most one element. + loggers = parse_logging_scopes(scopes) - # If the provided namespace does not start with "google", we don't need to do anything. - # We can update the regex to be more strict about the format of the namespace. - match = re.search(f"google", namespace) - if not match: - # TODO: raise an error? silently ignore? - return - + for namespace in loggers: + # This will either create a module level logger or get the reference of the base logger instantiated above. + logger = logging.getLogger(namespace) - # This will either create a module level logger or get the reference of the base logger instantiated above. - logger = logging.getLogger(namespace) + # Set default settings. + default_settings(logger) - # Set default settings. - if not logger.hasHandlers() and logger.level == logging.NOTSET: - console_handler = logging.StreamHandler() - logger.setLevel("DEBUG") - formatter = DefaultFormatter('%(asctime)s %(levelname)s %(name)s %(message)s') - # formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) +class StructuredLogFormatter(logging.Formatter): + def format(self, record): + log_obj = { + 'timestamp': self.formatTime(record), + 'severity': record.levelname, + 'name': record.name, + 'message': record.getMessage(), + } + + for field_name in _recognized_logging_fields: + value = getattr(record, field_name, None) + if value is not None: + log_obj[field_name] = value + return json.dumps(log_obj) \ No newline at end of file