Coverage for structlog_gcp/processors.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-25 10:19 +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 

4 

5 

6import structlog.processors 

7from structlog.typing import EventDict, Processor, WrappedLogger 

8 

9from .types import CLOUD_LOGGING_KEY, SOURCE_LOCATION_KEY 

10 

11 

12class CoreCloudLogging: 

13 """Initialize the Google Cloud Logging event message""" 

14 

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 ] 

23 

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 } 

31 

32 event_dict[CLOUD_LOGGING_KEY] = value 

33 return event_dict 

34 

35 

36class FormatAsCloudLogging: 

37 """Finalize the Google Cloud Logging event message and replace the logging event. 

38 

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. 

42 

43 See: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields 

44 """ 

45 

46 def setup(self) -> list[Processor]: 

47 return [self] 

48 

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) 

54 

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) 

58 

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 

65 

66 

67class LogSeverity: 

68 """Set the severity using the Google Cloud Logging severities""" 

69 

70 def __init__(self) -> None: 

71 self.default = "notset" 

72 

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 } 

86 

87 def setup(self) -> list[Processor]: 

88 # Add log level to event dict. 

89 return [structlog.processors.add_log_level, self] 

90 

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. 

95 

96 See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity 

97 """ 

98 

99 log_level = event_dict.pop("level") 

100 severity = self.mapping.get(log_level, self.default) 

101 

102 event_dict[CLOUD_LOGGING_KEY]["severity"] = severity 

103 return event_dict 

104 

105 

106class CodeLocation: 

107 """Inject the location of the logging message into the logs""" 

108 

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] 

120 

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 

131 

132 return event_dict