📋 How Kubernetes Handles Logs

Kubernetes doesn't provide a built-in cluster-wide logging system. Instead, it defines a node-level contract: container stdout/stderr is captured by the container runtime and written to a log file on the node. Everything above that — shipping, parsing, storing, querying — is your responsibility.

The log file path

containerd (and Docker) write each container's output to a file at:

/var/log/pods/<namespace>_<pod-name>_<uid>/<container-name>/<rotation-number>.log

# Example:
/var/log/pods/default_my-app-6f7d9c-xkp2j_abc123/app/0.log

# Symlinks also exist at:
/var/log/containers/<pod>_<namespace>_<container>-<id>.log

kubectl logs reads directly from these node files via the kubelet's log API — no shipping agent needed for basic debugging.

kubectl logs — your first tool

# Current logs
kubectl logs my-app-6f7d9c-xkp2j

# Previous container (after crash/restart)
kubectl logs my-app-6f7d9c-xkp2j --previous

# Follow live
kubectl logs -f my-app-6f7d9c-xkp2j

# Last 100 lines
kubectl logs --tail=100 my-app-6f7d9c-xkp2j

# All containers in a pod
kubectl logs my-app-6f7d9c-xkp2j --all-containers

# All pods matching a label (across replicas)
kubectl logs -l app=my-app --all-containers --prefix

# Since timestamp
kubectl logs my-app-6f7d9c-xkp2j --since-time="2024-01-15T10:00:00Z"
⚠️ Node log rotation limits kubectl logs Logs on the node are rotated (default: 10 MB, 5 files). Once rotated off, kubectl logs can't retrieve them. This is why you need a log shipping pipeline — to preserve logs beyond the node's rotation window.

Three Logging Architecture Patterns

1. Node-level agent

A DaemonSet (Fluent Bit, Fluentd) runs on every node, reads /var/log/pods/, and ships to a backend. Most common.

2. Sidecar container

A logging sidecar shares a volume with the app and ships logs independently. More overhead but enables per-app config.

3. Direct-to-backend

Application writes directly to a logging API (e.g. structured JSON to Loki HTTP endpoint). Simple but couples app to logging infra.

🏗️ Cluster-Level Logging Pipelines

Once logs leave the node, they flow into a log backend. Two dominant choices in the Kubernetes ecosystem:

Grafana Loki

Label-indexed log store. Doesn't full-text index content — very low storage cost. Query with LogQL. Pairs with Grafana dashboards. Best for cloud-native shops already using Grafana.

Elasticsearch + Kibana (ELK)

Full-text inverted index. Rich search and aggregations. Higher storage and CPU cost. Industry standard for compliance-heavy environments needing complex queries.

Cloud-managed

CloudWatch Logs (AWS), Cloud Logging (GCP), Azure Monitor. Zero operational overhead. Vendor lock-in and cost at scale.

OpenSearch

Open-source Elasticsearch fork (AWS-backed). Drop-in replacement for ELK stack with permissive Apache 2.0 license.

The PLG Stack (Promtail / Loki / Grafana)

Loki is the recommended stack for most greenfield Kubernetes deployments — operationally lightweight and tightly integrated with the Grafana observability suite:

Fluent Bit DaemonSet · enriches metadata Loki label-indexed log store Grafana LogQL queries · dashboards Alert Manager log-based alerts

LogQL — Loki's Query Language

# All error logs from the payments namespace
{namespace="payments"} |= "error"

# JSON log parsing + field filter
{namespace="payments", app="checkout"} | json | level="error"

# Rate of error logs per minute (metric query)
rate({namespace="payments"} |= "error" [1m])

# Log lines with a specific trace ID
{namespace="payments"} | json | traceID="abc123xyz"

# Exclude health check noise
{app="my-api"} != "/healthz" != "/readyz"

Structured Logging — the Right Way to Log

Logs as plain text strings are hard to query. Structure them as JSON so the log pipeline can index fields:

// Bad — hard to query, can't filter on level or requestId
log.Printf("ERROR processing request %s: %v", requestID, err)

// Good — structured JSON, Fluent Bit promotes these to labels
log.Info("request failed",
  "level",     "error",
  "requestId", requestID,
  "error",     err.Error(),
  "duration",  elapsed.Milliseconds(),
  "path",      r.URL.Path,
)

Log Retention and Rotation Best Practices

ConcernRecommendation
Node log rotationDefault 10 MB / 5 rotations. Increase for high-volume pods or ship faster to avoid gaps.
Loki retentionSet per-tenant retention via limits_config.retention_period. 30 days is typical; 90+ for compliance.
BackpressureConfigure Fluent Bit's Mem_Buf_Limit and storage.type filesystem to buffer to disk when the backend is slow, avoiding log loss.
Sensitive dataUse Fluent Bit lua or modify filters to redact PII/credentials before shipping. Never log tokens or passwords.
Multi-line logsConfigure multiline.parser docker,cri in Fluent Bit to correctly reassemble Java stack traces split across multiple lines.

🖥️ Pattern 1 — Node-Level Agent (DaemonSet)

The most operationally efficient pattern. A single Fluent Bit or Fluentd pod per node tails all container log files and forwards them to a backend. No app changes needed.

Node Pod A stdout → node Pod B stdout → node Pod C stdout → node /var/log/pods/ (node filesystem) Fluent Bit (DaemonSet) — tails all log files Log Backend Loki / Elasticsearch / CloudWatch forward

Fluent Bit — lightweight, preferred for K8s

Fluent Bit is written in C, uses ~1 MB of memory per node, and has native Kubernetes metadata enrichment. It's the default in most managed K8s offerings (GKE, EKS, AKS).

# Install Fluent Bit via Helm (outputs to Loki)
helm repo add fluent https://fluent.github.io/helm-charts
helm install fluent-bit fluent/fluent-bit \
  --namespace logging --create-namespace \
  --set config.outputs="[OUTPUT]
    Name loki
    Match *
    Host loki.logging.svc.cluster.local
    Port 3100
    Labels job=fluentbit, namespace=\$kubernetes['namespace_name']"

Fluent Bit config anatomy

# fluent-bit.conf key sections:

[SERVICE]
    Flush         5
    Log_Level     info
    Parsers_File  parsers.conf

[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    multiline.parser  docker, cri   # handle multi-line stack traces
    Tag               kube.*
    Refresh_Interval  5

[FILTER]
    Name                kubernetes
    Match               kube.*
    Kube_URL            https://kubernetes.default.svc:443
    Merge_Log           On   # parse JSON logs into structured fields
    Keep_Log            Off
    K8S-Logging.Parser  On
    K8S-Logging.Exclude On

[OUTPUT]
    Name  loki
    Match *
    Host  loki.logging.svc.cluster.local
    Port  3100
💡 Merge_Log = On is critical When your app logs JSON (e.g. {"level":"error","msg":"db timeout"}), Merge_Log On parses the JSON and promotes fields to top-level. This means you can filter on level=error in Loki/Grafana instead of string-searching raw log lines.

🔀 Pattern 2 — Sidecar Container

When an application writes logs to a file (not stdout) or needs custom per-app log processing, a sidecar is the right pattern. The sidecar shares a volume with the app container and ships or re-emits the logs.

Two sidecar variants

VariantHow it worksWhen to use
Streaming sidecar Sidecar reads the log file and writes it to its own stdout. The node agent then picks it up normally. App writes to a file; you don't want to modify the app
Shipping sidecar Sidecar runs Fluent Bit/Fluentd and ships directly to the backend. Bypasses node agent. Per-app routing, different retention, multi-tenancy
# Streaming sidecar — re-emit file log to stdout
spec:
  volumes:
  - name: app-logs
    emptyDir: {}
  containers:
  - name: app
    image: myapp:v1
    volumeMounts:
    - name: app-logs
      mountPath: /var/log/app
  - name: log-streamer
    image: busybox:1.36
    args: [/bin/sh, -c, "tail -n+1 -F /var/log/app/app.log"]
    volumeMounts:
    - name: app-logs
      mountPath: /var/log/app

🧠 Knowledge Check

Q1. Where does containerd write container log files on the node?

A) /var/log/containers/syslog
B) /var/log/pods/<namespace>_<pod-name>_<uid>/<container>/<n>.log
C) /etc/kubernetes/logs/
D) Inside the container at /var/log/app.log

Q2. You need logs from a pod that crashed 2 hours ago. Its node log rotation has already purged the file. How do you get those logs?

A) Use kubectl logs --previous
B) SSH to the node and check /var/log/pods/
C) Query the cluster-level log backend (Loki/Elasticsearch) — node-local logs are gone once rotated
D) Use kubectl debug to attach an ephemeral container to reconstruct logs

Q3. What does Fluent Bit's Merge_Log On option do?

A) Merges log files from multiple containers into a single stream
B) Combines logs from different nodes before shipping
C) Parses JSON log lines and promotes fields to top-level for structured querying
D) Deduplicates repeated log lines to reduce storage cost

Q4. When would you choose a sidecar logging pattern over a node-level DaemonSet agent?

A) When you want to reduce overall cluster resource usage
B) When using a managed Kubernetes service like GKE
C) When the log volume is high and you need batching
D) When the app writes logs to a file, or you need per-app routing to different backends with different configs