Implementar logging y audit trails de API
Configura logging de petición/respuesta y audit trails que pasan revisiones de compliance: salida estructurada, correlation IDs, enmascaramiento y almacenamiento append-only.
Descripción General
Tarde o temprano una revisión de compliance hace la pregunta incómoda: ¿quién cambió qué, cuándo y desde dónde? Los logs de petición/respuesta sirven para depurar y medir rendimiento; los audit trails responden a esa pregunta en almacenamiento append-only, que es lo que los auditores de SOC 2, ISO 27001 y GDPR realmente piden. El logging general de peticiones está cubierto en la receta de logging; esta se centra en la parte de auditoría: correlation IDs, enmascaramiento y almacenamiento resistente a manipulación.
Cuándo Usar
- Necesitas depurar problemas de API en producción sin reproducirlos localmente
- Los requisitos de compliance exigen un registro de operaciones sensibles (quién cambió qué y cuándo)
- Ejecutas servicios distribuidos y necesitas trazar una petición entre ellos
- Omite esto si tu API es una herramienta interna de bajo tráfico sin operaciones sensibles — un log de peticiones basta
Solución
Python (FastAPI)
import json
import logging
import uuid
from datetime import datetime, timezone
from fastapi import Request
from fastapi.middleware.base import BaseHTTPMiddleware
logging.basicConfig(level=logging.INFO)
request_logger = logging.getLogger("api.requests")
audit_logger = logging.getLogger("api.audit")
class AuditMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
correlation_id = request.headers.get("x-correlation-id") or str(uuid.uuid4())
request.state.correlation_id = correlation_id
response = await call_next(request)
request_logger.info(json.dumps({
"timestamp": datetime.now(timezone.utc).isoformat(),
"correlation_id": correlation_id,
"method": request.method,
"path": str(request.url),
"status_code": response.status_code,
"user_agent": request.headers.get("user-agent"),
"client_ip": request.client.host,
}))
# Audit record: only for state-changing or sensitive operations
if request.method not in ("GET", "HEAD", "OPTIONS"):
audit_logger.info(json.dumps({
"timestamp": datetime.now(timezone.utc).isoformat(),
"correlation_id": correlation_id,
"actor": getattr(request.state, "user_id", "anonymous"),
"action": request.method,
"resource": request.url.path,
"outcome": "success" if response.status_code < 400 else "failure",
"client_ip": request.client.host,
}))
response.headers["X-Correlation-Id"] = correlation_id
return response
JavaScript (Express)
const { v4: uuidv4 } = require('uuid');
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
function auditMiddleware(req, res, next) {
const correlationId = req.headers['x-correlation-id'] || uuidv4();
req.correlationId = correlationId;
res.setHeader('X-Correlation-Id', correlationId);
const start = Date.now();
res.on('finish', () => {
logger.info('api_request', {
correlation_id: correlationId,
method: req.method,
path: req.path,
status_code: res.statusCode,
duration_ms: Date.now() - start,
client_ip: req.ip,
user_agent: req.get('user-agent'),
});
// Audit record: only for state-changing or sensitive operations
if (!['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
logger.info('api_audit', {
timestamp: new Date().toISOString(),
correlation_id: correlationId,
actor: (req.user && req.user.id) || 'anonymous',
action: req.method,
resource: req.path,
outcome: res.statusCode < 400 ? 'success' : 'failure',
client_ip: req.ip,
});
}
});
next();
}
module.exports = auditMiddleware;
Java (Spring)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class AuditFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger("api.requests");
private static final Logger auditLogger = LoggerFactory.getLogger("api.audit");
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String correlationId = request.getHeader("X-Correlation-Id");
if (correlationId == null) correlationId = UUID.randomUUID().toString();
MDC.put("correlationId", correlationId);
response.setHeader("X-Correlation-Id", correlationId);
long start = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
logger.info("method={} path={} status={} duration={}ms",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
System.currentTimeMillis() - start);
// Audit record: only for state-changing or sensitive operations
if (!"GET".equals(request.getMethod()) && !"HEAD".equals(request.getMethod())) {
auditLogger.info("correlationId={} actor={} action={} resource={} outcome={} ip={}",
correlationId,
request.getUserPrincipal() != null
? request.getUserPrincipal().getName() : "anonymous",
request.getMethod(),
request.getRequestURI(),
response.getStatus() < 400 ? "success" : "failure",
request.getRemoteAddr());
}
MDC.clear();
}
}
}
Cómo Funciona
- El middleware asigna (o respeta) un correlation ID en el edge y lo devuelve en
X-Correlation-Idpara que clientes y servicios downstream referencien la misma petición — el patrón de correlation ID cubre la mecánica de propagación en profundidad. - La salida estructurada (JSON, campos MDC) hace cada entrada procesable por máquina: ELK, Datadog, Loki o CloudWatch filtran por campo en lugar de usar regex sobre texto.
- Dos flujos de log importan. Los logs operacionales son efímeros y de alto volumen; los audit logs son de bajo volumen, larga retención y a menudo legalmente requeridos. Mantenlos en sinks separados con controles de acceso distintos — consulta agregación de logs para la parte de envío.
- El registro de auditoría debe incluir timestamp (UTC), actor, acción, recurso, resultado e IP de origen — la OWASP Logging Cheat Sheet los exige explícitamente, y con menos que eso no pasas una revisión de compliance.
Qué Auditar
No toda petición merece una entrada de auditoría — la señal se ahoga si logueas lecturas. Reserva el stream de auditoría para operaciones que un auditor preguntaría:
- Eventos de autenticación: logins, logins fallidos, emisión de tokens, cambios de contraseña
- Cambios de privilegios: asignación de roles, actualizaciones de permisos, acciones de admin
- Acceso a datos sensibles: lecturas o exportaciones de PII, datos de pago, descargas masivas
- Cambios de configuración: ajustes, API keys, endpoints de webhooks, feature flags
- Mutaciones de datos: crear, actualizar y borrar sobre recursos protegidos
El tráfico de solo lectura se queda en el log operacional. Todo lo que cambia estado o toca datos sensibles merece una entrada de auditoría — el código de arriba emite una para peticiones que no son GET como punto de partida; ajusta el filtro a tu modelo de amenaza.
Variantes
| Herramienta | Lenguaje | Salida | Ideal para |
|---|---|---|---|
| structlog | Python | JSON | Logging semántico con vinculación de contexto |
| Pino | JavaScript | JSON | Logging de alto rendimiento para Node.js (async por diseño) |
| Logback + MDC | Java | JSON/Pattern | Contexto thread-local en Spring |
Cada una tiene una forma incorporada de mantener secretos fuera de la salida — configúrala antes de que el logger vea tráfico de producción:
// Pino — redact fields before they reach the sink
const logger = pino({ redact: ['req.headers.authorization', '*.password'] });
# structlog — drop sensitive keys in a processor
def redact(logger, method, event):
return {k: ("[REDACTED]" if "password" in k or "token" in k else v)
for k, v in event.items()}
<!-- Logback — mask values in the encoder pattern -->
<pattern>%replace(%msg){'password=\w+', 'password=[REDACTED]'}</pattern>
Mejores Prácticas
- Nunca logues datos sensibles — contraseñas, tokens y PII se enmascaran o hashean antes de llegar al pipeline de logs.
- Propaga el correlation ID en cada llamada downstream, no solo en el edge: la traza muere en el primer servicio que descarte el header.
- Loguea de forma asíncrona — usa buffers para que un sink lento nunca bloquee el camino de la petición.
- Separa audit de debug — las entradas de auditoría necesitan almacenamiento append-only o WORM, mayor retención y control de acceso más estricto. Envíalas a un SIEM o encadénalas con hashes para que la manipulación sea detectable, no solo almacenada.
- Loguea en el nivel correcto: INFO para tráfico normal, WARN para comportamiento degradado, ERROR para fallos que requieren intervención, DEBUG solo en desarrollo.
- Rota y archiva — comprime logs antiguos y muévelos a almacenamiento frío (S3 Glacier) antes de que el disco se convierta en el incidente.
Errores Comunes
- Loguear bodies de request/response por defecto — muchos frameworks capturan payloads completos incluyendo contraseñas y API keys. Configura el enmascaramiento antes de habilitar body logging cerca de producción.
- Logging síncrono en el camino de la petición — una escritura síncrona a un sink remoto puede añadir milisegundos por llamada; el modo async existe en Winston, Pino y Log4j por algo.
- Olvidar limpiar el contexto thread-local. MDC o
request.statefiltrado entre peticiones produce entradas de auditoría atribuidas al usuario equivocado. - Loguear todo a nivel INFO — volumen que oculta la señal; muestrea el tráfico rutinario y conserva todo WARN/ERROR.
- Guardar logs de auditoría junto a los de aplicación: un operador que puede purgar los logs de app también puede borrar la evidencia.
Solución de Problemas
- PII aparece en los logs: el filtro de enmascaramiento corre después de la serialización o solo cubre nombres de campo conocidos. Enmascara por patrón de clave (
password,token,authorization) y por forma de valor (JWT, regex de tarjeta). - Los correlation IDs se rompen en el gateway o proxy: un proxy upstream elimina
X-Correlation-Id. Incluye el header en la lista de permitidos del proxy y regenéralo solo si falta — nunca sobrescribas uno entrante. - MDC/contexto vacío dentro de código async: el contexto thread-local no cruza fronteras de thread. Captura el ID antes del traspaso asíncrono o usa un ejecutor que lo propague.
- Huecos de auditoría durante picos de tráfico: el sink de logs aplica backpressure y descarta entradas — he visto ráfagas enteras desaparecer del rastro justo durante el incidente que más las necesitaba. Usa un buffer local con overflow a disco para que las ráfagas no borren el rastro silenciosamente.
- La IP del cliente muestra el proxy, no el usuario: detrás de un balanceador,
request.client.hostoreq.ipdevuelve la dirección del proxy. Confía enX-Forwarded-Forsolo desde proxies conocidos — Express necesitaapp.set('trust proxy', ...), FastAPI unProxyHeadersMiddleware.
Operación a Escala
Cifras aproximadas a finales de 2026 — valida contra los precios actuales y tu propio volumen antes de presupuestar:
| Componente | Costo aprox. (1M logs/día) | Notas |
|---|---|---|
| ELK self-hosted | $200–$500/mes | cluster de 3 nodos, ~100 GB |
| Datadog | $1.500–$3.000/mes | Ingesta + retención |
| CloudWatch | $150–$400/mes | ~$0,50/GB ingesta, ~$0,03/GB almacenamiento |
| Loki + Grafana | $100–$300/mes | Self-hosted, backend S3 |
| Archivo de auditoría (S3 Glacier) | ~$0,004/GB/mes | Retención de 7 años casi gratis |
El volumen crece rápido: 10K req/s son unos 35–80 GB de logs al día con entradas de acceso mínimas (~40–90 bytes), pero un registro de auditoría completo con actor, recurso y resultado ocupa 200–500 bytes y lleva el mismo tráfico a 170–430 GB/día. Las escrituras síncronas a un sink remoto añaden milisegundos por petición y pueden recortar el throughput notablemente; un buffer asíncrono con flush en segundo plano mantiene el overhead cerca de cero. Para controlar costes, muestrea las entradas INFO rutinarias (p. ej. 10%) conservando todos los WARN y ERROR. En microservicios, una petición de usuario puede tocar 5–15 servicios — OpenTelemetry complementa los logs con el grafo de llamadas completo.
Checklist de Producción
- Campos sensibles (contraseñas, tokens, PII) redactados o hasheados antes de loguear
- Correlation ID generado en el edge y propagado a todos los servicios
- Entradas de auditoría con timestamp (UTC), actor, acción, recurso, resultado
- Almacenamiento de auditoría append-only o WORM y con control de acceso
- Nivel de log por variable de entorno, no hardcodeado
- Logging asíncrono habilitado con buffer de al menos 1.000 entradas
- Rotación configurada (p. ej. 100 MB por archivo, retención de 30 días según política)
- Envío a almacenamiento centralizado que maneja backpressure sin descartes
- Alertas por tasa de ERROR y por volumen >2× el normal
- Salida verificada en staging: formato, enmascaramiento y correlation IDs
Estrategia de Testing
- Enmascaramiento: envía peticiones con API keys y PII en headers y bodies; verifica que la salida muestre
[REDACTED]o***. - Propagación de correlation: lanza una petición y verifica que el mismo ID aparezca en todas las entradas de log que genera.
- Inmutabilidad: escribe una entrada de auditoría, intenta modificarla y verifica que el almacén append-only rechaza la escritura.
- Retención: crea entradas más antiguas que la ventana de retención y verifica que el archivo o borrado corre según lo previsto.
Lectura Adicional
- OWASP Logging Cheat Sheet — qué loguear, qué excluir y cómo manejar la inyección.
- W3C Trace Context — el estándar detrás de los headers de correlación entre servicios.
- Documentación de Pino — logging estructurado de alto rendimiento para Node.js.
- Documentación de structlog — logging estructurado con contexto para Python.
- Ejemplos ejecutables del repo companion — los tres middlewares de esta página como proyectos ejecutables.
Preguntas frecuentes
¿Qué debe llevar una entrada de audit log?
Timestamp en UTC, actor (ID de usuario o servicio), acción realizada, recurso objetivo, resultado (permitido/denegado/éxito/fallo), IP de origen y un correlation ID que la vincule a la petición. Con menos que eso, la mayoría de revisiones de compliance fallan.
¿Cuánto tiempo debo retener los logs de API?
Logs operacionales: 7–30 días es lo habitual. Audit logs: 1–7 años según la regulación — PCI DSS v4.0 exige al menos 1 año de retención de logs de auditoría (Requirement 10.5.1), SOC 2 sigue tu política declarada. Revisa tus requisitos concretos antes de fijar la retención.
¿Puede una herramienta APM reemplazar el audit logging?
No. Los APM (Datadog, New Relic) capturan trazas distribuidas para rendimiento, pero no garantizan el registro append-only con control de acceso que exigen los auditores. APM para rendimiento, un sink de auditoría aparte para compliance.
¿Cómo prevengo el log injection?
Nunca concatenes entrada de usuario cruda en un mensaje de log — una cadena manipulada puede falsear entradas o romper el parseo. Pasa los datos de usuario como campos estructurados y deja que el logger los escape; sanitiza lo que deba aparecer en el mensaje.
¿Debo muestrear logs en producción?
Para tráfico INFO rutinario, sí — muestrear al 10% reduce el coste casi 10× conservando todos los WARN y ERROR. Nunca muestrees entradas de auditoría; esas son el registro de compliance.
Recursos Relacionados
Logging en producción con Python, JavaScript y Java
Implementa logging estructurado por niveles con IDs de correlación y enmascarado de secretos en Python, JavaScript y Java.
RecipeMiddleware: Interceptores HTTP
Cómo implementar middleware de request/response para logging, auth y manejo de errores en Python, JavaScript y Java.
RecipeManejar Errores en APIs con RFC 7807
Patrones para un manejo de errores de API consistente y predecible en varios lenguajes y frameworks.
RecipeOpenAPI 3.1: docs con Swagger UI y Redoc
Guía práctica para documentar APIs REST con OpenAPI. Genera docs interactivas con Swagger UI y Redoc en Python, JavaScript y Java con linting en CI.
GuideGuía de Mejores Prácticas de Seguridad
Una Referencia Detallada de seguridad de aplicaciones: autenticación, autorización, validación de inputs, gestión de secretos y prevención de vulnerabilidades comunes.
GuideGuía de OpenTelemetry: Trazas, Métricas y Logs
Guía práctica de OpenTelemetry en producción: instrumentación, collectors, estrategias de muestreo y pipelines OTLP hacia Jaeger, Prometheus y Grafana.