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 9 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
89 changes: 89 additions & 0 deletions google/api_core/client_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 "
parthea 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 = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to note is that users will need to update google-api-core to get any additional logging fields in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true. Any fields that we add in the future will require users to update google-api-core.

"httpRequest",
"rpcName",
"serviceName",
ohmayr marked this conversation as resolved.
Show resolved Hide resolved
] # Additional fields to be Logged.
ohmayr marked this conversation as resolved.
Show resolved Hide resolved


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.
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)
104 changes: 104 additions & 0 deletions tests/unit/test_client_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import logging
import pytest
import mock

from google.api_core.client_logging import (
setup_logging,
initialize_logging,
)

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():
with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foo"):
parthea marked this conversation as resolved.
Show resolved Hide resolved
setup_logging()
base_logger = logging.getLogger("foo")
assert base_logger.handlers == []
assert base_logger.propagate == False
assert base_logger.level == logging.NOTSET

reset_logger("foo")


def test_setup_logging_w_base_scope():
with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foo"):
setup_logging("foo")
base_logger = logging.getLogger("foo")
assert isinstance(base_logger.handlers[0], logging.StreamHandler)
assert base_logger.propagate == False
assert base_logger.level == logging.DEBUG

reset_logger("foo")


def test_setup_logging_w_module_scope():
with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foo"):
setup_logging("foo.bar")

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

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

reset_logger("foo")
reset_logger("foo.bar")


def test_setup_logging_w_incorrect_scope():
with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foo"):
setup_logging("abc")

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

# TODO(https://github.com/googleapis/python-api-core/issues/759): update test once we add logic to ignore an incorrect scope.
logger = logging.getLogger("abc")
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("foo")
reset_logger("abc")


def test_initialize_logging():

with mock.patch("os.getenv", return_value="foo.bar"):
with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foo"):
initialize_logging()

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

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

ohmayr marked this conversation as resolved.
Show resolved Hide resolved
base_logger.propagate = True
module_logger.propagate = True

with mock.patch("os.getenv", return_value="foo.bar"):
with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foo"):
initialize_logging()

assert base_logger.propagate == True
assert module_logger.propagate == True

reset_logger("foo")
reset_logger("foo.bar")
Loading