Structured logging with ELK stack integration
Ryan Nakamura
Feb 2026
2 tabs
// Structured logging with Winston (Node.js)
const winston = require('winston');
const { v4: uuidv4 } = require('uuid');
// Create logger
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: process.env.SERVICE_NAME || 'web-app',
environment: process.env.NODE_ENV || 'development',
version: process.env.APP_VERSION || '1.0.0',
},
transports: [
new winston.transports.Console(),
// File transport for production
...(process.env.NODE_ENV === 'production'
? [
new winston.transports.File({
filename: '/var/log/app/error.log',
level: 'error',
maxsize: 50 * 1024 * 1024, // 50MB
maxFiles: 5,
}),
new winston.transports.File({
filename: '/var/log/app/combined.log',
maxsize: 100 * 1024 * 1024,
maxFiles: 10,
}),
]
: []),
],
});
// Sensitive field redaction
const SENSITIVE_FIELDS = ['password', 'token', 'secret', 'authorization', 'cookie'];
function redactSensitive(obj) {
if (!obj || typeof obj !== 'object') return obj;
const redacted = { ...obj };
for (const key of Object.keys(redacted)) {
if (SENSITIVE_FIELDS.some(f => key.toLowerCase().includes(f))) {
redacted[key] = '[REDACTED]';
}
}
return redacted;
}
// Request logging middleware (Express)
function requestLogger(req, res, next) {
// Generate correlation ID
req.correlationId = req.headers['x-correlation-id'] || uuidv4();
res.setHeader('x-correlation-id', req.correlationId);
const startTime = process.hrtime.bigint();
// Log request
logger.info('Incoming request', {
correlationId: req.correlationId,
method: req.method,
path: req.path,
query: req.query,
userAgent: req.headers['user-agent'],
ip: req.ip,
userId: req.user?.id,
});
// Log response on finish
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - startTime) / 1e6;
const logData = {
correlationId: req.correlationId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Math.round(duration * 100) / 100,
contentLength: res.getHeader('content-length'),
userId: req.user?.id,
};
if (res.statusCode >= 500) {
logger.error('Request failed', logData);
} else if (res.statusCode >= 400) {
logger.warn('Client error', logData);
} else {
logger.info('Request completed', logData);
}
});
next();
}
// Child logger with context
function createChildLogger(context) {
return logger.child(context);
}
// Usage in services
class OrderService {
constructor() {
this.logger = createChildLogger({ module: 'OrderService' });
}
async createOrder(userId, items) {
const orderLogger = this.logger.child({
userId,
action: 'createOrder',
itemCount: items.length,
});
orderLogger.info('Creating order');
try {
const order = await db.orders.create({ userId, items });
orderLogger.info('Order created successfully', {
orderId: order.id,
total: order.total,
});
return order;
} catch (error) {
orderLogger.error('Failed to create order', {
error: error.message,
stack: error.stack,
});
throw error;
}
}
}
module.exports = { logger, requestLogger, createChildLogger };
# Fluentd configuration for Kubernetes
# Collect container logs
<source>
@type tail
path /var/log/containers/*.log
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
read_from_head true
<parse>
@type json
time_key time
time_format %Y-%m-%dT%H:%M:%S.%NZ
</parse>
</source>
# Add Kubernetes metadata
<filter kubernetes.**>
@type kubernetes_metadata
@id filter_kube_metadata
</filter>
# Parse JSON log messages
<filter kubernetes.**>
@type parser
key_name log
reserve_data true
remove_key_name_field true
<parse>
@type json
</parse>
</filter>
# Route to Elasticsearch
<match kubernetes.**>
@type elasticsearch
@id out_es
host elasticsearch
port 9200
logstash_format true
logstash_prefix k8s-logs
include_tag_key true
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.buffer
flush_mode interval
flush_interval 5s
retry_max_interval 30
chunk_limit_size 2M
total_limit_size 500M
overflow_action drop_oldest_chunk
</buffer>
</match>
2 files · javascript, xml
Explain with highlit
Structured logging outputs JSON-formatted log entries for machine parsing. Each log line includes timestamp, level, message, and contextual fields like request_id, user_id, and service. Structured logs enable powerful queries in Elasticsearch through Kibana. Log levels (debug, info, warn, error) filter noise in different environments. Correlation IDs trace requests across microservices. The ELK stack (Elasticsearch, Logstash, Kibana) or EFK (Elasticsearch, Fluentd, Kibana) centralizes logs. Fluentd or Filebeat ships logs from containers. Log rotation prevents disk exhaustion. Sensitive data like passwords and tokens must be redacted. Structured logging transforms debugging from grepping text files to querying indexed, searchable data.