Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: CI

on:
push:
branches: [main]
tags: [v*.*.*]
branches: [ main ]
tags: [ v*.*.* ]

pull_request:
branches: [ "main" ]
branches: [ main ]
types:
- synchronize
- opened
Expand Down
9 changes: 8 additions & 1 deletion context_logger/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import socket
from importlib.metadata import version, PackageNotFoundError
from logging import Filter, LogRecord
from typing import Any


class ContextSetupFilter(Filter):

def __init__(self, application_name: str, message_field: str):
def __init__(self, application_name: str, message_field: str, global_context: dict[str, Any] | None = None):
super().__init__()
self._application_name = application_name
self._message_field = message_field
self._global_context = global_context if global_context else {}

def filter(self, record: LogRecord) -> bool:
try:
Expand All @@ -26,6 +28,11 @@ def filter(self, record: LogRecord) -> bool:

if 'process_name' in record.msg:
record.msg['process_name'] = record.processName

for key, value in self._global_context.items():
if key not in record.msg:
record.msg[key] = str(value)

except Exception as exception:
print('Failed to handle log record:', exception)

Expand Down
16 changes: 10 additions & 6 deletions context_logger/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def get_logger(logger_name: str) -> Any:

def setup_logging(application_name: str, log_level: str = 'INFO',
log_file_path: Optional[str] = None, max_bytes: int = 1024 * 1024, backup_count: int = 5,
add_call_info: bool = False, warn_on_overwrite: bool = True, message_field: str = 'message') -> None:
add_call_info: bool = False, warn_on_overwrite: bool = True, message_field: str = 'message',
global_context: dict[str, Any] | None = None) -> None:
global LOGGER

if LOGGER:
Expand All @@ -36,22 +37,25 @@ def setup_logging(application_name: str, log_level: str = 'INFO',

LOGGER.cleanup()

LOGGER = Logger(application_name, log_level, log_file_path, max_bytes, backup_count, add_call_info, message_field)
LOGGER = Logger(application_name, log_level, log_file_path, max_bytes, backup_count, add_call_info, message_field,
global_context)

LOGGER.setup()


class Logger(object):

def __init__(self, application_name: str, log_level: str, log_file_path: Optional[str], max_bytes: int,
backup_count: int, add_call_info: bool, message_field: str) -> None:
backup_count: int, add_call_info: bool, message_field: str,
global_context: dict[str, Any] | None = None) -> None:
self._application_name = application_name
self._log_level = logging.getLevelName(log_level.upper())
self._log_file_path = log_file_path
self._max_bytes = max_bytes
self._backup_count = backup_count
self._message_field = message_field
self._add_call_info = add_call_info
self._message_field = message_field
self._global_context = global_context
self._handlers: list[Handler] = []

def setup(self) -> None:
Expand Down Expand Up @@ -131,7 +135,7 @@ def _create_console_handler(self) -> Handler:

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
handler.addFilter(ContextSetupFilter(self._application_name, self._message_field))
handler.addFilter(ContextSetupFilter(self._application_name, self._message_field, self._global_context))

return handler

Expand All @@ -146,7 +150,7 @@ def _create_file_handler(self, log_file_path: str) -> RotatingFileHandler:

handler = RotatingFileHandler(log_file_path, maxBytes=self._max_bytes, backupCount=self._backup_count)
handler.setFormatter(formatter)
handler.addFilter(ContextSetupFilter(self._application_name, self._message_field))
handler.addFilter(ContextSetupFilter(self._application_name, self._message_field, self._global_context))

return handler

Expand Down
33 changes: 33 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[project]
name = "python-context-logger"
description = "Contextual structured logging library for Python"
authors = [
{ name = "Ferenc Nandor Janky & Attila Gombos", email = "info@effective-range.com" }
]
maintainers = [
{ name = "Ferenc Nandor Janky & Attila Gombos", email = "info@effective-range.com" }
]
dependencies = [
"structlog"
]
dynamic = ["version"]

[tool.setuptools]
package-dir = {"" = "."}
packages = ["context_logger"]

[tool.setuptools.package-data]
hello = ["py.typed"]

[build-system]
requires = ["setuptools>=61", "setuptools_scm"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
version_scheme = "guess-next-dev"
local_scheme = "node-and-date"

[tool.pytest]
addopts = ["--verbose", "--capture=no"]
python_files = ["*Test.py"]
python_classes = ["*Test"]
11 changes: 6 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[pack-python]
packaging =
wheel
fpm-deb

[mypy]
packages = context_logger
strict = True
Expand All @@ -22,6 +27,7 @@ python_classes = *Test
[coverage:run]
relative_files = true
branch = True
source = context_logger

[coverage:report]
; Regexes for lines to exclude from consideration
Expand Down Expand Up @@ -49,8 +55,3 @@ directory = coverage/html

[coverage:json]
output = coverage/coverage.json

[pack-python]
packaging =
wheel
fpm-deb
13 changes: 0 additions & 13 deletions setup.py

This file was deleted.

100 changes: 100 additions & 0 deletions tests/filterTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import logging
import unittest
from importlib.metadata import PackageNotFoundError
from unittest import TestCase
from unittest.mock import patch

from context_logger.filter import ContextSetupFilter


class FilterTest(TestCase):

def test_filter_enriches_string_message_and_context(self):
# Given
filter_ = ContextSetupFilter('example-app', 'message',
global_context={'team': 'platform', 'application': 'ignored'})
record = logging.LogRecord('ExampleClass', logging.INFO, __file__, 10, 'Hello %s', ('World',), None)

with patch('context_logger.filter.socket.gethostname', return_value='test-host'), \
patch.object(filter_, '_get_application_version', return_value='1.2.3'):
# When
result = filter_.filter(record)

# Then
self.assertTrue(result)
self.assertEqual((), record.args)
self.assertEqual('Hello World', record.msg.get('message'))
self.assertEqual('test-host', record.msg.get('hostname'))
self.assertEqual('example-app', record.msg.get('application'))
self.assertEqual('1.2.3', record.msg.get('app_version'))
self.assertEqual('platform', record.msg.get('team'))

def test_filter_updates_process_name_when_key_exists(self):
# Given
filter_ = ContextSetupFilter('example-app', 'message')
record = logging.LogRecord('ExampleClass', logging.INFO, __file__, 10, {'message': 'ok', 'process_name': 'x'},
(), None)

with patch('context_logger.filter.socket.gethostname', return_value='test-host'), \
patch.object(filter_, '_get_application_version', return_value='1.2.3'):
# When
filter_.filter(record)

# Then
self.assertEqual(record.processName, record.msg.get('process_name'))

def test_filter_handles_string_format_error(self):
# Given
filter_ = ContextSetupFilter('example-app', 'message')
record = logging.LogRecord('ExampleClass', logging.INFO, __file__, 10, 'broken %s %s', ('format',), None)

with patch('builtins.print') as print_mock:
# When
result = filter_.filter(record)

# Then
self.assertTrue(result)
self.assertEqual('broken %s %s', record.msg)
print_mock.assert_called_once()
self.assertEqual('Failed to handle log record:', print_mock.call_args.args[0])

def test_filter_handles_non_mapping_message(self):
# Given
filter_ = ContextSetupFilter('example-app', 'message')
record = logging.LogRecord('ExampleClass', logging.INFO, __file__, 10, 123, (), None)

with patch('builtins.print') as print_mock:
# When
result = filter_.filter(record)

# Then
self.assertTrue(result)
self.assertEqual(123, record.msg)
print_mock.assert_called_once()
self.assertEqual('Failed to handle log record:', print_mock.call_args.args[0])

def test_get_application_version_returns_package_version(self):
# Given
filter_ = ContextSetupFilter('example-app', 'message')

with patch('context_logger.filter.version', return_value='9.9.9'):
# When
version = filter_._get_application_version()

# Then
self.assertEqual('9.9.9', version)

def test_get_application_version_returns_none_when_package_is_missing(self):
# Given
filter_ = ContextSetupFilter('missing-package', 'message')

with patch('context_logger.filter.version', side_effect=PackageNotFoundError):
# When
version = filter_._get_application_version()

# Then
self.assertEqual('none', version)


if __name__ == '__main__':
unittest.main()
Loading
Loading