Coverage for stlog/setup.py: 80%
91 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 os
5import sys
6import traceback
7import types
8import typing
9import warnings
11from stlog.adapter import getLogger
12from stlog.base import GLOBAL_LOGGING_CONFIG, check_env_false
13from stlog.formatter import DEFAULT_STLOG_GCP_JSON_FORMAT, JsonFormatter
14from stlog.output import Output, StreamOutput, make_stream_or_rich_stream_output
16DEFAULT_LEVEL: str = os.environ.get("STLOG_LEVEL", "INFO")
17DEFAULT_CAPTURE_WARNINGS: bool = check_env_false("STLOG_CAPTURE_WARNINGS", True)
18DEFAULT_PROGRAM_NAME: str | None = os.environ.get("STLOG_PROGRAM_NAME", None)
19DEFAULT_DESTINATION: str = os.environ.get("STLOG_DESTINATION", "stderr").lower()
20DEFAULT_OUTPUT: str = os.environ.get("STLOG_OUTPUT", "console").lower()
23def _make_default_stream() -> typing.TextIO:
24 if DEFAULT_DESTINATION == "stderr":
25 return sys.stderr
26 elif DEFAULT_DESTINATION == "stdout":
27 return sys.stdout
28 raise Exception(
29 f"bad value:{DEFAULT_DESTINATION} for STLOG_DESTINATION env var => must be 'stderr' or 'stdout'"
30 )
33def _make_default_outputs() -> list[Output]:
34 if DEFAULT_OUTPUT == "console":
35 return [make_stream_or_rich_stream_output(stream=_make_default_stream())]
36 elif DEFAULT_OUTPUT == "json":
37 return [StreamOutput(stream=_make_default_stream(), formatter=JsonFormatter())]
38 elif DEFAULT_OUTPUT == "json-human":
39 return [
40 StreamOutput(
41 stream=_make_default_stream(), formatter=JsonFormatter(indent=4)
42 )
43 ]
44 elif DEFAULT_OUTPUT == "json-gcp":
45 return [
46 StreamOutput(
47 stream=_make_default_stream(),
48 formatter=JsonFormatter(fmt=DEFAULT_STLOG_GCP_JSON_FORMAT),
49 )
50 ]
51 else:
52 raise Exception(
53 f"bad value:{DEFAULT_OUTPUT} for STLOG_OUTPUT env var => must be 'console', 'json', 'json-human' or 'json-gcp'"
54 )
57def _logging_excepthook(
58 exc_type: type[BaseException],
59 value: BaseException,
60 tb: types.TracebackType | None = None,
61) -> None:
62 if issubclass(exc_type, KeyboardInterrupt):
63 sys.__excepthook__(exc_type, value, tb)
64 return
65 try:
66 program_logger = getLogger(GLOBAL_LOGGING_CONFIG.program_name)
67 program_logger.error(
68 "Exception catched in excepthook", exc_info=(exc_type, value, tb)
69 )
70 except Exception:
71 print(
72 "ERROR: Exception during exception handling => let's dump this on standard output"
73 )
74 print(traceback.format_exc(), file=sys.stderr)
77def setup(
78 *,
79 level: str | int = DEFAULT_LEVEL,
80 outputs: typing.Iterable[Output] | None = None,
81 program_name: str | None = DEFAULT_PROGRAM_NAME,
82 capture_warnings: bool = DEFAULT_CAPTURE_WARNINGS,
83 logging_excepthook: typing.Callable[
84 [type[BaseException], BaseException, types.TracebackType | None],
85 typing.Any,
86 ]
87 | None = _logging_excepthook,
88 extra_levels: typing.Mapping[str, str | int] = {},
89 reinject_context_in_standard_logging: bool | None = None,
90 read_extra_kwargs_from_standard_logging: bool | None = None,
91) -> None:
92 """Set up the Python logging with stlog (globally).
94 This removes all existing handlers and
95 sets up handlers/formatters/... for Python logging.
97 Args:
98 level: the root log level as int or as a string representation (in uppercase)
99 (see https://docs.python.org/3/library/logging.html#levels), the default
100 value is read in STLOG_LEVEL env var or set to INFO (if not set).
101 outputs: iterable of Output to log to.
102 program_name: the name of the program, the default value is read in STLOG_PROGRAM_NAME
103 env var or auto-detected if not set.
104 capture_warnings: capture warnings from the `warnings` module.
105 logging_excepthook: if not None, override sys.excepthook with the given callable
106 See https://docs.python.org/3/library/sys.html#sys.excepthook for details.
107 extra_levels: dict "logger name => log level" for quick override of
108 the root log level (for some loggers).
109 reinject_context_in_standard_logging: if True, reinject the LogContext
110 in log record emitted with python standard loggers (note: can be overriden per `stlog.output.Output`,
111 default to `STLOG_REINJECT_CONTEXT_IN_STANDARD_LOGGING` env var or True if not set).
112 read_extra_kwargs_from_standard_logging: if try to reinject the extra kwargs from standard logging
113 (note: can be overriden per `stlog.output.Output`, default to `STLOG_READ_EXTRA_KWARGS_FROM_STANDARD_LOGGING` env var
114 or False if not set).
116 """
117 GLOBAL_LOGGING_CONFIG.reinject_context_in_standard_logging = (
118 reinject_context_in_standard_logging
119 )
120 GLOBAL_LOGGING_CONFIG.read_extra_kwargs_from_standard_logging = (
121 read_extra_kwargs_from_standard_logging
122 )
123 if program_name is not None:
124 GLOBAL_LOGGING_CONFIG.program_name = program_name
126 if GLOBAL_LOGGING_CONFIG._unit_tests_mode:
127 # remove all configured loggers
128 for key in list(logging.Logger.manager.loggerDict.keys()):
129 logging.Logger.manager.loggerDict.pop(key)
131 root_logger = logging.getLogger(None)
132 # Remove all handlers
133 for handler in list(root_logger.handlers):
134 root_logger.removeHandler(handler)
136 # Add configured handlers
137 if outputs is None:
138 outputs = _make_default_outputs()
139 for out in outputs:
140 root_logger.addHandler(out.get_handler())
142 root_logger.setLevel(level)
144 if logging_excepthook:
145 sys.excepthook = logging_excepthook
147 if capture_warnings:
148 if GLOBAL_LOGGING_CONFIG._unit_tests_mode:
149 # to avoid the capture by pytest
150 logging._warnings_showwarning = None # type: ignore
151 logging.captureWarnings(True)
153 for lgger, lvel in extra_levels.items():
154 logging.getLogger(lgger).setLevel(lvel)
156 GLOBAL_LOGGING_CONFIG.setup = True
159ROOT_LOGGER = getLogger("root")
162def log(level: int, msg, *args, **kwargs):
163 """Log a message with the given integer severity level' on the root logger.
165 `setup()` is automatically called with default arguments if not already done before.
166 """
167 if not GLOBAL_LOGGING_CONFIG.setup:
168 setup()
169 ROOT_LOGGER.log(level, msg, *args, **kwargs)
172def debug(msg, *args, **kwargs):
173 """Log a message with severity 'DEBUG' on the root logger.
175 `setup()` is automatically called with default arguments if not already done before.
176 """
177 log(logging.DEBUG, msg, *args, **kwargs)
180def info(msg, *args, **kwargs):
181 """Log a message with severity 'INFO' on the root logger.
183 `setup()` is automatically called with default arguments if not already done before.
184 """
185 log(logging.INFO, msg, *args, **kwargs)
188def warning(msg, *args, **kwargs):
189 """Log a message with severity 'WARNING' on the root logger.
191 `setup()` is automatically called with default arguments if not already done before.
192 """
193 log(logging.WARNING, msg, *args, **kwargs)
196def warn(msg, *args, **kwargs):
197 if (
198 not GLOBAL_LOGGING_CONFIG.setup
199 ): # we do this here to be able to capture the next warning
200 setup()
201 warnings.warn(
202 "The 'warn' function is deprecated, " "use 'warning' instead",
203 DeprecationWarning,
204 2,
205 )
206 warning(msg, *args, **kwargs)
209def error(msg, *args, **kwargs):
210 """Log a message with severity 'ERROR' on the root logger.
212 `setup()` is automatically called with default arguments if not already done before.
213 """
214 log(logging.ERROR, msg, *args, **kwargs)
217def critical(msg, *args, **kwargs):
218 """Log a message with severity 'CRITICAL' on the root logger.
220 `setup()` is automatically called with default arguments if not already done before.
221 """
222 log(logging.CRITICAL, msg, *args, **kwargs)
225fatal = critical
228def exception(msg, *args, exc_info=True, **kwargs):
229 """Log a message with severity 'ERROR' on the root logger with exception information.
231 `setup()` is automatically called with default arguments if not already done before.
232 """
233 error(msg, *args, exc_info=exc_info, **kwargs)