Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/logging_v2/handlers/structured_log.py: 33%

49 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 07:30 +0000

1# Copyright 2021 Google LLC All Rights Reserved. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Logging handler for printing formatted structured logs to standard output. 

16""" 

17import collections 

18import json 

19import logging 

20import logging.handlers 

21 

22from google.cloud.logging_v2.handlers.handlers import CloudLoggingFilter 

23from google.cloud.logging_v2.handlers.handlers import _format_and_parse_message 

24import google.cloud.logging_v2 

25from google.cloud.logging_v2._instrumentation import _create_diagnostic_entry 

26 

27GCP_FORMAT = ( 

28 "{%(_payload_str)s" 

29 '"severity": "%(levelname)s", ' 

30 '"logging.googleapis.com/labels": %(_labels_str)s, ' 

31 '"logging.googleapis.com/trace": "%(_trace_str)s", ' 

32 '"logging.googleapis.com/spanId": "%(_span_id_str)s", ' 

33 '"logging.googleapis.com/trace_sampled": %(_trace_sampled_str)s, ' 

34 '"logging.googleapis.com/sourceLocation": %(_source_location_str)s, ' 

35 '"httpRequest": %(_http_request_str)s ' 

36 "}" 

37) 

38 

39# reserved fields taken from Structured Logging documentation: 

40# https://cloud.google.com/logging/docs/structured-logging 

41GCP_STRUCTURED_LOGGING_FIELDS = frozenset( 

42 { 

43 "severity", 

44 "httpRequest", 

45 "time", 

46 "timestamp", 

47 "timestampSeconds", 

48 "timestampNanos", 

49 "logging.googleapis.com/insertId", 

50 "logging.googleapis.com/labels", 

51 "logging.googleapis.com/operation", 

52 "logging.googleapis.com/sourceLocation", 

53 "logging.googleapis.com/spanId", 

54 "logging.googleapis.com/trace", 

55 "logging.googleapis.com/trace_sampled", 

56 } 

57) 

58 

59 

60class StructuredLogHandler(logging.StreamHandler): 

61 """Handler to format logs into the Cloud Logging structured log format, 

62 and write them to standard output 

63 """ 

64 

65 def __init__( 

66 self, *, labels=None, stream=None, project_id=None, json_encoder_cls=None 

67 ): 

68 """ 

69 Args: 

70 labels (Optional[dict]): Additional labels to attach to logs. 

71 stream (Optional[IO]): Stream to be used by the handler. 

72 project (Optional[str]): Project Id associated with the logs. 

73 json_encoder_cls (Optional[Type[JSONEncoder]]): Custom JSON encoder. Defaults to json.JSONEncoder 

74 """ 

75 super(StructuredLogHandler, self).__init__(stream=stream) 

76 self.project_id = project_id 

77 

78 # add extra keys to log record 

79 log_filter = CloudLoggingFilter(project=project_id, default_labels=labels) 

80 self.addFilter(log_filter) 

81 

82 # make logs appear in GCP structured logging format 

83 self._gcp_formatter = logging.Formatter(GCP_FORMAT) 

84 

85 self._json_encoder_cls = json_encoder_cls or json.JSONEncoder 

86 

87 def format(self, record): 

88 """Format the message into structured log JSON. 

89 Args: 

90 record (logging.LogRecord): The log record. 

91 Returns: 

92 str: A JSON string formatted for GCP structured logging. 

93 """ 

94 payload = None 

95 message = _format_and_parse_message(record, super(StructuredLogHandler, self)) 

96 

97 if isinstance(message, collections.abc.Mapping): 

98 # remove any special fields 

99 for key in list(message.keys()): 

100 if key in GCP_STRUCTURED_LOGGING_FIELDS: 

101 del message[key] 

102 # if input is a dictionary, encode it as a json string 

103 encoded_msg = json.dumps( 

104 message, ensure_ascii=False, cls=self._json_encoder_cls 

105 ) 

106 # all json.dumps strings should start and end with parentheses 

107 # strip them out to embed these fields in the larger JSON payload 

108 if len(encoded_msg) > 2: 

109 payload = encoded_msg[1:-1] + "," 

110 elif message: 

111 # properly break any formatting in string to make it json safe 

112 encoded_message = json.dumps( 

113 message, ensure_ascii=False, cls=self._json_encoder_cls 

114 ) 

115 payload = '"message": {},'.format(encoded_message) 

116 

117 record._payload_str = payload or "" 

118 # remove exception info to avoid duplicating it 

119 # https://github.com/googleapis/python-logging/issues/382 

120 record.exc_info = None 

121 record.exc_text = None 

122 # convert to GCP structred logging format 

123 gcp_payload = self._gcp_formatter.format(record) 

124 return gcp_payload 

125 

126 def emit(self, record): 

127 if google.cloud.logging_v2._instrumentation_emitted is False: 

128 self.emit_instrumentation_info() 

129 super().emit(record) 

130 

131 def emit_instrumentation_info(self): 

132 google.cloud.logging_v2._instrumentation_emitted = True 

133 diagnostic_object = _create_diagnostic_entry() 

134 struct_logger = logging.getLogger(__name__) 

135 struct_logger.addHandler(self) 

136 struct_logger.setLevel(logging.INFO) 

137 struct_logger.info(diagnostic_object.payload) 

138 struct_logger.handlers.clear()