Skip to content

Commit 6a96a69

Browse files
feat: improve / clean up logging (ietf-tools#7591)
* refactor: always use console log handler * refactor: json for k8s, plain otherwise * chore: remove syslog from wsgi.py * chore: remove debug.log() * chore: drop syslog from settings.py * refactor: use log.log() in person.utils * refactor: fetch_meeting_attendance->log.log() * chore: gunicorn logs as JSON (wip) * feat: better json log formatting * refactor: improve log config * feat: gunicorn access log fields * fix: remove type hints The gunicorn logger plays tricks with the LogRecord args parameter to let it have string keys instead of being a simple tuple. The mypy tests rightly flag this. Rather than fighting the typing, just remove the hints and leave a comment warning not to use the gunicorn-specific formatter with other loggers.
1 parent 704f996 commit 6a96a69

9 files changed

Lines changed: 95 additions & 52 deletions

File tree

debug.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,7 @@
33
import sys
44
import time as timeutils
55
import inspect
6-
from typing import Callable
76

8-
try:
9-
import syslog
10-
logger = syslog.syslog # type: Callable
11-
except ImportError: # import syslog will fail on Windows boxes
12-
import logging
13-
logging.basicConfig(filename='tracker.log',level=logging.INFO)
14-
logger = logging.info
157

168
try:
179
from pprint import pformat
@@ -155,13 +147,6 @@ def showpos(name):
155147
indent = ' ' * (_report_indent[0])
156148
sys.stderr.write("%s%s:%s: %s: '%s'\n" % (indent, fn, line, name, value))
157149

158-
def log(name):
159-
if debug:
160-
frame = inspect.stack()[1][0]
161-
value = eval(name, frame.f_globals, frame.f_locals)
162-
indent = ' ' * (_report_indent[0])
163-
logger("%s%s: %s" % (indent, name, value))
164-
165150
def pprint(name):
166151
if debug:
167152
frame = inspect.stack()[1][0]

dev/build/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ COPY . .
1818
COPY ./dev/build/start.sh ./start.sh
1919
COPY ./dev/build/datatracker-start.sh ./datatracker-start.sh
2020
COPY ./dev/build/celery-start.sh ./celery-start.sh
21+
COPY ./dev/build/gunicorn.conf.py ./gunicorn.conf.py
2122

2223
RUN pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt && \
2324
echo '# empty' > ietf/settings_local.py && \

dev/build/gunicorn.conf.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
3+
# Log as JSON on stdout (to distinguish from Django's logs on stderr)
4+
#
5+
# This is applied as an update to gunicorn's glogging.CONFIG_DEFAULTS.
6+
logconfig_dict = {
7+
"version": 1,
8+
"disable_existing_loggers": False,
9+
"root": {"level": "INFO", "handlers": ["console"]},
10+
"loggers": {
11+
"gunicorn.error": {
12+
"level": "INFO",
13+
"handlers": ["console"],
14+
"propagate": False,
15+
"qualname": "gunicorn.error"
16+
},
17+
18+
"gunicorn.access": {
19+
"level": "INFO",
20+
"handlers": ["access_console"],
21+
"propagate": False,
22+
"qualname": "gunicorn.access"
23+
}
24+
},
25+
"handlers": {
26+
"console": {
27+
"class": "logging.StreamHandler",
28+
"formatter": "json",
29+
"stream": "ext://sys.stdout"
30+
},
31+
"access_console": {
32+
"class": "logging.StreamHandler",
33+
"formatter": "access_json",
34+
"stream": "ext://sys.stdout"
35+
},
36+
},
37+
"formatters": {
38+
"json": {
39+
"class": "ietf.utils.jsonlogger.DatatrackerJsonFormatter",
40+
"style": "{",
41+
"format": "{asctime}{levelname}{message}{name}{process}",
42+
},
43+
"access_json": {
44+
"class": "ietf.utils.jsonlogger.GunicornRequestJsonFormatter",
45+
"style": "{",
46+
"format": "{asctime}{levelname}{message}{name}{process}",
47+
}
48+
}
49+
}

ietf/person/utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33

44

55
import datetime
6-
import os
76
import pprint
87
import sys
9-
import syslog
108

119
from django.contrib import admin
1210
from django.core.cache import cache
@@ -17,14 +15,14 @@
1715
import debug # pyflakes:ignore
1816

1917
from ietf.person.models import Person, Alias, Email
18+
from ietf.utils import log
2019
from ietf.utils.mail import send_mail
2120

2221
def merge_persons(request, source, target, file=sys.stdout, verbose=False):
2322
changes = []
2423

2524
# write log
26-
syslog.openlog(str(os.path.basename(__file__)), syslog.LOG_PID, syslog.LOG_USER)
27-
syslog.syslog("Merging person records {} => {}".format(source.pk,target.pk))
25+
log.log(f"Merging person records {source.pk} => {target.pk}")
2826

2927
# handle primary emails
3028
for email in get_extra_primary(source,target):
@@ -118,7 +116,7 @@ def handle_users(source,target,check_only=False):
118116
if source.user and target.user:
119117
message = "DATATRACKER LOGIN ACTION: retaining login: {}, removing login: {}".format(target.user,source.user)
120118
if not check_only:
121-
syslog.syslog('merge-person-records: deactivating user {}'.format(source.user.username))
119+
log.log(f"merge-person-records: deactivating user {source.user.username}")
122120
user = source.user
123121
source.user = None
124122
source.save()

ietf/settings.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@
2626
warnings.filterwarnings("ignore", message="Report.file_reporters will no longer be available in Coverage.py 4.2", module="coverage.report")
2727
warnings.filterwarnings("ignore", message="Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated", module="bleach")
2828
warnings.filterwarnings("ignore", message="HTTPResponse.getheader\\(\\) is deprecated", module='selenium.webdriver')
29-
try:
30-
import syslog
31-
syslog.openlog(str("datatracker"), syslog.LOG_PID, syslog.LOG_USER)
32-
except ImportError:
33-
pass
3429

3530
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
3631
sys.path.append(os.path.abspath(BASE_DIR + "/.."))
@@ -240,31 +235,31 @@ def skip_unreadable_post(record):
240235
#
241236
'loggers': {
242237
'django': {
243-
'handlers': ['debug_console', 'mail_admins'],
238+
'handlers': ['console', 'mail_admins'],
244239
'level': 'INFO',
245240
},
246241
'django.request': {
247-
'handlers': ['debug_console'],
242+
'handlers': ['console'],
248243
'level': 'ERROR',
249244
},
250245
'django.server': {
251246
'handlers': ['django.server'],
252247
'level': 'INFO',
253248
},
254249
'django.security': {
255-
'handlers': ['debug_console', ],
250+
'handlers': ['console', ],
256251
'level': 'INFO',
257252
},
258253
'oidc_provider': {
259-
'handlers': ['debug_console', ],
254+
'handlers': ['console', ],
260255
'level': 'DEBUG',
261256
},
262257
'datatracker': {
263-
'handlers': ['debug_console'],
258+
'handlers': ['console'],
264259
'level': 'INFO',
265260
},
266261
'celery': {
267-
'handlers': ['debug_console'],
262+
'handlers': ['console'],
268263
'level': 'INFO',
269264
},
270265
},
@@ -275,7 +270,7 @@ def skip_unreadable_post(record):
275270
'console': {
276271
'level': 'DEBUG',
277272
'class': 'logging.StreamHandler',
278-
'formatter': 'json',
273+
'formatter': 'plain',
279274
},
280275
'debug_console': {
281276
# Active only when DEBUG=True
@@ -331,7 +326,9 @@ def skip_unreadable_post(record):
331326
'format': '{levelname}: {name}:{lineno}: {message}',
332327
},
333328
'json' : {
334-
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter'
329+
"class": "ietf.utils.jsonlogger.DatatrackerJsonFormatter",
330+
"style": "{",
331+
"format": "{asctime}{levelname}{message}{name}{pathname}{lineno}{funcName}{process}",
335332
}
336333
},
337334
}

ietf/stats/management/commands/fetch_meeting_attendance.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
# Copyright The IETF Trust 2017-2019, All Rights Reserved
22
# Copyright 2016 IETF Trust
33

4-
import syslog
5-
64
from django.core.management.base import BaseCommand, CommandError
75
from django.utils import timezone
86

97
import debug # pyflakes:ignore
108

119
from ietf.meeting.models import Meeting
1210
from ietf.stats.utils import fetch_attendance_from_meetings
11+
from ietf.utils import log
1312

14-
logtag = __name__.split('.')[-1]
15-
logname = "user.log"
16-
syslog.openlog(str(logtag), syslog.LOG_PID, syslog.LOG_USER)
1713

1814
class Command(BaseCommand):
1915
help = "Fetch meeting attendee figures from ietf.org/registration/attendees."
@@ -43,4 +39,4 @@ def handle(self, *args, **options):
4339
if self.stdout.isatty():
4440
self.stdout.write(msg+'\n') # make debugging a bit easier
4541
else:
46-
syslog.syslog(msg)
42+
log.log(msg)

ietf/utils/jsonlogger.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
from pythonjsonlogger import jsonlogger
3+
import time
4+
5+
6+
class DatatrackerJsonFormatter(jsonlogger.JsonFormatter):
7+
converter = time.gmtime # use UTC
8+
default_msec_format = "%s.%03d" # '.' instead of ','
9+
10+
11+
class GunicornRequestJsonFormatter(DatatrackerJsonFormatter):
12+
"""Only works with Gunicorn's logging"""
13+
def add_fields(self, log_record, record, message_dict):
14+
super().add_fields(log_record, record, message_dict)
15+
log_record.setdefault("method", record.args["m"])
16+
log_record.setdefault("proto", record.args["H"])
17+
log_record.setdefault("remote_ip", record.args["h"])
18+
path = record.args["U"] # URL path
19+
if record.args["q"]: # URL query string
20+
path = "?".join([path, record.args["q"]])
21+
log_record.setdefault("path", path)
22+
log_record.setdefault("status", record.args["s"])
23+
log_record.setdefault("referer", record.args["f"])
24+
log_record.setdefault("user_agent", record.args["a"])
25+
log_record.setdefault("len_bytes", record.args["B"])
26+
log_record.setdefault("duration_ms", record.args["M"])

ietf/wsgi.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
# Copyright The IETF Trust 2013-2021, All Rights Reserved
1+
# Copyright The IETF Trust 2013-2024, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

5-
65
import os
76
import sys
8-
import syslog
97

108
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
119

12-
syslog.openlog(str("datatracker"), syslog.LOG_PID, syslog.LOG_USER)
13-
1410
if not path in sys.path:
1511
sys.path.insert(0, path)
1612

1713
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings")
1814

19-
syslog.syslog("Starting datatracker wsgi instance")
20-
2115
from django.core.wsgi import get_wsgi_application
22-
application = get_wsgi_application()
2316

17+
application = get_wsgi_application()

k8s/settings_local.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,5 @@ def _multiline_to_list(s):
261261
if _csrf_trusted_origins_str is not None:
262262
CSRF_TRUSTED_ORIGINS = _multiline_to_list(_csrf_trusted_origins_str)
263263

264-
# Send logs to console instead of debug_console when running in kubernetes
265-
LOGGING["loggers"]["django"]["handlers"] = ["console", "mail_admins"]
266-
LOGGING["loggers"]["django.security"]["handlers"] = ["console"]
267-
LOGGING["loggers"]["datatracker"]["handlers"] = ["console"]
268-
LOGGING["loggers"]["celery"]["handlers"] = ["console"]
264+
# Console logs as JSON instead of plain when running in k8s
265+
LOGGING["handlers"]["console"]["formatter"] = "json"

0 commit comments

Comments
 (0)