Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 70 additions & 11 deletions convention-template/config/settings.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ from pathlib import Path

import bleach.sanitizer
import djp
import structlog

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
Expand All @@ -35,9 +36,7 @@ TEMPLATE_DEBUG = cfg.debug

class InvalidStringShowWarning(str):
def __mod__(self, other):
import logging

logger = logging.getLogger(__name__)
logger = structlog.get_logger(__name__)
logger.warning(
f"In template, undefined variable or unknown value for: '{other}'"
)
Expand Down Expand Up @@ -93,6 +92,8 @@ INSTALLED_APPS = [
"watchman",
# Markdown field support
"markdownfield",
# Structured logging
"django_structlog",
# the convention theme; this MUST come before the nominate app, so that its templates can
# override the nominate ones.
"{{ app }}",
Expand Down Expand Up @@ -137,8 +138,11 @@ MIDDLEWARE = [
"django_htmx.middleware.HtmxMiddleware",
"nomnom.middleware.HtmxMessageMiddleware",
"waffle.middleware.WaffleMiddleware",
"django_structlog.middlewares.RequestMiddleware",
]

DJANGO_STRUCTLOG_CELERY_ENABLED = True

ROOT_URLCONF = "config.urls"

TEMPLATES = [
Expand Down Expand Up @@ -337,6 +341,24 @@ EMAIL_HOST_USER = cfg.email.host_user
EMAIL_HOST_PASSWORD = cfg.email.host_password
EMAIL_USE_TLS = cfg.email.use_tls

# Structlog configuration - must come before LOGGING
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.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
Expand All @@ -346,33 +368,70 @@ LOGGING = {
},
},
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
"plain_console": {
"()": "structlog.stdlib.ProcessorFormatter",
"processors": [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.dev.ConsoleRenderer(colors=True),
],
"foreign_pre_chain": [
structlog.contextvars.merge_contextvars,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
],
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
"json": {
"()": "structlog.stdlib.ProcessorFormatter",
"processors": [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.processors.JSONRenderer(),
],
"foreign_pre_chain": [
structlog.contextvars.merge_contextvars,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
],
},
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
"formatter": "plain_console" if DEBUG else "json",
},
"debug_console": {
"level": "DEBUG",
"filters": ["require_debug_true"],
"class": "logging.StreamHandler",
"formatter": "simple",
"formatter": "plain_console",
},
},
"loggers": {
"django.db.backends": {
"level": "DEBUG",
"handlers": ["debug_console"],
},
"django_structlog": {
"handlers": ["console"],
"level": "INFO",
},
"django.server": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
# Root logger - catches all logs not matched by more specific loggers
"": {
"handlers": ["console"],
"level": "DEBUG" if DEBUG else "INFO",
},
},
}

Expand Down
15 changes: 15 additions & 0 deletions docs/docs/dev/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ domain name.
When copier finishes, it outputs a series of further steps to follow to
set up and run NomNom.

## Development Server Notes

### Reducing Log Verbosity in Development

If you find the duplicate request logs too noisy during development, you can:

1. **Suppress django-structlog request logs** by setting the environment variable:
```bash
DJANGO_LOG_LEVEL=WARNING python manage.py runserver
```

2. **Filter out static file requests** by adjusting the logger level for specific paths in your application code.

The built-in Django request handler logs cannot be suppressed without creating a custom runserver command, but they're harmless and won't appear in production.

## Contributing

All contributions should be made through pull requests (yes, I'm a bit of a hypocrite in that regard, as I do frequently develop on `main`. Nevertheless.)
Expand Down
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ environment-check:
./scripts/setup-env.sh

services:
mkdir -p .local/logs
docker compose up --wait

docker-ports:
Expand Down
89 changes: 78 additions & 11 deletions nomnom_dev/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import bleach.sanitizer
import djp
import structlog

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
Expand Down Expand Up @@ -60,9 +61,7 @@ def get_docker_port(service, port):

class InvalidStringShowWarning(str):
def __mod__(self, other):
import logging

logger = logging.getLogger(__name__)
logger = structlog.get_logger(__name__)
logger.warning(
f"In template, undefined variable or unknown value for: '{other}'"
)
Expand Down Expand Up @@ -122,6 +121,8 @@ def __bool__(self): # if using Python 2, use __nonzero__ instead
"markdownfield",
# Feature flags
"waffle",
# Structured logging
"django_structlog",
# the convention theme; this MUST come before the nominate app, so that its templates can
# override the nominate ones.
"nomnom_dev",
Expand Down Expand Up @@ -164,8 +165,11 @@ def __bool__(self): # if using Python 2, use __nonzero__ instead
"django_htmx.middleware.HtmxMiddleware",
"nomnom.middleware.HtmxMessageMiddleware",
"waffle.middleware.WaffleMiddleware",
"django_structlog.middlewares.RequestMiddleware",
]

DJANGO_STRUCTLOG_CELERY_ENABLED = True

ROOT_URLCONF = "nomnom_dev.urls"

TEMPLATES = [
Expand Down Expand Up @@ -366,6 +370,24 @@ def __bool__(self): # if using Python 2, use __nonzero__ instead
EMAIL_HOST_PASSWORD = cfg.email.host_password
EMAIL_USE_TLS = cfg.email.use_tls

# Structlog configuration - must come before LOGGING
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.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
Expand All @@ -375,33 +397,78 @@ def __bool__(self): # if using Python 2, use __nonzero__ instead
},
},
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
"plain_console": {
"()": "structlog.stdlib.ProcessorFormatter",
"processors": [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.dev.ConsoleRenderer(colors=True),
],
"foreign_pre_chain": [
structlog.contextvars.merge_contextvars,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
],
},
"simple": {
"format": "{levelname} {message}",
"style": "{",
"json": {
"()": "structlog.stdlib.ProcessorFormatter",
"processors": [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.processors.JSONRenderer(),
],
"foreign_pre_chain": [
structlog.contextvars.merge_contextvars,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
],
},
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
"formatter": "plain_console" if DEBUG else "json",
},
"debug_console": {
"level": "DEBUG",
"filters": ["require_debug_true"],
"class": "logging.StreamHandler",
"formatter": "simple",
"formatter": "plain_console",
},
"json_file": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / ".local/logs" / "json.log",
"maxBytes": 10485760, # 10MB
"backupCount": 5,
"formatter": "json",
},
},
"loggers": {
"django.db.backends": {
"level": os.environ.get("DJANGO_DB_LOG_LEVEL", "INFO"),
"handlers": ["debug_console"],
},
"django_structlog": {
"handlers": ["console", "json_file"],
"level": "INFO",
},
"django.server": {
"handlers": ["console", "json_file"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["console", "json_file"],
"level": "INFO",
"propagate": False,
},
# Root logger - catches all logs not matched by more specific loggers
"": {
"handlers": ["console", "json_file"],
"level": "DEBUG" if DEBUG else "INFO",
},
},
}

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ dependencies = [
"inflect>=7.5.0",
"rich>=13.9.4",
"faker>=37.6.0",
"django-admin-sortable2>=2.3.1",
"structlog>=24.0",
"django-structlog[celery]>=9.0",
]
requires-python = ">=3.13.0,<3.14"
readme = "README.md"
Expand Down
16 changes: 16 additions & 0 deletions src/nomnom/celery.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import os

from celery import Celery
from celery.signals import setup_logging
from django_structlog.celery.steps import DjangoStructLogInitStep

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

app = Celery("nomnom")

app.config_from_object("django.conf:settings", namespace="CELERY")

app.steps["worker"].add(DjangoStructLogInitStep) # type: ignore[assignment]

app.autodiscover_tasks()


@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f"Request: {self.request!r}")


@setup_logging.connect
def setup_celery_logging(loglevel, logfile, format, colorize, **kwargs):
import logging

from django.conf import settings

# importing settings and using LOGGING should automatically
# configure structlog.
settings_log_config = settings.LOGGING
logging.config.dictConfig(settings_log_config)
Loading
Loading