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
« 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.
15"""Logging handler for printing formatted structured logs to standard output.
16"""
17import collections
18import json
19import logging
20import logging.handlers
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
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)
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)
60class StructuredLogHandler(logging.StreamHandler):
61 """Handler to format logs into the Cloud Logging structured log format,
62 and write them to standard output
63 """
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
78 # add extra keys to log record
79 log_filter = CloudLoggingFilter(project=project_id, default_labels=labels)
80 self.addFilter(log_filter)
82 # make logs appear in GCP structured logging format
83 self._gcp_formatter = logging.Formatter(GCP_FORMAT)
85 self._json_encoder_cls = json_encoder_cls or json.JSONEncoder
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))
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)
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
126 def emit(self, record):
127 if google.cloud.logging_v2._instrumentation_emitted is False:
128 self.emit_instrumentation_info()
129 super().emit(record)
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()