1import json
2import logging
3import os
4from typing import List, Optional
5
6_LOGGING_INITIALIZED = False
7_BASE_LOGGER_NAME = "google"
8
9# Fields to be included in the StructuredLogFormatter.
10#
11# TODO(https://github.com/googleapis/python-api-core/issues/761): Update this list to support additional logging fields.
12_recognized_logging_fields = [
13 "httpRequest",
14 "rpcName",
15 "serviceName",
16 "credentialsType",
17 "credentialsInfo",
18 "universeDomain",
19 "request",
20 "response",
21 "metadata",
22 "retryAttempt",
23 "httpResponse",
24] # Additional fields to be Logged.
25
26
27def 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
41def 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
63def 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
83def 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
94def 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.
128class 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)