Skip to content

Commit 2b2eadc

Browse files
authored
Adding a logging module to ETOS library (#8)
1 parent 142d7ba commit 2b2eadc

File tree

7 files changed

+280
-82
lines changed

7 files changed

+280
-82
lines changed

src/etos_lib/lib/debug.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
DEPRECATION WARNING: Some parameters which don't belong here will be removed.
1919
"""
2020
import os
21+
from pathlib import Path
2122
from collections import deque
2223

2324

@@ -36,6 +37,16 @@ def default_secret_path(self):
3637
"""Path to k8s secrets."""
3738
return os.getenv("ETOS_SECRET_PATH", "/etc/")
3839

40+
@property
41+
def default_log_path(self):
42+
""""Default log path."""
43+
path = os.getenv("ETOS_LOG_PATH")
44+
if path is None:
45+
path = Path.home().joinpath("logs/log.json")
46+
else:
47+
path = Path(path)
48+
return path
49+
3950
@property
4051
def disable_sending_events(self):
4152
"""Disable sending eiffel events."""

src/etos_lib/lib/logs.py

Lines changed: 0 additions & 82 deletions
This file was deleted.

src/etos_lib/logging/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2020 Axis Communications AB.
2+
#
3+
# For a full list of individual contributors, please see the commit history.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
"""ETOS logging module."""
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[loggers]
2+
keys=root
3+
4+
[handlers]
5+
keys=generic,etos
6+
7+
[formatters]
8+
keys=generic,etos
9+
10+
[logger_root]
11+
level=DEBUG
12+
handlers=generic,etos
13+
14+
[handler_generic]
15+
class=StreamHandler
16+
kwargs={"stream": sys.stdout}
17+
formatter=generic
18+
19+
[handler_etos]
20+
class=FileHandler
21+
args=("%(logfilename)s",)
22+
formatter=etos
23+
24+
[formatter_generic]
25+
format=[%(asctime)s][%(identifier)s] %(levelname)s: %(message)s
26+
datefmt=%Y-%m-%d %H:%M:%S
27+
class=logging.Formatter
28+
29+
[formatter_etos]
30+
class=etos_lib.logging.formatter.EtosLogFormatter

src/etos_lib/logging/filter.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2020 Axis Communications AB.
2+
#
3+
# For a full list of individual contributors, please see the commit history.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
"""ETOS filter."""
17+
import logging
18+
19+
20+
class EtosFilter(logging.Filter): # pylint:disable=too-few-public-methods
21+
"""Filter for adding extra application specific data to log messages."""
22+
23+
def __init__(self, application, version, environment):
24+
"""Initialize with a few ETOS application fields.
25+
26+
:param application: Name of application.
27+
:type application: str
28+
:param version: Version of application.
29+
:type version: str
30+
:param environment: In which environment is this executing.
31+
:type environment: str
32+
"""
33+
self.application = application
34+
self.version = version
35+
self.environment = environment
36+
super().__init__()
37+
38+
def filter(self, record):
39+
"""Add contextual data to log record.
40+
41+
:param record: Log record to add data to.
42+
:type record: :obj:`logging.LogRecord`
43+
:return: True
44+
:rtype: bool
45+
"""
46+
record.application = self.application
47+
record.version = self.version
48+
record.environment = self.environment
49+
return True

src/etos_lib/logging/formatter.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright 2020 Axis Communications AB.
2+
#
3+
# For a full list of individual contributors, please see the commit history.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
"""ETOS log formatter."""
17+
import datetime
18+
import json
19+
import logging
20+
import sys
21+
import traceback
22+
23+
24+
# LogRecord fields that we exclude, typically because we offer
25+
# something better.
26+
_EXCLUDED_FIELDS = (
27+
# Superfluous since we're saving the timestamp in '@timestamp'.
28+
"asctime",
29+
# The arguments to the format string can contain arbitrary objects
30+
# that can't be serialized to JSON. We could pass them to str(),
31+
# but that assumes that '%s' is used in the format string.
32+
"args",
33+
"msg",
34+
# Exception info is saved in exception.{type,message,stacktrace}
35+
# instead.
36+
"exc_info",
37+
"exc_text",
38+
"stack_info",
39+
)
40+
41+
42+
class EtosLogFormatter(logging.Formatter):
43+
"""Logging formatter that produces a JSON object.
44+
45+
The resulting JSON object contains the logging.LogRecord fields
46+
untouched. The following exception from this rule exist:
47+
48+
- A '@timestamp' key with the log record's UTC timestamp in ISO8601
49+
format, digestible by Logstash.
50+
- If an exception context exists (i.e. sys.exc_info() returns
51+
something) the resulting JSON object will contain an 'exception'
52+
key pointing to an object with the following keys:
53+
54+
- 'type': The fully-qualified type name (i.e. including
55+
package and module).
56+
- 'message': The exception message.
57+
- 'stacktrace': The formatted stacktrace as a multiline string
58+
(not ending in a newline character).
59+
"""
60+
61+
def format(self, record):
62+
"""Serialize LogRecord data as JSON.
63+
Overrides the inherited behavior by ignoring any configured
64+
format string and storing attributes of the passed LogRecord
65+
object in a dictionary and serializing it to JSON. See class
66+
docstring for details.
67+
"""
68+
fields = {
69+
k: v
70+
for k, v in record.__dict__.items()
71+
if k not in _EXCLUDED_FIELDS and not k.startswith("_")
72+
}
73+
fields["@timestamp"] = self.formatTime(record)
74+
fields["message"] = record.getMessage()
75+
exc_type, exc_message, stack = sys.exc_info()
76+
if exc_type and exc_message and stack:
77+
fields["exception"] = {
78+
"type": "{}.{}".format(exc_type.__module__, exc_type.__name__),
79+
"message": str(exc_message),
80+
# format_tb() returns a list of newline-terminated strings.
81+
# Don't include the final newline.
82+
"stacktrace": "".join(traceback.format_tb(stack)).rstrip(),
83+
}
84+
# No need to append a newline character; that's up to the handler.
85+
return json.dumps(fields)
86+
87+
def formatTime(self, record, datefmt=None):
88+
"""Formats the log record's timestamp in ISO8601 format, in UTC.
89+
Overrides the inherited behavior by always returning ISO8601
90+
strings without allowing custom date formats.
91+
Raises:
92+
NotImplementedError: If the `datefmt` parameter is not None.
93+
"""
94+
if datefmt is not None:
95+
raise NotImplementedError("Only default time format is supported.")
96+
# Make Logstash's @timestamp parser happy by including a "T"
97+
# between the date and the time. Append 'Z' to make it clear
98+
# that the timestamp is UTC.
99+
return datetime.datetime.utcfromtimestamp(record.created).strftime(
100+
"%Y-%m-%dT%H:%M:%S.%fZ"
101+
)

src/etos_lib/logging/logger.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2020 Axis Communications AB.
2+
#
3+
# For a full list of individual contributors, please see the commit history.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
"""ETOS logger.
17+
18+
Example::
19+
20+
from uuid import uuid4
21+
from etos_lib.logging.logger import setup_logging, get_logger
22+
23+
setup_logging("myApp", "1.0.0", "production")
24+
logger = get_logger(__name__, str(uuid4()))
25+
logger.info("Hello!")
26+
>>> [2020-12-16 10:35:00][cb7c8cd9-40a6-4ecc-8321-a1eae6beae35] INFO: Hello!
27+
28+
"""
29+
from pathlib import Path
30+
import logging
31+
import logging.config
32+
from etos_lib.logging.filter import EtosFilter
33+
from etos_lib.lib.debug import Debug
34+
from etos_lib.lib.config import Config
35+
36+
DEFAULT_CONFIG = Path(__file__).parent.joinpath("default_config.conf")
37+
DEFAULT_LOG_PATH = Debug().default_log_path
38+
DEFAULT_LOG_PATH.parent.mkdir(exist_ok=True, parents=True)
39+
40+
41+
def setup_logging(
42+
application, version, environment, filename=DEFAULT_CONFIG, output=DEFAULT_LOG_PATH
43+
):
44+
"""Set up basic logging.
45+
46+
:param application: Name of application to setup logging for.
47+
:type application: str
48+
:param version: Version of application to setup logging for.
49+
:type version: str
50+
:param environment: Environment in which this application resides.
51+
: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
56+
"""
57+
Config().set("log_filter", EtosFilter(application, version, environment))
58+
logging.config.fileConfig(filename, defaults={"logfilename": output})
59+
60+
61+
def get_logger(name, identifier):
62+
"""Get a logger adapter with attached identiifer.
63+
64+
:param name: Name of logger to get.
65+
:type name: str
66+
:param identifier: Unique identifier to attach to logger.
67+
:type identifier: str
68+
:return: LoggerAdapter instance.
69+
:rtype: :obj:`logging.LoggerAdapter`
70+
"""
71+
logger = logging.getLogger(name)
72+
logger.addFilter(Config().get("log_filter"))
73+
return logging.LoggerAdapter(logger, {"identifier": identifier})

0 commit comments

Comments
 (0)