From 86ccdd58ee03b453d0a1fb6dec8eb6818101b305 Mon Sep 17 00:00:00 2001
From: bronsen <kontakt+gitcommit@nrrd.de>
Date: Sat, 15 Mar 2025 21:57:13 +0100
Subject: [PATCH] [logs] set up and use structlog in our app

---
 collector/views.py |  8 ++++---
 config/settings.py | 54 +++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 58 insertions(+), 4 deletions(-)

diff --git a/collector/views.py b/collector/views.py
index cc6023d..13d7a09 100644
--- a/collector/views.py
+++ b/collector/views.py
@@ -1,4 +1,4 @@
-import logging
+import structlog
 from typing import Any
 
 from django.db import transaction
@@ -9,7 +9,7 @@ from django.views import generic
 
 from .models import Teil
 
-logger = logging.getLogger(__name__)
+logger = structlog.get_logger(__name__)
 
 
 DEFAULT_TEILE_NUMBER = 10
@@ -47,8 +47,10 @@ class DetailView(generic.DetailView):
 def enter(request: HttpRequest) -> HttpResponse:
     try:
         with transaction.atomic():
-            Teil.objects.create(name=request.POST["new_name"])
+            teil = Teil.objects.create(name=request.POST["new_name"])
     except Exception:
         logger.warning("Teil already existed")
+    else:
+        logger.info("New Teil entered", teil_id=teil.pk)
 
     return HttpResponseRedirect(reverse("collector:list"))
diff --git a/config/settings.py b/config/settings.py
index e9c707c..febcbb7 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -8,9 +8,12 @@ For the full list of settings and their values, see
 https://docs.djangoproject.com/en/5.1/ref/settings/
 """
 
-import environ
+import os
 from pathlib import Path
 
+import environ
+import structlog
+
 BASE_DIR = Path(__file__).resolve().parent.parent
 
 env = environ.Env(
@@ -32,6 +35,7 @@ CSRF_COOKIE_SECURE = True
 # Application definition
 
 INSTALLED_APPS = [
+    "django_structlog",
     "django.contrib.admin",
     "django.contrib.auth",
     "django.contrib.contenttypes",
@@ -48,6 +52,7 @@ if DEPLOYMENT == "DEV":
     SHELL_PLUS = "ipython"
 
 MIDDLEWARE = [
+    "django_structlog.middlewares.RequestMiddleware",
     "django.middleware.security.SecurityMiddleware",
     "django.contrib.sessions.middleware.SessionMiddleware",
     "django.middleware.common.CommonMiddleware",
@@ -104,3 +109,50 @@ STATIC_URL = "static/"
 # Default primary key field type
 # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
 DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+
+LOGGING = {
+    "version": 1,
+    "disable_existing_loggers": True,
+    "formatters": {
+        "logftm_formatter": {
+            "()": structlog.stdlib.ProcessorFormatter,
+            "processor": structlog.processors.LogfmtRenderer(sort_keys=True, key_order=["level", "event"]),
+        },
+    },
+    "handlers": {
+        "console": {
+            "class": "logging.StreamHandler",
+            "formatter": "logftm_formatter",
+        },
+    },
+    "loggers": {
+        "django_structlog": {
+            "handlers": ["console"],
+            "level": "DEBUG",
+        },
+        "root": {
+            "handlers": ["console"],
+            "level": "DEBUG",
+        },
+    },
+}
+
+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,
+)
+os.makedirs(BASE_DIR / "logs", exist_ok=True)
+
+# DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True