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

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 json 

7from typing import Any 

8 

9import structlog.processors 

10from structlog.typing import EventDict, Processor, WrappedLogger 

11 

12from .types import CLOUD_LOGGING_KEY, SOURCE_LOCATION_KEY 

13 

14 

15class CoreCloudLogging: 

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

17 

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 ] 

26 

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 } 

34 

35 event_dict[CLOUD_LOGGING_KEY] = value 

36 return event_dict 

37 

38 

39class FormatAsCloudLogging: 

40 """Finalize the Google Cloud Logging event message and replace the logging event""" 

41 

42 def __init__(self) -> None: 

43 self.renderer = structlog.processors.JSONRenderer() 

44 self.label = "logging.googleapis.com/labels" 

45 

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

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

48 

49 def _serialize(self, value: Any) -> str: 

50 if isinstance(value, str): 

51 return value 

52 return json.dumps(value, default=repr) 

53 

54 def __call__( 

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

56 ) -> EventDict: 

57 event: EventDict = event_dict.pop(CLOUD_LOGGING_KEY) 

58 

59 if event_dict: 

60 event[self.label] = {} 

61 

62 for key, item in event_dict.items(): 

63 value = self._serialize(item) 

64 event[self.label][key] = value 

65 

66 return event 

67 

68 

69class LogSeverity: 

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

71 

72 def __init__(self) -> None: 

73 self.default = "notset" 

74 

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 } 

88 

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

90 # Add log level to event dict. 

91 return [structlog.processors.add_log_level, self] 

92 

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. 

97 

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

99 """ 

100 

101 log_level = event_dict.pop("level") 

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

103 

104 event_dict[CLOUD_LOGGING_KEY]["severity"] = severity 

105 return event_dict 

106 

107 

108class CodeLocation: 

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

110 

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] 

122 

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 } 

131 

132 event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY] = location 

133 

134 return event_dict