Skip to content

Commit df97d00

Browse files
authored
chore: simplify logging output (#243)
1 parent d5248a4 commit df97d00

5 files changed

Lines changed: 206 additions & 196 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ DISCORD_TOKEN=your_bot_token_here
2929

3030
# Logging (optional)
3131
# Levels: DEBUG, INFO, WARNING, ERROR
32+
# Logs are plain single-line stdout.
3233
LOG_LEVEL=INFO
3334

3435
# Channel ID for reminders (optional)

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ guild_config (
116116
- `typer_bot/utils/config.py`: Centralized configuration (data paths via env vars).
117117
- `typer_bot/utils/prediction_parser.py`: Central logic for parsing "2-1" or "2:1" strings.
118118
- `typer_bot/utils/scoring.py`: Point calculation using season scoring rules.
119-
- `typer_bot/utils/logger.py`: structured logging configuration for local and deployed environments.
119+
- `typer_bot/utils/logger.py`: plain stdout logging setup with contextual fields.
120120
- `typer_bot/utils/db_backup.py`: Automatic database backup after successful score calculation.
121121
- `scripts/restore_db.py`: Manual database restore from a host or container shell.
122122

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ uv run python -m typer_bot
7777

7878
Disposable non-production deployments can auto-seed an empty database by setting `SEED_TEST_DATA=true` and `TEST_GUILD_ID`.
7979

80+
Logs are plain single-line stdout; set `LOG_LEVEL=DEBUG` when troubleshooting.
81+
8082
Run checks:
8183

8284
```bash

tests/test_logger.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Tests for runtime logging setup."""
2+
3+
import io
4+
import logging
5+
import re
6+
7+
import pytest
8+
9+
from typer_bot.utils import logger as logger_module
10+
11+
12+
@pytest.fixture(autouse=True)
13+
def restore_logging_state():
14+
root_logger = logging.getLogger()
15+
root_handlers = list(root_logger.handlers)
16+
root_level = root_logger.level
17+
discord_level = logging.getLogger("discord").level
18+
discord_http_level = logging.getLogger("discord.http").level
19+
trace_id = logger_module.get_trace_id()
20+
log_context = logger_module.get_log_context()
21+
22+
yield
23+
24+
root_logger.handlers.clear()
25+
root_logger.handlers.extend(root_handlers)
26+
root_logger.setLevel(root_level)
27+
logging.getLogger("discord").setLevel(discord_level)
28+
logging.getLogger("discord.http").setLevel(discord_http_level)
29+
logger_module.set_trace_id(trace_id)
30+
logger_module.clear_log_context()
31+
logger_module.set_log_context(**log_context)
32+
33+
34+
def _configure_and_emit(monkeypatch, output: io.StringIO, logger_name: str = "test.logger") -> str:
35+
monkeypatch.setattr(logger_module.sys, "stdout", output)
36+
37+
logger_module.setup_logging(logging.INFO)
38+
logging.getLogger(logger_name).info("readable message")
39+
40+
return output.getvalue().splitlines()[-1]
41+
42+
43+
def test_setup_logging_emits_plain_logs(monkeypatch):
44+
output = io.StringIO()
45+
46+
log_line = _configure_and_emit(monkeypatch, output, "test.plain")
47+
48+
assert "\x1b[" not in log_line
49+
assert not log_line.startswith("{")
50+
assert re.match(r"^20\d{2}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00\s", log_line)
51+
assert "INFO" in log_line
52+
assert "test.plain" in log_line
53+
assert "readable message" in log_line
54+
55+
56+
def test_setup_logging_includes_context_and_extra_fields(monkeypatch):
57+
output = io.StringIO()
58+
monkeypatch.setattr(logger_module.sys, "stdout", output)
59+
60+
logger_module.set_trace_id("req-1")
61+
logger_module.set_log_context(guild_id="guild-1")
62+
logger_module.setup_logging(logging.INFO)
63+
logging.getLogger("test.context").info(
64+
"context message",
65+
extra={
66+
"event_type": "prediction.saved",
67+
"error_detail": "Fixture not found",
68+
"payload": {2: "second", "token": "secret-value", "safe": "visible"},
69+
"token": "secret-value",
70+
},
71+
)
72+
73+
log_line = output.getvalue().splitlines()[-1]
74+
assert "context message" in log_line
75+
assert "secret-value" not in log_line
76+
assert 'error_detail="Fixture not found"' in log_line
77+
assert "event_type=prediction.saved" in log_line
78+
assert "guild_id=guild-1" in log_line
79+
assert "payload={2:second,safe:visible,token:[REDACTED]}" in log_line
80+
assert "token=[REDACTED]" in log_line
81+
assert "trace_id=req-1" in log_line
82+
83+
84+
def test_setup_logging_uses_stdout(monkeypatch):
85+
output = io.StringIO()
86+
87+
_configure_and_emit(monkeypatch, output)
88+
89+
assert "readable message" in output.getvalue()
90+
91+
92+
def test_setup_logging_respects_level(monkeypatch):
93+
output = io.StringIO()
94+
monkeypatch.setattr(logger_module.sys, "stdout", output)
95+
96+
logger_module.setup_logging(logging.WARNING)
97+
logging.getLogger("test.level").info("hidden message")
98+
logging.getLogger("test.level").warning("visible message")
99+
100+
logged = output.getvalue()
101+
assert "hidden message" not in logged
102+
assert "WARNING" in logged
103+
assert "visible message" in logged
104+
105+
106+
def test_setup_logging_uses_log_level_env(monkeypatch):
107+
output = io.StringIO()
108+
monkeypatch.setattr(logger_module.sys, "stdout", output)
109+
monkeypatch.setenv("LOG_LEVEL", "WARNING")
110+
111+
logger_module.setup_logging()
112+
logging.getLogger("test.env_level").info("hidden message")
113+
logging.getLogger("test.env_level").warning("visible message")
114+
115+
logged = output.getvalue()
116+
assert "hidden message" not in logged
117+
assert "WARNING" in logged
118+
assert "visible message" in logged

0 commit comments

Comments
 (0)