Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add automatic logging config to support debug logging #754

Merged
merged 18 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions google/api_core/client_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging
import json
import re
import os

_LOGGING_INITIALIZED = False
_BASE_LOGGER_NAME = "google"
# TODO(https://github.com/googleapis/python-api-core/issues/760): Update Request / Response messages.
REQUEST_MESSAGE = "Sending request ..."
RESPONSE_MESSAGE = "Receiving response ..."
ohmayr marked this conversation as resolved.
Show resolved Hide resolved

# TODO(https://github.com/googleapis/python-api-core/issues/761): Update this list to support additional logging fields
_recognized_logging_fields = ["httpRequest", "rpcName", "serviceName"] # Additional fields to be Logged.

def logger_configured(logger):
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
return logger.handlers != [] or logger.level != logging.NOTSET or logger.propagate == False

def initialize_logging():
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
global _LOGGING_INITIALIZED
if _LOGGING_INITIALIZED:
return
scopes = os.getenv("GOOGLE_SDK_PYTHON_LOGGING_SCOPE")
setup_logging(scopes)
_LOGGING_INITIALIZED = True

def parse_logging_scopes(scopes):
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
if not scopes:
return []
# TODO(https://github.com/googleapis/python-api-core/issues/759): check if the namespace is a valid namespace.
# TODO(b/380481951): Support logging multiple scopes.
# TODO(b/380483756): Raise or log a warning for an invalid scope.
namespaces = [scopes]
return namespaces

def configure_defaults(logger):
if not logger_configured(logger):
console_handler = logging.StreamHandler()
logger.setLevel("DEBUG")
logger.propagate = False
formatter = StructuredLogFormatter()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)

def setup_logging(scopes=[]):
ohmayr marked this conversation as resolved.
Show resolved Hide resolved

# only returns valid logger scopes (namespaces)
# this list has at most one element.
logger_names = parse_logging_scopes(scopes)

for namespace in logger_names:
parthea marked this conversation as resolved.
Show resolved Hide resolved
# This will either create a module level logger or get the reference of the base logger instantiated above.
logger = logging.getLogger(namespace)

# Configure default settings.
configure_defaults(logger)

# disable log propagation at base logger level to the root logger only if a base logger is not already configured via code changes.
# Maybe we do this at the end?
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
base_logger = logging.getLogger(_BASE_LOGGER_NAME)
if not logger_configured(base_logger):
base_logger.propagate = False

class StructuredLogFormatter(logging.Formatter):
def format(self, record):
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
log_obj = {
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
'timestamp': self.formatTime(record),
'severity': record.levelname,
'name': record.name,
'message': record.getMessage(),
}

for field_name in _recognized_logging_fields:
value = getattr(record, field_name, None)
if value is not None:
log_obj[field_name] = value
return json.dumps(log_obj)
62 changes: 62 additions & 0 deletions tests/unit/test_client_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import logging
import pytest

from google.api_core.client_logging import setup_logging
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
# Test expected behaviour for warnings, propagation, handler + formatter.

ohmayr marked this conversation as resolved.
Show resolved Hide resolved
def reset_logger(scope):
logger = logging.getLogger(scope)
logger.handlers = []
logger.setLevel(logging.NOTSET)
logger.propagate = True

def test_setup_logging_w_no_scopes():
setup_logging()
base_logger = logging.getLogger("google")
assert base_logger.handlers == []
assert base_logger.propagate == False
assert base_logger.level == logging.NOTSET

reset_logger("google")


def test_setup_logging_w_base_scope():
setup_logging("google")
base_logger = logging.getLogger("google")
assert isinstance(base_logger.handlers[0], logging.StreamHandler)
assert base_logger.propagate == False
assert base_logger.level == logging.DEBUG

reset_logger("google")

def test_setup_logging_w_module_scope():
setup_logging("google.foo")

base_logger = logging.getLogger("google")
assert base_logger.handlers == []
assert base_logger.propagate == False
assert base_logger.level == logging.NOTSET

module_logger = logging.getLogger("google.foo")
assert isinstance(module_logger.handlers[0], logging.StreamHandler)
assert module_logger.propagate == False
assert module_logger.level == logging.DEBUG


reset_logger("google")
ohmayr marked this conversation as resolved.
Show resolved Hide resolved

def test_setup_logging_w_incorrect_scope():
setup_logging("foo")

base_logger = logging.getLogger("google")
assert base_logger.handlers == []
assert base_logger.propagate == False
assert base_logger.level == logging.NOTSET

# TODO: update test once we add logic to ignore an incorrect scope.
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
logger = logging.getLogger("foo")
assert isinstance(logger.handlers[0], logging.StreamHandler)
assert logger.propagate == False
assert logger.level == logging.DEBUG

ohmayr marked this conversation as resolved.
Show resolved Hide resolved
reset_logger("google")
Loading