Skip to content

Commit 96d02ae

Browse files
authored
Make the logging module more configurable and easier to use (#9)
By utilizing the threading local construct we can add the identifiers at the start of each thread instead and all loggers within that thread will use the same identifier automatically. This also removes the need to execute the "get_logger" function in each thread and requires a lot less logging adapters to be created
1 parent 4d4a423 commit 96d02ae

File tree

6 files changed

+123
-59
lines changed

6 files changed

+123
-59
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ cryptography==3.1
77
redis==3.5.3
88
eiffellib==1.1.0
99
pydantic==1.6
10+
python-box==5.2.0

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ install_requires =
3333
pydantic==1.6
3434
requests==2.24.0
3535
kubernetes==7.0.1
36+
python-box==5.2.0
3637

3738
# Require a specific Python version, e.g. Python 2.7 or >= 3.4
3839
python_requires = >=3.4

src/etos_lib/logging/default_config.conf

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
logging:
2+
stream:
3+
loglevel: INFO
4+
file:
5+
loglevel: DEBUG

src/etos_lib/logging/filter.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
class EtosFilter(logging.Filter): # pylint:disable=too-few-public-methods
2121
"""Filter for adding extra application specific data to log messages."""
2222

23-
def __init__(self, application, version, environment):
23+
def __init__(self, application, version, environment, local):
2424
"""Initialize with a few ETOS application fields.
2525
2626
:param application: Name of application.
@@ -29,10 +29,13 @@ def __init__(self, application, version, environment):
2929
:type version: str
3030
:param environment: In which environment is this executing.
3131
:type environment: str
32+
:param local: Thread-local configuration information.
33+
:type local: :obj:`threading.local`
3234
"""
3335
self.application = application
3436
self.version = version
3537
self.environment = environment
38+
self.local = local
3639
super().__init__()
3740

3841
def filter(self, record):
@@ -43,9 +46,16 @@ def filter(self, record):
4346
:return: True
4447
:rtype: bool
4548
"""
46-
if not hasattr(record, "identifier"):
47-
record.identifier = "Unknown"
4849
record.application = self.application
4950
record.version = self.version
5051
record.environment = self.environment
52+
53+
# Add each thread-local attribute to record.
54+
for attr in dir(self.local):
55+
if attr.startswith("__") and attr.endswith("__"):
56+
continue
57+
setattr(record, attr, getattr(self.local, attr))
58+
if not hasattr(record, "identifier"):
59+
record.identifier = "Unknown"
60+
5161
return True

src/etos_lib/logging/logger.py

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,108 @@
1717
1818
Example::
1919
20+
import logging
2021
from uuid import uuid4
21-
from etos_lib.logging.logger import setup_logging, get_logger
22+
from etos_lib.logging.logger import setup_logging, FORMAT_CONFIG
2223
24+
FORMAT_CONFIG.identifier = str(uuid4())
2325
setup_logging("myApp", "1.0.0", "production")
24-
logger = get_logger(__name__, str(uuid4()))
26+
logger = logging.getLogger(__name__)
2527
logger.info("Hello!")
2628
>>> [2020-12-16 10:35:00][cb7c8cd9-40a6-4ecc-8321-a1eae6beae35] INFO: Hello!
2729
2830
"""
31+
import sys
2932
from pathlib import Path
33+
import threading
3034
import logging
3135
import logging.config
36+
from box import Box
3237
from etos_lib.logging.filter import EtosFilter
38+
from etos_lib.logging.formatter import EtosLogFormatter
3339
from etos_lib.lib.debug import Debug
34-
from etos_lib.lib.config import Config
3540

36-
DEFAULT_CONFIG = Path(__file__).parent.joinpath("default_config.conf")
41+
DEFAULT_CONFIG = Path(__file__).parent.joinpath("default_config.yaml")
3742
DEFAULT_LOG_PATH = Debug().default_log_path
38-
DEFAULT_LOG_PATH.parent.mkdir(exist_ok=True, parents=True)
43+
44+
FORMAT_CONFIG = threading.local()
45+
46+
47+
def setup_file_logging(config, log_filter):
48+
"""Set up logging to file using the ETOS log formatter.
49+
50+
Cofiguration file parameters ('file' must exist or no file handler is set up):
51+
52+
logging:
53+
file:
54+
# Log level for file logging. Default=DEBUG.
55+
loglevel: INFO
56+
# Where to store logfile. Default=/home/you/etos/output.log.json
57+
logfile: path/to/log/file
58+
# Maximum number of files to rotate. Default=10
59+
max_files: 5
60+
# Maximum number of bytes in each logfile. Default=1048576/1MB
61+
max_bytes: 100
62+
63+
:param config: File logging configuration.
64+
:type config: :obj:`Box`
65+
:param log_filter: Logfilter to add to file handler.
66+
:type log_filter: :obj:`EtosFilter`
67+
"""
68+
loglevel = getattr(logging, config.get("loglevel", "DEBUG"))
69+
logfile = Path(config.get("logfile", DEFAULT_LOG_PATH))
70+
logfile.parent.mkdir(parents=True, exist_ok=True)
71+
72+
max_files = config.get("max_files", 10)
73+
max_bytes = config.get("max_bytes", 10485760) # Default is 10 MB
74+
root_logger = logging.getLogger()
75+
76+
file_handler = logging.handlers.RotatingFileHandler(
77+
logfile, maxBytes=max_bytes, backupCount=max_files
78+
)
79+
file_handler.setFormatter(EtosLogFormatter())
80+
file_handler.setLevel(loglevel)
81+
file_handler.addFilter(log_filter)
82+
root_logger.addHandler(file_handler)
83+
84+
85+
def setup_stream_logging(config, log_filter):
86+
"""Set up logging to stdout stream.
87+
88+
Cofiguration file parameters ('stream' must exist or no stream handler is set up):
89+
90+
logging:
91+
stream:
92+
# Log level for stream logging. Default=INFO.
93+
loglevel: ERROR
94+
# Format to print logs with.
95+
# Default: [%(asctime)s][%(identifier)s] %(levelname)s:%(name)s: %(message)s
96+
logformat: %(message)s
97+
# Dateformat for %(asctime) format. Default: %Y-%m-%d %H:%M:%S
98+
dateformat: %Y-%d-%m %H:%M:%S
99+
100+
:param config: Stream logging configuration.
101+
:type config: :obj:`Box`
102+
:param log_filter: Logfilter to add to stream handler.
103+
:type log_filter: :obj:`EtosFilter`
104+
"""
105+
loglevel = getattr(logging, config.get("loglevel", "INFO"))
106+
107+
logformat = config.get(
108+
"logformat",
109+
"[%(asctime)s][%(identifier)s] %(levelname)s:%(name)s: %(message)s"
110+
)
111+
dateformat = config.get("dateformat", "%Y-%m-%d %H:%M:%S")
112+
root_logger = logging.getLogger()
113+
stream_handler = logging.StreamHandler(sys.stdout)
114+
stream_handler.setFormatter(logging.Formatter(logformat, datefmt=dateformat))
115+
stream_handler.setLevel(loglevel)
116+
stream_handler.addFilter(log_filter)
117+
root_logger.addHandler(stream_handler)
39118

40119

41120
def setup_logging(
42-
application, version, environment, filename=DEFAULT_CONFIG, output=DEFAULT_LOG_PATH
121+
application, version, environment, config_file=DEFAULT_CONFIG
43122
):
44123
"""Set up basic logging.
45124
@@ -49,27 +128,25 @@ def setup_logging(
49128
:type version: str
50129
:param environment: Environment in which this application resides.
51130
:type environment: str
52-
:param filename: Filename of logging configuration.
53-
:type filename: str
54-
:param output: Output filename for logging to file.
55-
:type output: str
131+
:param config_file: Filename of logging configuration.
132+
:type config_file: str
56133
"""
57-
Config().set("log_filter", EtosFilter(application, version, environment))
58-
logging.config.fileConfig(filename, defaults={"logfilename": output})
59-
root_logger = logging.getLogger()
60-
root_logger.addFilter(Config().get("log_filter"))
134+
with open(config_file) as yaml_file:
135+
config = Box.from_yaml(yaml_file)
136+
logging_config = config.logging
61137

138+
log_filter = EtosFilter(application, version, environment, FORMAT_CONFIG)
62139

63-
def get_logger(name, identifier):
64-
"""Get a logger adapter with attached identiifer.
140+
# Create a default logger which will not propagate messages
141+
# to the root logger. This logger will create records for all
142+
# messages, but not print them to stdout. Stdout printing
143+
# is setup in "setup_stream_logging" if the "stream" key exists
144+
# in the configuration file.
145+
root_logger = logging.getLogger()
146+
root_logger.setLevel(logging.DEBUG)
147+
root_logger.propagate = 0
65148

66-
:param name: Name of logger to get.
67-
:type name: str
68-
:param identifier: Unique identifier to attach to logger.
69-
:type identifier: str
70-
:return: LoggerAdapter instance.
71-
:rtype: :obj:`logging.LoggerAdapter`
72-
"""
73-
logger = logging.getLogger(name)
74-
logger.addFilter(Config().get("log_filter"))
75-
return logging.LoggerAdapter(logger, {"identifier": identifier})
149+
if logging_config.get("stream"):
150+
setup_stream_logging(logging_config.get("stream"), log_filter)
151+
if logging_config.get("file"):
152+
setup_file_logging(logging_config.get("file"), log_filter)

0 commit comments

Comments
 (0)