• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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