reconplogger 5.0.0.dev1__tar.gz → 5.0.0.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reconplogger-5.0.0.dev1/reconplogger.egg-info → reconplogger-5.0.0.dev2}/PKG-INFO +3 -2
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/README.rst +2 -1
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2/reconplogger.egg-info}/PKG-INFO +3 -2
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger.py +66 -78
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger_tests.py +99 -27
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/LICENSE.rst +0 -0
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/pyproject.toml +0 -0
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger.egg-info/SOURCES.txt +0 -0
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger.egg-info/dependency_links.txt +0 -0
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger.egg-info/requires.txt +0 -0
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger.egg-info/top_level.txt +0 -0
- {reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reconplogger
|
|
3
|
-
Version: 5.0.0.
|
|
3
|
+
Version: 5.0.0.dev2
|
|
4
4
|
Summary: omni:us python logging package
|
|
5
5
|
Author-email: Mauricio Villegas <mauricio@omnius.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -75,7 +75,8 @@ The package contains essentially the following things:
|
|
|
75
75
|
- A default logging configuration.
|
|
76
76
|
- A function for loading logging configuration for regular python code.
|
|
77
77
|
- A function for loading logging configuration for flask-based microservices.
|
|
78
|
-
- Root logger configuration via ``LOGGER_ROOT_HANDLER``
|
|
78
|
+
- Root logger configuration via ``LOGGER_ROOT_HANDLER`` with independent ``LOGGER_ROOT_LEVEL`` control.
|
|
79
|
+
- Singleton logger setup (``logger_setup``); call ``reset_configs`` before reconfiguring.
|
|
79
80
|
- Automatic correlation ID management in Flask services via ``flask_app_logger_setup``.
|
|
80
81
|
- An inheritable class to add a logger property.
|
|
81
82
|
- A context manager to set and get the correlation id.
|
|
@@ -22,7 +22,8 @@ The package contains essentially the following things:
|
|
|
22
22
|
- A default logging configuration.
|
|
23
23
|
- A function for loading logging configuration for regular python code.
|
|
24
24
|
- A function for loading logging configuration for flask-based microservices.
|
|
25
|
-
- Root logger configuration via ``LOGGER_ROOT_HANDLER``
|
|
25
|
+
- Root logger configuration via ``LOGGER_ROOT_HANDLER`` with independent ``LOGGER_ROOT_LEVEL`` control.
|
|
26
|
+
- Singleton logger setup (``logger_setup``); call ``reset_configs`` before reconfiguring.
|
|
26
27
|
- Automatic correlation ID management in Flask services via ``flask_app_logger_setup``.
|
|
27
28
|
- An inheritable class to add a logger property.
|
|
28
29
|
- A context manager to set and get the correlation id.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reconplogger
|
|
3
|
-
Version: 5.0.0.
|
|
3
|
+
Version: 5.0.0.dev2
|
|
4
4
|
Summary: omni:us python logging package
|
|
5
5
|
Author-email: Mauricio Villegas <mauricio@omnius.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -75,7 +75,8 @@ The package contains essentially the following things:
|
|
|
75
75
|
- A default logging configuration.
|
|
76
76
|
- A function for loading logging configuration for regular python code.
|
|
77
77
|
- A function for loading logging configuration for flask-based microservices.
|
|
78
|
-
- Root logger configuration via ``LOGGER_ROOT_HANDLER``
|
|
78
|
+
- Root logger configuration via ``LOGGER_ROOT_HANDLER`` with independent ``LOGGER_ROOT_LEVEL`` control.
|
|
79
|
+
- Singleton logger setup (``logger_setup``); call ``reset_configs`` before reconfiguring.
|
|
79
80
|
- Automatic correlation ID management in Flask services via ``flask_app_logger_setup``.
|
|
80
81
|
- An inheritable class to add a logger property.
|
|
81
82
|
- A context manager to set and get the correlation id.
|
|
@@ -11,7 +11,7 @@ from typing import Optional, Union
|
|
|
11
11
|
import pythonjsonlogger
|
|
12
12
|
import yaml
|
|
13
13
|
|
|
14
|
-
__version__ = "5.0.0.
|
|
14
|
+
__version__ = "5.0.0.dev2"
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"RLoggerProperty",
|
|
@@ -118,6 +118,12 @@ configs_loaded = set()
|
|
|
118
118
|
# Internal state for singleton primary logger
|
|
119
119
|
_primary_logger: Optional[logging.Logger] = None
|
|
120
120
|
|
|
121
|
+
ENV_CFG = "LOGGER_CFG"
|
|
122
|
+
ENV_NAME = "LOGGER_NAME"
|
|
123
|
+
ENV_LEVEL = "LOGGER_LEVEL"
|
|
124
|
+
ENV_ROOT_HANDLER = "LOGGER_ROOT_HANDLER"
|
|
125
|
+
ENV_ROOT_LEVEL = "LOGGER_ROOT_LEVEL"
|
|
126
|
+
|
|
121
127
|
|
|
122
128
|
def reset_configs():
|
|
123
129
|
"""Resets reconplogger's internal configuration state.
|
|
@@ -130,7 +136,7 @@ def reset_configs():
|
|
|
130
136
|
_primary_logger = None
|
|
131
137
|
|
|
132
138
|
|
|
133
|
-
def load_config(cfg: Optional[Union[str, dict]] = None
|
|
139
|
+
def load_config(cfg: Optional[Union[str, dict]] = None):
|
|
134
140
|
"""Loads a logging configuration from path or environment variable or dictionary object.
|
|
135
141
|
|
|
136
142
|
Args:
|
|
@@ -140,14 +146,14 @@ def load_config(cfg: Optional[Union[str, dict]] = None, reload: bool = False):
|
|
|
140
146
|
Returns:
|
|
141
147
|
The logging package object.
|
|
142
148
|
"""
|
|
143
|
-
if
|
|
144
|
-
cfg is None
|
|
145
|
-
or cfg in {"", "reconplogger_default_cfg"}
|
|
146
|
-
or (cfg in os.environ and os.environ[cfg] == "reconplogger_default_cfg")
|
|
147
|
-
):
|
|
149
|
+
if cfg is None:
|
|
148
150
|
cfg_dict = reconplogger_default_cfg
|
|
149
151
|
elif isinstance(cfg, dict):
|
|
150
152
|
cfg_dict = cfg
|
|
153
|
+
elif cfg in {"", "reconplogger_default_cfg"} or (
|
|
154
|
+
cfg in os.environ and os.environ[cfg] == "reconplogger_default_cfg"
|
|
155
|
+
):
|
|
156
|
+
cfg_dict = reconplogger_default_cfg
|
|
151
157
|
elif isinstance(cfg, str):
|
|
152
158
|
try:
|
|
153
159
|
if os.path.isfile(cfg):
|
|
@@ -172,7 +178,7 @@ def load_config(cfg: Optional[Union[str, dict]] = None, reload: bool = False):
|
|
|
172
178
|
cfg_dict["disable_existing_loggers"] = False
|
|
173
179
|
|
|
174
180
|
cfg_hash = yaml.safe_dump(cfg_dict).__hash__()
|
|
175
|
-
if
|
|
181
|
+
if cfg_hash not in configs_loaded:
|
|
176
182
|
logging.config.dictConfig(cfg_dict)
|
|
177
183
|
configs_loaded.add(cfg_hash)
|
|
178
184
|
|
|
@@ -234,13 +240,6 @@ def add_file_handler(
|
|
|
234
240
|
return file_handler
|
|
235
241
|
|
|
236
242
|
|
|
237
|
-
def test_logger(logger: logging.Logger):
|
|
238
|
-
"""Logs one message to each debug, info and warning levels intended for testing."""
|
|
239
|
-
logger.debug("reconplogger test debug message.")
|
|
240
|
-
logger.info("reconplogger test info message.")
|
|
241
|
-
logger.warning("reconplogger test warning message.")
|
|
242
|
-
|
|
243
|
-
|
|
244
243
|
def get_logger(logger_name: str) -> logging.Logger:
|
|
245
244
|
"""Returns an already existing logger.
|
|
246
245
|
|
|
@@ -254,7 +253,7 @@ def get_logger(logger_name: str) -> logging.Logger:
|
|
|
254
253
|
ValueError: If the logger does not exist.
|
|
255
254
|
"""
|
|
256
255
|
if logger_name not in logging.Logger.manager.loggerDict and logger_name not in logging.root.manager.loggerDict:
|
|
257
|
-
raise ValueError('Logger "
|
|
256
|
+
raise ValueError(f'Logger "{logger_name}" not defined.')
|
|
258
257
|
return logging.getLogger(logger_name)
|
|
259
258
|
|
|
260
259
|
|
|
@@ -262,103 +261,82 @@ def logger_setup(
|
|
|
262
261
|
logger_name: str = "plain_logger",
|
|
263
262
|
config: Optional[str] = None,
|
|
264
263
|
level: Optional[str] = None,
|
|
265
|
-
env_prefix: str = "LOGGER",
|
|
266
|
-
reload: bool = False,
|
|
267
|
-
init_messages: bool = False,
|
|
268
264
|
) -> logging.Logger:
|
|
269
265
|
"""Sets up logging configuration and returns the logger.
|
|
270
266
|
|
|
271
|
-
If the environment variable ``
|
|
267
|
+
If the environment variable ``LOGGER_ROOT_HANDLER`` is set to the name of a handler
|
|
272
268
|
defined in the logging config, that handler is installed on the root logger so that
|
|
273
269
|
all third-party loggers (which propagate to the root by default) are also captured.
|
|
270
|
+
The primary logger level remains controlled by ``level`` / ``LOGGER_LEVEL``, while the
|
|
271
|
+
root logger level can be controlled independently through ``LOGGER_ROOT_LEVEL``.
|
|
274
272
|
On subsequent calls the same primary logger is returned without reconfiguring the root.
|
|
273
|
+
To force a fresh configuration pass, call :func:`reset_configs` first.
|
|
275
274
|
|
|
276
275
|
Args:
|
|
277
276
|
logger_name: Name of the logger that needs to be used.
|
|
278
277
|
config: Configuration string or path to configuration file or configuration file via environment variable.
|
|
279
278
|
level: Optional logging level that overrides one in config.
|
|
280
|
-
env_prefix: Environment variable names prefix for overriding logger configuration.
|
|
281
|
-
reload: Whether to reload logging configuration overriding any previous settings.
|
|
282
|
-
init_messages: Whether to log init and test messages.
|
|
283
279
|
|
|
284
280
|
Returns:
|
|
285
281
|
The logger object.
|
|
286
282
|
"""
|
|
287
283
|
global _primary_logger
|
|
288
284
|
|
|
289
|
-
if not isinstance(env_prefix, str) or not env_prefix:
|
|
290
|
-
raise ValueError("env_prefix is required to be a non-empty string.")
|
|
291
|
-
env_cfg = env_prefix + "_CFG"
|
|
292
|
-
env_name = env_prefix + "_NAME"
|
|
293
|
-
env_level = env_prefix + "_LEVEL"
|
|
294
|
-
env_root_handler = env_prefix + "_ROOT_HANDLER"
|
|
295
|
-
|
|
296
285
|
# Return primary logger on subsequent calls (singleton behaviour)
|
|
297
|
-
if _primary_logger is not None
|
|
298
|
-
name = os.getenv(
|
|
299
|
-
if name != _primary_logger.name or config or level
|
|
286
|
+
if _primary_logger is not None:
|
|
287
|
+
name = os.getenv(ENV_NAME, logger_name)
|
|
288
|
+
if name != _primary_logger.name or config or level:
|
|
300
289
|
_primary_logger.debug(
|
|
301
290
|
"logger_setup called again with different arguments; returning existing primary logger."
|
|
302
291
|
)
|
|
303
292
|
return _primary_logger
|
|
304
293
|
|
|
305
294
|
# Configure logging
|
|
306
|
-
load_config(os.getenv(
|
|
295
|
+
load_config(os.getenv(ENV_CFG, config))
|
|
307
296
|
|
|
308
297
|
# Get logger
|
|
309
|
-
name = os.getenv(
|
|
298
|
+
name = os.getenv(ENV_NAME, logger_name)
|
|
310
299
|
logger = get_logger(name)
|
|
311
300
|
|
|
312
|
-
# Resolve the effective log level
|
|
301
|
+
# Resolve the effective log level for the primary logger
|
|
313
302
|
effective_level: Optional[int] = None
|
|
314
|
-
if
|
|
315
|
-
level = os.getenv(
|
|
303
|
+
if ENV_LEVEL in os.environ:
|
|
304
|
+
level = os.getenv(ENV_LEVEL)
|
|
316
305
|
if level:
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# loggers' handlers so records flow through the single root handler.
|
|
329
|
-
_configure_root_logger(root_handler_name, effective_level)
|
|
330
|
-
else:
|
|
331
|
-
# Apply log level overrides to the named logger's handlers
|
|
332
|
-
if effective_level is not None:
|
|
333
|
-
for handler in logger.handlers:
|
|
334
|
-
if not isinstance(handler, logging.FileHandler):
|
|
335
|
-
handler.setLevel(effective_level)
|
|
306
|
+
if level not in logging_levels:
|
|
307
|
+
raise ValueError('Invalid logging level: "' + str(level) + '".')
|
|
308
|
+
effective_level = logging_levels[level]
|
|
309
|
+
|
|
310
|
+
_configure_root_logger()
|
|
311
|
+
|
|
312
|
+
# Apply log level overrides to the named logger's handlers
|
|
313
|
+
if effective_level is not None:
|
|
314
|
+
for handler in logger.handlers:
|
|
315
|
+
if not isinstance(handler, logging.FileHandler):
|
|
316
|
+
handler.setLevel(effective_level)
|
|
336
317
|
|
|
337
318
|
# Add correlation id filter
|
|
338
319
|
logger.addFilter(_CorrelationIdLoggingFilter())
|
|
339
320
|
|
|
340
|
-
# Log configured done and test logger
|
|
341
|
-
if init_messages:
|
|
342
|
-
logger.info("reconplogger (v" + __version__ + ") logger configured.")
|
|
343
|
-
test_logger(logger)
|
|
344
|
-
|
|
345
321
|
logger._reconplogger_setup = True
|
|
346
322
|
_primary_logger = logger
|
|
347
323
|
return logger
|
|
348
324
|
|
|
349
325
|
|
|
350
|
-
def _configure_root_logger(
|
|
351
|
-
"""Installs a named handler on the root logger and
|
|
326
|
+
def _configure_root_logger() -> None:
|
|
327
|
+
"""Installs a named handler on the root logger and removes stream handlers from named loggers.
|
|
352
328
|
|
|
353
329
|
After this call every log record in the process flows through the single
|
|
354
330
|
root handler, regardless of which named logger emitted it. All named
|
|
355
|
-
loggers (except ``
|
|
356
|
-
``propagate`` set to ``True`` so
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
handler_name: Name of the handler as defined in the logging config (e.g. ``json_handler``).
|
|
360
|
-
level: Optional numeric log level; falls back to the handler's own level.
|
|
331
|
+
loggers (except those with only ``NullHandler`` instances) have their
|
|
332
|
+
``StreamHandler`` instances removed and ``propagate`` set to ``True`` so
|
|
333
|
+
records bubble up to the root while keeping non-stream handlers such as
|
|
334
|
+
file handlers attached.
|
|
361
335
|
"""
|
|
336
|
+
handler_name = os.getenv(ENV_ROOT_HANDLER)
|
|
337
|
+
if not handler_name:
|
|
338
|
+
return
|
|
339
|
+
|
|
362
340
|
# Retrieve the already-configured handler by name
|
|
363
341
|
assert logging.root.manager.loggerDict # just to verify config is loaded
|
|
364
342
|
handler_obj = logging._handlers.get(handler_name) # type: ignore[attr-defined]
|
|
@@ -370,15 +348,28 @@ def _configure_root_logger(handler_name: str, level: Optional[int]) -> None:
|
|
|
370
348
|
|
|
371
349
|
root = logging.getLogger()
|
|
372
350
|
root.handlers = [handler_obj]
|
|
373
|
-
|
|
374
|
-
|
|
351
|
+
root_level_name = os.getenv(ENV_ROOT_LEVEL)
|
|
352
|
+
level = handler_obj.level
|
|
353
|
+
if root_level_name:
|
|
354
|
+
if root_level_name not in logging_levels:
|
|
355
|
+
raise ValueError('Invalid logging level: "' + str(root_level_name) + '".')
|
|
356
|
+
level = logging_levels[root_level_name]
|
|
357
|
+
handler_obj.setLevel(level)
|
|
358
|
+
root.setLevel(level)
|
|
375
359
|
|
|
376
360
|
logging.captureWarnings(True)
|
|
377
361
|
|
|
378
|
-
#
|
|
362
|
+
# Remove stream handlers from named loggers so their remaining handlers, such
|
|
363
|
+
# as FileHandler, keep working without duplicating root stream output.
|
|
379
364
|
for lg_obj in logging.Logger.manager.loggerDict.values():
|
|
380
|
-
if isinstance(lg_obj, logging.Logger)
|
|
381
|
-
lg_obj.handlers
|
|
365
|
+
if isinstance(lg_obj, logging.Logger):
|
|
366
|
+
if any(isinstance(h, logging.NullHandler) for h in lg_obj.handlers):
|
|
367
|
+
continue
|
|
368
|
+
lg_obj.handlers = [
|
|
369
|
+
handler
|
|
370
|
+
for handler in lg_obj.handlers
|
|
371
|
+
if not isinstance(handler, logging.StreamHandler) or isinstance(handler, logging.FileHandler)
|
|
372
|
+
]
|
|
382
373
|
lg_obj.propagate = True
|
|
383
374
|
|
|
384
375
|
|
|
@@ -390,7 +381,6 @@ def flask_app_logger_setup(
|
|
|
390
381
|
logger_name: str = "plain_logger",
|
|
391
382
|
config: Optional[str] = None,
|
|
392
383
|
level: Optional[str] = None,
|
|
393
|
-
env_prefix: str = "LOGGER",
|
|
394
384
|
) -> logging.Logger:
|
|
395
385
|
"""Sets up logging configuration, configures flask to use it, and returns the logger.
|
|
396
386
|
|
|
@@ -399,7 +389,6 @@ def flask_app_logger_setup(
|
|
|
399
389
|
logger_name: Name of the logger that needs to be used.
|
|
400
390
|
config: Configuration string or path to configuration file or configuration file via environment variable.
|
|
401
391
|
level: Optional logging level that overrides one in config.
|
|
402
|
-
env_prefix: Environment variable names prefix for overriding logger configuration.
|
|
403
392
|
|
|
404
393
|
Returns:
|
|
405
394
|
The logger object.
|
|
@@ -409,7 +398,6 @@ def flask_app_logger_setup(
|
|
|
409
398
|
logger_name=logger_name,
|
|
410
399
|
config=config,
|
|
411
400
|
level=level,
|
|
412
|
-
env_prefix=env_prefix,
|
|
413
401
|
)
|
|
414
402
|
|
|
415
403
|
# Apply WSGI middleware to manage correlation ID at the transport layer
|
|
@@ -63,14 +63,14 @@ class TestReconplogger(unittest.TestCase):
|
|
|
63
63
|
|
|
64
64
|
def test_log_level(self):
|
|
65
65
|
"""Test load config with the default config and plain logger changing the log level."""
|
|
66
|
-
logger = reconplogger.logger_setup(level="INFO"
|
|
66
|
+
logger = reconplogger.logger_setup(level="INFO")
|
|
67
67
|
self.assertEqual(logger.handlers[0].level, logging.INFO)
|
|
68
68
|
reconplogger.reset_configs()
|
|
69
|
-
logger = reconplogger.logger_setup(level="ERROR"
|
|
69
|
+
logger = reconplogger.logger_setup(level="ERROR")
|
|
70
70
|
self.assertEqual(logger.handlers[0].level, logging.ERROR)
|
|
71
71
|
reconplogger.reset_configs()
|
|
72
72
|
with patch.dict(os.environ, {"LOGGER_LEVEL": "WARNING"}):
|
|
73
|
-
logger = reconplogger.logger_setup(level="INFO"
|
|
73
|
+
logger = reconplogger.logger_setup(level="INFO")
|
|
74
74
|
self.assertEqual(logger.handlers[0].level, logging.WARNING)
|
|
75
75
|
|
|
76
76
|
def test_default_logger_with_exception(self):
|
|
@@ -125,18 +125,20 @@ class TestReconplogger(unittest.TestCase):
|
|
|
125
125
|
self.assertRaises(ValueError, lambda: reconplogger.replace_logger_handlers(logger, False))
|
|
126
126
|
self.assertRaises(ValueError, lambda: reconplogger.replace_logger_handlers(False, False))
|
|
127
127
|
|
|
128
|
-
def
|
|
128
|
+
def test_logger_writes_messages(self):
|
|
129
129
|
logger = reconplogger.logger_setup()
|
|
130
130
|
with capture_logs(logger) as captured:
|
|
131
|
-
reconplogger.
|
|
131
|
+
logger.debug("reconplogger test debug message.")
|
|
132
|
+
logger.info("reconplogger test info message.")
|
|
133
|
+
logger.warning("reconplogger test warning message.")
|
|
132
134
|
self.assertIn("WARNING", captured.getvalue())
|
|
133
135
|
self.assertIn("reconplogger test warning message", captured.getvalue())
|
|
134
136
|
|
|
135
137
|
@patch.dict(
|
|
136
138
|
os.environ,
|
|
137
139
|
{
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
+
"LOGGER_NAME": "example_logger",
|
|
141
|
+
"LOGGER_CFG": """{
|
|
140
142
|
"version": 1,
|
|
141
143
|
"formatters": {
|
|
142
144
|
"verbose": {
|
|
@@ -159,28 +161,25 @@ class TestReconplogger(unittest.TestCase):
|
|
|
159
161
|
}""",
|
|
160
162
|
},
|
|
161
163
|
)
|
|
162
|
-
def
|
|
163
|
-
logger = reconplogger.logger_setup(
|
|
164
|
+
def test_logger_setup_env_vars(self):
|
|
165
|
+
logger = reconplogger.logger_setup()
|
|
164
166
|
info_msg = "info message env logger"
|
|
165
167
|
with LogCapture(names="example_logger") as log:
|
|
166
168
|
logger.info(info_msg)
|
|
167
169
|
log.check(("example_logger", "INFO", info_msg))
|
|
168
170
|
|
|
169
|
-
def test_logger_setup_env_prefix_invalid(self):
|
|
170
|
-
for env_prefix in [None, ""]:
|
|
171
|
-
with self.subTest(env_prefix):
|
|
172
|
-
with self.assertRaises(ValueError):
|
|
173
|
-
reconplogger.logger_setup(env_prefix=env_prefix)
|
|
174
|
-
|
|
175
171
|
def test_undefined_logger(self):
|
|
176
172
|
"""Test setting up a logger not already defined."""
|
|
177
173
|
self.assertRaises(ValueError, lambda: reconplogger.logger_setup("undefined_logger"))
|
|
178
174
|
|
|
179
175
|
def test_logger_setup_invalid_level(self):
|
|
180
176
|
with self.assertRaises(ValueError):
|
|
181
|
-
reconplogger.logger_setup(level="INVALID"
|
|
177
|
+
reconplogger.logger_setup(level="INVALID")
|
|
182
178
|
with self.assertRaises(ValueError):
|
|
183
|
-
reconplogger.logger_setup(level=True
|
|
179
|
+
reconplogger.logger_setup(level=True)
|
|
180
|
+
with patch.dict(os.environ, {"LOGGER_ROOT_HANDLER": "json_handler", "LOGGER_ROOT_LEVEL": "INVALID"}):
|
|
181
|
+
with self.assertRaises(ValueError):
|
|
182
|
+
reconplogger.logger_setup()
|
|
184
183
|
|
|
185
184
|
@patch.dict(os.environ, {"LOGGER_NAME": "json_logger"})
|
|
186
185
|
def test_correlation_id_context(self):
|
|
@@ -203,13 +202,13 @@ class TestReconplogger(unittest.TestCase):
|
|
|
203
202
|
@patch.dict(
|
|
204
203
|
os.environ,
|
|
205
204
|
{
|
|
206
|
-
"
|
|
207
|
-
"
|
|
205
|
+
"LOGGER_CFG": "reconplogger_default_cfg",
|
|
206
|
+
"LOGGER_NAME": "json_logger",
|
|
208
207
|
},
|
|
209
208
|
)
|
|
210
209
|
def test_flask_app_logger_setup(self):
|
|
211
210
|
app = Flask(__name__)
|
|
212
|
-
reconplogger.flask_app_logger_setup(
|
|
211
|
+
reconplogger.flask_app_logger_setup(flask_app=app)
|
|
213
212
|
assert app.logger.filters # pylint: disable=no-member
|
|
214
213
|
assert app.before_request_funcs
|
|
215
214
|
assert app.after_request_funcs
|
|
@@ -227,8 +226,8 @@ class TestReconplogger(unittest.TestCase):
|
|
|
227
226
|
@patch.dict(
|
|
228
227
|
os.environ,
|
|
229
228
|
{
|
|
230
|
-
"
|
|
231
|
-
"
|
|
229
|
+
"LOGGER_CFG": "reconplogger_default_cfg",
|
|
230
|
+
"LOGGER_NAME": "json_logger",
|
|
232
231
|
},
|
|
233
232
|
)
|
|
234
233
|
def test_flask_app_correlation_id(self):
|
|
@@ -251,7 +250,7 @@ class TestReconplogger(unittest.TestCase):
|
|
|
251
250
|
logs.check((app.logger.name, "ERROR"))
|
|
252
251
|
self.assertEqual(response.status_code, 500)
|
|
253
252
|
|
|
254
|
-
reconplogger.flask_app_logger_setup(
|
|
253
|
+
reconplogger.flask_app_logger_setup(flask_app=app)
|
|
255
254
|
client = app.test_client()
|
|
256
255
|
|
|
257
256
|
self.assertRaises(RuntimeError, lambda: reconplogger.get_correlation_id())
|
|
@@ -372,7 +371,8 @@ class TestReconplogger(unittest.TestCase):
|
|
|
372
371
|
self.assertTrue(any([debug_msg in line for line in open(log_file).readlines()]))
|
|
373
372
|
|
|
374
373
|
log_file = os.path.join(tmpdir, "file2.log")
|
|
375
|
-
|
|
374
|
+
reconplogger.reset_configs()
|
|
375
|
+
logger = reconplogger.logger_setup(logger_name="plain_logger", level="DEBUG")
|
|
376
376
|
reconplogger.add_file_handler(logger, file_path=log_file, level="ERROR")
|
|
377
377
|
self.assertEqual(logger.handlers[0].level, logging.DEBUG)
|
|
378
378
|
self.assertEqual(logger.handlers[1].level, logging.ERROR)
|
|
@@ -435,17 +435,89 @@ class TestReconplogger(unittest.TestCase):
|
|
|
435
435
|
)
|
|
436
436
|
def test_root_logger_handler(self):
|
|
437
437
|
"""When LOGGER_ROOT_HANDLER is set, the root logger receives the handler and
|
|
438
|
-
|
|
438
|
+
LOGGER_LEVEL no longer changes the root logger level."""
|
|
439
439
|
logger = reconplogger.logger_setup()
|
|
440
440
|
root = logging.getLogger()
|
|
441
441
|
# Root logger should have the json_handler installed
|
|
442
442
|
handler_names = [h.__class__.__name__ for h in root.handlers]
|
|
443
443
|
self.assertIn("StreamHandler", handler_names)
|
|
444
|
-
self.assertEqual(root.level, logging.
|
|
445
|
-
|
|
444
|
+
self.assertEqual(root.level, logging.WARNING)
|
|
445
|
+
self.assertEqual(root.handlers[0].level, logging.WARNING)
|
|
446
|
+
# Named logger should have no own stream handlers; records bubble up to root
|
|
446
447
|
self.assertEqual(logger.handlers, [])
|
|
447
448
|
self.assertTrue(logger.propagate)
|
|
448
449
|
|
|
450
|
+
@patch.dict(
|
|
451
|
+
os.environ,
|
|
452
|
+
{
|
|
453
|
+
"LOGGER_ROOT_HANDLER": "json_handler",
|
|
454
|
+
"LOGGER_ROOT_LEVEL": "DEBUG",
|
|
455
|
+
},
|
|
456
|
+
)
|
|
457
|
+
def test_root_logger_level_emits_debug_logs(self):
|
|
458
|
+
"""LOGGER_ROOT_LEVEL controls the root logger and root handler thresholds."""
|
|
459
|
+
logger = reconplogger.logger_setup()
|
|
460
|
+
root = logging.getLogger()
|
|
461
|
+
self.assertEqual(root.level, logging.DEBUG)
|
|
462
|
+
self.assertEqual(root.handlers[0].level, logging.DEBUG)
|
|
463
|
+
|
|
464
|
+
captured = StringIO()
|
|
465
|
+
with patch.object(root.handlers[0], "stream", captured):
|
|
466
|
+
logger.debug("debug message via root handler")
|
|
467
|
+
|
|
468
|
+
self.assertIn("debug message via root handler", captured.getvalue())
|
|
469
|
+
|
|
470
|
+
def test_root_logger_keeps_file_handlers(self):
|
|
471
|
+
"""Root logger setup removes stream handlers from named loggers but keeps file handlers."""
|
|
472
|
+
tmpdir = tempfile.mkdtemp(prefix="_reconplogger_root_test_")
|
|
473
|
+
log_file = os.path.join(tmpdir, "root.log")
|
|
474
|
+
config = {
|
|
475
|
+
"version": 1,
|
|
476
|
+
"formatters": {
|
|
477
|
+
"plain": {
|
|
478
|
+
"format": "%(levelname)s %(message)s",
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
"handlers": {
|
|
482
|
+
"plain_handler": {
|
|
483
|
+
"class": "logging.StreamHandler",
|
|
484
|
+
"formatter": "plain",
|
|
485
|
+
"level": "WARNING",
|
|
486
|
+
},
|
|
487
|
+
"file_handler": {
|
|
488
|
+
"class": "logging.FileHandler",
|
|
489
|
+
"formatter": "plain",
|
|
490
|
+
"filename": log_file,
|
|
491
|
+
"level": "ERROR",
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
"loggers": {
|
|
495
|
+
"plain_logger": {
|
|
496
|
+
"level": "DEBUG",
|
|
497
|
+
"handlers": ["plain_handler", "file_handler"],
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
with patch.dict(os.environ, {"LOGGER_ROOT_HANDLER": "plain_handler"}, clear=False):
|
|
504
|
+
logger = reconplogger.logger_setup(config=config)
|
|
505
|
+
|
|
506
|
+
self.assertEqual(len(logger.handlers), 1)
|
|
507
|
+
self.assertIsInstance(logger.handlers[0], logging.FileHandler)
|
|
508
|
+
self.assertTrue(logger.propagate)
|
|
509
|
+
|
|
510
|
+
captured = StringIO()
|
|
511
|
+
root = logging.getLogger()
|
|
512
|
+
with patch.object(root.handlers[0], "stream", captured):
|
|
513
|
+
logger.error("root logger keeps file handlers")
|
|
514
|
+
|
|
515
|
+
logger.handlers[0].close()
|
|
516
|
+
self.assertIn("root logger keeps file handlers", captured.getvalue())
|
|
517
|
+
self.assertTrue(any("root logger keeps file handlers" in line for line in open(log_file).readlines()))
|
|
518
|
+
finally:
|
|
519
|
+
shutil.rmtree(tmpdir)
|
|
520
|
+
|
|
449
521
|
@patch.dict(
|
|
450
522
|
os.environ,
|
|
451
523
|
{"LOGGER_ROOT_HANDLER": "nonexistent_handler"},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reconplogger-5.0.0.dev1 → reconplogger-5.0.0.dev2}/reconplogger.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|