A comprehensive guide to log management best practices for the ELK stack.
Part of: Log Management Toolkit by Datanest Digital
1. Log Levels & When to Use Them
3. What to Log (and What Not To)
10. Troubleshooting Common Issues
Consistent log levels are critical for filtering, alerting, and dashboard accuracy.
| Level | Numeric | When to Use |
|---|---|---|
TRACE | 0 | Granular debugging (loop iterations, variable states). Never in production. |
DEBUG | 1 | Diagnostic info for developers (SQL queries, HTTP calls). Off in production by default. |
INFO | 2 | Normal operations (request handled, job completed, service started). |
WARN | 3 | Unexpected but recoverable (retry attempted, cache miss, deprecated API). |
ERROR | 4 | Operation failed but service continues (unhandled exception, timeout, failed request). |
FATAL | 5 | Service cannot continue (database unreachable, out of memory, corrupted state). |
Rules of thumb:
INFO and aboveDEBUG per-service temporarily for troubleshootingWARN should always be actionable — if nobody will act on it, it's INFOERROR means something broke. Every ERROR should eventually be investigatedAlways use structured (JSON) logging. Human-readable text logs are difficult to parse, search, and aggregate.
{
"timestamp": "2026-01-15T14:30:00.123Z",
"level": "ERROR",
"service": "api-gateway",
"message": "Payment processing failed",
"error": {
"type": "TimeoutError",
"message": "Upstream timeout after 30s"
},
"request_id": "req_abc123",
"trace_id": "trace_def456",
"user_id": "usr_789",
"duration_ms": 30012,
"endpoint": "/api/v1/payments",
"method": "POST"
}2026-01-15 14:30:00 ERROR Payment processing failed for user usr_789 on /api/v1/payments: TimeoutError: Upstream timeout after 30s
Node.js (pino):
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
base: { service: 'api-gateway', environment: process.env.NODE_ENV }
});Python (structlog):
import structlog
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
]
)
logger = structlog.get_logger(service="api-gateway")Go (zerolog):
logger := zerolog.New(os.Stdout).With().
Timestamp().
Str("service", "api-gateway").
Logger()If sensitive fields might appear in logs, implement redaction:
// Redact sensitive fields before logging
const REDACT_FIELDS = ['password', 'token', 'authorization', 'cookie', 'ssn', 'credit_card'];
function redactObject(obj) {
const redacted = { ...obj };
for (const key of Object.keys(redacted)) {
if (REDACT_FIELDS.some(f => key.toLowerCase().includes(f))) {
redacted[key] = '[REDACTED]';
}
}
return redacted;
}Retention should balance storage costs, compliance requirements, and operational needs.
| Tier | Duration | Data | Storage |
|---|---|---|---|
| Hot | 0-7 days | All logs, full resolution | SSD/NVMe |
| Warm | 7-30 days | All logs, force-merged | Standard SSD |
| Cold | 30-90 days | Aggregated, read-only | HDD/object |
| Archive | 90-365 days | Snapshots only (compliance) | S3/Glacier |
| Delete | >365 days | Permanently removed | — |
The rotate-indices.sh script can create an ILM policy automatically:
bash scripts/rotate-indices.sh --setup-ilm --days 90| Log Volume | Daily Storage | 30-Day Hot | 90-Day Total |
|---|---|---|---|
| 1 GB/day | ~1 GB | ~30 GB | ~70 GB |
| 10 GB/day | ~10 GB | ~300 GB | ~700 GB |
| 100 GB/day | ~100 GB | ~3 TB | ~7 TB |
Elasticsearch typically achieves 1.5-3x compression on log data.
Use a consistent naming pattern: {type}-{source}-{date}
logs-nginx-access-2026.01.15
logs-app-2026.01.15
logs-system-2026.01.15
logs-auth-2026.01.15
Always define templates before ingesting data. Templates control:
| Shard Size | Recommendation |
|---|---|
| <1 GB | Too small — reduce shard count or frequency |
| 10-30 GB | Ideal for most workloads |
| >50 GB | Too large — increase shard count |
| Priority | Response Time | Examples |
|---|---|---|
| P1 | Immediate | Service down, data loss, security breach |
| P2 | <1 hour | Error rate spike, response time degradation |
| P3 | <4 hours | Disk space warning, certificate expiring |
| P4 | Next business day | Deprecation warnings, minor anomalies |
Error rate spike:
WHEN count() of log_level:ERROR
OVER last 5 minutes
GROUPED BY service_name
IS ABOVE 50
Slow responses:
WHEN avg(response_time)
OVER last 15 minutes
IS ABOVE 2.0
Disk space:
WHEN max(elasticsearch.node.stats.fs.total.available_in_bytes)
IS BELOW 10737418240 (10 GB)
# JVM heap: 50% of available RAM, max 31GB
ES_JAVA_OPTS: "-Xms4g -Xmx4g"
# Bulk indexing optimization
index.refresh_interval: "30s" # Reduce for lower latency, increase for throughput
index.translog.durability: "async" # Faster writes, slight data loss risk on crash
index.translog.sync_interval: "5s"# Pipeline tuning
pipeline.workers: 4 # Match CPU cores
pipeline.batch.size: 1000 # Increase for throughput
pipeline.batch.delay: 50 # ms to wait for batch to fill
# JVM heap
LS_JAVA_OPTS: "-Xms1g -Xmx1g"# Queue and batching
queue.mem.events: 4096
queue.mem.flush.min_events: 2048
queue.mem.flush.timeout: 1s
# Output tuning
output.logstash.bulk_max_size: 2048
output.logstash.worker: 2Key metrics to watch:
curl -X POST "localhost:9200/_security/api_key" \
-u elastic:YOUR_PASSWORD \
-H "Content-Type: application/json" \
-d '{
"name": "filebeat-server-01",
"role_descriptors": {
"filebeat_writer": {
"cluster": ["monitor", "read_ilm", "read_pipeline"],
"index": [
{
"names": ["logs-*", "filebeat-*"],
"privileges": ["create_index", "create_doc", "auto_configure"]
}
]
}
}
}'| Framework | Retention | Key Requirements |
|---|---|---|
| GDPR | Varies | Right to erasure, data minimization |
| PCI DSS | 1 year | Audit trails, access logging |
| HIPAA | 6 years | Access controls, encryption at rest |
| SOC 2 | 1 year | Monitoring, incident response logging |
| Symptom | Solution |
|---|---|
| Slow indexing | Add Elasticsearch data nodes |
| Logstash CPU at 100% | Add Logstash instances |
| Filebeat queue overflow | Add Logstash instances or capacity |
| Kibana slow with many users | Add Kibana instances behind LB |
| Disk space running out | Add nodes or reduce retention |
For production workloads beyond a single server:
# 3-node minimum for high availability
discovery.seed_hosts: ["es-01", "es-02", "es-03"]
cluster.initial_master_nodes: ["es-01", "es-02", "es-03"]
# Dedicated roles per node type
node.roles: [master] # Master-eligible nodes (3 minimum)
node.roles: [data_hot] # Hot data nodes (SSD, write-heavy)
node.roles: [data_warm] # Warm data nodes (HDD, read-heavy)
node.roles: [ingest] # Ingest nodes (pipeline processing)Run multiple Logstash instances with Filebeat load balancing:
# filebeat.yml
output.logstash:
hosts: ["logstash-01:5044", "logstash-02:5044"]
loadbalance: true
worker: 2# Check vm.max_map_count
sysctl vm.max_map_count
# Fix: sudo sysctl -w vm.max_map_count=262144
# Check disk space
df -h /var/lib/docker
# Check container logs
docker compose logs elasticsearch# Check pipeline status
curl -s localhost:9600/_node/stats/pipelines | python3 -m json.tool
# Validate config
docker compose exec logstash logstash --config.test_and_exit
# Check for grok parse failures
# Search Kibana for: tags: "_grokparsefailure"# Test config
filebeat test config
filebeat test output
# Check registry (tracks file read positions)
ls -la /var/lib/filebeat/registry/
# Check permissions
ls -la /var/log/nginx/access.log
# Filebeat needs read access to log files# Check Elasticsearch heap
curl -s localhost:9200/_nodes/stats/jvm | python3 -m json.tool | grep heap
# Check for large aggregations
curl -s localhost:9200/_nodes/stats/breaker | python3 -m json.tool
# Reduce heap if oversized (>31GB triggers no compressed oops)
# ES_JAVA_OPTS="-Xms16g -Xmx16g"1. Check Filebeat is running and has permission to read log files
2. Check Logstash pipeline isn't dropping events (look for _grokparsefailure tags)
3. Check Elasticsearch indexing rate and rejection counts
4. Verify time synchronization (NTP) across all servers
5. Check for bulk queue rejections in Logstash logs
| Task | Command |
|---|---|
| Deploy stack | bash scripts/setup.sh |
| Check stack health | docker compose ps |
| View live logs | docker compose logs -f |
| Rotate old indices | bash scripts/rotate-indices.sh |
| Backup Kibana | bash scripts/backup-kibana.sh |
| Restore Kibana | bash scripts/backup-kibana.sh --restore FILE.ndjson |
| Check ES cluster health | curl -u elastic:PASS localhost:9200/_cluster/health |
| Check index sizes | curl -u elastic:PASS localhost:9200/_cat/indices?v |
Log Management Toolkit is a product of Datanest Digital
Questions or issues? Email support@datanest.dev
Production-ready ELK stack deployment with structured logging pipelines and operational dashboards.
Complete Elasticsearch, Logstash, Kibana, and Filebeat configurations for centralized log management. Includes ready-to-use parsing pipelines for Nginx, application JSON logs, and system logs, plus operational scripts for index management, backups, and a comprehensive log strategy guide.
Get the full Log Management Toolkit and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.