ELK Stack — Elasticsearch, Logstash, and Kibana Centralized Logging Guide

Sanjeev SharmaSanjeev Sharma
5 min read

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 -Xmx512m on a production node — Elasticsearch needs at least 4-8GB heap for production
  • Not setting discovery.type=single-node for 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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading