Skip to content

Commit a1ea7f1

Browse files
Force all API loggers to be structured (#4331)
1 parent 292946e commit a1ea7f1

File tree

1 file changed

+72
-54
lines changed

1 file changed

+72
-54
lines changed

api/conf/settings/logging.py

Lines changed: 72 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,72 +31,101 @@ def health_check_filter(record: LogRecord) -> bool:
3131
"GC_DEBUG_LOGGING", cast=lambda x: x.split("|") if x else [], default=""
3232
)
3333

34+
# This shared_processors approach is modified from structlog's
35+
# documentation for how to handle non-structured logs in a structured format
36+
# https://www.structlog.org/en/stable/standard-library.html#rendering-using-structlog-based-formatters-within-logging
37+
timestamper = structlog.processors.TimeStamper(fmt="iso")
38+
39+
shared_processors = [
40+
structlog.contextvars.merge_contextvars,
41+
structlog.stdlib.add_logger_name,
42+
structlog.stdlib.add_log_level,
43+
structlog.stdlib.PositionalArgumentsFormatter(),
44+
structlog.processors.CallsiteParameterAdder(
45+
{
46+
structlog.processors.CallsiteParameter.FILENAME,
47+
structlog.processors.CallsiteParameter.FUNC_NAME,
48+
structlog.processors.CallsiteParameter.LINENO,
49+
}
50+
),
51+
structlog.processors.StackInfoRenderer(),
52+
structlog.processors.format_exc_info,
53+
structlog.processors.UnicodeDecoder(),
54+
]
55+
56+
57+
# These loggers duplicate django-structlog's request start/finish
58+
# logs, as well as our nginx request logs. We want to keep only the
59+
# start/finish logs and the nginx logs for access logging, otherwise
60+
# we are just duplicating information!
61+
_UNWANTED_LOGGERS = {
62+
"uvicorn.access",
63+
"django.request",
64+
}
65+
66+
67+
def suppress_unwanted_logs(record: LogRecord) -> bool:
68+
return (
69+
record.name not in _UNWANTED_LOGGERS
70+
and "GET /healthcheck" not in record.getMessage()
71+
)
72+
73+
3474
# Logging configuration
3575
LOGGING = {
36-
# NOTE: Most of this is inherited from the default configuration
3776
"version": 1,
3877
"disable_existing_loggers": False,
3978
"filters": {
40-
"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"},
41-
"require_debug_true": {"()": "django.utils.log.RequireDebugTrue"},
42-
"health_check": {
79+
"suppress_unwanted_logs": {
4380
"()": "django.utils.log.CallbackFilter",
44-
"callback": health_check_filter,
81+
"callback": suppress_unwanted_logs,
4582
},
4683
},
4784
"formatters": {
48-
"console": {
49-
"format": "[{asctime} - {name} - {lineno:>3}][{levelname}] {message}",
50-
"style": "{",
51-
},
5285
"structured": {
5386
"()": structlog.stdlib.ProcessorFormatter,
54-
"processor": structlog.processors.JSONRenderer()
55-
if LOG_PROCESSOR == "json"
56-
else structlog.dev.ConsoleRenderer(),
87+
"processor": (
88+
structlog.processors.JSONRenderer()
89+
if LOG_PROCESSOR == "json"
90+
else structlog.dev.ConsoleRenderer()
91+
),
92+
"foreign_pre_chain": [
93+
timestamper,
94+
# Explanations from https://www.structlog.org/en/stable/standard-library.html#rendering-using-structlog-based-formatters-within-logging
95+
# Add the log level and a timestamp to the event_dict if the log entry
96+
# is not from structlog.
97+
structlog.stdlib.add_log_level,
98+
# Add extra attributes of LogRecord objects to the event dictionary
99+
# so that values passed in the extra parameter of log methods pass
100+
# through to log output.
101+
structlog.stdlib.ExtraAdder(),
102+
]
103+
+ shared_processors,
57104
},
58105
},
59106
"handlers": {
60-
# Default console logger
61-
"console": {
62-
"level": LOG_LEVEL,
63-
"class": "logging.StreamHandler",
64-
"formatter": "console",
65-
},
66107
"console_structured": {
67108
"level": LOG_LEVEL,
68109
"class": "logging.StreamHandler",
69110
"formatter": "structured",
111+
"filters": ["suppress_unwanted_logs"],
70112
},
71113
},
114+
"root": {
115+
"handlers": ["console_structured"],
116+
"level": LOG_LEVEL,
117+
"propagate": False,
118+
},
72119
"loggers": {
73-
# Application
74-
"django_structlog": {
75-
"handlers": ["console_structured"],
76-
"level": LOG_LEVEL,
77-
"propagate": False,
78-
},
79-
"api": {
80-
"handlers": ["console_structured"],
81-
"level": LOG_LEVEL,
82-
"propagate": False,
83-
},
84-
# External
85120
"django": {
86-
"handlers": ["console"],
121+
"handlers": ["console_structured"],
87122
# Keep this at info to avoid django internal debug logs;
88123
# we just want our own debug logs when log level is set to debug
89124
"level": "INFO",
90125
"propagate": False,
91126
},
92-
"uvicorn.error": { # Using just "uvicorn" will re-enable access logs
93-
"handlers": ["console"],
94-
"level": LOG_LEVEL,
95-
"propagate": False,
96-
},
97-
# Default handler for all other loggers
98-
"": {
99-
"handlers": ["console"],
127+
"uvicorn": {
128+
"handlers": ["console_structured"],
100129
"level": LOG_LEVEL,
101130
},
102131
},
@@ -105,22 +134,11 @@ def health_check_filter(record: LogRecord) -> bool:
105134
# https://django-structlog.readthedocs.io/en/latest/getting_started.html
106135
structlog.configure(
107136
processors=[
108-
structlog.contextvars.merge_contextvars,
137+
timestamper,
109138
structlog.stdlib.filter_by_level,
110-
structlog.processors.TimeStamper(fmt="iso"),
111-
structlog.stdlib.add_logger_name,
112-
structlog.stdlib.add_log_level,
113-
structlog.stdlib.PositionalArgumentsFormatter(),
114-
structlog.processors.CallsiteParameterAdder(
115-
{
116-
structlog.processors.CallsiteParameter.FILENAME,
117-
structlog.processors.CallsiteParameter.FUNC_NAME,
118-
structlog.processors.CallsiteParameter.LINENO,
119-
}
120-
),
121-
structlog.processors.StackInfoRenderer(),
122-
structlog.processors.format_exc_info,
123-
structlog.processors.UnicodeDecoder(),
139+
]
140+
+ shared_processors
141+
+ [
124142
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
125143
],
126144
logger_factory=structlog.stdlib.LoggerFactory(),

0 commit comments

Comments
 (0)