Skip to content

Commit 6dcf1f9

Browse files
authored
Merge pull request #156 from thawn/copilot/fix-server-log-level
Fix log level configuration and suppress werkzeug request log clutter
2 parents eeedc3b + 51c3dfd commit 6dcf1f9

File tree

5 files changed

+318
-15
lines changed

5 files changed

+318
-15
lines changed

src/config.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
});
9797
}
9898

99-
// Auto-refresh logs every 3 seconds
99+
// Auto-refresh logs every 5 seconds
100100
var logRefreshInterval;
101101

102102
$(function(){
@@ -118,8 +118,8 @@
118118
// Initial log load
119119
loadLogs();
120120

121-
// Auto-refresh logs every 3 seconds
122-
logRefreshInterval = setInterval(loadLogs, 3000);
121+
// Auto-refresh logs every 5 seconds
122+
logRefreshInterval = setInterval(loadLogs, 5000);
123123
});
124124

125125
</script>
@@ -199,7 +199,7 @@ <h4 class="panel-title">Download OID Images:</h4>
199199
<h4 class="panel-title">Server Logs:</h4>
200200
</div>
201201
<div class="panel-body">
202-
<p>View recent server logs. Logs are automatically refreshed every 3 seconds. Change the log level above to control verbosity.</p>
202+
<p>View recent server logs. Logs are automatically refreshed every 5 seconds. Change the log level above to control verbosity.</p>
203203
<button type="button" id="refresh-logs" class="btn btn-info btn-sm" data-toggle="tooltip"
204204
title="Manually refresh the log display">
205205
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Refresh Now

src/ttmp32gme/log_handler.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,39 @@
55
from threading import Lock
66
from typing import List
77

8+
# Get logger for this module
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def apply_log_level(level_str: str) -> None:
13+
"""Apply log level to all relevant loggers.
14+
15+
Args:
16+
level_str: Log level string (DEBUG, INFO, WARNING, ERROR, CRITICAL)
17+
"""
18+
level = getattr(logging, level_str, logging.WARNING)
19+
20+
# Set root logger level
21+
logging.getLogger().setLevel(level)
22+
23+
# Set werkzeug logger level
24+
# When not in DEBUG/INFO mode, suppress werkzeug's INFO logs to reduce clutter
25+
werkzeug_logger = logging.getLogger("werkzeug")
26+
if level_str in ["DEBUG", "INFO"]:
27+
werkzeug_logger.setLevel(level)
28+
else:
29+
# Suppress werkzeug's INFO logs (like request logs) when not in verbose mode
30+
werkzeug_logger.setLevel(logging.WARNING)
31+
32+
# Set waitress logger level (for production server)
33+
waitress_logger = logging.getLogger("waitress")
34+
if level_str in ["DEBUG", "INFO"]:
35+
waitress_logger.setLevel(level)
36+
else:
37+
waitress_logger.setLevel(logging.WARNING)
38+
39+
logger.info(f"Log level changed to {level_str}")
40+
841

942
class MemoryLogHandler(logging.Handler):
1043
"""Custom log handler that stores recent log records in memory."""

src/ttmp32gme/ttmp32gme.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
DBHandler,
3737
LibraryActionModel,
3838
)
39-
from ttmp32gme.log_handler import MemoryLogHandler
39+
from ttmp32gme.log_handler import MemoryLogHandler, apply_log_level
4040
from ttmp32gme.print_handler import (
4141
create_print_layout,
4242
format_print_button,
@@ -181,9 +181,7 @@ def save_config(config_params: Dict[str, Any]) -> tuple[Dict[str, Any], str]:
181181
# Handle log level changes
182182
if "log_level" in config_params:
183183
level_str = config_params["log_level"]
184-
level = getattr(logging, level_str, logging.WARNING)
185-
logging.getLogger().setLevel(level)
186-
logger.info(f"Log level changed to {level_str}")
184+
apply_log_level(level_str)
187185

188186
# Validate DPI and pixel size
189187
if "tt_dpi" in config_params and "tt_pixel-size" in config_params:
@@ -628,6 +626,7 @@ def get_logs():
628626
"""Get recent log entries."""
629627
num_lines = request.args.get("lines", default=100, type=int)
630628
logs = memory_handler.get_logs(num_lines)
629+
logger.debug(f"Serving {len(logs)} log lines")
631630
return jsonify({"success": True, "logs": logs})
632631

633632

@@ -645,9 +644,7 @@ def set_log_level():
645644
return jsonify({"success": False, "error": "Invalid log level"}), 400
646645

647646
# Set the log level
648-
level = getattr(logging, level_str)
649-
logging.getLogger().setLevel(level)
650-
logger.info(f"Log level changed to {level_str}")
647+
apply_log_level(level_str)
651648

652649
# Save to config
653650
db = get_db()
@@ -775,10 +772,10 @@ def main():
775772

776773
# Set logging level based on verbose flag (do this early)
777774
if args.verbose == 1:
778-
logging.getLogger().setLevel(logging.INFO)
775+
apply_log_level("INFO")
779776
logger.info("Verbose mode enabled (INFO level)")
780777
elif args.verbose >= 2:
781-
logging.getLogger().setLevel(logging.DEBUG)
778+
apply_log_level("DEBUG")
782779
logger.debug("Verbose mode enabled (DEBUG level)")
783780

784781
if args.version:
@@ -812,8 +809,7 @@ def main():
812809
# Apply log level from config if not overridden by command line
813810
if args.verbose == 0: # Only apply config if -v/-vv not used
814811
log_level_str = config.get("log_level", "WARNING")
815-
log_level = getattr(logging, log_level_str, logging.WARNING)
816-
logging.getLogger().setLevel(log_level)
812+
apply_log_level(log_level_str)
817813
logger.info(f"Log level set to {log_level_str} from config")
818814

819815
# Override config with command-line args
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Integration test for log level changes via config page."""
2+
3+
import logging
4+
import sys
5+
import tempfile
6+
from pathlib import Path
7+
8+
import pytest
9+
10+
# Add src to path to import ttmp32gme
11+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
12+
13+
14+
@pytest.fixture
15+
def test_app():
16+
"""Create a test Flask app with a temporary database."""
17+
from ttmp32gme import ttmp32gme
18+
19+
# Save original state
20+
original_db = ttmp32gme.db_handler
21+
original_config = ttmp32gme.config
22+
original_custom_db_path = ttmp32gme.custom_db_path
23+
24+
with tempfile.TemporaryDirectory() as tmpdir:
25+
tmpdir = Path(tmpdir)
26+
custom_db = tmpdir / "test.sqlite"
27+
28+
# Reset global state
29+
ttmp32gme.db_handler = None
30+
ttmp32gme.custom_db_path = custom_db
31+
ttmp32gme.config = {}
32+
33+
# Initialize database
34+
ttmp32gme.get_db()
35+
ttmp32gme.config = ttmp32gme.fetch_config()
36+
37+
yield ttmp32gme.app
38+
39+
# Restore original state
40+
ttmp32gme.db_handler = original_db
41+
ttmp32gme.config = original_config
42+
ttmp32gme.custom_db_path = original_custom_db_path
43+
44+
45+
def test_config_log_level_change_via_config_page(test_app):
46+
"""Test that changing log level via config page updates all loggers."""
47+
client = test_app.test_client()
48+
49+
# Get initial log level
50+
response = client.post("/config", data={"action": "load"})
51+
assert response.status_code == 200
52+
data = response.get_json()
53+
assert data["success"] is True
54+
55+
# Change log level to DEBUG via config update
56+
response = client.post(
57+
"/config",
58+
data={
59+
"action": "update",
60+
"data": '{"log_level": "DEBUG"}',
61+
},
62+
)
63+
assert response.status_code == 200
64+
data = response.get_json()
65+
assert data["success"] is True
66+
# Note: log_level might not be in the response if not all config fields are returned
67+
# Check the actual logger level instead
68+
root_logger = logging.getLogger()
69+
assert root_logger.level == logging.DEBUG
70+
71+
# Verify loggers are set correctly
72+
root_logger = logging.getLogger()
73+
werkzeug_logger = logging.getLogger("werkzeug")
74+
waitress_logger = logging.getLogger("waitress")
75+
assert root_logger.level == logging.DEBUG
76+
assert werkzeug_logger.level == logging.DEBUG
77+
assert waitress_logger.level == logging.DEBUG
78+
79+
# Change log level to WARNING
80+
response = client.post(
81+
"/config",
82+
data={
83+
"action": "update",
84+
"data": '{"log_level": "WARNING"}',
85+
},
86+
)
87+
assert response.status_code == 200
88+
data = response.get_json()
89+
assert data["success"] is True
90+
91+
# Verify werkzeug and waitress are set to WARNING when not in verbose mode
92+
assert root_logger.level == logging.WARNING
93+
assert werkzeug_logger.level == logging.WARNING
94+
assert waitress_logger.level == logging.WARNING
95+
96+
97+
def test_log_level_change_via_logs_endpoint(test_app):
98+
"""Test that changing log level via /logs/level endpoint works."""
99+
client = test_app.test_client()
100+
101+
# Change log level to INFO
102+
response = client.post(
103+
"/logs/level",
104+
json={"level": "INFO"},
105+
content_type="application/json",
106+
)
107+
assert response.status_code == 200
108+
data = response.get_json()
109+
assert data["success"] is True
110+
assert data["level"] == "INFO"
111+
112+
# Verify loggers are set correctly
113+
root_logger = logging.getLogger()
114+
werkzeug_logger = logging.getLogger("werkzeug")
115+
waitress_logger = logging.getLogger("waitress")
116+
assert root_logger.level == logging.INFO
117+
assert werkzeug_logger.level == logging.INFO
118+
assert waitress_logger.level == logging.INFO
119+
120+
# Change log level to ERROR
121+
response = client.post(
122+
"/logs/level",
123+
json={"level": "ERROR"},
124+
content_type="application/json",
125+
)
126+
assert response.status_code == 200
127+
data = response.get_json()
128+
assert data["success"] is True
129+
assert data["level"] == "ERROR"
130+
131+
# Verify werkzeug and waitress are set to WARNING when not in verbose mode
132+
assert root_logger.level == logging.ERROR
133+
assert werkzeug_logger.level == logging.WARNING
134+
assert waitress_logger.level == logging.WARNING
135+
136+
137+
def test_invalid_log_level_rejected(test_app):
138+
"""Test that invalid log levels are rejected."""
139+
client = test_app.test_client()
140+
141+
# Try to set invalid log level
142+
response = client.post(
143+
"/logs/level",
144+
json={"level": "INVALID"},
145+
content_type="application/json",
146+
)
147+
assert response.status_code == 400
148+
data = response.get_json()
149+
assert data["success"] is False

0 commit comments

Comments
 (0)