Command Palette

Search for a command to run...

Unit 4.3 · Search and Log Analytics: Elasticsearch, OpenSearch, ELK

The Log Pipeline: Fluent Bit, Logstash, Kibana, and Friends

ELK vs EFK vs Loki, shipping container logs with Fluent Bit, parsing with ingest pipelines or Logstash, buffering and backpressure, index templates for logs, mapping explosions, and dashboards.

Intermediate 50 min 4 lab steps 2 failure drills

Start here

The mental model

A log pipeline is a conveyor belt with four stations: COLLECT (read log files on every node), PROCESS (parse lines into fields, add Kubernetes metadata, drop noise, mask secrets), STORE (index into OpenSearch/Elasticsearch), and VIEW (search and dashboards in Kibana or OpenSearch Dashboards). When the store slows down, the belt must slow down too without dropping everything. That's BUFFERING and BACKPRESSURE.

Go deeper

How it works inside

01ELK, EFK, and the alternatives

ELK = Elasticsearch + Logstash + Kibana. EFK swaps Logstash for Fluentd or, more commonly now, FLUENT BIT: a tiny C agent that runs as a DaemonSet, tails /var/log/containers/*.log, enriches records with pod labels from the Kubernetes API, and ships them. LOGSTASH is heavier (JVM) but has powerful filters (grok, dissect, translate, aggregate); many teams now parse in the agent or in Elasticsearch/OpenSearch INGEST PIPELINES instead. The OpenTelemetry Collector can also collect logs.

LOKI (Observability course) indexes only labels, not the log text, so it's much cheaper to store at scale, with grep-like queries. Search engines index everything, so they're better for rich ad-hoc queries, analytics, and security use cases (SIEM), at a higher cost. Many companies run both, or choose by budget.

ELK, EFK, and the alternativesdiagram
Rendering diagram…

02Structure at the source

The best parser is not needing one: have apps log JSON with consistent field names (level, service, trace_id, msg), using a shared schema such as Elastic Common Schema (ECS) or OpenTelemetry semantic conventions. Then the pipeline only decodes JSON and adds metadata. Parsing free-text lines with grok works, but it's CPU-heavy and breaks when the format changes (Observability course, parsing logs you didn't write).

03Mapping explosion

With dynamic mapping, every NEW JSON key becomes a new field in the index mapping. An app that logs {"user_42_cart": ...} or dumps arbitrary request headers creates thousands of fields; the cluster state bloats, the managers slow down, and at index.mapping.total_fields.limit (1000) documents are rejected. Protect log indexes: templates with explicit fields for known keys, dynamic: false (unknown fields are kept in _source but not indexed) or the flattened/flat_object type for free-form objects.

04Buffering and backpressure

When OpenSearch returns 429 (too many requests) or is down, the agent must hold data. Fluent Bit's in-memory buffer is small and lost on restart; use storage.type filesystem with a size limit so logs survive restarts and outages up to the limit, and alert when it fills. For very large volumes, put Kafka between agents and indexers (agents → Kafka → Logstash/consumers → OpenSearch): Kafka absorbs spikes and lets you replay into a new cluster (Unit 2.1).

Do it

Hands-on lab

  1. 1

    An ingest pipeline that parses and masks

    Parse a JSON message, set the timestamp, and mask anything that looks like a card number before it's stored (DevSecOps course, secrets and sensitive data).

    terminal
    $ curl -s -XPUT localhost:9200/_ingest/pipeline/shop-logs -H 'content-type: application/json' -d '{
    "processors": [
    { "json": { "field": "log", "target_field": "app", "ignore_failure": true } },
    { "date": { "field": "app.ts", "formats": ["ISO8601"], "ignore_failure": true } },
    { "gsub": { "field": "app.msg", "pattern": "\\b\\d{13,16}\\b", "replacement": "[CARD]", "ignore_missing": true } },
    { "remove": { "field": "log", "ignore_missing": true } } ] }'
    curl -s -XPOST 'localhost:9200/_ingest/pipeline/shop-logs/_simulate' -H 'content-type: application/json' -d '{"docs":[{"_source":{"log":"{\"ts\":\"2026-09-27T10:00:00Z\",\"level\":\"error\",\"msg\":\"charge failed for 4111111111111111\"}"}}]}' | jq '.docs[0].doc._source'
    ── expected output ──
    {
    "app": { "ts": "2026-09-27T10:00:00Z", "level": "error", "msg": "charge failed for [CARD]" },
    "@timestamp": "2026-09-27T10:00:00.000Z"
    }
  2. 2

    An index template that prevents mapping explosions

    Known fields get explicit types; everything else under app.extra is stored as one flat_object field; unknown top-level fields aren't indexed.

    logs-template.jsonwhole filejson
    {
      "index_patterns": ["logs-shop-*"],
      "template": {
        "settings": { "number_of_shards": 1, "number_of_replicas": 1, "refresh_interval": "30s",
                      "default_pipeline": "shop-logs", "index.mapping.total_fields.limit": 500 },
        "mappings": {
          "dynamic": false,
          "properties": {
            "@timestamp": { "type": "date" },
            "kubernetes": { "properties": {
              "namespace_name": { "type": "keyword" }, "pod_name": { "type": "keyword" },
              "labels": { "type": "flat_object" } } },
            "app": { "properties": {
              "level": { "type": "keyword" }, "service": { "type": "keyword" },
              "trace_id": { "type": "keyword" }, "msg": { "type": "text" },
              "extra": { "type": "flat_object" } } }
          }
        }
      }
    }
    terminal
    $ curl -s -XPUT localhost:9200/_index_template/logs-shop -H 'content-type: application/json' -d @logs-template.json | jq .acknowledged
    ── expected output ──
    true
  3. 3

    Fluent Bit as a DaemonSet

    Install with the Helm chart and give it this output config: filesystem buffering, retries, and the Kubernetes filter for pod metadata. The Logstash_Format-style daily indexes are avoided; we write to the rollover alias from Unit 4.2.

    fluent-bit-values.yamlwhole fileyaml
    config:
      service: |
        [SERVICE]
            Flush                     5
            storage.path              /var/fluent-bit/state/buffer
            storage.max_chunks_up     128
      inputs: |
        [INPUT]
            Name              tail
            Path              /var/log/containers/*.log
            multiline.parser  docker, cri
            Tag               kube.*
            storage.type      filesystem
            Mem_Buf_Limit     50MB
      filters: |
        [FILTER]
            Name        kubernetes
            Match       kube.*
            Labels      On
            Annotations Off
      outputs: |
        [OUTPUT]
            Name                      opensearch
            Match                     kube.*
            Host                      opensearch.logging.svc
            Port                      9200
            Index                     logs-shop
            Suppress_Type_Name        On
            Retry_Limit               False
            storage.total_limit_size  5G
    terminal
    $ helm repo add fluent https://fluent.github.io/helm-charts
    helm upgrade --install fluent-bit fluent/fluent-bit -n logging --create-namespace -f fluent-bit-values.yaml
    kubectl -n logging get ds fluent-bit
    ── expected output ──
    NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE
    fluent-bit 3 3 3 3 3
  4. 4

    Query logs like an operator

    In Dashboards' Discover, or via the API: errors from checkout in the last 15 minutes, grouped by pod. The same query in PPL/SQL is available in OpenSearch (source=logs-shop | where app.level='error').

    terminal
    $ curl -s localhost:9200/logs-shop/_search -H 'content-type: application/json' -d '{"size":0,"query":{"bool":{"filter":[{"term":{"app.level":"error"}},{"term":{"kubernetes.labels.app":"checkout"}},{"range":{"@timestamp":{"gte":"now-15m"}}}]}},"aggs":{"pods":{"terms":{"field":"kubernetes.pod_name"}}}}' | jq -c '.aggregations.pods.buckets'
    ── expected output ──
    [{"key":"checkout-6d8f9c7b5-x4k2p","doc_count":412},{"key":"checkout-6d8f9c7b5-q9w7n","doc_count":3}]

Operate it

Knobs that matter

SettingDefaultWhat it doesWhen to change it
storage.type (Fluent Bit input)memoryWhere chunks are buffered.filesystem with storage.total_limit_size so outages don't lose logs or OOM the agent.
Mem_Buf_LimitunlimitedMemory cap per input.Set (e.g. 50MB) so a backlog can't OOMKill the DaemonSet.
index.mapping.total_fields.limit1000Max fields per index.Keep it; fix the source of new fields rather than raising it.
Logstash pipeline.workers / batch.sizeCPU cores / 125Parallelism and batch size of Logstash filters.Raise batch size for throughput; add persistent queues (queue.type: persisted) for durability.

3am practice

Failure drills

Each drill is a real failure mode. Read the scenario and the output, decide what's wrong, then reveal the diagnosis.

Drill #1

Documents rejected: too many fields

A new release logs the whole request header map as JSON. An hour later, log ingestion errors spike and cluster-manager CPU is high.

terminal
$ kubectl -n logging logs ds/fluent-bit | grep -m1 limit
── what you'll see ──
{"type":"illegal_argument_exception","reason":"Limit of total fields [1000] has been exceeded while adding new fields [3]"}

Drill #2

Fluent Bit pods OOMKilled during an outage

OpenSearch was unavailable for 20 minutes. During that time the Fluent Bit DaemonSet crash-looped on busy nodes, and some logs were lost.

terminal
$ kubectl -n logging get pods -o wide | grep -v Running
── what you'll see ──
fluent-bit-x7k2p 0/1 OOMKilled 6 2d ip-10-20-11-14

Decide

Log backends

OpenSearch / ElasticsearchLokiCloudWatch Logs
IndexesEvery field (full-text)Labels only; content scanned at query timeManaged; Logs Insights queries
Query powerRich search, aggregations, SIEMLogQL: filter, parse, metrics from logsGood for AWS-native, basic analytics
Cost at scaleHigh (compute + disk)Low (object storage)Per-GB ingest + storage, can get expensive
Ops burdenHigh (or managed service)MediumNone
Pick whenSearch-heavy, security, analyticsKubernetes logs with Grafana, cost-sensitiveAWS services, small teams

The bigger picture

Connects to

Prove it

Interview questions

01

Walk through how logs get from a Kubernetes pod to Kibana.

02

ELK vs Loki?

03

What is a mapping explosion and how do you prevent it?

0/3 · 0%