ELK Stack — Elasticsearch, Logstash, and Kibana Centralized Logging Guide
Advertisement
Introduction
Why This Matters
The ELK Stack (Elasticsearch, Logstash, Kibana) plus Beats is the most widely deployed open-source log management platform. Centralizing logs from hundreds of services into a single searchable store transforms debugging from SSH-and-grep sessions into structured queries across millions of events. Proper ELK configuration — index lifecycle management, mappings, and Logstash filters — determines whether your logging platform remains performant as log volume grows.
Docker Compose Deployment
# docker-compose.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms2g -Xmx2g
- xpack.security.enabled=true
- ELASTIC_PASSWORD=change-me-secure-password
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
ulimits:
memlock:
soft: -1
hard: -1
deploy:
resources:
limits:
memory: 4g
healthcheck:
test: ["CMD-SHELL", "curl -s -u elastic:change-me-secure-password http://localhost:9200/_cluster/health | grep -v red"]
interval: 30s
logstash:
image: docker.elastic.co/logstash/logstash:8.11.0
container_name: logstash
ports:
- "5044:5044" # Beats input
- "5000:5000" # TCP input
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline
- ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml
environment:
- LS_JAVA_OPTS=-Xms1g -Xmx1g
depends_on:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
container_name: kibana
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=change-me-secure-password
depends_on:
- elasticsearch
volumes:
es_data:Logstash Pipeline Configuration
# logstash/pipeline/main.conf
input {
# Receive logs from Filebeat
beats {
port => 5044
}
# Receive JSON logs via TCP
tcp {
port => 5000
codec => json_lines
}
}
filter {
# Parse JSON application logs
if [fields][log_type] == "application" {
json {
source => "message"
target => "app"
}
# Extract structured fields
mutate {
rename => { "[app][level]" => "log_level" }
rename => { "[app][service]" => "service_name" }
rename => { "[app][requestId]" => "trace_id" }
}
}
# Parse nginx access logs
if [fields][log_type] == "nginx" {
grok {
match => {
"message" => '%{IPORHOST:client_ip} - %{USER:ident} \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{DATA:path} HTTP/%{NUMBER:http_version}" %{NUMBER:response_code:int} %{NUMBER:bytes:int}'
}
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
target => "@timestamp"
}
}
# Geolocation enrichment
if [client_ip] {
geoip {
source => "client_ip"
target => "geoip"
}
}
# Drop health check logs
if [path] == "/health" {
drop {}
}
# Tag high-severity events
if [response_code] >= 500 {
mutate {
add_tag => ["server_error"]
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
user => "elastic"
password => "${ELASTIC_PASSWORD}"
index => "logs-%{[fields][environment]}-%{+YYYY.MM.dd}"
action => "create"
}
# Debug output (disable in production)
# stdout { codec => rubydebug }
}Filebeat Configuration
# filebeat.yml — runs on each application host
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/my-app/*.log
fields:
log_type: application
environment: production
service: my-api
fields_under_root: false
json.keys_under_root: true
json.add_error_key: true
multiline.pattern: '^\d{4}-\d{2}-\d{2}'
multiline.negate: true
multiline.match: after
- type: log
enabled: true
paths:
- /var/log/nginx/access.log
fields:
log_type: nginx
environment: production
output.logstash:
hosts: ["logstash.example.com:5044"]
ssl.enabled: true
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_docker_metadata: ~Elasticsearch Index Lifecycle Management
# Create ILM policy — hot/warm/cold/delete phases
curl -X PUT "localhost:9200/_ilm/policy/logs-policy" \
-u elastic:password \
-H "Content-Type: application/json" \
-d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": {"number_of_shards": 1},
"forcemerge": {"max_num_segments": 1}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}'Elasticsearch Queries
# Full-text search across all logs
curl -X GET "localhost:9200/logs-*/_search" \
-H "Content-Type: application/json" \
-d '{
"query": {
"bool": {
"must": [
{"match": {"message": "database connection failed"}},
{"range": {"@timestamp": {"gte": "now-1h"}}}
],
"filter": [
{"term": {"service_name": "my-api"}},
{"terms": {"log_level": ["error", "fatal"]}}
]
}
},
"sort": [{"@timestamp": {"order": "desc"}}],
"size": 50
}'
# Aggregate error count by service
curl -X GET "localhost:9200/logs-*/_search" \
-H "Content-Type: application/json" \
-d '{
"aggs": {
"errors_by_service": {
"terms": {"field": "service_name.keyword"},
"aggs": {
"error_count": {"filter": {"term": {"log_level": "error"}}}
}
}
},
"size": 0
}'Common Mistakes
- Setting
ES_JAVA_OPTS=-Xms512m -Xmx512mon a production node — Elasticsearch needs at least 4-8GB heap for production - Not setting
discovery.type=single-nodefor single-node deployments — Elasticsearch waits for cluster formation indefinitely - Using default shard settings — too many shards degrades performance; 1-2 shards per index per 50GB is a common guideline
- Storing raw logs without structured parsing — unstructured text is unsearchable and Kibana dashboards cannot visualize it
- Not configuring ILM — without lifecycle policies, indices grow unbounded and fill disk
Best Practices
- Use Filebeat or Fluent Bit as lightweight log shippers — avoid sending directly from application to Elasticsearch
- Parse logs into structured JSON fields in Logstash — enables Kibana visualizations and aggregations
- Configure ILM with hot/warm/cold/delete phases to automatically manage index storage costs
- Enable Elasticsearch authentication (X-Pack security) — never expose Elasticsearch on public interfaces
- Monitor Elasticsearch cluster health with Metricbeat and set alerts on red cluster status
- Use data tiers (hot/warm/cold) with different hardware — SSDs for hot indices, HDDs for cold
Key Takeaways
- The ELK Stack consists of Elasticsearch (storage/search), Logstash (processing), Kibana (visualization), and Beats (shippers)
- Filebeat runs on each host and ships logs to Logstash with minimal resource overhead
- Logstash pipelines parse, filter, and enrich log events using Grok patterns, JSON parsing, and GeoIP lookup
- Index Lifecycle Management automatically transitions indices through hot/warm/cold phases and deletes old data
- Elasticsearch uses inverted indices for full-text search and BKD trees for range queries on numeric/date fields
- High shard count is the most common cause of Elasticsearch performance degradation
- Kibana Discover enables ad-hoc log search; Kibana Visualize and Dashboards provide operational views
- X-Pack security (free since Elastic 6.8) provides TLS encryption and role-based access control for all stack components
Advertisement
Related reading
Grafana Loki Log Aggregation 2026 — The Prometheus-Native Logging Stack6 min readMonitoring and Observability Guide 2026 — Prometheus, Grafana, and OpenTelemetry5 min readHealth Check Patterns — Liveness, Readiness, and Deep Dependency Checks7 min readLog Aggregation at Scale — Structured Logging, Loki, and Querying Millions of Log Lines10 min readLogging Everything and Nothing Useful — The Noise Problem5 min readNo Observability Strategy — Flying Blind in Production4 min read