Skip to content

Commit ff594d3

Browse files
committed
feat: add base logger to enable debug logging
1 parent 46b3d3a commit ff594d3

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

google/api_core/client_logging.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import logging
2+
import json
3+
import re
4+
import os
5+
6+
LOGGING_INITIALIZED = False
7+
8+
# TODO(<add-link>): Update Request / Response messages.
9+
REQUEST_MESSAGE = "Sending request ..."
10+
RESPONSE_MESSAGE = "Receiving response ..."
11+
12+
# TODO(<add-link>): Update this list to support additional logging fields
13+
_recognized_logging_fields = ["httpRequest", "rpcName", "serviceName"] # Additional fields to be Logged.
14+
15+
def logger_configured(logger):
16+
return logger.hasHandlers() or logger.level != logging.NOTSET
17+
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
25+
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
33+
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.
45+
base_logger = logging.getLogger("google")
46+
if not logger_configured(base_logger):
47+
base_logger.propagate = False
48+
49+
# only returns valid logger scopes (namespaces)
50+
# this list has at most one element.
51+
loggers = parse_logging_scopes(scopes)
52+
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)
56+
57+
# Set default settings.
58+
default_settings(logger)
59+
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)

tests/unit/test_client_logging.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import logging
2+
import pytest
3+
4+
from google.api_core.client_logging import BaseLogger
5+
6+
7+
def test_base_logger(caplog):
8+
9+
logger = BaseLogger().get_logger()
10+
11+
with caplog.at_level(logging.INFO, logger="google"):
12+
logger.info("This is a test message.")
13+
14+
assert "This is a test message." in caplog.text
15+
assert caplog.records[0].name == "google"
16+
assert caplog.records[0].levelname == "INFO"
17+
assert caplog.records[0].message == "This is a test message."

0 commit comments

Comments
 (0)