Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/logging.py: 100.00%
51 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +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 logging
16from enum import Enum
17from types import TracebackType
18from typing import Any, Dict, Optional, Tuple, Type, Union
20from google.protobuf import text_format
21from google.protobuf.message import Message
23from buildgrid._protos.build.bazel.remote.execution.v2.remote_execution_pb2 import Digest
25Exc = Union[
26 bool,
27 Tuple[Type[BaseException], BaseException, Optional[TracebackType]],
28 Tuple[None, None, None],
29 BaseException,
30]
32Tags = Dict[str, Any]
35def _str_escape(s: str) -> str:
36 return str(s).replace('"', r"\"")
39def _format_log_tag_value(value: Any) -> Any:
40 if value is None:
41 return '""'
42 elif isinstance(value, int):
43 return value
44 elif isinstance(value, float):
45 return f"{value:.2f}"
46 elif isinstance(value, Digest):
47 return f'"{value.hash}/{value.size_bytes}"'
48 elif isinstance(value, Message):
49 return f'"{_str_escape(text_format.MessageToString(value, as_one_line=True))}"'
50 elif isinstance(value, Enum):
51 return value.name
52 else:
53 return f'"{_str_escape(value)}"'
56def _format_log_tags(tags: Optional[Tags]) -> str:
57 if not tags:
58 return ""
59 return "".join([f" {key}={_format_log_tag_value(value)}" for key, value in sorted(tags.items())])
62class BuildgridLogger:
63 def __init__(self, logger: logging.Logger) -> None:
64 """
65 The buildgrid logger is a helper utility wrapped around a standard logger instance.
66 It allows placing key=value strings at the end of log lines, reducing boilerplate in
67 displaying values and adding standardization to our log lines. Within each logging method,
68 tags may be added by setting the "tags" argument.
70 Each logger is set to log at stacklevel=2 such that function names and source line numbers
71 show the line at which this utility is invoked.
73 Special encoding rules for tag values:
74 - int: reported as is. `value=1`
75 - float: rounded to the nearest two decimals. `value=1.23`
76 - Digest: unpacked as hash/size. `value=deadbeef/123`
77 - proto.Message: text_format, escaped, and quoted. `value="blob_digests { hash: \"deadbeef\" }"`
78 - Enum: attribute name is used. `value=OK`
79 - others: converted to str, escaped, and quoted. `value="foo: \"bar\""`
81 Encoding is only performed if logging is enabled for that level.
82 """
83 self._logger = logger
85 def is_enabled_for(self, level: int) -> bool:
86 return self._logger.isEnabledFor(level)
88 def debug(self, msg: Any, *, exc_info: Optional[Exc] = None, tags: Optional[Tags] = None) -> None:
89 if self._logger.isEnabledFor(logging.DEBUG):
90 self._logger.debug(str(msg) + _format_log_tags(tags), exc_info=exc_info, stacklevel=2)
92 def info(self, msg: Any, *, exc_info: Optional[Exc] = None, tags: Optional[Tags] = None) -> None:
93 if self._logger.isEnabledFor(logging.INFO):
94 self._logger.info(str(msg) + _format_log_tags(tags), exc_info=exc_info, stacklevel=2)
96 def warning(self, msg: Any, *, exc_info: Optional[Exc] = None, tags: Optional[Tags] = None) -> None:
97 if self._logger.isEnabledFor(logging.WARNING):
98 self._logger.warning(str(msg) + _format_log_tags(tags), exc_info=exc_info, stacklevel=2)
100 def error(self, msg: Any, *, exc_info: Optional[Exc] = None, tags: Optional[Tags] = None) -> None:
101 if self._logger.isEnabledFor(logging.ERROR):
102 self._logger.error(str(msg) + _format_log_tags(tags), exc_info=exc_info, stacklevel=2)
104 def exception(self, msg: Any, *, exc_info: Optional[Exc] = True, tags: Optional[Tags] = None) -> None:
105 if self._logger.isEnabledFor(logging.ERROR):
106 # Note we call error here instead of exception.
107 # logger.exception is a helper around calling error with exc_info defaulting to True.
108 # On python<3.11 that helper causes the stacklevel to report incorrectly.
109 self._logger.error(str(msg) + _format_log_tags(tags), exc_info=exc_info, stacklevel=2)
112def buildgrid_logger(name: str) -> BuildgridLogger:
113 return BuildgridLogger(logging.getLogger(name))