Coverage for structlog_gcp/processors.py: 100%
36 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-09-03 20:33 +0000
« prev ^ index » next coverage.py v7.3.0, created at 2023-09-03 20:33 +0000
1# https://cloud.google.com/functions/docs/monitoring/logging#writing_structured_logs
2# https://cloud.google.com/logging/docs/agent/logging/configuration#process-payload
3# https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
6import structlog.processors
7from structlog.typing import EventDict, Processor, WrappedLogger
9from .types import CLOUD_LOGGING_KEY, SOURCE_LOCATION_KEY
12class CoreCloudLogging:
13 """Initialize the Google Cloud Logging event message"""
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,
22 ]
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 }
32 event_dict[CLOUD_LOGGING_KEY] = value
33 return event_dict
36class FormatAsCloudLogging:
37 """Finalize the Google Cloud Logging event message and replace the logging event.
39 This is not exactly the format the Cloud Logging directly ingests, but
40 Cloud Logging is smart enough to transform basic JSON-like logging events
41 into Cloud Logging-compatible events.
43 See: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
44 """
46 def setup(self) -> list[Processor]:
47 return [self, structlog.processors.JSONRenderer()]
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)
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)
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
64 return event_dict
67class LogSeverity:
68 """Set the severity using the Google Cloud Logging severities"""
70 def __init__(self) -> None:
71 self.default = "notset"
73 # From Python's logging level to Google level
74 self.mapping = {
75 "notset": "DEFAULT", # The log entry has no assigned severity level.
76 "debug": "DEBUG", # Debug or trace information.
77 "info": "INFO", # Routine information, such as ongoing status or performance.
78 # "notice": "NOTICE", # Normal but significant events, such as start up, shut down, or a configuration change.
79 "warn": "WARNING", # Warning events might cause problems.
80 "warning": "WARNING", # Warning events might cause problems.
81 "error": "ERROR", # Error events are likely to cause problems.
82 "critical": "CRITICAL", # Critical events cause more severe problems or outages.
83 # "alert": "ALERT", # A person must take an action immediately.
84 # "emergency": "EMERGENCY", # One or more systems are unusable.
85 }
87 def setup(self) -> list[Processor]:
88 # Add log level to event dict.
89 return [structlog.processors.add_log_level, self]
91 def __call__(
92 self, logger: WrappedLogger, method_name: str, event_dict: EventDict
93 ) -> EventDict:
94 """Format a Python log level value as a GCP log severity.
96 See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
97 """
99 log_level = event_dict.pop("level")
100 severity = self.mapping.get(log_level, self.default)
102 event_dict[CLOUD_LOGGING_KEY]["severity"] = severity
103 return event_dict
106class CodeLocation:
107 """Inject the location of the logging message into the logs"""
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]
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 }
130 event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location
132 return event_dict