Coverage for stlog/setup.py: 80%

91 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-08-21 07:31 +0000

1from __future__ import annotations 

2 

3import logging 

4import os 

5import sys 

6import traceback 

7import types 

8import typing 

9import warnings 

10 

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 

15 

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

21 

22 

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 ) 

31 

32 

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 ) 

55 

56 

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) 

75 

76 

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). 

93 

94 This removes all existing handlers and 

95 sets up handlers/formatters/... for Python logging. 

96 

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). 

115 

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 

125 

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) 

130 

131 root_logger = logging.getLogger(None) 

132 # Remove all handlers 

133 for handler in list(root_logger.handlers): 

134 root_logger.removeHandler(handler) 

135 

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

141 

142 root_logger.setLevel(level) 

143 

144 if logging_excepthook: 

145 sys.excepthook = logging_excepthook 

146 

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) 

152 

153 for lgger, lvel in extra_levels.items(): 

154 logging.getLogger(lgger).setLevel(lvel) 

155 

156 GLOBAL_LOGGING_CONFIG.setup = True 

157 

158 

159ROOT_LOGGER = getLogger("root") 

160 

161 

162def log(level: int, msg, *args, **kwargs): 

163 """Log a message with the given integer severity level' on the root logger. 

164 

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) 

170 

171 

172def debug(msg, *args, **kwargs): 

173 """Log a message with severity 'DEBUG' on the root logger. 

174 

175 `setup()` is automatically called with default arguments if not already done before. 

176 """ 

177 log(logging.DEBUG, msg, *args, **kwargs) 

178 

179 

180def info(msg, *args, **kwargs): 

181 """Log a message with severity 'INFO' on the root logger. 

182 

183 `setup()` is automatically called with default arguments if not already done before. 

184 """ 

185 log(logging.INFO, msg, *args, **kwargs) 

186 

187 

188def warning(msg, *args, **kwargs): 

189 """Log a message with severity 'WARNING' on the root logger. 

190 

191 `setup()` is automatically called with default arguments if not already done before. 

192 """ 

193 log(logging.WARNING, msg, *args, **kwargs) 

194 

195 

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) 

207 

208 

209def error(msg, *args, **kwargs): 

210 """Log a message with severity 'ERROR' on the root logger. 

211 

212 `setup()` is automatically called with default arguments if not already done before. 

213 """ 

214 log(logging.ERROR, msg, *args, **kwargs) 

215 

216 

217def critical(msg, *args, **kwargs): 

218 """Log a message with severity 'CRITICAL' on the root logger. 

219 

220 `setup()` is automatically called with default arguments if not already done before. 

221 """ 

222 log(logging.CRITICAL, msg, *args, **kwargs) 

223 

224 

225fatal = critical 

226 

227 

228def exception(msg, *args, exc_info=True, **kwargs): 

229 """Log a message with severity 'ERROR' on the root logger with exception information. 

230 

231 `setup()` is automatically called with default arguments if not already done before. 

232 """ 

233 error(msg, *args, exc_info=exc_info, **kwargs)