Coverage for structlog_gcp/processors.py: 100%
49 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-25 14:01 +0000
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-25 14:01 +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 json
7from typing import Any
9import structlog.processors
10from structlog.typing import EventDict, Processor, WrappedLogger
12from .types import CLOUD_LOGGING_KEY, SOURCE_LOCATION_KEY
15class CoreCloudLogging:
16 """Initialize the Google Cloud Logging event message"""
18 def setup(self) -> list[Processor]:
19 return [
20 # If some value is in bytes, decode it to a unicode str.
21 structlog.processors.UnicodeDecoder(),
22 # Add a timestamp in ISO 8601 format.
23 structlog.processors.TimeStamper(fmt="iso"),
24 self,
25 ]
27 def __call__(
28 self, logger: WrappedLogger, method_name: str, event_dict: EventDict
29 ) -> EventDict:
30 value = {
31 "message": event_dict.pop("event"),
32 "time": event_dict.pop("timestamp"),
33 }
35 event_dict[CLOUD_LOGGING_KEY] = value
36 return event_dict
39class FormatAsCloudLogging:
40 """Finalize the Google Cloud Logging event message and replace the logging event"""
42 def __init__(self) -> None:
43 self.renderer = structlog.processors.JSONRenderer()
44 self.label = "logging.googleapis.com/labels"
46 def setup(self) -> list[Processor]:
47 return [self, structlog.processors.JSONRenderer()]
49 def _serialize(self, value: Any) -> str:
50 if isinstance(value, str):
51 return value
52 return json.dumps(value, default=repr)
54 def __call__(
55 self, logger: WrappedLogger, method_name: str, event_dict: EventDict
56 ) -> EventDict:
57 event: EventDict = event_dict.pop(CLOUD_LOGGING_KEY)
59 if event_dict:
60 event[self.label] = {}
62 for key, item in event_dict.items():
63 value = self._serialize(item)
64 event[self.label][key] = value
66 return event
69class LogSeverity:
70 """Set the severity using the Google Cloud Logging severities"""
72 def __init__(self) -> None:
73 self.default = "notset"
75 # From Python's logging level to Google level
76 self.mapping = {
77 "notset": "DEFAULT", # The log entry has no assigned severity level.
78 "debug": "DEBUG", # Debug or trace information.
79 "info": "INFO", # Routine information, such as ongoing status or performance.
80 # "notice": "NOTICE", # Normal but significant events, such as start up, shut down, or a configuration change.
81 "warn": "WARNING", # Warning events might cause problems.
82 "warning": "WARNING", # Warning events might cause problems.
83 "error": "ERROR", # Error events are likely to cause problems.
84 "critical": "CRITICAL", # Critical events cause more severe problems or outages.
85 # "alert": "ALERT", # A person must take an action immediately.
86 # "emergency": "EMERGENCY", # One or more systems are unusable.
87 }
89 def setup(self) -> list[Processor]:
90 # Add log level to event dict.
91 return [structlog.processors.add_log_level, self]
93 def __call__(
94 self, logger: WrappedLogger, method_name: str, event_dict: EventDict
95 ) -> EventDict:
96 """Format a Python log level value as a GCP log severity.
98 See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
99 """
101 log_level = event_dict.pop("level")
102 severity = self.mapping.get(log_level, self.default)
104 event_dict[CLOUD_LOGGING_KEY]["severity"] = severity
105 return event_dict
108class CodeLocation:
109 """Inject the location of the logging message into the logs"""
111 def setup(self) -> list[Processor]:
112 # Add callsite parameters.
113 call_site_proc = structlog.processors.CallsiteParameterAdder(
114 parameters=[
115 structlog.processors.CallsiteParameter.PATHNAME,
116 structlog.processors.CallsiteParameter.MODULE,
117 structlog.processors.CallsiteParameter.FUNC_NAME,
118 structlog.processors.CallsiteParameter.LINENO,
119 ]
120 )
121 return [call_site_proc, self]
123 def __call__(
124 self, logger: WrappedLogger, method_name: str, event_dict: EventDict
125 ) -> EventDict:
126 location = {
127 "file": event_dict.pop("pathname"),
128 "line": str(event_dict.pop("lineno")),
129 "function": f"{event_dict.pop('module')}:{event_dict.pop('func_name')}",
130 }
132 event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location
134 return event_dict