Coverage for /usr/local/lib/python3.12/site-packages/prefect/logging/formatters.py: 57%

51 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-12-05 13:38 +0000

1from __future__ import annotations 1a

2 

3import logging 1a

4import sys 1a

5import traceback 1a

6from types import TracebackType 1a

7from typing import Any, Literal, Optional, Tuple, Type, Union 1a

8 

9import orjson 1a

10 

11from prefect.serializers import JSONSerializer 1a

12 

13ExceptionInfoType = Union[ 1a

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

15 Tuple[None, None, None], 

16] 

17 

18 

19def format_exception_info(exc_info: ExceptionInfoType) -> dict[str, Any]: 1a

20 # if sys.exc_info() returned a (None, None, None) tuple, 

21 # then there's nothing to format 

22 if exc_info[0] is None: 

23 return {} 

24 

25 (exception_type, exception_obj, exception_traceback) = exc_info 

26 return { 

27 "type": exception_type.__name__, 

28 "message": str(exception_obj), 

29 "traceback": ( 

30 "".join(traceback.format_tb(exception_traceback)) 

31 if exception_traceback 

32 else None 

33 ), 

34 } 

35 

36 

37class JsonFormatter(logging.Formatter): 1a

38 """ 

39 Formats log records as a JSON string. 

40 

41 The format may be specified as "pretty" to format the JSON with indents and 

42 newlines. 

43 """ 

44 

45 def __init__( 1a

46 self, fmt: Literal["pretty", "default"], dmft: str, style: str 

47 ) -> None: # noqa 

48 super().__init__() 1a

49 

50 if fmt not in ["pretty", "default"]: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true1a

51 raise ValueError("Format must be either 'pretty' or 'default'.") 

52 

53 self.serializer: JSONSerializer = JSONSerializer( 1a

54 jsonlib="orjson", 

55 dumps_kwargs={"option": orjson.OPT_INDENT_2} if fmt == "pretty" else {}, 

56 ) 

57 

58 def format(self, record: logging.LogRecord) -> str: 1a

59 record_dict = record.__dict__.copy() 

60 

61 # GCP severity detection compatibility 

62 record_dict.setdefault("severity", record.levelname) 

63 

64 # replace any exception tuples returned by `sys.exc_info()` 

65 # with a JSON-serializable `dict`. 

66 if record.exc_info: 

67 record_dict["exc_info"] = format_exception_info(record.exc_info) 

68 

69 log_json_bytes = self.serializer.dumps(record_dict) 

70 

71 # JSONSerializer returns bytes; decode to string to conform to 

72 # the `logging.Formatter.format` interface 

73 return log_json_bytes.decode() 

74 

75 

76class PrefectFormatter(logging.Formatter): 1a

77 def __init__( 1a

78 self, 

79 format: str | None = None, 

80 datefmt: str | None = None, 

81 style: Literal["%", "{", "$"] = "%", 

82 validate: bool = True, 

83 *, 

84 defaults: dict[str, Any] | None = None, 

85 task_run_fmt: str | None = None, 

86 flow_run_fmt: Optional[str] = None, 

87 ) -> None: 

88 """ 

89 Implementation of the standard Python formatter with support for multiple 

90 message formats. 

91 

92 """ 

93 # See https://github.com/python/cpython/blob/c8c6113398ee9a7867fe9b08bc539cceb61e2aaa/Lib/logging/__init__.py#L546 

94 # for implementation details 

95 

96 init_kwargs: dict[str, Any] = {} 1a

97 style_kwargs: dict[str, Any] = {} 1a

98 

99 # defaults added in 3.10 

100 if sys.version_info >= (3, 10): 100 ↛ 104line 100 didn't jump to line 104 because the condition on line 100 was always true1a

101 init_kwargs["defaults"] = defaults 1a

102 style_kwargs["defaults"] = defaults 1a

103 

104 init_kwargs["validate"] = validate 1a

105 

106 super().__init__(format, datefmt, style, **init_kwargs) 1a

107 

108 self.flow_run_fmt = flow_run_fmt 1a

109 self.task_run_fmt = task_run_fmt 1a

110 

111 # Retrieve the style class from the base class to avoid importing private 

112 # `_STYLES` mapping 

113 style_class = type(self._style) 1a

114 

115 self._flow_run_style = ( 1a

116 style_class(flow_run_fmt, **style_kwargs) if flow_run_fmt else self._style 

117 ) 

118 self._task_run_style = ( 1a

119 style_class(task_run_fmt, **style_kwargs) if task_run_fmt else self._style 

120 ) 

121 if validate: 121 ↛ exitline 121 didn't return from function '__init__' because the condition on line 121 was always true1a

122 self._flow_run_style.validate() 1a

123 self._task_run_style.validate() 1a

124 

125 def formatMessage(self, record: logging.LogRecord) -> str: 1a

126 if record.name == "prefect.flow_runs": 

127 style = self._flow_run_style 

128 elif record.name == "prefect.task_runs": 

129 style = self._task_run_style 

130 else: 

131 style = self._style 

132 

133 return style.format(record)