Skip to content

Commit 97a30d0

Browse files
authored
Merge pull request #296 from cmu-delphi/development
Changed logging format
2 parents 9368d17 + 0cc6be7 commit 97a30d0

File tree

3 files changed

+73
-5
lines changed

3 files changed

+73
-5
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Logging formatters for Elasticsearch-compatible structured output.
3+
"""
4+
5+
import json
6+
import logging
7+
from datetime import datetime, timezone
8+
9+
10+
# Standard LogRecord attributes - excluded from extra when building JSON
11+
_RECORD_ATTRS = frozenset(
12+
{
13+
"name",
14+
"msg",
15+
"args",
16+
"created",
17+
"filename",
18+
"funcName",
19+
"levelname",
20+
"levelno",
21+
"lineno",
22+
"module",
23+
"msecs",
24+
"pathname",
25+
"process",
26+
"processName",
27+
"relativeCreated",
28+
"stack_info",
29+
"exc_info",
30+
"exc_text",
31+
"thread",
32+
"threadName",
33+
"message",
34+
"asctime",
35+
"taskName",
36+
}
37+
)
38+
39+
40+
class JsonFormatter(logging.Formatter):
41+
"""
42+
Format log records as single-line JSON for Elasticsearch.
43+
44+
Each log line is a valid JSON object with @timestamp, level, logger, message,
45+
and any extra fields passed via logger.info(..., extra={...}).
46+
"""
47+
48+
def format(self, record: logging.LogRecord) -> str:
49+
log_obj = {
50+
"@timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
51+
"level": record.levelname,
52+
"logger": record.name,
53+
"message": record.getMessage(),
54+
}
55+
56+
# Add extra fields (passed via extra= in log calls)
57+
for key, value in record.__dict__.items():
58+
if key not in _RECORD_ATTRS and value is not None:
59+
log_obj[key] = value
60+
61+
# Add exception info if present
62+
if record.exc_info:
63+
log_obj["exception"] = self.formatException(record.exc_info)
64+
65+
return json.dumps(log_obj, default=str)

src/epiportal/middleware.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Request logging middleware that captures comprehensive data from all HTTP requests.
33
"""
44

5-
import json
65
import logging
76
import time
87
import uuid
@@ -140,11 +139,11 @@ def process_response(self, request, response):
140139
log_data["duration_ms"] = round(duration_ms, 2)
141140

142141
logger.info(
143-
"%s %s %s | %s",
142+
"%s %s %s",
144143
request.method,
145144
request.path,
146145
response.status_code,
147-
json.dumps(log_data, default=str),
146+
extra=log_data,
148147
)
149148

150149
except Exception as e:

src/epiportal/settings.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@
237237
'format': '[%(asctime)s] %(levelname)s | epiportal.requests | %(message)s',
238238
'datefmt': '%Y-%m-%d %H:%M:%S',
239239
},
240+
'json': {
241+
'()': 'epiportal.logging_formatters.JsonFormatter',
242+
},
240243
},
241244
'handlers': {
242245
'console': {
@@ -246,7 +249,7 @@
246249
},
247250
'request_console': {
248251
'class': 'logging.StreamHandler',
249-
'formatter': 'request_verbose',
252+
'formatter': 'json',
250253
'stream': sys.stdout,
251254
},
252255
},
@@ -266,7 +269,8 @@
266269

267270
if DEBUG:
268271
for logger in LOGGING['loggers']:
269-
LOGGING['loggers'][logger]['handlers'] = ['console']
272+
if logger != 'epiportal.requests':
273+
LOGGING['loggers'][logger]['handlers'] = ['console']
270274

271275

272276
# DRF Spectacular settings

0 commit comments

Comments
 (0)