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

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. 

14 

15import logging 

16from enum import Enum 

17from types import TracebackType 

18from typing import Any, Dict, Optional, Tuple, Type, Union 

19 

20from google.protobuf import text_format 

21from google.protobuf.message import Message 

22 

23from buildgrid._protos.build.bazel.remote.execution.v2.remote_execution_pb2 import Digest 

24 

25Exc = Union[ 

26 bool, 

27 Tuple[Type[BaseException], BaseException, Optional[TracebackType]], 

28 Tuple[None, None, None], 

29 BaseException, 

30] 

31 

32Tags = Dict[str, Any] 

33 

34 

35def _str_escape(s: str) -> str: 

36 return str(s).replace('"', r"\"") 

37 

38 

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)}"' 

54 

55 

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())]) 

60 

61 

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. 

69 

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. 

72 

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\""` 

80 

81 Encoding is only performed if logging is enabled for that level. 

82 """ 

83 self._logger = logger 

84 

85 def is_enabled_for(self, level: int) -> bool: 

86 return self._logger.isEnabledFor(level) 

87 

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) 

91 

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) 

95 

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) 

99 

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) 

103 

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) 

110 

111 

112def buildgrid_logger(name: str) -> BuildgridLogger: 

113 return BuildgridLogger(logging.getLogger(name))