Logging in Kubernetes on GKE: kubectl logs, Cloud Logging, and Structured Logs
When a container in GKE crashes or misbehaves, the first question is always: what did it log? GKE answers that question automatically. Every line written to stdout or stderr is collected by Fluent Bit, tagged with the pod name, namespace, and container name, and stored in Cloud Logging. The logs survive container restarts, pod rescheduling, and node replacements. Getting useful answers from them depends on knowing where to look and how to write logs in the first place.
How GKE logging works in plain terms
Your application writes messages to stdout or stderr, the same output streams that any process has. GKE picks those messages up automatically, labels them with metadata about the pod and cluster, and stores them in Cloud Logging where you can search them at any time.
There are two main ways to read those logs:
kubectl logs — go directly to the running container and ask what it said recently. Fast, but limited to what is still available on that node.
Cloud Logging — search the full historical archive. Works even after the pod is gone, and lets you search across all pods at once.
A factory floor with a central archive
Imagine every container is a worker on a factory floor. Each worker calls out status updates as they work: “starting task”, “processed order 1234”, “error: connection timed out.” GKE has someone listening at every workstation, writing down every word, labelling who said it and when, and filing the notes in a searchable archive. That archive is Cloud Logging. You can look up what any container said at any point in time, even after the container has been replaced or moved to a different machine.
How the logging pipeline works
The logging pipeline in GKE operates at the node level in three stages:
Application writes to stdout or stderr. The container runtime (containerd on GKE) captures these streams and writes them to log files on the node’s filesystem under
/var/log/pods/. The container itself does not need to know it is running in Kubernetes.Node logging agent forwards logs. GKE deploys Fluent Bit as a DaemonSet (one pod per node). Fluent Bit tails the log files written by the container runtime, enriches each entry with Kubernetes metadata (pod name, namespace, container name, cluster name, node name), and forwards the entries to Cloud Logging.
Cloud Logging stores and indexes logs. Cloud Logging receives the enriched entries and stores them in the
_Defaultlog bucket for the project. Entries are indexed and queryable immediately via Logs Explorer, the gcloud CLI, or the Cloud Logging API.
For metrics-based observability alongside log data, see Monitoring GKE Clusters.
GKE uses Fluent Bit rather than the older Fluentd for its lower memory footprint and higher throughput. Both forward logs automatically. If you have an older cluster you may be running Fluentd — the behaviour from your application’s perspective is identical.
kubectl logs vs Cloud Logging
These two tools serve different purposes. Knowing when to use each saves time during debugging.
| kubectl logs | Cloud Logging | |
|---|---|---|
| Best for | Live debugging of running pods | Historical search and multi-pod investigation |
| Speed | Immediate, no query delay | Near-real-time (seconds of ingestion lag) |
| Historical retention | Current or previous container instance only | 30 days by default, configurable up to 3,650 days |
| Works after pod reschedule? | No, logs may be gone | Yes, always available |
| Filtering power | Basic (tail, grep) | Rich label and field filters via LQL |
| Multi-pod querying | Label selector (-l app=my-app) | Full namespace and cluster-wide queries |
| Structured field search | Not supported | jsonPayload.field=“value” queries |
| Typical question | What is happening right now? | What happened at 3am last Tuesday? |
- Use
kubectl logswhen the pod is running and you want a fast look at what it is currently doing. - Use Cloud Logging when the pod is gone, the issue is historical, or you need to search across many pods at once.
Reading logs with kubectl
For immediate log inspection during live debugging, use the
kubectl logs subcommand:
# Stream live logs from a pod (Ctrl+C to stop)
kubectl logs -f my-pod -n production
# Read the last 200 lines only
kubectl logs my-pod --tail=200 -n production
# Read logs from a specific container in a multi-container pod
kubectl logs my-pod -c sidecar -n production
# Read logs from all pods matching a label selector
kubectl logs -l app=my-app -n production
# Read logs from the PREVIOUS (crashed) container instance
kubectl logs my-pod --previous -n productionWhen a container crashes, Kubernetes restarts it immediately. Running
kubectl logs my-pod shows the new instance, which is
often empty. The crash logs are in the previous instance. Always run
kubectl logs my-pod —previous first when diagnosing a restart.
This is the most commonly missed step when investigating repeated pod crashes.
kubectl logs only shows logs from the current or previous container instance on a node. If a pod was rescheduled onto a different node after a failure, logs from the earlier node may no longer be accessible via kubectl. Cloud Logging retains all historical log entries regardless of pod or node lifetime.
For more on pod lifecycle and restarts, see Kubernetes Pods Explained.
Querying logs in Cloud Logging
Logs Explorer is the browser-based search tool in the Google Cloud console. It uses the Logging Query Language (LQL): a filter syntax that combines resource labels, field values, and text searches.
Basic container log filter:
resource.type="k8s_container"
resource.labels.namespace_name="production"
resource.labels.pod_name=~"my-app-.*"The =~ operator matches using regular expressions, so
my-app-.* matches every pod in a deployment without needing to
know the exact generated suffix.
Filter by severity:
resource.type="k8s_container"
resource.labels.namespace_name="production"
severity>=ERRORFilter by a specific container name (useful in multi-container pods):
resource.type="k8s_container"
resource.labels.container_name="my-app"
resource.labels.namespace_name="production"Reading logs with gcloud:
# Read recent logs for a specific pod
gcloud logging read \
'resource.type="k8s_container" AND resource.labels.pod_name="my-pod-7d9f8c-xkzpl"' \
--limit=50 \
--format="table(timestamp,severity,textPayload)"
# Read ERROR and above from a namespace in the last hour
gcloud logging read \
'resource.type="k8s_container" AND resource.labels.namespace_name="production" AND severity>=ERROR' \
--freshness=1h \
--project=my-projectLog entry structure in Cloud Logging
Every log entry from a GKE container arrives with a consistent set of metadata
labels. The monitored resource type is k8s_container. Each entry
carries:
| Label | Description |
|---|---|
cluster_name | The name of the GKE cluster |
namespace_name | The Kubernetes namespace the pod belongs to |
pod_name | The full name of the pod including the random suffix |
container_name | The name of the container within the pod |
location | The region or zone where the cluster runs |
project_id | The Google Cloud project ID |
For plain text log lines, Cloud Logging stores the content in
textPayload. For JSON log lines, it parses the object and stores
it in jsonPayload, making every field individually searchable.
For example, if your app logs
{“severity”:“ERROR”,“message”:“timeout”,“requestId”:“abc123”},
you can filter directly on jsonPayload.requestId=“abc123” in
Logs Explorer. With textPayload, you can only do a full-text
search across the entire raw line.
Structured logging
Plain text logs are hard to query at scale. Searching for the word “error” returns every line containing it: function names, variable names, error messages, and debug statements all mixed together. Structured logging writes log entries as JSON objects to stdout, so each piece of information is a named, individually queryable field.
Sticky notes vs a filing cabinet
Plain text logging is like throwing sticky notes into a cardboard box. To find something, you read every note from top to bottom. Structured logging is like a filing cabinet with labeled drawers. You go straight to the “requestId=abc123” drawer and pull exactly what you need.
When Cloud Logging receives a JSON-formatted log line from a container, it
automatically parses the object and stores it in jsonPayload.
For a deeper look at patterns and field conventions, see
Structured
Logging.
Example structured log entry (what your app writes to stdout):
{
"severity": "ERROR",
"message": "Database connection failed",
"requestId": "req-abc123",
"userId": "user-456",
"durationMs": 3042
}Minimal Python implementation:
import json
import logging
import sys
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"severity": record.levelname,
"message": record.getMessage(),
"logger": record.name,
}
return json.dumps(log_entry)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)Cloud Logging recognises these special fields in JSON log entries:
| JSON field | Effect in Cloud Logging |
|---|---|
severity | Maps to log entry severity (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
message | Used as the main log message display text |
httpRequest | Parsed into Cloud Logging’s structured HTTP request type |
logging.googleapis.com/trace | Links the log entry to a Cloud Trace trace |
logging.googleapis.com/spanId | Links to a specific span within a trace |
Including a logging.googleapis.com/trace field with the trace ID
from your incoming request automatically correlates log entries with the
corresponding trace in Cloud Trace, letting you jump from a log line directly
to the full distributed trace for that request.
Logging in multi-container pods
Each container in a pod produces its own log stream, stored separately in
Cloud Logging under the container_name label. To query only the
application container’s logs and exclude a sidecar:
resource.type="k8s_container"
resource.labels.pod_name=~"my-app-.*"
resource.labels.container_name="my-app"When reading via kubectl, specify the container with -c:
kubectl logs my-pod -c my-app -n production
kubectl logs my-pod -c envoy-proxy -n productionInit containers (which run to completion before the main containers start) also produce logs. Their output is available in Cloud Logging for the full retention period even after the init container has finished.
Log retention, costs, and log-based metrics
The _Default log bucket retains logs for 30 days by default.
You can increase this up to 3,650 days in the bucket settings, or export
logs long-term using a log sink:
gcloud logging sinks create gke-logs-archive \
storage.googleapis.com/my-log-archive-bucket \
--log-filter='resource.type="k8s_container"'Sinks export matching entries to Cloud Storage, BigQuery, or Pub/Sub in near-real time for cost-effective long-term archival, without changing retention inside Cloud Logging itself.
You can also convert any log query into a Cloud Monitoring metric. This lets you alert on application-level events that infrastructure metrics do not capture — for example, alerting when the ERROR rate in the production namespace exceeds 10 per minute.
Log-based metrics have a 1 to 2 minute ingestion delay. They are not suitable for sub-minute real-time alerting, but work well for sustained error rate monitoring over 5-minute or longer windows.
Cloud Logging has a free ingestion tier. High-frequency DEBUG logs at production scale can exceed it quickly. Use log exclusion rules to drop noisy, low-value entries (such as health check pings from a load balancer) before they are ingested and counted.
Application logging best practices for GKE
Never write logs to files inside a container. Those files disappear on
restart and are never visible to kubectl or Cloud Logging. Always write to
stdout or stderr. If your logging library
writes to a file, configure it to target /dev/stdout.
Use structured JSON logging. Even a minimal structure (severity, message, and a request ID) dramatically improves your ability to filter and query logs at scale. See Structured Logging for patterns and examples.
Include a request or trace ID in every log entry. Without a shared ID, you cannot find every log line produced during a specific request. Without one, you are reduced to guessing based on timestamps.
Use appropriate severity levels. In production, use INFO for normal operations, WARNING for recoverable issues, and ERROR for failures that need attention. Avoid excessive DEBUG logging at production scale — it drives up ingestion costs and makes real errors harder to find.
Control workload access to Cloud Logging. Use Workload Identity for GKE to control which workloads can write to Cloud Logging, and review Securing GKE Clusters for the broader cluster access model.
Common mistakes
Writing logs to files inside the container instead of stdout. Log files inside a container’s filesystem do not survive container restarts, are not forwarded by Fluent Bit, and are not visible in Cloud Logging or kubectl logs. Always write to stdout or stderr. If you use a file-based logging library, configure it to write to
/dev/stdout.Forgetting the
—previousflag when a container has crashed. When a container crashes, Kubernetes restarts it immediately.kubectl logs my-podshows the new instance’s logs, which are often empty. The crash logs are in the previous instance:kubectl logs my-pod —previous. Missing this flag is the most common reason developers say “I can’t find the error.”Using plain text logging in production. Plain text logs are hard to filter and impossible to query by field. Switching to structured JSON logging costs little effort but dramatically improves usability when debugging incidents under pressure.
Not filtering by
container_namein multi-container pods. If a pod has a sidecar (such as an Envoy proxy), Cloud Logging contains logs from every container mixed together. Failing to filter bycontainer_namemeans sidecar traffic logs obscure the application logs you are looking for.Ignoring log ingestion costs. Cloud Logging charges for ingestion above the free tier. Verbose debug logging in production can generate significant costs at scale. Use log exclusion filters to drop high-volume, low-value entries before they are ingested.
Summary
- GKE automatically captures stdout and stderr from all containers via Fluent Bit and forwards entries to Cloud Logging with full Kubernetes metadata
- Log entries are stored under the
k8s_containerresource type, labelled with cluster name, namespace, pod name, and container name - Use
kubectl logs -fto stream live logs;—previousto read a crashed container’s last output;-cto target a specific container in a multi-container pod - Use Cloud Logging for historical search, multi-pod filtering, and structured field queries — especially after a pod has been rescheduled or deleted
- Write logs as JSON to stdout; Cloud Logging parses JSON fields automatically into
jsonPayload, making each field individually queryable - Log retention defaults to 30 days in the
_Defaultbucket; use log sinks to export to Cloud Storage for long-term archival - Log-based metrics bridge logs and alerting: convert any Logs Explorer filter into a Cloud Monitoring metric and set an alert threshold
Frequently asked questions
Does GKE collect container logs automatically?
Yes. GKE runs Fluent Bit as a DaemonSet on every node. It automatically captures stdout and stderr from all containers and forwards them to Cloud Logging, enriched with pod name, namespace, container name, and cluster metadata. Your application just needs to write to stdout or stderr — no SDK, no log file path, no additional configuration required.
What is the difference between kubectl logs and Cloud Logging?
kubectl logs gives you fast, direct access to a running or recently-stopped container's log stream, ideal for live debugging. Cloud Logging stores all historical entries regardless of pod lifetime, supports powerful label and field filters, and lets you search across multiple pods and namespaces at once. Use kubectl for quick checks; use Cloud Logging for anything historical or multi-pod.
Why can't I see old logs with kubectl logs?
kubectl logs only shows logs from the current or previous container instance on the node where the pod ran. If the pod was rescheduled onto a different node, or if the node was replaced, the earlier logs are no longer accessible via kubectl. They are still available in Cloud Logging, which retains all entries independently of pod and node lifecycle.
Should Kubernetes apps write logs to files or stdout?
Always write to stdout or stderr. Log files inside a container's filesystem are lost when the container restarts, and are never forwarded to Cloud Logging or visible to kubectl logs. If you use a logging library that writes to files, configure it to write to /dev/stdout instead.
How do structured logs help in GKE?
When you write JSON to stdout, Cloud Logging automatically parses each field and stores it in jsonPayload, making every field individually searchable. For example, you can filter on jsonPayload.requestId='abc123' or jsonPayload.userId='user456' directly in Logs Explorer. With plain text, you are limited to full-text search across the entire log line.