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.

How the logging pipeline works

The logging pipeline in GKE operates at the node level in three stages:

  1. 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.

  2. 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.

  3. Cloud Logging stores and indexes logs. Cloud Logging receives the enriched entries and stores them in the _Default log 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.

Note

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 logsCloud Logging
Best forLive debugging of running podsHistorical search and multi-pod investigation
SpeedImmediate, no query delayNear-real-time (seconds of ingestion lag)
Historical retentionCurrent or previous container instance only30 days by default, configurable up to 3,650 days
Works after pod reschedule?No, logs may be goneYes, always available
Filtering powerBasic (tail, grep)Rich label and field filters via LQL
Multi-pod queryingLabel selector (-l app=my-app)Full namespace and cluster-wide queries
Structured field searchNot supportedjsonPayload.field=“value” queries
Typical questionWhat is happening right now?What happened at 3am last Tuesday?
Tip
  • Use kubectl logs when 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 production
Tip

When 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.

Note

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>=ERROR

Filter 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-project

Log 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:

LabelDescription
cluster_nameThe name of the GKE cluster
namespace_nameThe Kubernetes namespace the pod belongs to
pod_nameThe full name of the pod including the random suffix
container_nameThe name of the container within the pod
locationThe region or zone where the cluster runs
project_idThe 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.

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 fieldEffect in Cloud Logging
severityMaps to log entry severity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
messageUsed as the main log message display text
httpRequestParsed into Cloud Logging’s structured HTTP request type
logging.googleapis.com/traceLinks the log entry to a Cloud Trace trace
logging.googleapis.com/spanIdLinks 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 production

Init 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.

Warning

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.

Tip

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

Warning

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

  1. 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.

  2. Forgetting the —previous flag when a container has crashed. When a container crashes, Kubernetes restarts it immediately. kubectl logs my-pod shows 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.”

  3. 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.

  4. Not filtering by container_name in 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 by container_name means sidecar traffic logs obscure the application logs you are looking for.

  5. 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.

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.

Last verified: 23 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.