Home / Backend / Logging en Django: cómo depurar tus proyectos con el módulo de Python
Logging en Django: cómo depurar tus proyectos con el módulo de Python

Logging en Django: cómo depurar tus proyectos con el módulo de Python

21 October 2026 · 18 min de lectura ··

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:

NivelValorCuándo usarlo
DEBUG10Detalles internos útiles solo al depurar
INFO20Eventos normales relevantes: un pedido creado, un usuario que inicia sesión
WARNING30Algo inesperado que no impide continuar: stock bajo, reintento de una petición
ERROR40Un fallo que impide completar una operación
CRITICAL50Un 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 como ERROR y las 4xx como WARNING.
  • django.server: las peticiones que recibe el servidor de desarrollo de runserver.
  • django.template: problemas al renderizar plantillas.
  • django.db.backends: todas las consultas SQL que ejecuta el ORM, con su duración, en nivel DEBUG.
  • django.security.*: incidencias de seguridad, como peticiones con un Host no 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.