From 2896c60c5f4388d71e8c4e786a4d5abaf260ac21 Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Sun, 26 Apr 2026 13:05:50 +0530 Subject: [PATCH] feat: implement better logging using structlog --- backend/Dockerfile | 2 +- backend/config/logging.py | 89 ++++++++++++++++++++++++++++++++++++ backend/config/settings.py | 8 +--- backend/letters/tasks.py | 4 +- backend/pyproject.toml | 3 ++ backend/uv.lock | 94 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 backend/config/logging.py diff --git a/backend/Dockerfile b/backend/Dockerfile index 44a2a91..787e926 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,7 +10,7 @@ RUN uv sync --frozen --no-dev COPY . . RUN uv run manage.py migrate -RUN mkdir -p logs +RUN mkdir -p /app/logs EXPOSE 8000 diff --git a/backend/config/logging.py b/backend/config/logging.py new file mode 100644 index 0000000..6349337 --- /dev/null +++ b/backend/config/logging.py @@ -0,0 +1,89 @@ +import structlog + +structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.filter_by_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + logger_factory=structlog.stdlib.LoggerFactory(), + cache_logger_on_first_use=True, +) + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "json_formatter": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.processors.JSONRenderer(), + }, + "plain_console": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.dev.ConsoleRenderer(colors=True), + }, + "key_value": { + "()": structlog.stdlib.ProcessorFormatter, + "processor": structlog.processors.KeyValueRenderer(key_order=["timestamp", "level", "event", "logger"]), + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "plain_console", + }, + "json_file": { + "class": "logging.handlers.WatchedFileHandler", + "filename": "logs/json.log", + "formatter": "json_formatter", + }, + "flat_line_file": { + "class": "logging.handlers.WatchedFileHandler", + "filename": "logs/flat_line.log", + "formatter": "key_value", + }, + "letters_log": { + "class": "logging.handlers.WatchedFileHandler", + "filename": "logs/letters.log", + "formatter": "key_value", + }, + "scheduler_log": { + "class": "logging.handlers.WatchedFileHandler", + "filename": "logs/scheduler.log", + "formatter": "key_value", + }, + }, + "loggers": { + "django_structlog": { + "handlers": ["console", "flat_line_file", "json_file"], + "level": "INFO", + "propagate": False, + }, + "django.core.mail": { + "handlers": ["console", "flat_line_file", "json_file"], + "level": "DEBUG", + "propagate": False, + }, + "letters": { + "handlers": ["console", "flat_line_file", "json_file", "letters_log"], + "level": "INFO", + "propagate": False, + }, + "scheduler": { + "handlers": ["console", "scheduler_log"], + "level": "INFO", + "propagate": False, + }, + "": { + "handlers": ["console", "flat_line_file", "json_file"], + "level": "INFO", + }, + }, +} diff --git a/backend/config/settings.py b/backend/config/settings.py index 0630e36..ab6b144 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.staticfiles", "django_extensions", + "django_structlog", "rest_framework", "corsheaders", "users", @@ -74,6 +75,7 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_structlog.middlewares.RequestMiddleware", ] ROOT_URLCONF = "config.urls" @@ -169,9 +171,3 @@ USE_TZ = True STATIC_URL = "static/" MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" - -LOGGING = { - "version": 1, - "handlers": {"console": {"class": "logging.StreamHandler"}}, - "loggers": {"letters": {"handlers": ["console"], "level": "INFO"}}, -} diff --git a/backend/letters/tasks.py b/backend/letters/tasks.py index f3805f4..d794018 100644 --- a/backend/letters/tasks.py +++ b/backend/letters/tasks.py @@ -1,13 +1,13 @@ -import logging from datetime import UTC, datetime +import structlog from apscheduler.schedulers.background import BackgroundScheduler from django.core.mail import send_mail from config import settings from letters.models import Letter -logger = logging.getLogger(__name__) +logger = structlog.get_logger(__name__) def get_vault_letters_to_notify(): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ce375d8..d578fa2 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "django-cors-headers>=4.9.0", "django-environ>=0.13.0", "django-extensions>=4.1", + "django-structlog>=10.0.0", "djangorestframework>=3.17.1", "djangorestframework-simplejwt>=5.5.1", "djangorestframework-stubs>=3.16.9", @@ -18,7 +19,9 @@ dependencies = [ "gunicorn>=25.3.0", "psycopg2-binary>=2.9.11", "pyopenssl>=26.0.0", + "rich>=15.0.0", "ruff>=0.15.9", + "structlog>=25.5.0", "werkzeug>=3.1.8", ] diff --git a/backend/uv.lock b/backend/uv.lock index b8a39fd..01b7caf 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -170,6 +170,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" }, ] +[[package]] +name = "django-ipware" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-ipware" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/64/c7e4791edf01ba483cce444770b3e6a930ba12195ba1eeb37b5bf6dce8a8/django-ipware-7.0.1.tar.gz", hash = "sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47", size = 6827, upload-time = "2024-04-19T20:02:49.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/33/bf539925b102d68200da5b1d3eacb8aa5d5d9a065972e8b8724d0d53bb0d/django_ipware-7.0.1-py2.py3-none-any.whl", hash = "sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709", size = 6425, upload-time = "2024-04-19T20:02:47.469Z" }, +] + +[[package]] +name = "django-structlog" +version = "10.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, + { name = "django-ipware" }, + { name = "structlog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/a9/102f316fb60dbec46642168979c4f57c9d8140fe43624ddca1ca6106274a/django_structlog-10.0.0.tar.gz", hash = "sha256:4e3fa4a930697fb9b649470e389419bb73b916a1ecf4f4bf2f8727f5cbfdb002", size = 23054, upload-time = "2025-10-22T21:20:21.14Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/83/d7245a2a2bb46ae65ecff00686181d632553b131ea0c5cbfcbdb8f89c190/django_structlog-10.0.0-py3-none-any.whl", hash = "sha256:4f9db3cb7b308df6aa4afe1353d9c19d5bac757022ddbbb5c24f3d0d6a91a240", size = 18159, upload-time = "2025-10-22T21:20:19.804Z" }, +] + [[package]] name = "django-stubs" version = "6.0.2" @@ -262,6 +289,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -292,6 +331,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "packaging" version = "26.1" @@ -312,6 +360,7 @@ dependencies = [ { name = "django-cors-headers" }, { name = "django-environ" }, { name = "django-extensions" }, + { name = "django-structlog" }, { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, { name = "djangorestframework-stubs" }, @@ -319,7 +368,9 @@ dependencies = [ { name = "gunicorn" }, { name = "psycopg2-binary" }, { name = "pyopenssl" }, + { name = "rich" }, { name = "ruff" }, + { name = "structlog" }, { name = "werkzeug" }, ] @@ -331,6 +382,7 @@ requires-dist = [ { name = "django-cors-headers", specifier = ">=4.9.0" }, { name = "django-environ", specifier = ">=0.13.0" }, { name = "django-extensions", specifier = ">=4.1" }, + { name = "django-structlog", specifier = ">=10.0.0" }, { name = "djangorestframework", specifier = ">=3.17.1" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, { name = "djangorestframework-stubs", specifier = ">=3.16.9" }, @@ -338,7 +390,9 @@ requires-dist = [ { name = "gunicorn", specifier = ">=25.3.0" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pyopenssl", specifier = ">=26.0.0" }, + { name = "rich", specifier = ">=15.0.0" }, { name = "ruff", specifier = ">=0.15.9" }, + { name = "structlog", specifier = ">=25.5.0" }, { name = "werkzeug", specifier = ">=3.1.8" }, ] @@ -370,6 +424,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyjwt" version = "2.12.1" @@ -403,6 +466,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-ipware" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/60/da4426c3e9aee56f08b24091a9e85a0414260f928f97afd0013dfbd0332f/python_ipware-3.0.0.tar.gz", hash = "sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062", size = 16609, upload-time = "2024-04-19T20:00:58.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/bd/ccd7416fdb30f104ddf6cfd8ee9f699441c7d9880a26f9b3089438adee05/python_ipware-3.0.0-py3-none-any.whl", hash = "sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60", size = 10761, upload-time = "2024-04-19T20:00:57.171Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.15.9" @@ -446,6 +531,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, ] +[[package]] +name = "structlog" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20260408"