GCP Structured Logging Explained: JSON Logs vs Plain Text
Structured logging means writing log entries as JSON objects instead of plain text strings. When Cloud Logging receives a valid JSON object from your application, it parses every field and indexes them individually. Instead of searching a wall of text, you can filter by user ID, endpoint, status code, or duration with a single precise query. This page explains how it works, how to implement it on Cloud Run and GKE, and what you gain in return.
Compare the same error logged two ways:
# Plain text: what lands in textPayload
ERROR: connection refused after 3 retries for user u-8821 at /api/checkout
# JSON: what lands in jsonPayload
{"severity":"ERROR","message":"connection refused after 3 retries","user_id":"u-8821","endpoint":"/api/checkout","retry_count":3}
The first you can only search as a string. The second you can filter, count, alert on, and extract as a metric. All without touching the application again.
Simple explanation
Think of it this way
Plain text logs are sticky notes: readable by a human, impossible to sort or filter automatically. Structured logs are spreadsheet rows: each field has a column name and a value, so you can sort, filter, and query precisely. Cloud Logging is the spreadsheet engine that does the querying for you.
When your app writes {“severity”:“ERROR”,“user_id”:“u-123”} to stdout, Cloud Logging stores user_id as its own indexed field. You can then query jsonPayload.user_id=“u-123” and get back only that user’s entries, not every log that happens to contain that string somewhere in the body.
This is the core difference between jsonPayload and textPayload. With jsonPayload, every field is queryable individually. With textPayload, you have one big string and substring matching only.
Why structured logging matters in GCP
Structured logs are not just a style preference. They unlock capabilities in Cloud Monitoring that plain text cannot provide:
- Precise filtering. Find every request from a specific user, to a specific endpoint, with a specific status code. In Logs Explorer, a query like
jsonPayload.endpoint=“/api/checkout” AND jsonPayload.status_code=500returns exactly the entries you care about. - Numeric comparisons. Filter by
jsonPayload.retry_count>2orjsonPayload.duration_ms>1000. Impossible with text logs unless you parse the string yourself. - Log-based metrics. Log-based metrics extract values from
jsonPayloadfields and turn them into Cloud Monitoring metrics. For example, a latency histogram built directly fromjsonPayload.duration_ms. - Alerting. Build alert policies that fire when error rates or latency values cross thresholds, derived from your structured log fields.
- Trace correlation. Include the
logging.googleapis.com/tracefield and your log entries link directly to spans in Cloud Trace. During an incident you can jump from a slow trace to every log entry emitted during that specific request.
Imagine your checkout API starts failing at 2am. With textPayload, your only option is substring-searching a stream of strings. You cannot filter by user ID, you cannot count 500s by endpoint, and you cannot measure how long each step took. With structured logs, you can answer “which users were affected, which endpoint failed, and how often did it retry” in under a minute. The difference is not cosmetic. It is the difference between a 10-minute fix and a 2-hour incident.
How it works in Cloud Logging
The path from your application to a queryable log entry is straightforward:
- Your app writes a line to stdout (or stderr).
- The GCP logging agent (or Cloud Run’s built-in collector) captures that line.
- If the line is a plain string, Cloud Logging stores it in
textPayload. - If the line is a valid JSON object, Cloud Logging parses it and stores it in
jsonPayload. - Any field named
severitybecomes the entry’s log level. Any field namedmessageappears as the summary line in the console. - Every other field is indexed under
jsonPayloadand available for filtering, metrics, alerts, and trace linking.
There is no configuration step. If what you write to stdout is valid JSON, it becomes jsonPayload. If not, it becomes textPayload. The behavior is entirely automatic based on what your app outputs.
Structured logging vs plain text logs
| Capability | Plain text (textPayload) | Structured JSON (jsonPayload) |
|---|---|---|
| Exact field filtering | No | Yes |
| Numeric comparisons | No | Yes |
| Log-based metrics extraction | No | Yes |
| Field-level alerting | No | Yes |
| Trace correlation | No | Yes (via trace field) |
| Parsing effort | Manual regex or custom parser | None, fields already indexed |
| Production debugging speed | Slow | Fast |
| Suitable for high-traffic production | Marginal | Yes |
Key fields to include in your log entries
Cloud Logging recognizes certain field names and treats them specially. These are worth including in every log entry:
severity: the log level. Valid values:DEBUG,INFO,NOTICE,WARNING,ERROR,CRITICAL,ALERT,EMERGENCY.message: the human-readable summary shown in Logs Explorer. Keep it short and descriptive.labels: a map of string key-value pairs for metadata you want to filter on but not extract as a metric.httpRequest: a structured object for HTTP request details. Cloud Logging renders it with special formatting and makes fields likehttpRequest.statusandhttpRequest.latencydirectly queryable.sourceLocation: the file, line number, and function that emitted the log. Useful for linking entries back to specific code.logging.googleapis.com/trace: links the entry to a trace in Cloud Trace. Value format:projects/PROJECT_ID/traces/TRACE_ID.logging.googleapis.com/spanId: links to a specific span within the trace.
If you omit the severity field, all your entries appear as DEFAULT severity in Logs Explorer. You lose severity>=ERROR filtering and severity-based alerting. These are two of the most-used queries during incident response. Always include it.
Beyond these special fields, include application-specific fields you will actually use when debugging or building metrics:
user_id: find all logs for a specific userendpoint: filter by API routestatus_code: filter by HTTP response statusduration_ms: measure latency and build histogramsretry_count: surface flaky infrastructure issuesorder_idorsession_id: trace a single transaction across services
Example Logs Explorer queries
Once your logs are structured, these Logs Explorer queries work immediately with no additional setup:
# Find all logs for a specific user
jsonPayload.user_id="u-8821"
# Filter by API endpoint
jsonPayload.endpoint="/api/checkout"
# Errors only
severity="ERROR"
# HTTP 500 responses
jsonPayload.status_code=500
# Requests with more than 2 retries
jsonPayload.retry_count>2
# Scope to a specific GKE namespace
resource.type="k8s_container"
resource.labels.namespace_name="production"
# Combine filters: checkout errors for one user
jsonPayload.user_id="u-8821"
jsonPayload.endpoint="/api/checkout"
severity="ERROR"
# Find logs linked to a specific trace
logging.googleapis.com/trace="projects/my-project/traces/abc123"All of these work only because the fields are individually indexed. With textPayload, none of these queries are possible.
Structured logging on Cloud Run
Cloud Run captures everything your container writes to stdout and forwards it to Cloud Logging. You do not need a logging agent, an SDK, or any configuration. If what you write is valid JSON, it becomes jsonPayload. See Monitoring Cloud Run for how structured logs feed into Cloud Run observability dashboards.
Here is a minimal Python helper that produces structured log entries:
import json
import sys
def log(severity, message, **kwargs):
entry = {
"severity": severity,
"message": message,
**kwargs
}
print(json.dumps(entry), file=sys.stdout, flush=True)
# Every keyword argument becomes a queryable jsonPayload field
log("INFO", "Request received", user_id="u-123", endpoint="/api/orders")
log("ERROR", "Database connection failed", error="connection timeout", retry_count=3)
log("WARNING", "Cache miss", cache_key="product:42", duration_ms=12)Always pass flush=True. Without it, Python buffers stdout and log entries may not appear in Cloud Logging until the buffer is full. During active debugging that delay is extremely frustrating. Alternatively, set PYTHONUNBUFFERED=1 as an environment variable on your Cloud Run service.
To correlate logs with traces, include the trace field in your entries:
def log_with_trace(severity, message, project_id, trace_id, span_id=None, **kwargs):
entry = {
"severity": severity,
"message": message,
"logging.googleapis.com/trace": f"projects/{project_id}/traces/{trace_id}",
**kwargs
}
if span_id:
entry["logging.googleapis.com/spanId"] = span_id
print(json.dumps(entry), file=sys.stdout, flush=True)Trace IDs are like receipt numbers
When you place an order, every department that touches it stamps the same receipt number. Weeks later, you can look up that number and see every step. A trace ID works the same way across your services. Include it in every log entry for a request and you can pull the full story of any request from any service in one query, even months later.
If you use OpenTelemetry for tracing, the trace ID and span ID are available from the current span context. Most OpenTelemetry logging integrations inject these fields automatically.
Structured logging on GKE
GKE works the same way. Any container writing valid JSON to stdout has its logs parsed into jsonPayload automatically. The Cloud Logging agent runs as a DaemonSet on each node and collects stdout and stderr from every container via the Kubernetes logging infrastructure.
In addition to your own JSON fields, every GKE log entry includes these resource labels automatically:
resource.labels.cluster_name: the GKE clusterresource.labels.namespace_name: the Kubernetes namespaceresource.labels.pod_name: the specific podresource.labels.container_name: the container within the pod
You can combine these with your structured fields to scope queries tightly. To find all errors in the checkout container in the production namespace:
resource.type="k8s_container"
resource.labels.namespace_name="production"
resource.labels.container_name="checkout"
severity="ERROR"GKE adds these resource labels for you automatically. You do not need to include cluster_name or namespace_name in your JSON log output. They come from the infrastructure layer. Your job is to add the application-specific fields like user_id, endpoint, and status_code.
See Monitoring GKE for how structured logs integrate with GKE dashboards and alerting, and Logging in Kubernetes for how the Kubernetes logging pipeline works end-to-end.
When to use structured logging
Structured logging pays off whenever you need to debug, monitor, or alert on production systems. Use it for:
- Production APIs. Any service handling real user traffic. When something breaks, you want to filter by user, endpoint, and status code right away, not grep through text.
- Checkout and payment flows. High-stakes paths where you need to trace exactly what happened for a specific order ID or session.
- Error-rate monitoring. Use log-based metrics to count
status_code=500entries and alert when the rate exceeds a threshold. - Tracing a single request end-to-end. Include a
request_idor trace ID in every log entry for a request. You can then find every log entry across every service that handled it with one query. - Multi-service incident response. During an incident, structured logs let you correlate failures across services without parsing text from multiple systems.
Plain text logs are still fine for local development output, one-off scripts, and debug tooling that never runs in production. For anything in Cloud Run, GKE, or other GCP compute platforms that you plan to monitor or alert on, use structured JSON.
Common mistakes
- Writing multi-line log entries. Cloud Logging treats each newline from stdout as a separate log entry. A Python exception traceback printed with
print(traceback.format_exc())becomes dozens of disconnected entries. Serialize the full error (including the stack trace) as a single JSON object with the traceback stored as a string field. - Using a text formatter instead of JSON. Many logging libraries emit human-readable text by default. In Python, configure
python-json-loggeror write a custom JSON formatter. In Node.js, usepinoor configurewinston’s JSON transport. Your application output must be one valid JSON object per line. - Omitting the
severityfield. Without it, all entries appear as DEFAULT severity. You loseseverity>=ERRORfiltering and severity-based alerting. These are two of the most-used queries during incident response. - High-cardinality fields in log-based metrics. If you extract
user_idas a metric label, Cloud Monitoring creates a separate time series for every user ID it sees. For large services this can mean tens of thousands of time series and a sharp cost increase. Only use bounded, low-cardinality values as metric labels. - Oversized or noisy log entries. Including large payloads, full request bodies, or excessive debug fields in every entry bloats log volume and increases ingestion cost. Log what you need to debug the most important issues, not everything available.
- Not flushing stdout. Python and other runtimes buffer stdout by default in containerized environments. Use
flush=Trueor setPYTHONUNBUFFERED=1so log entries appear in real time.
The cardinality mistake deserves extra attention. Extracting user_id as a log-based metric label sounds useful until you have 500,000 users. That is 500,000 time series in Cloud Monitoring, which can cost hundreds of dollars per month and make dashboards unusable. Only extract fields with a small, bounded set of values as metric labels: things like endpoint, status_code, or region. Never user_id, session_id, or any ID that grows with your user base.
Structured logging vs Cloud Audit Logs
Two different kinds of logs
Your application logs are like notes you take during your shift: you decide what to write down, when, and in how much detail. Cloud Audit Logs are like security camera footage: the system records everything automatically, whether you like it or not, and you cannot change what gets captured. Both end up in Cloud Logging, but they serve completely different purposes.
- Structured application logs are generated by your code. You decide what to log, when, and what fields to include. They appear in Cloud Logging under whatever resource your service runs on.
- Cloud Audit Logs are generated by GCP itself. They record who did what to which GCP resource: who created a Cloud Storage bucket, who changed a firewall rule, who read a secret from Secret Manager. You do not write these; GCP writes them automatically.
Use structured application logs for operational debugging and monitoring. Use Audit Logs for security, compliance, and access auditing. For a broader overview of every log type available in GCP, see Log Types in GCP.
FAQ
What is the difference between textPayload and jsonPayload?
textPayload holds a plain string. jsonPayload holds a parsed JSON object with individually indexed fields. With jsonPayload you can filter on jsonPayload.user_id=“u-123” and do numeric comparisons like jsonPayload.retry_count>2. With textPayload, you can only substring-search the entire string.
Do I need a client library to use structured logging in Cloud Run?
No. Write a valid JSON object to stdout and Cloud Run forwards it to Cloud Logging, which parses it into jsonPayload automatically. A simple json.dumps() call is enough. No SDK, agent, or additional configuration is required.
How do I link logs to traces in Cloud Trace?
Include “logging.googleapis.com/trace”: “projects/PROJECT_ID/traces/TRACE_ID” in your JSON log entry. Cloud Logging links the entry to that trace in Cloud Trace and you can navigate between them in the console. Optionally add logging.googleapis.com/spanId to link to a specific span.
Can I create alerts and metrics from structured logs?
Yes. Log-based metrics extract values from jsonPayload fields and turn them into Cloud Monitoring metrics. You can then create alert policies that fire when error counts spike or latency exceeds a threshold, without changing your application.
Does structured logging increase logging cost or cardinality risk?
Structured logs do not cost more to ingest than plain text logs of the same size. The cardinality risk is in log-based metrics: extracting a high-cardinality field like user_id as a metric label creates a separate time series per unique value and can significantly increase Cloud Monitoring costs. Only extract bounded, low-cardinality values as metric labels.
Summary
- Structured logs are valid JSON objects written to stdout. Cloud Logging parses them into
jsonPayloadautomatically - Plain text logs land in
textPayloadand support substring search only - Always include
severityandmessagefields for correct display and severity filtering - The
logging.googleapis.com/tracefield links a log entry to a Cloud Trace trace - Works on Cloud Run and GKE with no configuration. Just write JSON to stdout
- Structured logs unlock log-based metrics, field-level alerting, and trace correlation
- Avoid high-cardinality metric labels and multi-line log entries
Frequently asked questions
What is the difference between textPayload and jsonPayload?
textPayload holds a plain string. jsonPayload holds a parsed JSON object with individually indexed fields. With jsonPayload you can filter on jsonPayload.user_id="u-123" to find every log entry for one user. With textPayload, you can only substring-search the entire string and cannot do numeric comparisons, field-level filtering, or extract metrics.
Do I need a client library to use structured logging in Cloud Run?
No. Write a valid JSON object to stdout and Cloud Run forwards it to Cloud Logging, which parses it into jsonPayload automatically. No SDK, agent, or configuration is required. A simple json.dumps() call in Python is enough.
How do I link logs to traces in Cloud Trace?
Include a logging.googleapis.com/trace field in your JSON log entry set to projects/PROJECT_ID/traces/TRACE_ID. Cloud Logging links the entry to that trace and you can jump between them in the console. Optionally add logging.googleapis.com/spanId to link to a specific span.
Can I create alerts and metrics from structured logs?
Yes. Log-based metrics extract values from jsonPayload fields and turn them into Cloud Monitoring metrics. You can then alert when jsonPayload.status_code=500 spikes, or chart jsonPayload.duration_ms as a latency histogram without changing your application.
Does structured logging increase logging cost or cardinality risk?
Structured logs do not cost more to ingest than plain text logs of the same size. The risk is cardinality in log-based metrics: if you extract a high-cardinality field like user_id as a metric label, it creates thousands of time series and can increase Cloud Monitoring costs significantly. Only extract bounded, low-cardinality values as metric labels.