Por qué tu aplicación necesita contar lo que hace
Todo proyecto de software termina encontrándose con errores. Algunos aparecen en el entorno de desarrollo, con un traceback claro en la terminal que señala la línea exacta del problema. Pero los más difíciles son los que ocurren en producción: un pago que falla solo para ciertos usuarios, un proceso que se vuelve lento a determinadas horas o una integración externa que devuelve respuestas inesperadas de vez en cuando. En esos casos no hay una terminal a la vista ni un depurador al que conectarse. Lo único que tienes es lo que tu aplicación haya dejado escrito.
Ese registro de lo que ocurre durante la ejecución se conoce como logging. Un buen sistema de logs permite reconstruir la secuencia de eventos que llevó a un fallo, reproducir el problema y encontrar la causa en minutos en lugar de horas. También sirve para auditar acciones, detectar comportamientos anómalos y entender cómo se usa realmente la aplicación.
Django no inventa su propio sistema de logs: utiliza directamente el módulo logging de Python, una de las piezas más completas y flexibles de la biblioteca estándar. En esta guía veremos cómo funciona ese módulo, cómo se configura en un proyecto Django, cómo registrar mensajes y excepciones desde tus vistas, cómo enviar los logs a la consola, a archivos con rotación o por correo, y qué prácticas seguir en producción. Todos los ejemplos funcionan con Django 6.1, la versión estable actual del framework.
Por qué no basta con print
Muchos desarrolladores empiezan depurando con print. Es rápido y funciona, pero tiene limitaciones importantes en cuanto el proyecto crece:
- No distingue la gravedad de los mensajes: una información trivial y un error crítico se ven igual.
- No incluye contexto automáticamente, como la fecha, el módulo o la línea de código.
- No se puede activar o desactivar por partes de la aplicación sin tocar el código.
- Siempre escribe en la salida estándar; no puede enviar mensajes a archivos, servicios externos o correo.
- En producción, con varios procesos y servidores, los mensajes se mezclan y se pierden.
Compara un print con un mensaje de log:
print("Pedido creado")
Pedido creado
logger.info("Pedido %s creado por el usuario %s", pedido.id, request.user.id)
2026-10-17 10:58:14 [INFO] pedidos.views:42 Pedido 1042 creado por el usuario 87
El segundo indica cuándo ocurrió, con qué gravedad, en qué módulo y línea, y con qué datos. Y su destino y nivel de detalle se pueden cambiar desde la configuración sin modificar ni una línea de la vista.
Las cuatro piezas del sistema de logging
El módulo logging se organiza en cuatro componentes que trabajan juntos. Entenderlos es la clave para configurar cualquier escenario.
Los loggers son el punto de entrada. Tu código pide un logger con un nombre y le envía mensajes. Cada logger tiene un nivel mínimo: los mensajes por debajo de ese nivel se descartan. Los nombres forman una jerarquía separada por puntos, de modo que pedidos.views es hijo de pedidos, que a su vez es hijo del logger raíz.
Los handlers deciden adónde va cada mensaje: la consola, un archivo, un correo electrónico, un servicio de red o el registro del sistema. Un mismo logger puede tener varios handlers, cada uno con su propio nivel. Por ejemplo, todos los mensajes informativos pueden ir a un archivo mientras que solo los errores se envían por correo.
Los filtros se sitúan entre el logger y el handler y deciden, mensaje a mensaje, si se procesa o no. También pueden modificar el registro para añadirle información.
Los formatters definen cómo se presenta el texto final: qué campos aparecen y en qué orden.
El recorrido de un mensaje es el siguiente:
logger.warning("...")
│
├─ ¿nivel >= nivel del logger? ── no ──> descartado
│
├─ filtros del logger
│
├─ para cada handler:
│ ├─ ¿nivel >= nivel del handler?
│ ├─ filtros del handler
│ └─ formatter ──> destino (consola, archivo, correo...)
│
└─ si propagate=True, el mensaje sube al logger padre
Los niveles de gravedad
Cada mensaje se emite con un nivel que indica su importancia. Python define cinco niveles estándar, cada uno asociado a un valor numérico:
| Nivel | Valor | Cuándo usarlo |
|---|---|---|
| DEBUG | 10 | Detalles internos útiles solo al depurar |
| INFO | 20 | Eventos normales relevantes: un pedido creado, un usuario que inicia sesión |
| WARNING | 30 | Algo inesperado que no impide continuar: stock bajo, reintento de una petición |
| ERROR | 40 | Un fallo que impide completar una operación |
| CRITICAL | 50 | Un fallo grave que compromete la aplicación entera |
Cuando configuras un logger o un handler con un nivel, se procesan los mensajes de ese nivel y superiores. Un logger en WARNING deja pasar WARNING, ERROR y CRITICAL, pero ignora INFO y DEBUG.
Elegir bien el nivel es más importante de lo que parece. Si todo se registra como ERROR, las alertas pierden su valor; si todo es DEBUG, en producción no verás nada útil. Una regla práctica: en producción, el nivel habitual es INFO o WARNING, y DEBUG se activa temporalmente para investigar un problema concreto.
Preparar un proyecto de ejemplo
Para practicar, crearemos un proyecto pequeño de una tienda con una aplicación de pedidos. Empieza por un entorno virtual e instala Django:
mkdir tienda-logging && cd tienda-logging
python3 -m venv .venv
source .venv/bin/activate # En Windows: .venv\Scripts\activate
python -m pip install "django>=6.1,<6.2"
django-admin startproject tienda .
python manage.py startapp pedidos
Registra la aplicación en INSTALLED_APPS:
# tienda/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"pedidos",
]
La estructura resultante será esta:
tienda-logging
├── manage.py
├── logs
├── pedidos
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── tienda
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
Crea también la carpeta logs en la raíz, donde guardaremos los archivos de registro:
mkdir logs
Emitir mensajes desde el código
En cada módulo donde quieras registrar mensajes, crea un logger con el nombre del propio módulo. La convención es usar la variable especial __name__, que contiene la ruta del módulo, como pedidos.views:
# pedidos/views.py
import logging
from django.http import HttpResponse, JsonResponse
logger = logging.getLogger(__name__)
def inicio(request):
logger.info("Página de inicio visitada desde %s", request.META.get("REMOTE_ADDR"))
return HttpResponse("Bienvenido a la tienda")
def detalle_pedido(request, pedido_id):
logger.debug("Consultando el pedido %s", pedido_id)
pedido = {"id": pedido_id, "total": 58.40, "estado": "enviado"}
if pedido["total"] > 50:
logger.warning("Pedido %s supera el umbral de revisión manual", pedido_id)
return JsonResponse(pedido)
# tienda/urls.py
from django.contrib import admin
from django.urls import path
from pedidos import views
urlpatterns = [
path("admin/", admin.site.urls),
path("", views.inicio),
path("pedidos/<int:pedido_id>/", views.detalle_pedido),
]
Usar __name__ tiene una ventaja enorme: la jerarquía de loggers reproduce automáticamente la estructura de paquetes del proyecto, y podrás configurar el nivel de detalle de toda la aplicación pedidos, o solo de su módulo de vistas, desde la configuración.
Fíjate también en cómo se pasan los datos: como argumentos separados, con marcadores %s, en lugar de construir la cadena con una f-string. Este formato diferido tiene dos ventajas. La primera es el rendimiento, porque el texto solo se construye si el mensaje va a procesarse realmente. La segunda es que los servicios de agregación de logs pueden agrupar todos los mensajes con la misma plantilla aunque cambien los valores.
# Recomendado: el texto se construye solo si el mensaje se procesa
logger.debug("Respuesta del proveedor: %s", respuesta_grande)
# Evitar: la f-string se evalúa siempre, aunque DEBUG esté desactivado
logger.debug(f"Respuesta del proveedor: {respuesta_grande}")
Configurar el logging en settings.py
Django lee la configuración de logs de la variable LOGGING en settings.py. Su formato es un diccionario que sigue el esquema dictConfig de la biblioteca estándar. Esta es una configuración de logging en settings.py completa y comentada para el proyecto:
# tienda/settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOG_DIR = BASE_DIR / "logs"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detallado": {
"format": "{asctime} [{levelname}] {name}:{lineno} {message}",
"style": "{",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {
"format": "[{levelname}] {message}",
"style": "{",
},
},
"handlers": {
"consola": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"archivo_avisos": {
"level": "WARNING",
"class": "logging.FileHandler",
"filename": LOG_DIR / "avisos.log",
"formatter": "detallado",
"encoding": "utf-8",
},
},
"loggers": {
"pedidos": {
"handlers": ["consola", "archivo_avisos"],
"level": "DEBUG",
"propagate": False,
},
},
}
Repasemos cada clave.
version indica la versión del esquema de configuración; actualmente solo existe la 1 y es obligatoria.
disable_existing_loggers en False mantiene activos los loggers que Django y otras librerías ya tienen configurados. Si la pones en True, silenciarás también los mensajes internos de Django, algo que casi nunca interesa.
formatters define los formatos de salida. Con "style": "{" se usan llaves en lugar del antiguo formato con %. Los campos más útiles son asctime, la fecha y hora; levelname, el nivel; name, el nombre del logger; module, funcName y lineno, para localizar el origen; process y thread, útiles con varios trabajadores; y message, el texto del mensaje.
handlers define los destinos. Aquí hay dos: la consola, que recibe todos los mensajes del logger, y un archivo que solo recibe los de nivel WARNING o superior.
loggers asocia los loggers con sus handlers. Al configurar pedidos, la configuración se aplica también a todos sus hijos, como pedidos.views o pedidos.services.
Arranca el servidor y visita http://127.0.0.1:8000/pedidos/1042/:
python manage.py runserver
En la consola aparecerán ambos mensajes, y en logs/avisos.log solo el aviso:
2026-10-17 10:58:14 [WARNING] pedidos.views:20 Pedido 1042 supera el umbral de revisión manual
La propagación y el logger raíz
La opción propagate controla si un mensaje, después de pasar por los handlers de su logger, sube también al logger padre. Por defecto es True.
Esto explica uno de los problemas más comunes: los mensajes duplicados. Si configuras un handler de consola tanto en el logger pedidos como en el logger raíz, y pedidos propaga, cada mensaje aparecerá dos veces:
"root": {"handlers": ["consola"], "level": "WARNING"},
"loggers": {
"pedidos": {"handlers": ["consola"], "level": "DEBUG"}, # propagate=True por defecto
},
[WARNING] Pedido 1042 supera el umbral de revisión manual
[WARNING] Pedido 1042 supera el umbral de revisión manual
La solución es poner "propagate": False en el logger hijo o no repetir el mismo handler en ambos niveles.
El logger raíz recoge todos los mensajes que propagan y no han sido detenidos. Se configura con la clave root o con un logger de nombre vacío "". Puede ser útil como red de seguridad para capturar avisos de cualquier librería, pero conviene usarlo con un nivel alto, como WARNING, para no inundar los registros con mensajes de depuración de terceros:
LOGGING = {
# ...
"root": {
"handlers": ["consola"],
"level": "WARNING",
},
}
Los loggers integrados de Django
Django emite sus propios mensajes a través de varios loggers con nombre, y conocerlos permite obtener mucha información sin escribir código:
django: el logger padre de todos los mensajes del framework.django.request: errores en el procesamiento de peticiones. Las respuestas 5xx se registran comoERRORy las 4xx comoWARNING.django.server: las peticiones que recibe el servidor de desarrollo derunserver.django.template: problemas al renderizar plantillas.django.db.backends: todas las consultas SQL que ejecuta el ORM, con su duración, en nivelDEBUG.django.security.*: incidencias de seguridad, como peticiones con unHostno permitido o fallos de CSRF.
El logger django.db.backends es especialmente útil para detectar el problema de las consultas N+1, en el que un bucle provoca una consulta a la base de datos por cada elemento:
"loggers": {
"django.db.backends": {
"handlers": ["consola"],
"level": "DEBUG",
"propagate": False,
},
},
[DEBUG] (0.001) SELECT "pedidos_pedido"."id", "pedidos_pedido"."cliente_id" FROM "pedidos_pedido"; args=(); alias=default
[DEBUG] (0.000) SELECT "clientes_cliente"."id", "clientes_cliente"."nombre" FROM "clientes_cliente" WHERE "clientes_cliente"."id" = 3 LIMIT 21; args=(3,); alias=default
Por razones de rendimiento, Django solo registra las consultas SQL cuando DEBUG = True, así que esta técnica está pensada para el entorno de desarrollo.
Para la seguridad, conviene enviar django.security a un archivo o servicio específico, porque esos avisos pueden revelar intentos de ataque:
"django.security": {
"handlers": ["archivo_avisos"],
"level": "WARNING",
"propagate": False,
},
Registrar excepciones con todo su contexto
Cuando capturas una excepción, el método logger.exception registra el mensaje con nivel ERROR y añade automáticamente el traceback completo. Es la forma más eficaz de registrar excepciones con traceback completo sin perder información:
# pedidos/services.py
import logging
logger = logging.getLogger(__name__)
def aplicar_descuento(pedido_id, total, porcentaje):
try:
return round(total - total * porcentaje / 100, 2)
except TypeError:
logger.exception("No se pudo aplicar el descuento al pedido %s", pedido_id)
raise
2026-10-17 10:58:14 [ERROR] pedidos.services:10 No se pudo aplicar el descuento al pedido 1042
Traceback (most recent call last):
File "/app/pedidos/services.py", line 8, in aplicar_descuento
return round(total - total * porcentaje / 100, 2)
~~~~~~^~~~~~~~~~~~
TypeError: can't multiply sequence by non-int of type 'float'
logger.exception solo debe llamarse dentro de un bloque except. Si quieres el traceback con otro nivel, usa el parámetro exc_info=True:
logger.warning("Reintentando la conexión con el proveedor", exc_info=True)
Un error muy común es capturar una excepción, registrar solo su mensaje con logger.error(str(e)) y perder así el traceback, que es precisamente la información que permite saber dónde ocurrió el fallo.
Ten en cuenta también que no hace falta registrar las excepciones que no capturas en las vistas: Django ya las envía al logger django.request con su traceback completo. Registrarlas tú y volver a lanzarlas sin más produciría entradas duplicadas.
Añadir contexto con extra y filtros
A menudo quieres que cada mensaje incluya información adicional, como el identificador de la petición o el usuario. El parámetro extra añade campos al registro, que luego puede usar el formatter:
logger.info(
"Pago confirmado",
extra={"pedido_id": pedido.id, "importe": pedido.total},
)
Para no tener que pasar el mismo contexto en cada llamada, un filtro puede añadirlo automáticamente. Este ejemplo genera un identificador único por petición mediante un middleware y un filtro lo inyecta en todos los mensajes:
# tienda/logging_utils.py
import contextvars
import logging
import uuid
id_peticion = contextvars.ContextVar("id_peticion", default="-")
class IdPeticionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
token = id_peticion.set(uuid.uuid4().hex[:8])
try:
return self.get_response(request)
finally:
id_peticion.reset(token)
class IdPeticionFilter(logging.Filter):
def filter(self, record):
record.id_peticion = id_peticion.get()
return True
# tienda/settings.py
MIDDLEWARE = [
"tienda.logging_utils.IdPeticionMiddleware",
# ... resto de middleware
]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"id_peticion": {"()": "tienda.logging_utils.IdPeticionFilter"},
},
"formatters": {
"detallado": {
"format": "{asctime} [{levelname}] [{id_peticion}] {name} {message}",
"style": "{",
},
},
"handlers": {
"consola": {
"class": "logging.StreamHandler",
"filters": ["id_peticion"],
"formatter": "detallado",
},
},
"loggers": {
"pedidos": {"handlers": ["consola"], "level": "INFO", "propagate": False},
},
}
2026-10-17 10:58:14,512 [INFO] [3f9a1c2e] pedidos.views Pedido 1042 creado
2026-10-17 10:58:14,630 [WARNING] [3f9a1c2e] pedidos.services Stock bajo para SKU-77
Con este identificador puedes filtrar todos los mensajes de una misma petición, algo muy valioso cuando el servidor atiende cientos de peticiones simultáneas. El uso de contextvars hace que funcione correctamente también con vistas asíncronas.
Django incluye además dos filtros útiles: RequireDebugFalse y RequireDebugTrue, que activan un handler solo en producción o solo en desarrollo.
Rotación de archivos y aviso por correo
Un FileHandler escribe siempre en el mismo archivo, que crece sin límite hasta llenar el disco. En producción, la rotación de archivos de log es imprescindible. El módulo logging.handlers ofrece dos opciones:
"handlers": {
"archivo_rotativo": {
"class": "logging.handlers.RotatingFileHandler",
"filename": LOG_DIR / "tienda.log",
"maxBytes": 10 * 1024 * 1024, # 10 MB por archivo
"backupCount": 5, # conserva tienda.log.1 ... tienda.log.5
"formatter": "detallado",
"encoding": "utf-8",
},
"archivo_diario": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": LOG_DIR / "accesos.log",
"when": "midnight",
"backupCount": 30, # conserva 30 días
"formatter": "detallado",
"encoding": "utf-8",
},
},
RotatingFileHandler crea un archivo nuevo cuando el actual alcanza un tamaño, y TimedRotatingFileHandler lo hace por tiempo. Ambos eliminan los archivos más antiguos cuando se supera backupCount.
Hay una advertencia importante: estos handlers no son seguros cuando varios procesos escriben en el mismo archivo, como ocurre con Gunicorn o uWSGI y varios trabajadores. En ese caso, es preferible escribir en la salida estándar y dejar la rotación a herramientas del sistema como logrotate o al servicio de logs de la plataforma.
Django incluye también un handler, AdminEmailHandler, que envía por correo a las personas definidas en ADMINS los errores graves, con el traceback y los detalles de la petición:
ADMINS = [("Equipo de guardia", "[email protected]")]
LOGGING = {
# ...
"filters": {
"solo_produccion": {"()": "django.utils.log.RequireDebugFalse"},
},
"handlers": {
"correo_admins": {
"level": "ERROR",
"class": "django.utils.log.AdminEmailHandler",
"filters": ["solo_produccion"],
},
},
"loggers": {
"django.request": {
"handlers": ["correo_admins"],
"level": "ERROR",
"propagate": True,
},
},
}
Es útil en proyectos pequeños, pero en aplicaciones con mucho tráfico puede generar avalanchas de correos ante un fallo repetido. Para esos casos son más adecuadas las herramientas de monitorización de errores que veremos más adelante.
Configuración por entorno y logs en contenedores
El nivel de detalle que necesitas en desarrollo no es el mismo que en producción. Una forma sencilla de adaptarlo es leer el nivel desde una variable de entorno:
import os
NIVEL_LOG = os.environ.get("NIVEL_LOG", "INFO")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"detallado": {
"format": "{asctime} [{levelname}] {name} {process:d} {message}",
"style": "{",
},
},
"handlers": {
"consola": {"class": "logging.StreamHandler", "formatter": "detallado"},
},
"root": {"handlers": ["consola"], "level": "WARNING"},
"loggers": {
"pedidos": {"handlers": ["consola"], "level": NIVEL_LOG, "propagate": False},
"django": {"handlers": ["consola"], "level": "INFO", "propagate": False},
},
}
NIVEL_LOG=DEBUG python manage.py runserver
Cuando la aplicación se ejecuta en contenedores Docker, en Kubernetes o en plataformas gestionadas, la práctica recomendada es escribir los logs en la salida estándar en lugar de en archivos. La plataforma se encarga de recogerlos, almacenarlos, rotarlos y enviarlos a un sistema centralizado. Los archivos dentro de un contenedor se pierden cuando este se reinicia.
Logs estructurados en JSON
Los logs en texto plano son cómodos para leer en la terminal, pero difíciles de consultar a gran escala. Los sistemas de agregación, como Elasticsearch, Loki, Datadog o CloudWatch, trabajan mucho mejor con logs estructurados, normalmente en formato JSON, donde cada campo se puede filtrar y agrupar.
Puedes escribir un formatter propio con unas pocas líneas:
# tienda/logging_utils.py
import json
import logging
from datetime import datetime, timezone
class JsonFormatter(logging.Formatter):
def format(self, record):
datos = {
"fecha": datetime.fromtimestamp(record.created, timezone.utc).isoformat(),
"nivel": record.levelname,
"logger": record.name,
"mensaje": record.getMessage(),
}
for campo in ("pedido_id", "importe", "id_peticion"):
if hasattr(record, campo):
datos[campo] = getattr(record, campo)
if record.exc_info:
datos["traceback"] = self.formatException(record.exc_info)
return json.dumps(datos, ensure_ascii=False)
"formatters": {
"json": {"()": "tienda.logging_utils.JsonFormatter"},
},
{"fecha": "2026-10-17T15:58:14.512000+00:00", "nivel": "INFO", "logger": "pedidos.views", "mensaje": "Pago confirmado", "pedido_id": 1042, "importe": 58.4}
Librerías como python-json-logger o structlog ofrecen formatters JSON más completos y listos para usar.
Seguridad: qué no debe aparecer en los logs
Los logs se copian, se envían a servicios externos, se conservan durante meses y los consultan muchas personas. Por eso, nunca deben incluir datos sensibles en los logs como contraseñas, tokens de acceso, claves de API, números de tarjeta, documentos de identidad o información médica. Además del riesgo de seguridad, en muchos países registrar datos personales sin necesidad incumple la normativa de protección de datos.
# Peligroso: registra el cuerpo completo de la petición, incluida la contraseña
logger.info("Datos de inicio de sesión: %s", request.POST)
# Correcto: registra solo lo necesario
logger.info("Intento de inicio de sesión para el usuario %s", request.POST.get("username"))
Django ayuda en parte: los informes de error que genera filtran automáticamente las variables con nombres como password, secret, token o key, y ofrece los decoradores sensitive_variables y sensitive_post_parameters para marcar datos adicionales que deben ocultarse:
from django.views.decorators.debug import sensitive_post_parameters
@sensitive_post_parameters("numero_tarjeta", "cvv")
def procesar_pago(request):
...
Otra precaución es evitar la inyección de logs: si registras texto introducido por el usuario, un atacante podría incluir saltos de línea para falsificar entradas. Los logs estructurados en JSON mitigan este problema, porque escapan los caracteres especiales.
Errores comunes y cómo resolverlos
El primero es que no aparezca ningún mensaje. Casi siempre se debe a que el nivel del logger o del handler es más alto que el del mensaje, o a que el logger no tiene handlers y el mensaje no propaga a ninguno que los tenga. Revisa los niveles de ambos y el nombre del logger: si configuras pedido y el código usa pedidos.views, la configuración no se aplica.
El segundo es que falte la carpeta del archivo de log. Django no crea los directorios, y el servidor fallará al arrancar:
ValueError: Unable to configure handler 'archivo_avisos'
Crea la carpeta previamente o hazlo desde settings.py con LOG_DIR.mkdir(exist_ok=True).
El tercero son los mensajes duplicados, causados por la propagación hacia un logger padre con el mismo handler, como vimos antes.
El cuarto es usar disable_existing_loggers: True sin querer, lo que silencia los mensajes de Django y de las librerías que ya habían creado sus loggers.
El quinto es crear loggers dentro de las funciones o con nombres fijos repetidos en todo el proyecto. Crea uno por módulo, a nivel de módulo, con logging.getLogger(__name__).
El sexto es llamar a logging.basicConfig en el código de la aplicación. En un proyecto Django, la configuración debe centralizarse en LOGGING; basicConfig puede añadir handlers inesperados al logger raíz.
Y el séptimo es registrar en exceso. Un mensaje INFO dentro de un bucle que procesa miles de elementos genera ruido, ocupa espacio y ralentiza la aplicación. Registra resúmenes en lugar de cada iteración.
Alternativas y herramientas complementarias
El módulo estándar cubre la mayoría de necesidades, pero existen herramientas que lo complementan.
structlog facilita los logs estructurados y la acumulación de contexto, y se integra con el módulo estándar. loguru ofrece una API más sencilla y un formato agradable con muy poca configuración, aunque en proyectos Django suele resultar más natural mantener el sistema estándar, que es el que usan el framework y las librerías.
Para los errores en producción, servicios como Sentry, Rollbar o Honeybadger capturan las excepciones con su contexto completo, las agrupan, muestran su frecuencia y alertan al equipo. Su integración con Django se hace con unas pocas líneas:
python -m pip install sentry-sdk
import sentry_sdk
sentry_sdk.init(
dsn="https://[email protected]/0",
traces_sample_rate=0.1,
send_default_pii=False,
)
Por último, OpenTelemetry se ha convertido en el estándar abierto para la observabilidad, unificando logs, métricas y trazas distribuidas, algo especialmente útil en arquitecturas con varios servicios.
Conclusiones
El logging es una de las herramientas más valiosas para entender y depurar una aplicación Django, sobre todo cuando los problemas ocurren en producción y no hay otra forma de saber qué pasó. Django se apoya en el módulo estándar de Python, de modo que todo lo aprendido sirve también para cualquier otro proyecto en este lenguaje.
El sistema se basa en cuatro piezas: loggers que reciben los mensajes, handlers que los envían a su destino, filtros que deciden qué se procesa y formatters que definen su aspecto. Todo se configura de forma centralizada en la variable LOGGING de settings.py, y en el código basta con crear un logger por módulo con logging.getLogger(__name__) y elegir el nivel adecuado para cada mensaje.
Los loggers integrados de Django, como django.request, django.db.backends y django.security, ofrecen información muy útil sin escribir código. logger.exception conserva el traceback de los errores, los filtros y el parámetro extra añaden contexto, y los handlers con rotación evitan que los archivos crezcan sin control.
En producción, conviene escribir en la salida estándar cuando se trabaja con contenedores, usar logs estructurados en JSON para facilitar las consultas, no registrar nunca datos sensibles y complementar el sistema con una herramienta de monitorización de errores. Con una buena configuración de logs, diagnosticar un fallo deja de ser una búsqueda a ciegas y se convierte en seguir un rastro claro.
