Skip to content

Commit 1a5b909

Browse files
authored
feat: Customised logging (#115)
* feat: Allow logging customisation * Add colour setting
1 parent e202dd1 commit 1a5b909

File tree

3 files changed

+96
-57
lines changed

3 files changed

+96
-57
lines changed

src/edge_proxy/logging.py

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import logging.config
23
import logging.handlers
34

45
import structlog
@@ -24,36 +25,37 @@ def _extract_gunicorn_access_log_event(
2425
return event_dict
2526

2627

27-
def setup_logging(settings: LoggingSettings) -> None:
28-
"""
29-
Configure stdlib logger to use structlog processors and formatters so that
30-
uvicorn and application logs are consistent.
31-
"""
32-
is_generic_format = settings.log_format is LogFormat.GENERIC
28+
def _drop_color_message(
29+
record: logging.LogRecord,
30+
name: str,
31+
event_dict: structlog.types.EventDict,
32+
) -> structlog.types.EventDict:
33+
# Uvicorn logs the message a second time in the extra `color_message`, but we don't
34+
# need it. This processor drops the key from the event dict if it exists.
35+
event_dict.pop("color_message", None)
36+
return event_dict
3337

34-
processors: list[structlog.types.Processor] = [
35-
structlog.contextvars.merge_contextvars,
36-
structlog.stdlib.add_logger_name,
37-
structlog.stdlib.add_log_level,
38-
_extract_gunicorn_access_log_event,
39-
structlog.stdlib.PositionalArgumentsFormatter(),
40-
structlog.stdlib.ExtraAdder(),
41-
structlog.processors.StackInfoRenderer(),
42-
structlog.processors.TimeStamper(fmt="iso"),
43-
]
4438

45-
if is_generic_format:
46-
# For `generic` format, set `exc_info` on the log event if the log method is
47-
# `exception` and `exc_info` is not already set.
48-
#
49-
# Rendering of `exc_info` is handled by ConsoleRenderer.
50-
processors.append(structlog.dev.set_exc_info)
51-
else:
52-
# For `json` format `exc_info` must be set explicitly when
53-
# needed, and is translated into a formatted `exception` field.
54-
processors.append(structlog.processors.format_exc_info)
39+
COMMON_PROCESSORS: list[structlog.types.Processor] = [
40+
structlog.contextvars.merge_contextvars,
41+
structlog.stdlib.add_logger_name,
42+
structlog.stdlib.add_log_level,
43+
_extract_gunicorn_access_log_event,
44+
structlog.stdlib.PositionalArgumentsFormatter(),
45+
structlog.stdlib.ExtraAdder(),
46+
_drop_color_message,
47+
structlog.processors.StackInfoRenderer(),
48+
structlog.processors.TimeStamper(fmt="iso"),
49+
]
5550

56-
processors.append(structlog.processors.EventRenamer(settings.log_event_field_name))
51+
52+
def setup_logging(settings: LoggingSettings) -> None:
53+
processors = [
54+
*COMMON_PROCESSORS,
55+
structlog.processors.EventRenamer(settings.log_event_field_name),
56+
structlog.dev.set_exc_info,
57+
structlog.processors.format_exc_info,
58+
]
5759

5860
structlog.configure(
5961
processors=processors
@@ -62,34 +64,60 @@ def setup_logging(settings: LoggingSettings) -> None:
6264
cache_logger_on_first_use=True,
6365
)
6466

65-
if is_generic_format:
66-
log_renderer = structlog.dev.ConsoleRenderer(
67-
event_key=settings.log_event_field_name
68-
)
69-
else:
70-
log_renderer = structlog.processors.JSONRenderer()
71-
72-
formatter = structlog.stdlib.ProcessorFormatter(
73-
use_get_message=False,
74-
pass_foreign_args=True,
75-
foreign_pre_chain=processors,
76-
processors=[
77-
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
78-
log_renderer,
79-
],
80-
)
81-
82-
handler = logging.StreamHandler()
83-
handler.setFormatter(formatter)
84-
85-
root = logging.getLogger()
86-
root.addHandler(handler)
87-
root.setLevel(settings.log_level.to_logging_log_level())
88-
8967
# Propagate uvicorn logs instead of letting uvicorn configure the format
9068
for name in ["uvicorn", "uvicorn.error"]:
9169
logging.getLogger(name).handlers.clear()
9270
logging.getLogger(name).propagate = True
9371

9472
logging.getLogger("uvicorn.access").handlers.clear()
9573
logging.getLogger("uvicorn.access").propagate = settings.enable_access_log
74+
75+
override = settings.override
76+
logging.config.dictConfig(
77+
{
78+
"version": 1,
79+
"disable_existing_loggers": False,
80+
"formatters": {
81+
LogFormat.GENERIC.value: {
82+
"()": structlog.stdlib.ProcessorFormatter,
83+
"use_get_message": False,
84+
"pass_foreign_args": True,
85+
"processors": [
86+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
87+
structlog.dev.ConsoleRenderer(
88+
event_key=settings.log_event_field_name,
89+
colors=settings.colours,
90+
),
91+
],
92+
"foreign_pre_chain": processors,
93+
},
94+
LogFormat.JSON.value: {
95+
"()": structlog.stdlib.ProcessorFormatter,
96+
"use_get_message": False,
97+
"pass_foreign_args": True,
98+
"processors": [
99+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
100+
structlog.processors.JSONRenderer(),
101+
],
102+
"foreign_pre_chain": processors,
103+
},
104+
**(override.get("formatters") or {}),
105+
},
106+
"handlers": {
107+
"default": {
108+
"level": settings.log_level.to_logging_log_level(),
109+
"class": "logging.StreamHandler",
110+
"formatter": settings.log_format.value,
111+
},
112+
**(override.get("handlers") or {}),
113+
},
114+
"loggers": {
115+
"": {
116+
"handlers": ["default"],
117+
"level": settings.log_level.to_logging_log_level(),
118+
"propagate": True,
119+
},
120+
**(override.get("loggers") or {}),
121+
},
122+
}
123+
)

src/edge_proxy/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def serve():
1010
host=str(settings.server.host),
1111
port=settings.server.port,
1212
reload=settings.server.reload,
13+
use_colors=settings.logging.colours,
1314
)
1415

1516

src/edge_proxy/settings.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ class LoggingSettings(BaseModel):
8484
log_format: LogFormat = LogFormat.GENERIC
8585
log_level: LogLevel = LogLevel.INFO
8686
log_event_field_name: str = "message"
87+
colours: bool = Field(
88+
default=False,
89+
validation_alias=AliasChoices(
90+
"colours",
91+
"colors",
92+
),
93+
)
94+
override: dict[str, Any] = Field(default_factory=dict)
8795

8896

8997
class ServerSettings(BaseModel):
@@ -93,12 +101,14 @@ class ServerSettings(BaseModel):
93101

94102

95103
class AppSettings(BaseModel):
96-
environment_key_pairs: list[EnvironmentKeyPair] = [
97-
EnvironmentKeyPair(
98-
server_side_key="ser.environment_key",
99-
client_side_key="environment_key",
100-
)
101-
]
104+
environment_key_pairs: list[EnvironmentKeyPair] = Field(
105+
default_factory=lambda: [
106+
EnvironmentKeyPair(
107+
server_side_key="ser.environment_key",
108+
client_side_key="environment_key",
109+
)
110+
]
111+
)
102112
api_url: HttpUrl = "https://edge.api.flagsmith.com/api/v1"
103113
api_poll_frequency_seconds: int = Field(
104114
default=10,

0 commit comments

Comments
 (0)