Coverage for stlog/output.py: 86%
90 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-21 07:31 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-21 07:31 +0000
1from __future__ import annotations
3import logging
4import logging.handlers
5import os
6import sys
7import typing
8from dataclasses import dataclass, field
10from stlog.base import (
11 GLOBAL_LOGGING_CONFIG,
12 StlogError,
13 check_env_false,
14)
15from stlog.filter import ContextReinjectFilter
16from stlog.formatter import (
17 Formatter,
18 HumanFormatter,
19 RichHumanFormatter,
20)
21from stlog.handler import CustomRichHandler
23RICH_INSTALLED: bool = False
24try:
25 from rich.console import Console
27 RICH_INSTALLED = True
28except ImportError:
29 pass
32def _get_default_use_rich() -> bool | None:
33 tmp = os.environ.get("STLOG_USE_RICH")
34 if tmp is None:
35 return None
36 if tmp.strip().upper() in ("NONE", "AUTO", ""):
37 return None
38 return tmp.strip().upper() in ("1", "TRUE", "YES")
41DEFAULT_USE_RICH = _get_default_use_rich()
44@dataclass
45class Output:
46 """Abstract output base class.
48 Attributes:
49 formatter: the Python logging Formatter to use.
50 level: python logging level specific to this output
51 (None means "use the global logging level").
52 filters: list of logging Filters (or simple callables) to filter some LogRecord
53 for this specific output.
54 reinject_context_in_standard_logging: if True, reinject the LogContext
55 in log record emitted with python standard loggers
56 (note: override `stlog.setup` default value for this output).
57 read_extra_kwargs_from_standard_logging: if try to reinject the extra kwargs from standard logging
58 (note: override `stlog.setup` default value for this output).
60 """
62 _handler: logging.Handler = field(init=False, default_factory=logging.NullHandler)
63 formatter: logging.Formatter | None = None
64 level: int | str | None = None
65 filters: typing.Iterable[
66 typing.Callable[[logging.LogRecord], bool] | logging.Filter
67 ] = field(default_factory=list)
68 reinject_context_in_standard_logging: bool | None = None
69 read_extra_kwargs_from_standard_logging: bool | None = None
71 @property
72 def _reinject_context_in_standard_logging(self) -> bool:
73 # lazy evaluation because Output are built before setup() call
74 if self.reinject_context_in_standard_logging is not None:
75 return self.reinject_context_in_standard_logging
76 if GLOBAL_LOGGING_CONFIG.reinject_context_in_standard_logging is not None:
77 return GLOBAL_LOGGING_CONFIG.reinject_context_in_standard_logging
78 return check_env_false("STLOG_REINJECT_CONTEXT_IN_STANDARD_LOGGING", True)
80 @property
81 def _read_extra_kwargs_from_standard_logging(self) -> bool:
82 # lazy evaluation because Output are built before setup() call
83 if self.read_extra_kwargs_from_standard_logging is not None:
84 return self.read_extra_kwargs_from_standard_logging
85 if GLOBAL_LOGGING_CONFIG.read_extra_kwargs_from_standard_logging is not None:
86 return GLOBAL_LOGGING_CONFIG.read_extra_kwargs_from_standard_logging
87 return check_env_false("STLOG_READ_EXTRA_KWARGS_FROM_STANDARD_LOGGING", True)
89 def set_handler(
90 self,
91 handler: logging.Handler,
92 ):
93 """Configure the Python logging Handler to use."""
94 self._handler = handler
95 self._handler.setFormatter(self.get_formatter_or_raise())
96 if self.level is not None:
97 self._handler.setLevel(self.level)
98 if self._reinject_context_in_standard_logging:
99 self._handler.addFilter(
100 ContextReinjectFilter(
101 read_extra_kwargs_from_standard_logging=self._read_extra_kwargs_from_standard_logging
102 )
103 )
104 for filter in self.filters:
105 self._handler.addFilter(filter)
107 def get_handler(self) -> logging.Handler:
108 """Get the configured Python logging Handler."""
109 return self._handler
111 def get_formatter_or_raise(self) -> logging.Formatter:
112 if self.formatter is None:
113 raise StlogError("formatter is not set")
114 return self.formatter
117@dataclass
118class StreamOutput(Output):
119 """Represent an output to a stream (stdout, stderr...).
121 Attributes:
122 stream: the stream to use (`typing.TextIO`), default to `sys.stderr`.
124 """
126 stream: typing.TextIO = sys.stderr
128 def __post_init__(self):
129 if self.formatter is None:
130 self.formatter = HumanFormatter()
131 self.set_handler(
132 logging.StreamHandler(self.stream),
133 )
136@dataclass
137class RichStreamOutput(StreamOutput):
138 force_terminal: bool = False
139 console: typing.Any = None
141 def __post_init__(self):
142 if not RICH_INSTALLED:
143 raise StlogError("Rich is not installed and RichStreamOutput is specified")
144 if self.formatter is None:
145 self.formatter = RichHumanFormatter()
146 if self.console is None:
147 self.console = Console(
148 file=self.stream,
149 force_terminal=True if self.force_terminal else None,
150 highlight=False,
151 )
152 self.set_handler(CustomRichHandler(console=self.console))
155def make_stream_or_rich_stream_output(
156 stream: typing.TextIO = sys.stderr,
157 use_rich: bool | None = DEFAULT_USE_RICH,
158 rich_formatter: Formatter | None = None,
159 not_rich_formatter: Formatter | None = None,
160 **kwargs,
161) -> StreamOutput:
162 """Create automatically a `stlog.output.RichStreamOutput` or a (classic)`stlog.output.StreamOutput`.
164 To get a `stlog.output.RichStreamOutput`, following conditions must be true:
166 - `rich` library must be installed and available in python path
167 - `use_rich` parameter must be `True` (forced mode) or `None` (automatic mode)
168 - (if `use_rich` is `None`): the selected `stream` must "output" in a real terminal (not in a shell filter
169 or in a file through redirection...)
171 NOTE: the default value of the `use_rich` parameter is `None` (automatic) but it can be forced by
172 the `STLOG_USE_RICH` env variable.
174 Attributes:
175 stream: the stream to use (`typing.TextIO`), default to `sys.stderr`.
176 use_rich: if None, use [rich output](https://github.com/Textualize/rich/blob/master/README.md) if possible
177 (rich installed and supported tty), if True/False force the usage (or not).
178 rich_formatter: Formatter to use if rich is available/selected (None => default RichHumanFormatter instance).
179 not_rich_formatter: Formatter to use if rich is not available/selected (None => default HumanFormatter instance).
181 """
182 if "formatter" in kwargs:
183 raise StlogError(
184 "you can't use formatter in kwargs for this function (buy you can use rich_formatter/not_rich_formatter instead)"
185 )
186 _use_rich: bool = False
187 if use_rich is not None:
188 # manual mode
189 _use_rich = use_rich
190 else:
191 # automatic mode
192 if RICH_INSTALLED:
193 c = Console(file=stream)
194 _use_rich = c.is_terminal
195 if _use_rich:
196 return RichStreamOutput(
197 stream=stream,
198 formatter=rich_formatter or RichHumanFormatter(),
199 **kwargs,
200 )
201 else:
202 return StreamOutput(
203 stream=stream,
204 formatter=not_rich_formatter or HumanFormatter(),
205 **kwargs,
206 )