Skip to content

Commit 9fc3e8b

Browse files
committed
configuration: add logging section
Allow to configure both the logging-levels and targets of various loggers by adding a new section `logging` to the configuration file. Each entry in that section configures one logger / channel. An example config is: ```yaml logging: - name root level: INFO - name: foomodule.barcollector: level: WARNING target: /path/to/my/collector/logfile.log ```
1 parent fb5a956 commit 9fc3e8b

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a Ch
99

1010
### New
1111
* Fix \#47 - add environment variables for fundamental parameters
12+
* configuration: add logging section
1213

1314
### Changes
1415

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,20 @@ collector_opts:
113113
blacklist:
114114
- docker0
115115
- lo
116+
logging:
117+
- name: root
118+
level: INFO
119+
- name: foomodule.barcollector
120+
level: WARNING
121+
target: /path/to/my/collector/logfile.log
116122
```
117123
124+
Logging can optionally be configured for any logger. The entries must
125+
specify the name of the logger and can optionally specify a
126+
logging-level (default: stay at whatever the default logging-level for
127+
that logger is) and/or can specify a file to write the log to (default:
128+
log to stderr).
129+
118130
### Start-up Configuration
119131
120132
You can define two fundamental Parameters on program start-up. The following table summarized you options:

p3.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ collector_opts:
99
blacklist:
1010
- docker0
1111
- lo
12+
logging:
13+
- name: root
14+
level: INFO

p3exporter/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,35 @@
1313
from p3exporter.web import create_app
1414

1515

16+
def setup_logging(cfg: dict):
17+
"""Set up logging as configured.
18+
19+
The configuration may optionally contain an entry `logging`,
20+
if it does not or if that entry is not an array then does nothing.
21+
Each array element must be a dict that contains at least a key
22+
`name` that refers to the logger to configure. It may also contain
23+
the optional keys `level` and `target` that configure the
24+
logging-level and a file-target, respectively if present.
25+
26+
:param cfg: Configuration as read from config-file.
27+
:type cfg: dict
28+
"""
29+
if not isinstance(cfg.get('logging'), list):
30+
return
31+
for c in cfg['logging']:
32+
if not isinstance(c, dict):
33+
return
34+
if not isinstance(c.get('name'), str):
35+
return
36+
logger = logging.getLogger(c["name"])
37+
level = c.get('level')
38+
if level is not None:
39+
logger.setLevel(level)
40+
target = c.get('target')
41+
if target is not None:
42+
logger.addHandler(logging.FileHandler(target))
43+
44+
1645
def shutdown():
1746
"""Shutdown the app in a clean way."""
1847
logging.info('Shutting down, see you next time!')
@@ -41,6 +70,7 @@ def main():
4170
with open(args.config, 'r') as config_file:
4271
cfg = yaml.load(config_file, Loader=yaml.SafeLoader)
4372
collector_config = CollectorConfig(**cfg)
73+
setup_logging(cfg)
4474

4575
Collector(collector_config)
4676

tests/test_cases/logging_config.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from p3exporter import setup_logging
2+
import logging
3+
import os.path
4+
import pytest
5+
6+
7+
loggers = ["", "foo", "bar"]
8+
files = ["file1.log", "file2.log"]
9+
10+
11+
def setup_function(fn):
12+
"""Start with a clean slate of default logging-levels and no handlers."""
13+
for name in loggers:
14+
logger = logging.getLogger(name)
15+
level = logging.WARNING if name == "" else logging.NOTSET
16+
logger.setLevel(level)
17+
for handler in logger.handlers:
18+
logger.removeHandler(handler)
19+
20+
21+
def teardown_function(fn):
22+
"""Remove any files we may have created."""
23+
for file in files:
24+
if os.path.exists(file):
25+
os.remove(file)
26+
27+
28+
data_logging_levels = [
29+
pytest.param(None,
30+
[logging.WARNING, logging.NOTSET, logging.NOTSET],
31+
[None, None, None],
32+
id="no logging-section at all"),
33+
pytest.param("Not an array",
34+
[logging.WARNING, logging.NOTSET, logging.NOTSET],
35+
[None, None, None],
36+
id="logging-section has wrong type"),
37+
pytest.param([{"level": "INFO"},
38+
{"target": "file1.log"},
39+
{"level": "DEBUG", "target": "file2.log"}],
40+
[logging.WARNING, logging.NOTSET, logging.NOTSET],
41+
[None, None, None],
42+
id="no names in otherwise valid entries"),
43+
pytest.param([{"name": "", "level": "INFO"},
44+
{"name": "foo", "level": "DEBUG"}],
45+
[logging.INFO, logging.DEBUG, logging.NOTSET],
46+
[None, None, None],
47+
id="levels only, using empty-string for root"),
48+
pytest.param([{"name": "root", "level": "ERROR"},
49+
{"name": "bar", "level": "CRITICAL"}],
50+
[logging.ERROR, logging.NOTSET, logging.CRITICAL],
51+
[None, None, None],
52+
id="levels only, using name of root"),
53+
pytest.param([{"name": "foo", "level": 10},
54+
{"name": "bar", "level": 20}],
55+
[logging.WARNING, logging.DEBUG, logging.INFO],
56+
[None, None, None],
57+
id="levels only, using integers for levels"),
58+
pytest.param([{"name": "root", "target": "file1.log"},
59+
{"name": "foo", "target": "file2.log"}],
60+
[logging.WARNING, logging.NOTSET, logging.NOTSET],
61+
["file1.log", "file2.log", None],
62+
id="targets only"),
63+
pytest.param([{"name": "foo", "level": "INFO", "target": "file1.log"}],
64+
[logging.WARNING, logging.INFO, logging.NOTSET],
65+
[None, "file1.log", None],
66+
id="both level and target"),
67+
]
68+
69+
70+
@pytest.mark.parametrize("cfg_logging,levels,targets", data_logging_levels)
71+
def test_logging_levels(cfg_logging, levels, targets):
72+
# pytest adds lots of extra handlers, so remember the starting state
73+
orig_handlers = []
74+
for name in loggers:
75+
logger = logging.getLogger(name)
76+
orig_handlers.append(logger.handlers.copy())
77+
78+
# GIVEN an input config-dictionary
79+
cfg = {
80+
"exporter_name": "Test only",
81+
"collectors": [],
82+
"collector_opts": {},
83+
}
84+
if cfg_logging is not None:
85+
cfg["logging"] = cfg_logging
86+
87+
# WHEN calling setup_logging()
88+
setup_logging(cfg)
89+
90+
# THEN the logging-levels should get changed to the expected
91+
for i, name in enumerate(loggers):
92+
logger = logging.getLogger(name)
93+
assert logger.level == levels[i]
94+
95+
# AND the expected file-handlers should get added
96+
for i, name in enumerate(loggers):
97+
logger = logging.getLogger(name)
98+
added_handlers = [h for h in logger.handlers
99+
if h not in orig_handlers[i]]
100+
if targets[i] is None:
101+
assert len(added_handlers) == 0
102+
else:
103+
assert len(added_handlers) == 1
104+
handler = added_handlers[0]
105+
assert isinstance(handler, logging.FileHandler)
106+
assert handler.baseFilename == os.path.abspath(targets[i])

0 commit comments

Comments
 (0)