Coverage for structlog_gcp/processors.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-18 13:10 +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 def setup(self) -> list[Processor]: 

40 return [self, structlog.processors.JSONRenderer()] 

41 

42 def __call__( 

43 self, logger: WrappedLogger, method_name: str, event_dict: EventDict 

44 ) -> EventDict: 

45 event: EventDict = event_dict.pop(CLOUD_LOGGING_KEY) 

46 

47 if event_dict: 

48 event["logging.googleapis.com/labels"] = event_dict 

49 

50 return event 

51 

52 

53class LogSeverity: 

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

55 

56 def __init__(self) -> None: 

57 self.default = "notset" 

58 

59 # From Python's logging level to Google level 

60 self.mapping = { 

61 "notset": "DEFAULT", # The log entry has no assigned severity level. 

62 "debug": "DEBUG", # Debug or trace information. 

63 "info": "INFO", # Routine information, such as ongoing status or performance. 

64 # "notice": "NOTICE", # Normal but significant events, such as start up, shut down, or a configuration change. 

65 "warn": "WARNING", # Warning events might cause problems. 

66 "warning": "WARNING", # Warning events might cause problems. 

67 "error": "ERROR", # Error events are likely to cause problems. 

68 "critical": "CRITICAL", # Critical events cause more severe problems or outages. 

69 # "alert": "ALERT", # A person must take an action immediately. 

70 # "emergency": "EMERGENCY", # One or more systems are unusable. 

71 } 

72 

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

74 # Add log level to event dict. 

75 return [structlog.processors.add_log_level, self] 

76 

77 def __call__( 

78 self, logger: WrappedLogger, method_name: str, event_dict: EventDict 

79 ) -> EventDict: 

80 """Format a Python log level value as a GCP log severity. 

81 

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

83 """ 

84 

85 log_level = event_dict.pop("level") 

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

87 

88 event_dict[CLOUD_LOGGING_KEY]["severity"] = severity 

89 return event_dict 

90 

91 

92class CodeLocation: 

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

94 

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

96 # Add callsite parameters. 

97 call_site_proc = structlog.processors.CallsiteParameterAdder( 

98 parameters=[ 

99 structlog.processors.CallsiteParameter.PATHNAME, 

100 structlog.processors.CallsiteParameter.MODULE, 

101 structlog.processors.CallsiteParameter.FUNC_NAME, 

102 structlog.processors.CallsiteParameter.LINENO, 

103 ] 

104 ) 

105 return [call_site_proc, self] 

106 

107 def __call__( 

108 self, logger: WrappedLogger, method_name: str, event_dict: EventDict 

109 ) -> EventDict: 

110 location = { 

111 "file": event_dict.pop("pathname"), 

112 "line": str(event_dict.pop("lineno")), 

113 "function": f"{event_dict.pop('module')}:{event_dict.pop('func_name')}", 

114 } 

115 

116 event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location 

117 

118 return event_dict