1import logging 2import json 3import os 4 5from typing import List, Optional 6 7_LOGGING_INITIALIZED = False 8_BASE_LOGGER_NAME = "google" 9 10# Fields to be included in the StructuredLogFormatter. 11# 12# TODO(https://github.com/googleapis/python-api-core/issues/761): Update this list to support additional logging fields. 13_recognized_logging_fields = [ 14 "httpRequest", 15 "rpcName", 16 "serviceName", 17 "credentialsType", 18 "credentialsInfo", 19 "universeDomain", 20 "request", 21 "response", 22 "metadata", 23 "retryAttempt", 24 "httpResponse", 25] # Additional fields to be Logged. 26 27 28def logger_configured(logger) -> bool: 29 """Determines whether `logger` has non-default configuration 30 31 Args: 32 logger: The logger to check. 33 34 Returns: 35 bool: Whether the logger has any non-default configuration. 36 """ 37 return ( 38 logger.handlers != [] or logger.level != logging.NOTSET or not logger.propagate 39 ) 40 41 42def initialize_logging(): 43 """Initializes "google" loggers, partly based on the environment variable 44 45 Initializes the "google" logger and any loggers (at the "google" 46 level or lower) specified by the environment variable 47 GOOGLE_SDK_PYTHON_LOGGING_SCOPE, as long as none of these loggers 48 were previously configured. If any such loggers (including the 49 "google" logger) are initialized, they are set to NOT propagate 50 log events up to their parent loggers. 51 52 This initialization is executed only once, and hence the 53 environment variable is only processed the first time this 54 function is called. 55 """ 56 global _LOGGING_INITIALIZED 57 if _LOGGING_INITIALIZED: 58 return 59 scopes = os.getenv("GOOGLE_SDK_PYTHON_LOGGING_SCOPE", "") 60 setup_logging(scopes) 61 _LOGGING_INITIALIZED = True 62 63 64def parse_logging_scopes(scopes: Optional[str] = None) -> List[str]: 65 """Returns a list of logger names. 66 67 Splits the single string of comma-separated logger names into a list of individual logger name strings. 68 69 Args: 70 scopes: The name of a single logger. (In the future, this will be a comma-separated list of multiple loggers.) 71 72 Returns: 73 A list of all the logger names in scopes. 74 """ 75 if not scopes: 76 return [] 77 # TODO(https://github.com/googleapis/python-api-core/issues/759): check if the namespace is a valid namespace. 78 # TODO(b/380481951): Support logging multiple scopes. 79 # TODO(b/380483756): Raise or log a warning for an invalid scope. 80 namespaces = [scopes] 81 return namespaces 82 83 84def configure_defaults(logger): 85 """Configures `logger` to emit structured info to stdout.""" 86 if not logger_configured(logger): 87 console_handler = logging.StreamHandler() 88 logger.setLevel("DEBUG") 89 logger.propagate = False 90 formatter = StructuredLogFormatter() 91 console_handler.setFormatter(formatter) 92 logger.addHandler(console_handler) 93 94 95def setup_logging(scopes: str = ""): 96 """Sets up logging for the specified `scopes`. 97 98 If the loggers specified in `scopes` have not been previously 99 configured, this will configure them to emit structured log 100 entries to stdout, and to not propagate their log events to their 101 parent loggers. Additionally, if the "google" logger (whether it 102 was specified in `scopes` or not) was not previously configured, 103 it will also configure it to not propagate log events to the root 104 logger. 105 106 Args: 107 scopes: The name of a single logger. (In the future, this will be a comma-separated list of multiple loggers.) 108 109 """ 110 111 # only returns valid logger scopes (namespaces) 112 # this list has at most one element. 113 logger_names = parse_logging_scopes(scopes) 114 115 for namespace in logger_names: 116 # This will either create a module level logger or get the reference of the base logger instantiated above. 117 logger = logging.getLogger(namespace) 118 119 # Configure default settings. 120 configure_defaults(logger) 121 122 # disable log propagation at base logger level to the root logger only if a base logger is not already configured via code changes. 123 base_logger = logging.getLogger(_BASE_LOGGER_NAME) 124 if not logger_configured(base_logger): 125 base_logger.propagate = False 126 127 128# TODO(https://github.com/googleapis/python-api-core/issues/763): Expand documentation. 129class StructuredLogFormatter(logging.Formatter): 130 # TODO(https://github.com/googleapis/python-api-core/issues/761): ensure that additional fields such as 131 # function name, file name, and line no. appear in a log output. 132 def format(self, record: logging.LogRecord): 133 log_obj = { 134 "timestamp": self.formatTime(record), 135 "severity": record.levelname, 136 "name": record.name, 137 "message": record.getMessage(), 138 } 139 140 for field_name in _recognized_logging_fields: 141 value = getattr(record, field_name, None) 142 if value is not None: 143 log_obj[field_name] = value 144 return json.dumps(log_obj) 145