Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/logging.py: 100.00%
82 statements
« prev ^ index » next coverage.py v7.4.1, created at 2025-04-14 16:27 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2025-04-14 16:27 +0000
1# Copyright (C) 2024 Bloomberg LP
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.
15import json
16import logging
17from enum import Enum
18from types import TracebackType
19from typing import Any, Union
21from google.protobuf import text_format
22from google.protobuf.message import Message
24from buildgrid._protos.build.bazel.remote.execution.v2.remote_execution_pb2 import Digest
25from buildgrid.server.settings import LOG_RECORD_FORMAT
26from buildgrid.server.version import __version__
28Exc = Union[
29 bool,
30 tuple[type[BaseException], BaseException, TracebackType | None],
31 tuple[None, None, None],
32 BaseException,
33]
35Tags = dict[str, Any]
38def _str_escape(s: str) -> str:
39 return str(s).replace('"', r"\"")
42def _format_log_tag_value(value: Any) -> Any:
43 if value is None:
44 return '""'
45 elif isinstance(value, int):
46 return value
47 elif isinstance(value, float):
48 return f"{value:.2f}"
49 elif isinstance(value, Digest):
50 return f'"{value.hash}/{value.size_bytes}"'
51 elif isinstance(value, Message):
52 return f'"{_str_escape(text_format.MessageToString(value, as_one_line=True))}"'
53 elif isinstance(value, Enum):
54 return value.name
55 else:
56 return f'"{_str_escape(value)}"'
59def _json_format_log_tag_value(value: Any) -> Any:
60 if value is None:
61 return ""
62 elif isinstance(value, int):
63 return value
64 elif isinstance(value, float):
65 return f"{value:.2f}"
66 elif isinstance(value, Digest):
67 return f"{value.hash}/{value.size_bytes}"
68 elif isinstance(value, Message):
69 return f"{text_format.MessageToString(value, as_one_line=True)}"
70 elif isinstance(value, Enum):
71 return value.name
72 else:
73 return f"{value}"
76def _format_log_tags(tags: Tags | None) -> str:
77 if not tags:
78 return ""
79 return "".join([f" {key}={_format_log_tag_value(value)}" for key, value in sorted(tags.items())])
82def _format_message(record: logging.LogRecord) -> str:
83 # LOG_RECORD_FORMAT should still be used for the message field
84 msg_formatter = logging.Formatter(LOG_RECORD_FORMAT)
85 return msg_formatter.format(record)
88class JSONFormatter(logging.Formatter):
89 def format(self, record: logging.LogRecord) -> str:
90 log_record = {
91 "date": self.formatTime(record),
92 "message": _format_message(record),
93 "log_level": record.levelname,
94 "logger_name": record.name,
95 "request_id": getattr(record, "request_id", ""),
96 "bgd_version": __version__,
97 }
99 if record_tags := getattr(record, "tags", {}):
100 tags = {}
101 for tag, tag_value in record_tags.items():
102 if tag in log_record.keys():
103 tag = "tag_" + tag
104 tags[tag] = _json_format_log_tag_value(tag_value)
105 log_record.update(tags)
107 return json.dumps(log_record)
110class BuildgridLogger:
111 def __init__(self, logger: logging.Logger) -> None:
112 """
113 The buildgrid logger is a helper utility wrapped around a standard logger instance.
114 It allows placing key=value strings at the end of log lines, reducing boilerplate in
115 displaying values and adding standardization to our log lines. Within each logging method,
116 tags may be added by setting the "tags" argument.
118 Each logger is set to log at stacklevel=2 such that function names and source line numbers
119 show the line at which this utility is invoked.
121 Special encoding rules for tag values:
122 - int: reported as is. `value=1`
123 - float: rounded to the nearest two decimals. `value=1.23`
124 - Digest: unpacked as hash/size. `value=deadbeef/123`
125 - proto.Message: text_format, escaped, and quoted. `value="blob_digests { hash: \"deadbeef\" }"`
126 - Enum: attribute name is used. `value=OK`
127 - others: converted to str, escaped, and quoted. `value="foo: \"bar\""`
129 Encoding is only performed if logging is enabled for that level.
130 """
131 self._logger = logger
133 def is_enabled_for(self, level: int) -> bool:
134 return self._logger.isEnabledFor(level)
136 def debug(self, msg: Any, *, exc_info: Exc | None = None, tags: Tags | None = None) -> None:
137 if self._logger.isEnabledFor(logging.DEBUG):
138 self._logger.debug(
139 str(msg) + _format_log_tags(tags), extra={"tags": tags or {}}, exc_info=exc_info, stacklevel=2
140 )
142 def info(self, msg: Any, *, exc_info: Exc | None = None, tags: Tags | None = None) -> None:
143 if self._logger.isEnabledFor(logging.INFO):
144 self._logger.info(
145 str(msg) + _format_log_tags(tags), extra={"tags": tags or {}}, exc_info=exc_info, stacklevel=2
146 )
148 def warning(self, msg: Any, *, exc_info: Exc | None = None, tags: Tags | None = None) -> None:
149 if self._logger.isEnabledFor(logging.WARNING):
150 self._logger.warning(
151 str(msg) + _format_log_tags(tags), extra={"tags": tags or {}}, exc_info=exc_info, stacklevel=2
152 )
154 def error(self, msg: Any, *, exc_info: Exc | None = None, tags: Tags | None = None) -> None:
155 if self._logger.isEnabledFor(logging.ERROR):
156 self._logger.error(
157 str(msg) + _format_log_tags(tags), extra={"tags": tags or {}}, exc_info=exc_info, stacklevel=2
158 )
160 def exception(self, msg: Any, *, exc_info: Exc | None = True, tags: Tags | None = None) -> None:
161 if self._logger.isEnabledFor(logging.ERROR):
162 # Note we call error here instead of exception.
163 # logger.exception is a helper around calling error with exc_info defaulting to True.
164 # On python<3.11 that helper causes the stacklevel to report incorrectly.
165 self._logger.error(
166 str(msg) + _format_log_tags(tags), extra={"tags": tags or {}}, exc_info=exc_info, stacklevel=2
167 )
170def buildgrid_logger(name: str) -> BuildgridLogger:
171 return BuildgridLogger(logging.getLogger(name))