Skip to content

Commit 63f4e38

Browse files
authored
Simplify the internal definition of processors (#36)
This removes the weird "extend(class().setup())" pattern in favor of a more straightforward approach: * Some processors are simply functions: let's define them as such * Some processors need other processors to run in the big picture: they are connected by specific "setup" functions
1 parent 64b0292 commit 63f4e38

File tree

5 files changed

+118
-128
lines changed

5 files changed

+118
-128
lines changed

structlog_gcp/base.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
import structlog.processors
22
from structlog.typing import Processor
33

4-
from . import errors, processors
4+
from . import error_reporting, processors
55

66

77
def build_processors(
88
service: str | None = None,
99
version: str | None = None,
1010
) -> list[Processor]:
11-
procs = []
12-
13-
procs.extend(processors.CoreCloudLogging().setup())
14-
procs.extend(processors.LogSeverity().setup())
15-
procs.extend(processors.CodeLocation().setup())
16-
procs.extend(errors.ReportException().setup())
17-
procs.extend(errors.ReportError(["CRITICAL"]).setup())
18-
procs.extend(errors.ServiceContext(service, version).setup())
19-
procs.extend(processors.FormatAsCloudLogging().setup())
11+
procs: list[Processor] = []
12+
13+
# Add a timestamp in ISO 8601 format.
14+
procs.append(structlog.processors.TimeStamper(fmt="iso"))
15+
procs.append(processors.init_cloud_logging)
16+
17+
procs.extend(processors.setup_log_severity())
18+
procs.extend(processors.setup_code_location())
19+
20+
# Errors: log exceptions
21+
procs.extend(error_reporting.setup_exceptions())
22+
23+
# Errors: formatter for Error Reporting
24+
procs.append(error_reporting.ReportError(["CRITICAL"]))
25+
26+
# Errors: add service context
27+
procs.append(error_reporting.ServiceContext(service, version))
28+
29+
# Finally: Cloud Logging formatter
30+
procs.append(processors.finalize_cloud_logging)
31+
32+
# Format as JSON
2033
procs.append(structlog.processors.JSONRenderer())
34+
2135
return procs
Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,13 @@
11
import os
22

3-
import structlog
3+
import structlog.processors
44
from structlog.typing import EventDict, Processor, WrappedLogger
55

66
from .types import CLOUD_LOGGING_KEY, ERROR_EVENT_TYPE, SOURCE_LOCATION_KEY
77

88

9-
class ServiceContext:
10-
def __init__(self, service: str | None = None, version: str | None = None) -> None:
11-
# https://cloud.google.com/functions/docs/configuring/env-var#runtime_environment_variables_set_automatically
12-
if service is None:
13-
service = os.environ.get("K_SERVICE", "unknown service")
14-
15-
if version is None:
16-
version = os.environ.get("K_REVISION", "unknown version")
17-
18-
self.service_context = {"service": service, "version": version}
19-
20-
def setup(self) -> list[Processor]:
21-
return [self]
22-
23-
def __call__(
24-
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
25-
) -> EventDict:
26-
"""Add a service context in which an error has occurred.
27-
28-
This is part of the Error Reporting API, so it's only added when an error happens.
29-
"""
30-
31-
event_type = event_dict[CLOUD_LOGGING_KEY].get("@type")
32-
if event_type != ERROR_EVENT_TYPE:
33-
return event_dict
34-
35-
# https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext
36-
event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = self.service_context
37-
38-
return event_dict
9+
def setup_exceptions(log_level: str = "CRITICAL") -> list[Processor]:
10+
return [structlog.processors.format_exc_info, ReportException(log_level)]
3911

4012

4113
class ReportException:
@@ -47,9 +19,6 @@ class ReportException:
4719
def __init__(self, log_level: str = "CRITICAL") -> None:
4820
self.log_level = log_level
4921

50-
def setup(self) -> list[Processor]:
51-
return [structlog.processors.format_exc_info, self]
52-
5322
def __call__(
5423
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
5524
) -> EventDict:
@@ -80,19 +49,6 @@ class ReportError:
8049
def __init__(self, severities: list[str]) -> None:
8150
self.severities = severities
8251

83-
def setup(self) -> list[Processor]:
84-
return [self]
85-
86-
def _build_service_context(self) -> dict[str, str]:
87-
# https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext
88-
service_context = {
89-
# https://cloud.google.com/functions/docs/configuring/env-var#runtime_environment_variables_set_automatically
90-
"service": os.environ.get("K_SERVICE", "unknown service"),
91-
"version": os.environ.get("K_REVISION", "unknown version"),
92-
}
93-
94-
return service_context
95-
9652
def __call__(
9753
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
9854
) -> EventDict:
@@ -108,6 +64,37 @@ def __call__(
10864

10965
event_dict[CLOUD_LOGGING_KEY]["@type"] = ERROR_EVENT_TYPE
11066
event_dict[CLOUD_LOGGING_KEY]["context"] = error_context
111-
event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = self._build_service_context()
67+
68+
# "serviceContext" should be added by the ServiceContext processor.
69+
# event_dict[CLOUD_LOGGING_KEY]["serviceContext"]
70+
71+
return event_dict
72+
73+
74+
class ServiceContext:
75+
def __init__(self, service: str | None = None, version: str | None = None) -> None:
76+
# https://cloud.google.com/functions/docs/configuring/env-var#runtime_environment_variables_set_automatically
77+
if service is None:
78+
service = os.environ.get("K_SERVICE", "unknown service")
79+
80+
if version is None:
81+
version = os.environ.get("K_REVISION", "unknown version")
82+
83+
self.service_context = {"service": service, "version": version}
84+
85+
def __call__(
86+
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
87+
) -> EventDict:
88+
"""Add a service context in which an error has occurred.
89+
90+
This is part of the Error Reporting API, so it's only added when an error happens.
91+
"""
92+
93+
event_type = event_dict[CLOUD_LOGGING_KEY].get("@type")
94+
if event_type != ERROR_EVENT_TYPE:
95+
return event_dict
96+
97+
# https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext
98+
event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = self.service_context
11299

113100
return event_dict

structlog_gcp/processors.py

Lines changed: 55 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,40 @@
99
from .types import CLOUD_LOGGING_KEY, SOURCE_LOCATION_KEY
1010

1111

12-
class CoreCloudLogging:
13-
"""Initialize the Google Cloud Logging event message"""
12+
def setup_log_severity() -> list[Processor]:
13+
return [structlog.processors.add_log_level, LogSeverity()]
14+
1415

15-
def setup(self) -> list[Processor]:
16-
return [
17-
# If some value is in bytes, decode it to a unicode str.
18-
structlog.processors.UnicodeDecoder(),
19-
# Add a timestamp in ISO 8601 format.
20-
structlog.processors.TimeStamper(fmt="iso"),
21-
self,
16+
def setup_code_location() -> list[Processor]:
17+
call_site_processors = structlog.processors.CallsiteParameterAdder(
18+
parameters=[
19+
structlog.processors.CallsiteParameter.PATHNAME,
20+
structlog.processors.CallsiteParameter.MODULE,
21+
structlog.processors.CallsiteParameter.FUNC_NAME,
22+
structlog.processors.CallsiteParameter.LINENO,
2223
]
24+
)
2325

24-
def __call__(
25-
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
26-
) -> EventDict:
27-
value = {
28-
"message": event_dict.pop("event"),
29-
"time": event_dict.pop("timestamp"),
30-
}
26+
return [call_site_processors, code_location]
3127

32-
event_dict[CLOUD_LOGGING_KEY] = value
33-
return event_dict
28+
29+
def init_cloud_logging(
30+
logger: WrappedLogger, method_name: str, event_dict: EventDict
31+
) -> EventDict:
32+
"""Initialize the Google Cloud Logging event message"""
33+
34+
value = {
35+
"message": event_dict.pop("event"),
36+
"time": event_dict.pop("timestamp"),
37+
}
38+
39+
event_dict[CLOUD_LOGGING_KEY] = value
40+
return event_dict
3441

3542

36-
class FormatAsCloudLogging:
43+
def finalize_cloud_logging(
44+
logger: WrappedLogger, method_name: str, event_dict: EventDict
45+
) -> EventDict:
3746
"""Finalize the Google Cloud Logging event message and replace the logging event.
3847
3948
This is not exactly the format the Cloud Logging directly ingests, but
@@ -43,29 +52,28 @@ class FormatAsCloudLogging:
4352
See: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
4453
"""
4554

46-
def setup(self) -> list[Processor]:
47-
return [self]
55+
# Take out the Google Cloud Logging set of fields from the event dict
56+
gcp_event: EventDict = event_dict.pop(CLOUD_LOGGING_KEY)
4857

49-
def __call__(
50-
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
51-
) -> EventDict:
52-
# Take out the Google Cloud Logging set of fields from the event dict
53-
gcp_event: EventDict = event_dict.pop(CLOUD_LOGGING_KEY)
58+
# Override whatever is left from the event dict with the content of all
59+
# the Google Cloud Logging-formatted fields.
60+
event_dict.update(gcp_event)
5461

55-
# Override whatever is left from the event dict with the content of all
56-
# the Google Cloud Logging-formatted fields.
57-
event_dict.update(gcp_event)
62+
# Fields which are not known by Google Cloud Logging will be added to
63+
# the `jsonPayload` field.
64+
#
65+
# See the `message` field documentation in:
66+
# https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
5867

59-
# Fields which are not known by Google Cloud Logging will be added to
60-
# the `jsonPayload` field.
61-
# See the `message` field documentation in:
62-
# https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
63-
64-
return event_dict
68+
return event_dict
6569

6670

6771
class LogSeverity:
68-
"""Set the severity using the Google Cloud Logging severities"""
72+
"""Set the severity using the Google Cloud Logging severities.
73+
74+
75+
See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
76+
"""
6977

7078
def __init__(self) -> None:
7179
self.default = "notset"
@@ -84,17 +92,10 @@ def __init__(self) -> None:
8492
# "emergency": "EMERGENCY", # One or more systems are unusable.
8593
}
8694

87-
def setup(self) -> list[Processor]:
88-
# Add log level to event dict.
89-
return [structlog.processors.add_log_level, self]
90-
9195
def __call__(
9296
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
9397
) -> EventDict:
94-
"""Format a Python log level value as a GCP log severity.
95-
96-
See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
97-
"""
98+
"""Format a Python log level value as a GCP log severity."""
9899

99100
log_level = event_dict.pop("level")
100101
severity = self.mapping.get(log_level, self.default)
@@ -103,30 +104,17 @@ def __call__(
103104
return event_dict
104105

105106

106-
class CodeLocation:
107+
def code_location(
108+
logger: WrappedLogger, method_name: str, event_dict: EventDict
109+
) -> EventDict:
107110
"""Inject the location of the logging message into the logs"""
108111

109-
def setup(self) -> list[Processor]:
110-
# Add callsite parameters.
111-
call_site_proc = structlog.processors.CallsiteParameterAdder(
112-
parameters=[
113-
structlog.processors.CallsiteParameter.PATHNAME,
114-
structlog.processors.CallsiteParameter.MODULE,
115-
structlog.processors.CallsiteParameter.FUNC_NAME,
116-
structlog.processors.CallsiteParameter.LINENO,
117-
]
118-
)
119-
return [call_site_proc, self]
112+
location = {
113+
"file": event_dict.pop("pathname"),
114+
"line": str(event_dict.pop("lineno")),
115+
"function": f"{event_dict.pop('module')}:{event_dict.pop('func_name')}",
116+
}
120117

121-
def __call__(
122-
self, logger: WrappedLogger, method_name: str, event_dict: EventDict
123-
) -> EventDict:
124-
location = {
125-
"file": event_dict.pop("pathname"),
126-
"line": str(event_dict.pop("lineno")),
127-
"function": f"{event_dict.pop('module')}:{event_dict.pop('func_name')}",
128-
}
129-
130-
event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location
118+
event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location
131119

132-
return event_dict
120+
return event_dict

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def mock_logger_env():
2222
):
2323
yield
2424

25+
2526
@pytest.fixture
2627
def logger(mock_logger_env):
2728
"""Setup a logger for testing and return it"""

tests/test_log.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_normal(stdout, logger):
2222
"severity": "INFO",
2323
"time": "2023-04-01T08:00:00.000000Z",
2424
}
25-
assert expected == msg
25+
assert msg == expected
2626

2727

2828
def test_error(stdout, logger):
@@ -57,7 +57,7 @@ def test_error(stdout, logger):
5757
"stack_trace": "oh noes\nTraceback blabla",
5858
"time": "2023-04-01T08:00:00.000000Z",
5959
}
60-
assert expected == msg
60+
assert msg == expected
6161

6262

6363
def test_service_context_default(stdout, logger):
@@ -144,4 +144,4 @@ def test_extra_labels(stdout, logger):
144144
"test4": {"foo": "bar"},
145145
"test5": {"date": "datetime.date(2023, 1, 1)"},
146146
}
147-
assert expected == msg
147+
assert msg == expected

0 commit comments

Comments
 (0)