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