📋 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"
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:
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
| Concern | Recommendation |
|---|---|
| Node log rotation | Default 10 MB / 5 rotations. Increase for high-volume pods or ship faster to avoid gaps. |
| Loki retention | Set per-tenant retention via limits_config.retention_period. 30 days is typical; 90+ for compliance. |
| Backpressure | Configure Fluent Bit's Mem_Buf_Limit and storage.type filesystem to buffer to disk when the backend is slow, avoiding log loss. |
| Sensitive data | Use Fluent Bit lua or modify filters to redact PII/credentials before shipping. Never log tokens or passwords. |
| Multi-line logs | Configure 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.
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
{"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
| Variant | How it works | When 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?
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?
kubectl logs --previous