Skip to content

Commit e1d4b58

Browse files
fix: Propagate uvicorn logs (#1250)
Logs produced by Uvicorn's internals and blueapi are currently processed by different handlers. Due to this nothing produced by Uvicorn is being propagated to Graylog. This PR relegates handling of Uvicorn logs to BlueAPI's handlers and standardises terminal formatting. Fixes #1248 --------- Co-authored-by: Joseph Ware <[email protected]>
1 parent 0ce8c59 commit e1d4b58

File tree

3 files changed

+112
-4
lines changed

3 files changed

+112
-4
lines changed

src/blueapi/log.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import enum
12
import logging
23
import os
34
from contextlib import contextmanager
5+
from copy import copy
46

7+
import click
58
from graypy import GELFTCPHandler
69

710
from blueapi.config import LoggingConfig
@@ -91,6 +94,9 @@ def set_up_stream_handler(
9194
stream_handler = logging.StreamHandler()
9295
stream_handler.setLevel(logging_config.level)
9396

97+
formatter = ColorFormatter("%(asctime)s %(levelname)s %(message)s")
98+
stream_handler.setFormatter(formatter)
99+
94100
for filter in filters:
95101
stream_handler.addFilter(filter)
96102

@@ -120,3 +126,47 @@ def set_up_graylog_handler(
120126

121127
logger.addHandler(graylog_handler)
122128
return graylog_handler
129+
130+
131+
class IBMColorBlindSafeColors(enum.Enum):
132+
ultramarine = (100, 143, 255)
133+
indigo = (120, 94, 240)
134+
magenta = (220, 38, 127)
135+
orange = (254, 97, 0)
136+
gold = (255, 176, 0)
137+
138+
139+
class ColorFormatter(logging.Formatter):
140+
"""Colors level_name of log using IBM color blind safe palette."""
141+
142+
def color_level_name(self, level_name: str, level_no: int) -> str:
143+
match level_no:
144+
case logging.DEBUG:
145+
return click.style(
146+
str(level_name), fg=IBMColorBlindSafeColors.ultramarine.value
147+
)
148+
case logging.INFO:
149+
return click.style(
150+
str(level_name), fg=IBMColorBlindSafeColors.indigo.value
151+
)
152+
case logging.WARNING:
153+
return click.style(
154+
str(level_name), fg=IBMColorBlindSafeColors.gold.value
155+
)
156+
case logging.ERROR:
157+
return click.style(
158+
str(level_name), fg=IBMColorBlindSafeColors.magenta.value
159+
)
160+
case logging.CRITICAL:
161+
return click.style(
162+
str(level_name), fg=IBMColorBlindSafeColors.orange.value
163+
)
164+
return level_name
165+
166+
def formatMessage(self, record: logging.LogRecord) -> str: # noqa: N802
167+
# Copy record to avoid modifying for other handlers etc.
168+
recordcopy = copy(record)
169+
recordcopy.levelname = self.color_level_name(
170+
recordcopy.levelname, recordcopy.levelno
171+
)
172+
return super().formatMessage(recordcopy)

src/blueapi/service/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,13 +555,13 @@ def start(config: ApplicationConfig):
555555
import uvicorn
556556
from uvicorn.config import LOGGING_CONFIG
557557

558-
LOGGING_CONFIG["formatters"]["default"]["fmt"] = (
559-
"%(asctime)s %(levelprefix)s %(message)s"
560-
)
558+
# Enable propagation and disable default handlers, so that blueapi handles all logs
559+
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"] = []
561560
LOGGING_CONFIG["formatters"]["access"]["fmt"] = (
562561
"%(asctime)s %(levelprefix)s %(client_addr)s"
563562
+ " - '%(request_line)s' %(status_code)s"
564563
)
564+
LOGGING_CONFIG["loggers"]["uvicorn"]["propagate"] = True
565565
app = get_app(config)
566566

567567
FastAPIInstrumentor().instrument_app(

tests/unit_tests/test_log.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
from dodal.log import LOGGER as DODAL_LOGGER
88

99
from blueapi.config import GraylogConfig, LoggingConfig
10-
from blueapi.log import PlanTagFilter, plan_tag_filter_context, set_up_logging
10+
from blueapi.log import (
11+
ColorFormatter,
12+
PlanTagFilter,
13+
plan_tag_filter_context,
14+
set_up_logging,
15+
)
1116

1217

1318
def clear_all_loggers_and_handlers(logger):
@@ -124,3 +129,56 @@ def test_filter_tags_plan_name(logger, mock_handler_emit):
124129
logger.addFilter(PlanTagFilter("foo"))
125130
logger.info("FOO")
126131
assert mock_handler_emit.call_args[0][0].plan_name == "foo"
132+
133+
134+
@pytest.fixture
135+
def color_formatter():
136+
return ColorFormatter("%(levelname)s %(message)s")
137+
138+
139+
@pytest.fixture
140+
def foo_record():
141+
return logging.LogRecord("foo", logging.INFO, "foo", 0, "foo", None, None)
142+
143+
144+
class TestColorFormatter:
145+
def test_debug(self, color_formatter, foo_record):
146+
foo_record.levelno = logging.DEBUG
147+
148+
assert (
149+
color_formatter.format(foo_record)
150+
== "\x1b[38;2;100;143;255mINFO\x1b[0m foo"
151+
)
152+
153+
def test_info(self, color_formatter, foo_record):
154+
foo_record.levelno = logging.INFO
155+
156+
assert (
157+
color_formatter.format(foo_record) == "\x1b[38;2;120;94;240mINFO\x1b[0m foo"
158+
)
159+
160+
def test_warning(self, color_formatter, foo_record):
161+
foo_record.levelno = logging.WARNING
162+
163+
assert (
164+
color_formatter.format(foo_record) == "\x1b[38;2;255;176;0mINFO\x1b[0m foo"
165+
)
166+
167+
def test_error(self, color_formatter, foo_record):
168+
foo_record.levelno = logging.ERROR
169+
170+
assert (
171+
color_formatter.format(foo_record) == "\x1b[38;2;220;38;127mINFO\x1b[0m foo"
172+
)
173+
174+
def test_critical(self, color_formatter, foo_record):
175+
foo_record.levelno = logging.CRITICAL
176+
177+
assert (
178+
color_formatter.format(foo_record) == "\x1b[38;2;254;97;0mINFO\x1b[0m foo"
179+
)
180+
181+
def test_other(self, color_formatter, foo_record):
182+
foo_record.levelno = -1
183+
184+
assert color_formatter.format(foo_record) == "INFO foo"

0 commit comments

Comments
 (0)