Coverage for structlog_gcp/errors.py: 100%

43 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-13 18:00 +0000

1import os 

2 

3import structlog 

4from structlog.typing import EventDict, Processor, WrappedLogger 

5 

6from .types import CLOUD_LOGGING_KEY, ERROR_EVENT_TYPE, SOURCE_LOCATION_KEY 

7 

8 

9def add_service_context( 

10 logger: WrappedLogger, method_name: str, event_dict: EventDict 

11) -> EventDict: 

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

13 

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

15 """ 

16 

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

18 if event_type != ERROR_EVENT_TYPE: 

19 return event_dict 

20 

21 service_context = { 

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

23 "service": os.environ.get("K_SERVICE", "unknown service"), 

24 "version": os.environ.get("K_REVISION", "unknown version"), 

25 } 

26 

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

28 event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = service_context 

29 

30 return event_dict 

31 

32 

33class ReportException: 

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

35 

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

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

38 

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

40 self.log_level = log_level 

41 

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

43 return [structlog.processors.format_exc_info, self] 

44 

45 def __call__( 

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

47 ) -> EventDict: 

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

49 if exception is None: 

50 return event_dict 

51 

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

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

54 

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

56 message = event_dict[CLOUD_LOGGING_KEY]["message"] 

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

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

59 

60 return event_dict 

61 

62 

63class ReportError: 

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

65 

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

67 """ 

68 

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

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

71 

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

73 self.severities = severities 

74 

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

76 return [self] 

77 

78 def _build_service_context(self) -> dict[str, str]: 

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

80 service_context = { 

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

82 "service": os.environ.get("K_SERVICE", "unknown service"), 

83 "version": os.environ.get("K_REVISION", "unknown version"), 

84 } 

85 

86 return service_context 

87 

88 def __call__( 

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

90 ) -> EventDict: 

91 severity = event_dict[CLOUD_LOGGING_KEY]["severity"] 

92 

93 if severity not in self.severities: 

94 return event_dict 

95 

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

97 error_context = { 

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

99 } 

100 

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

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

103 event_dict[CLOUD_LOGGING_KEY]["serviceContext"] = self._build_service_context() 

104 

105 return event_dict