Patrón Structured Logging
Cómo emitir structured JSON logs con campos consistentes para searchability. Cubre Python structlog, Winston, Serilog, log levels, y log aggregation.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
El structured logging emite log entries como JSON objects con campos consistentes en vez de free-form text. Esto hace los logs machine-parseable, searchable en log aggregation tools (ELK, Datadog, Splunk), y filterable por cualquier field. Un structured log entry incluye un timestamp, level, message, y contextual fields como correlation ID, user ID, service name, y duration. El patrón reemplaza print(f"User {user_id} did {action}") con logger.info("user_action", user_id=user_id, action=action) — produciendo {"timestamp": "...", "level": "INFO", "message": "user_action", "user_id": 42, "action": "login"}.
When to Use
- Cualquier aplicación de producción que produce logs consumidos por aggregation tools
- Microservices donde los logs de múltiples services necesitan ser correlated
- Aplicaciones donde necesitás buscar y filtrar logs por structured fields
- Compliance requirements que demandan specific log fields
- Debuggear production issues donde grep en text logs es insuficiente
When NOT to Use
- CLI tools o scripts donde human-readable output es preferred
- Development debugging donde
print()es más rápido - Aplicaciones con muy bajo log volume donde text logs alcanzan
- Embedded systems con strict memory constraints
Solution
Python con structlog
# Python — structlog para structured logging
import structlog
import logging
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
# Usage
logger.info("user_login", user_id=42, ip_address="192.168.1.1", method="oauth")
logger.error("payment_failed", order_id=123, reason="insufficient_funds", amount=99.99)
logger.warning("rate_limit_approaching", endpoint="/api/orders", current_rate=95, limit=100)
# Output:
# {"event": "user_login", "user_id": 42, "ip_address": "192.168.1.1",
# "method": "oauth", "level": "info", "timestamp": "2026-01-15T10:00:00Z"}
Python con standard logging + JSON formatter
# Python — standard logging con JSON formatter
import logging
import json
from datetime import datetime
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"level": record.levelname.lower(),
"message": record.getMessage(),
"logger": record.name,
}
# Agregar extra fields
for key, value in record.__dict__.items():
if key not in ("name", "msg", "args", "levelname", "levelno",
"pathname", "filename", "module", "exc_info",
"exc_text", "stack_info", "lineno", "funcName",
"created", "msecs", "relativeCreated", "thread",
"threadName", "processName", "process", "message"):
log_entry[key] = value
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry, default=str)
# Setup
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger = logging.getLogger("myapp")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Usage
logger.info("Order created", extra={"order_id": 123, "customer_id": 456, "total": 99.99})
logger.error("Database connection failed", extra={"host": "db.example.com", "port": 5432})
JavaScript con Winston
// JavaScript — Winston structured logging
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: {
service: 'order-service',
version: '1.0.0',
environment: process.env.NODE_ENV || 'development',
},
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
}),
new winston.transports.File({
filename: 'logs/combined.log',
}),
],
});
// Usage
logger.info('order_created', {
orderId: 123,
customerId: 456,
total: 99.99,
items: 3,
});
logger.error('payment_failed', {
orderId: 123,
reason: 'insufficient_funds',
amount: 99.99,
paymentMethod: 'credit_card',
});
logger.warn('rate_limit_warning', {
endpoint: '/api/orders',
currentRate: 95,
limit: 100,
windowMs: 60000,
});
JavaScript con pino
// JavaScript — pino para high-performance structured logging
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
formatters: {
level: (label) => ({ level: label }),
},
base: {
service: 'order-service',
version: '1.0.0',
},
});
// Usage
logger.info({ orderId: 123, customerId: 456, total: 99.99 }, 'order_created');
logger.error({ orderId: 123, reason: 'insufficient_funds' }, 'payment_failed');
// Child loggers con persistent context
const requestLogger = logger.child({ requestId: 'abc-123', userId: 42 });
requestLogger.info('processing request');
requestLogger.info('request completed', { durationMs: 150 });
Java con SLF4J + Logstash encoder
// Java — Logback con Logstash JSON encoder
// pom.xml: net.logstash.logback:logstash-logback-encoder
// logback.xml
/*
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service":"order-service","version":"1.0.0"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
</configuration>
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void createOrder(OrderRequest request) {
MDC.put("orderId", String.valueOf(request.getOrderId()));
MDC.put("customerId", String.valueOf(request.getCustomerId()));
logger.info("order_created total={} items={}",
request.getTotal(), request.getItems().size());
try {
processPayment(request);
logger.info("payment_successful amount={}", request.getTotal());
} catch (PaymentException e) {
logger.error("payment_failed reason={} amount={}",
e.getReason(), request.getTotal(), e);
} finally {
MDC.clear();
}
}
}
.NET con Serilog
// C# — Serilog structured logging
using Serilog;
using Serilog.Formatting.Json;
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Service", "order-service")
.Enrich.WithProperty("Version", "1.0.0")
.Enrich.FromLogContext()
.WriteTo.Console(new JsonFormatter())
.Writeto.File(new JsonFormatter(), "logs/app.log")
.CreateLogger();
// Usage con structured properties
Log.Information("Order {OrderId} created for customer {CustomerId} with total {Total}",
order.Id, order.CustomerId, order.Total);
Log.Error("Payment failed for order {OrderId}: {Reason}",
order.Id, ex.Message);
// Usando LogContext para scoped properties
using (LogContext.PushProperty("CorrelationId", correlationId))
{
Log.Information("Processing order {OrderId}", order.Id);
// Todos los logs dentro de este scope incluyen CorrelationId
}
Log levels y cuándo usarlos
# Python — log level guidelines
import structlog
logger = structlog.get_logger()
# DEBUG — detailed diagnostic info, disabled en production
logger.debug("cache_hit", key="user:42", ttl=300)
# INFO — general operational events (request processed, order created)
logger.info("order_created", order_id=123, total=99.99)
# WARNING — algo unexpected pero not fatal (rate limit approaching, retry needed)
logger.warning("retry_attempt", attempt=2, max_attempts=3, delay_ms=500)
# ERROR — un failure que debería ser investigado (payment failed, db error)
logger.error("payment_failed", order_id=123, reason="insufficient_funds")
# CRITICAL — system-wide failure requiring immediate action
logger.critical("database_unreachable", host="db.example.com", port=5432)
Contextual logging con bound fields
# Python — bind context al logger
import structlog
logger = structlog.get_logger()
# Bind persistent context
request_logger = logger.bind(
correlation_id="abc-123",
user_id=42,
endpoint="/api/orders",
)
# Todos los logs subsiguientes incluyen bound fields
request_logger.info("request_started")
request_logger.info("db_query_executed", query="SELECT * FROM orders", duration_ms=15)
request_logger.info("request_completed", status_code=200, duration_ms=150)
# Output:
# {"correlation_id": "abc-123", "user_id": 42, "endpoint": "/api/orders",
# "event": "request_started", "level": "info", "timestamp": "..."}
ELK integration
# Logstash pipeline configuration para structured logs
input:
file:
path: "/var/log/myapp/*.log"
codec: json
start_position: "beginning"
filter:
# Ensure timestamp es parsed correctly
date:
match: ["timestamp", "ISO8601"]
target: "@timestamp"
# Add service field si missing
if ![service]:
mutate:
add_field:
service: "unknown"
output:
elasticsearch:
hosts: ["http://elasticsearch:9200"]
index: "myapp-logs-%{+YYYY.MM.dd}"
Datadog integration
# datadog.yaml — collect structured logs
logs:
- type: file
path: "/var/log/myapp/*.log"
service: "order-service"
source: "python"
sourcecategory: "sourcecode"
log_processing_rules:
- type: multi_line
name: "json_logs"
pattern: '^\{'
Variants
Async logging para high throughput
# Python — async logging con queue handler
import logging
import logging.handlers
import queue
log_queue = queue.Queue(-1) # Unlimited size
# Queue handler — non-blocking
queue_handler = logging.handlers.QueueHandler(log_queue)
# Queue listener — procesa logs en un separate thread
file_handler = logging.FileHandler("app.log")
file_handler.setFormatter(JsonFormatter())
listener = logging.handlers.QueueListener(log_queue, file_handler)
listener.start()
logger = logging.getLogger("myapp")
logger.addHandler(queue_handler)
logger.setLevel(logging.INFO)
# Logs se enqueue sin blocking el main thread
logger.info("high_throughput_log", extra={"count": 1000000})
Sampling para high-volume logs
// JavaScript — pino con sampling para high-volume logs
const pino = require('pino');
const logger = pino({
level: 'debug',
// Logear solo 10% de debug logs en production
levelVal: process.env.NODE_ENV === 'production' ? 20 : 10,
});
// O usar una custom sampling strategy
const sampledLogger = pino({
timestamp: pino.stdTimeFunctions.isoTime,
base: { service: 'api' },
}, pino.destination({ sync: false }));
// Samplear 1 in every 100 requests at debug level
let requestCount = 0;
function logRequest(req) {
requestCount++;
if (requestCount % 100 === 0 || req.path !== '/health') {
sampledLogger.info({ path: req.path, method: req.method }, 'request_received');
}
}
Redacting sensitive fields
# Python — redact sensitive fields en logs
import structlog
import re
def redact_sensitive(processor, logger, event_dict):
sensitive_keys = {"password", "token", "api_key", "credit_card", "ssn"}
for key in list(event_dict.keys()):
if key.lower() in sensitive_keys:
event_dict[key] = "[REDACTED]"
elif isinstance(event_dict[key], str):
# Redact credit card numbers en cualquier string field
event_dict[key] = re.sub(
r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b',
'[REDACTED]',
event_dict[key],
)
return event_dict
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
redact_sensitive,
structlog.processors.JSONRenderer(),
],
)
Best Practices
-
For a deeper guide, see Structured Logging: JSON Logs, Correlation IDs, Aggregation.
-
Usá JSON format en production — machine-parseable, searchable en aggregation tools
-
Incluí context, no solo messages —
logger.info("order_created", order_id=123)nologger.info("Order 123 created") -
Usá consistent field names —
user_iden todos lados, nouserIden un service yuser_iden otro -
Incluí un timestamp en ISO 8601 —
2026-01-15T10:00:00.123Z -
Usá appropriate log levels — DEBUG para dev, INFO para operations, ERROR para failures
-
Bind request context — correlation ID, user ID, endpoint deberían estar en cada log entry para un request
-
Redact sensitive data — passwords, tokens, credit card numbers nunca deberían aparecer en logs
-
Usá async logging para high throughput — no dejes que logging blockee request processing
-
Logeá en boundaries — service entry/exit, external calls, state transitions
-
Incluí duration para operations —
duration_msfield para cualquier timed operation
Common Mistakes
- String interpolation en vez de structured fields:
logger.info(f"User {user_id} logged in")en vez delogger.info("user_login", user_id=user_id). La structured version es searchable poruser_id. - Inconsistent field names:
user_iden un service,userIden otro. Estandarizá across todos los services. - Loggear sensitive data: passwords, tokens, PII en logs. Redact u omití estos fields.
- Demasiados log levels: usar los 6 levels inconsistentemente. Stickte a DEBUG, INFO, WARN, ERROR.
- No incluir timestamps: depender del log shipper para agregar timestamps. Incluilos en el log entry mismo.
- Loggear en hot loops: loggear dentro de tight loops genera enormous volume. Sampleá o agregá.
FAQ
¿Qué es structured logging?
Emitir log entries como JSON objects con campos consistentes en vez de free-form text. Cada entry tiene un timestamp, level, message, y contextual fields que pueden ser searched y filtered.
¿Por qué JSON logs en vez de text?
Los JSON logs son machine-parseable. Los log aggregation tools (ELK, Datadog, Splunk) pueden indexar individual fields, enabling queries como level:error AND service:order-service AND order_id:123.
¿Qué log levels debería usar?
DEBUG para development diagnostics, INFO para operational events (request processed, order created), WARN para unexpected pero non-fatal events, ERROR para failures. Skip CRITICAL a menos que tengas una specific need.
¿Cómo manejo sensitive data en logs?
Redact sensitive fields antes de loggear. Mantené una lista de sensitive keys (password, token, api_key) y reemplazá sus values con [REDACTED]. Usá un custom log processor o middleware.
¿Debería usar async logging?
Para high-throughput applications (thousands of log entries per second), sí. Async logging usa un queue para prevenir que logging blockee request processing. Para low-volume applications, sync logging está fine.
Recursos Relacionados
Correlation ID: Trace Requests Across Distributed Services
How to propagate correlation IDs across service boundaries for end-to-end request tracing. Covers HTTP headers, message queues, and logging integration.
PatternMetrics Aggregation: Collect, Tag
How to collect, tag, and aggregate business metrics for observability. Covers Prometheus, OpenTelemetry, custom metrics, histograms, and dashboarding.
PatternDistributed Tracing: Propagate Trace Context Across Services
How to propagate trace context across service boundaries with OpenTelemetry. Covers span creation, context propagation, sampling, and trace analysis.
PatternCircuit Breaker with Monitoring
How to expose circuit breaker state as metrics for observability. Covers Prometheus integration, alerting rules, dashboards, and state transitions.
PatternHealth Check Pattern: Expose Liveness and Readiness Probes
How to implement liveness and readiness probes for container orchestration. Covers Kubernetes probes, dependency checks, graceful degradation, and probe endpoints.