Coverage for structlog_gcp/error_reporting.py: 100%

43 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-21 14:36 +0000

1import os 

2 

3import structlog.processors 

4from structlog.typing import EventDict, Processor, WrappedLogger 

5 

6from .types import CLOUD_LOGGING_KEY, ERROR_EVENT_TYPE, SOURCE_LOCATION_KEY 

7 

8 

9def setup_exceptions(log_level: str = "CRITICAL") -> list[Processor]: 

10 return [structlog.processors.format_exc_info, ReportException(log_level)] 

11 

12 

13class ReportException: 

14 """Transform exception into a Google Cloud Error Reporting event.""" 

15 

16 # https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report 

17 # https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-entry-examples 

18 

19 def __init__(self, log_level: str = "CRITICAL") -> None: 

20 self.log_level = log_level 

21 

22 def __call__( 

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

24 ) -> EventDict: 

25 exception = event_dict.pop("exception", None) 

26 if exception is None: 

27 return event_dict 

28 

29 event_dict[CLOUD_LOGGING_KEY]["@type"] = ERROR_EVENT_TYPE 

30 event_dict[CLOUD_LOGGING_KEY]["severity"] = self.log_level 

31 

32 # https://cloud.google.com/error-reporting/docs/formatting-error-messages 

33 message = event_dict[CLOUD_LOGGING_KEY]["message"] 

34 error_message = f"{message}\n{exception}" 

35 event_dict[CLOUD_LOGGING_KEY]["stack_trace"] = error_message 

36 

37 return event_dict 

38 

39 

40class ReportError: 

41 """Report to Google Cloud Error Reporting specific log severities 

42 

43 This class assumes the :ref:`.processors.CodeLocation` processor ran before. 

44 """ 

45 

46 # https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report 

47 # https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-entry-examples 

48 

49 def __init__(self, severities: list[str]) -> None: 

50 self.severities = severities 

51 

52 def __call__( 

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

54 ) -> EventDict: 

55 severity = event_dict[CLOUD_LOGGING_KEY]["severity"] 

56 

57 if severity not in self.severities: 

58 return event_dict 

59 

60 # https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorContext 

61 error_context = { 

62 "reportLocation": event_dict[CLOUD_LOGGING_KEY][SOURCE_LOCATION_KEY], 

63 } 

64 

65 event_dict[CLOUD_LOGGING_KEY]["@type"] = ERROR_EVENT_TYPE 

66 event_dict[CLOUD_LOGGING_KEY]["context"] = error_context 

67 

68 # "serviceContext" should be added by the ServiceContext processor. 

69 # event_dict[CLOUD_LOGGING_KEY]["serviceContext"] 

70 

71 return event_dict 

72 

73 

74class ServiceContext: 

75 def __init__(self, service: str | None = None, version: str | None = None) -> None: 

76 # https://cloud.google.com/functions/docs/configuring/env-var#runtime_environment_variables_set_automatically 

77 if service is None: 

78 service = os.environ.get("K_SERVICE", "unknown service") 

79 

80 if version is None: 

81 version = os.environ.get("K_REVISION", "unknown version") 

82 

83 self.service_context = {"service": service, "version": version} 

84 

85 def __call__( 

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

87 ) -> EventDict: 

88 """Add a service context in which an error has occurred. 

89 

90 This is part of the Error Reporting API, so it's only added when an error happens. 

91 """ 

92 

93 event_type = event_dict[CLOUD_LOGGING_KEY].get("@type") 

94 if event_type != ERROR_EVENT_TYPE: 

95 return event_dict 

96 

97 # https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext 

98 event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = self.service_context 

99 

100 return event_dict