Skip to content

Commit 11f48b7

Browse files
committed
Split logs by severity
1 parent 0e47882 commit 11f48b7

File tree

2 files changed

+51
-5
lines changed

2 files changed

+51
-5
lines changed

src/stopliga/logging_utils.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from contextvars import ContextVar
77
import json
88
import logging
9+
import sys
910
from typing import Any
1011

1112

@@ -159,7 +160,7 @@ def _visible_fields(event: str | None, fields: dict[str, Any], levelno: int) ->
159160

160161

161162
class KeyValueFormatter(logging.Formatter):
162-
"""Simple key=value formatter that stays readable in Docker stdout."""
163+
"""Simple key=value formatter that stays readable in container logs."""
163164

164165
def format(self, record: logging.LogRecord) -> str:
165166
fields = getattr(record, "fields", {})
@@ -193,12 +194,20 @@ def format(self, record: logging.LogRecord) -> str:
193194
def configure_logging(level_name: str) -> None:
194195
"""Configure application-wide logging."""
195196

196-
handler = logging.StreamHandler()
197-
handler.setFormatter(KeyValueFormatter())
197+
formatter = KeyValueFormatter()
198+
stdout_handler = logging.StreamHandler(sys.stdout)
199+
stdout_handler.setFormatter(formatter)
200+
stdout_handler.addFilter(lambda record: record.levelno < logging.ERROR)
201+
202+
stderr_handler = logging.StreamHandler(sys.stderr)
203+
stderr_handler.setFormatter(formatter)
204+
stderr_handler.addFilter(lambda record: record.levelno >= logging.ERROR)
205+
198206
root = logging.getLogger()
199207
root.handlers.clear()
200208
root.setLevel(getattr(logging, level_name.upper(), logging.INFO))
201-
root.addHandler(handler)
209+
root.addHandler(stdout_handler)
210+
root.addHandler(stderr_handler)
202211

203212

204213
def log_event(logger: logging.Logger, level: int, event: str, **fields: Any) -> None:

tests/test_logging.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import annotations
22

3+
import io
34
import logging
45
import unittest
56
from pathlib import Path
67
import sys
8+
from unittest.mock import patch
79

810

911
ROOT = Path(__file__).resolve().parents[1]
@@ -12,7 +14,7 @@
1214
if str(SRC) not in sys.path:
1315
sys.path.insert(0, str(SRC))
1416

15-
from stopliga.logging_utils import KeyValueFormatter # noqa: E402
17+
from stopliga.logging_utils import KeyValueFormatter, configure_logging # noqa: E402
1618

1719

1820
class LoggingFormatterTests(unittest.TestCase):
@@ -91,3 +93,38 @@ def test_missing_vpn_client_network_log_includes_docs_url(self) -> None:
9193
'docs_url="https://github.com/jcastro/stopliga/blob/main/README.md#vpn-client-network-required"',
9294
output,
9395
)
96+
97+
98+
class LoggingConfigurationTests(unittest.TestCase):
99+
def setUp(self) -> None:
100+
root = logging.getLogger()
101+
self._original_handlers = list(root.handlers)
102+
self._original_level = root.level
103+
104+
def tearDown(self) -> None:
105+
root = logging.getLogger()
106+
root.handlers.clear()
107+
root.handlers.extend(self._original_handlers)
108+
root.setLevel(self._original_level)
109+
110+
def test_logs_are_split_between_stdout_and_stderr_by_severity(self) -> None:
111+
stdout = io.StringIO()
112+
stderr = io.StringIO()
113+
114+
with patch("sys.stdout", stdout), patch("sys.stderr", stderr):
115+
configure_logging("DEBUG")
116+
logger = logging.getLogger("stopliga.test")
117+
logger.info("info_message")
118+
logger.warning("warning_message")
119+
logger.error("error_message")
120+
121+
stdout_output = stdout.getvalue()
122+
stderr_output = stderr.getvalue()
123+
124+
self.assertIn("INFO info_message", stdout_output)
125+
self.assertIn("WARNING warning_message", stdout_output)
126+
self.assertNotIn("ERROR error_message", stdout_output)
127+
128+
self.assertIn("ERROR error_message", stderr_output)
129+
self.assertNotIn("INFO info_message", stderr_output)
130+
self.assertNotIn("WARNING warning_message", stderr_output)

0 commit comments

Comments
 (0)