11import logging
22import json
33import re
4+ import os
45
5- # NOTE: We can essentially just use `json.dumps` and make structured logging our default format.
6- class DefaultFormatter (logging .Formatter ):
7- def format (self , record ):
8- # Base format with time, severity, and main message
9- # base_message = f"{self.formatTime(record)} - {record.levelname} - {record.name} - {record.getMessage()}"
10- base_message = super ().format (record )
11-
12- # Add any extra fields below:
13- extra_info = []
14- if hasattr (record , 'client_id' ):
15- extra_info .append (f"client_id={ record .client_id } " )
16- if hasattr (record , 'response_id' ):
17- extra_info .append (f"client_id={ record .client_id } " )
18- if hasattr (record , 'httpRequest' ):
19- extra_info .append (f"httpRequest={ record .httpRequest } " )
20- if hasattr (record , 'json_fields' ):
21- for key , val in record .json_fields .items ():
22- extra_info .append (f"{ key } ={ val } " )
23-
24- # Combine base message with extra fields
25- if extra_info :
26- base_message += " | " + " - " .join (extra_info )
27-
28- return base_message
6+ LOGGING_INITIALIZED = False
297
8+ # TODO(<add-link>): Update Request / Response messages.
9+ REQUEST_MESSAGE = "Sending request ..."
10+ RESPONSE_MESSAGE = "Receiving response ..."
3011
31- # NOTE: Option 1: Allow users to configure log levels.
32- # def setup_logging(log_level, namespace="google"):
33-
34- # # NOTE: A logger with namespace="google" is only configured if all of the below conditions hold true:
35- # # - A root logger is not configured.
36- # # - N/A: A logger with namespace="google" is not already configured (This statement is removed.)
37- # # - GOOGLE_SDK_PYTHON_LOGGING_LEVEL is set.
38- # if not logging.getLogger().hasHandlers() and log_level:
12+ # TODO(<add-link>): Update this list to support additional logging fields
13+ _recognized_logging_fields = ["httpRequest" , "rpcName" , "serviceName" ] # Additional fields to be Logged.
3914
40- # # define a module for our repositories
41- # logger = logging.getLogger(namespace)
42- # try:
43- # logger.setLevel(log_level)
44- # except ValueError:
45- # logger.setLevel("WARNING")
46- # logger.warning(f"Configured log level `{log_level}` is incorrect. Defaulting to WARNING.")
15+ def logger_configured (logger ):
16+ return logger .hasHandlers () or logger .level != logging .NOTSET
4717
48- # # Default settings
49- # console_handler = logging.StreamHandler()
50- # formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
51- # console_handler.setFormatter(formatter)
52- # logger.addHandler(console_handler)
18+ def initialize_logging ():
19+ global LOGGING_INITIALIZED
20+ if LOGGING_INITIALIZED :
21+ return
22+ scopes = os .getenv ("GOOGLE_SDK_PYTHON_LOGGING_SCOPE" )
23+ setup_logging (scopes )
24+ LOGGING_INITIALIZED = True
5325
26+ def parse_logging_scopes (scopes ):
27+ if not scopes :
28+ return []
29+ # TODO(<add-link>): check if the namespace is a valid namespace.
30+ # TODO(<add-link>): parse a list of namespaces. Current flow expects a single string for now.
31+ namespaces = [scopes ]
32+ return namespaces
5433
55- # NOTE: Option 2: Allow users to configure log systems.
56- def setup_logging (namespace ):
57-
58- # Instantiate a base logger unconditionally to avoid propogating logs to the root logger.
34+ def default_settings (logger ):
35+ if not logger_configured (logger ):
36+ console_handler = logging .StreamHandler ()
37+ logger .setLevel ("DEBUG" )
38+ logger .propagate = False
39+ formatter = StructuredLogFormatter ()
40+ console_handler .setFormatter (formatter )
41+ logger .addHandler (console_handler )
42+
43+ def setup_logging (scopes ):
44+ # disable log propagation at base logger level to the root logger only if a base logger is not already configured via code changes.
5945 base_logger = logging .getLogger ("google" )
60- base_logger .propagate = False
46+ if not logger_configured (base_logger ):
47+ base_logger .propagate = False
6148
62- # If a namespace is not provided, we don't need to do anything.
63- if not namespace :
64- return
49+ # only returns valid logger scopes (namespaces)
50+ # this list has at most one element.
51+ loggers = parse_logging_scopes ( scopes )
6552
66- # If the provided namespace does not start with "google", we don't need to do anything.
67- # We can update the regex to be more strict about the format of the namespace.
68- match = re .search (f"google" , namespace )
69- if not match :
70- # TODO: raise an error? silently ignore?
71- return
72-
53+ for namespace in loggers :
54+ # This will either create a module level logger or get the reference of the base logger instantiated above.
55+ logger = logging .getLogger (namespace )
7356
74- # This will either create a module level logger or get the reference of the base logger instantiated above .
75- logger = logging . getLogger ( namespace )
57+ # Set default settings .
58+ default_settings ( logger )
7659
77- # Set default settings.
78- if not logger .hasHandlers () and logger .level == logging .NOTSET :
79- console_handler = logging .StreamHandler ()
80- logger .setLevel ("DEBUG" )
81- formatter = DefaultFormatter ('%(asctime)s %(levelname)s %(name)s %(message)s' )
82- # formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
83- console_handler .setFormatter (formatter )
84- logger .addHandler (console_handler )
60+ class StructuredLogFormatter (logging .Formatter ):
61+ def format (self , record ):
62+ log_obj = {
63+ 'timestamp' : self .formatTime (record ),
64+ 'severity' : record .levelname ,
65+ 'name' : record .name ,
66+ 'message' : record .getMessage (),
67+ }
68+
69+ for field_name in _recognized_logging_fields :
70+ value = getattr (record , field_name , None )
71+ if value is not None :
72+ log_obj [field_name ] = value
73+ return json .dumps (log_obj )
0 commit comments