Skip to main content

starlette-gcp-logging

Structured JSON logging for Starlette services running on Google Cloud Run (or any GCP compute platform).

Provides two components that work together:

  • GCPFormatter — a logging.Formatter that serialises every log record as a single-line JSON object understood by Cloud Logging, including severity mapping, source location, and trace/span correlation.
  • GCPRequestLoggingMiddleware — a Starlette middleware that emits one structured log entry per request/response (with httpRequest metadata) and propagates trace context to every logger used inside that request via contextvars — no manual plumbing needed.

Installation

pip install starlette-gcp-logging

Quick start

import logging
from starlette.applications import Starlette
import starlette_gcp_logging

# Wire up the formatter once at startup — all loggers inherit it.
handler = logging.StreamHandler()
handler.setFormatter(
    starlette_gcp_logging.GCPFormatter()
)  # project_id auto-detected on GCP
logging.basicConfig(handlers=[handler], level=logging.INFO)

app = Starlette(...)
app.add_middleware(starlette_gcp_logging.GCPRequestLoggingMiddleware)

With uvicorn:

uvicorn myapp:app --log-config /dev/null   # let GCPFormatter own the output

Configuration

GCPFormatter(project_id="", *, labels=None)

Parameter Description
project_id GCP project ID used to build the full trace resource name projects/<id>/traces/<trace_id>. When omitted (the default) it is fetched automatically from the GCP instance metadata server on the first log call and cached for the lifetime of the process. Outside GCP the trace is still written — just without the project prefix.
labels Optional mapping of custom GCP label name → the ContextVar its value should be read from. See Custom labels below.

Log record fields emitted

JSON key Value
severity DEBUG / INFO / WARNING / ERROR / CRITICAL
message Formatted log message
time RFC 3339 UTC timestamp
logging.googleapis.com/sourceLocation file, line, function
logging.googleapis.com/trace Full trace resource name (when a trace ID is present)
logging.googleapis.com/spanId 16-char hex span ID
logging.googleapis.com/traceSampled Boolean sampling flag
exception Formatted traceback (when exc_info is set)
@type GCP Error Reporting type URI (when exc_info is set)
(extra keys) Any fields passed via extra={...} to the logger

GCPRequestLoggingMiddleware(app, *, project_id="", logger_name=..., default_level=logging.INFO)

Parameter Description
project_id Same as GCPFormatter. Auto-detected when omitted.
logger_name Logger to write request entries to. Defaults to starlette_gcp_logging.middleware.
default_level Log level for 1xx/2xx/3xx responses. 4xx → WARNING; 5xx → ERROR.

Custom labels

Application code often stores per-request data (tenant ID, feature flag, API key ID, ...) in its own contextvars.ContextVar. Pass a labels mapping to GCPFormatter to surface any of those as GCP labels on every log entry emitted while the variable holds a value:

import contextvars
import logging
import starlette_gcp_logging

tenant_id: contextvars.ContextVar[str] = contextvars.ContextVar("tenant_id", default="")

handler = logging.StreamHandler()
handler.setFormatter(
    starlette_gcp_logging.GCPFormatter(labels={"tenant_id": tenant_id})
)
logging.basicConfig(handlers=[handler], level=logging.INFO)

Somewhere earlier in the middleware stack (or in a dependency), set the variable for the duration of the request:

tok = tenant_id.set("acme-corp")
try:
    ...
finally:
    tenant_id.reset(tok)

Every log entry emitted while tenant_id holds a value (including falsy values like False, 0, or "") will include:

jsonPayload."logging.googleapis.com/labels"."tenant_id" == "acme-corp"

A label is only omitted when its ContextVar has not been .set() in the current context (i.e. it is unset), matching the behavior of the built-in authenticated_user_email and starlette.dev/route labels. This lets you distinguish an explicitly-set falsy value (e.g. a False feature flag) from an unset variable — unlike a truthiness check, None is the only value treated as "unset".

Trace context extraction

The middleware reads incoming trace headers in this priority order:

  1. traceparent (W3C / OpenTelemetry) — used when an upstream service propagates its own trace. Cloud Run appends its own span to this header, so it is always preferred when present.
  2. X-Cloud-Trace-Context — GCP's own header, injected when there is no upstream traceparent.

The extracted trace ID and span ID are stored in contextvars for the duration of the request, so every logger in the call stack automatically picks them up without any explicit propagation.

Accessing trace context manually

from starlette_gcp_logging import formatter

# Inside a request handler:
print(formatter.request_trace.get())  # "projects/my-project/traces/abc123..."
print(formatter.request_span.get())  # "00f067aa0ba902b7"

Project ID auto-detection

from starlette_gcp_logging import _metadata

project = _metadata.get_project_id()  # fetches from metadata server, then cached

Outside GCP (local dev, CI) the metadata request times out after 1 second and an empty string is returned — logging continues to work, just without the project prefix in the trace resource name.

Example GCP alerts grouped by Starlette route

The middleware writes the route template to:

  • jsonPayload."logging.googleapis.com/labels"."starlette.dev/route"

Use that field as a log-based metric label so alerts are grouped per route template (for example, "/items/{item_id}").

1) 4xx error-rate alert by route

Create two log-based counter metrics with the same label extractor:

  • logging.googleapis.com/user/starlette_requests_total
  • logging.googleapis.com/user/starlette_requests_4xx

Filter / extractor examples:

# total requests
resource.type="cloud_run_revision"
jsonPayload.httpRequest.status>=100
jsonPayload.httpRequest.status<600
jsonPayload."logging.googleapis.com/labels"."starlette.dev/route"!=""

labelExtractors.route=EXTRACT(jsonPayload."logging.googleapis.com/labels"."starlette.dev/route")
# 4xx requests
resource.type="cloud_run_revision"
jsonPayload.httpRequest.status>=400
jsonPayload.httpRequest.status<500
jsonPayload."logging.googleapis.com/labels"."starlette.dev/route"!=""

labelExtractors.route=EXTRACT(jsonPayload."logging.googleapis.com/labels"."starlette.dev/route")

Alerting condition (MQL example, threshold 5% over 5m):

fetch logging.googleapis.com/user/starlette_requests_4xx
| align rate(5m)
| group_by [metric.route], [err: sum(value.rate)]
| join
    (fetch logging.googleapis.com/user/starlette_requests_total
     | align rate(5m)
     | group_by [metric.route], [all: sum(value.rate)])
| value [error_rate: err / all]
| condition error_rate > 0.05 '1'

2) 5xx error-rate alert by route

Create a logging.googleapis.com/user/starlette_requests_5xx counter metric:

resource.type="cloud_run_revision"
jsonPayload.httpRequest.status>=500
jsonPayload.httpRequest.status<600
jsonPayload."logging.googleapis.com/labels"."starlette.dev/route"!=""

labelExtractors.route=EXTRACT(jsonPayload."logging.googleapis.com/labels"."starlette.dev/route")

Alerting condition (MQL example, threshold 1% over 5m):

fetch logging.googleapis.com/user/starlette_requests_5xx
| align rate(5m)
| group_by [metric.route], [err: sum(value.rate)]
| join
    (fetch logging.googleapis.com/user/starlette_requests_total
     | align rate(5m)
     | group_by [metric.route], [all: sum(value.rate)])
| value [error_rate: err / all]
| condition error_rate > 0.01 '1'

3) Successful-request latency alert by route

Create a log-based distribution metric logging.googleapis.com/user/starlette_success_latency_seconds:

resource.type="cloud_run_revision"
jsonPayload.httpRequest.status>=200
jsonPayload.httpRequest.status<400
jsonPayload.httpRequest.latency=~".*s"
jsonPayload."logging.googleapis.com/labels"."starlette.dev/route"!=""

labelExtractors.route=EXTRACT(jsonPayload."logging.googleapis.com/labels"."starlette.dev/route")
valueExtractor=REGEXP_EXTRACT(jsonPayload.httpRequest.latency, "^([0-9.]+)s$")

Alerting condition (MQL example, p95 latency > 1s over 5m):

fetch logging.googleapis.com/user/starlette_success_latency_seconds
| group_by 5m, [metric.route], [p95: percentile(value.distribution, 95)]
| condition p95 > 1

Metadata

Release files for starlette-gcp-logging 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for starlette-gcp-logging 0.0.2
File Interpreter ABI Platform Actions
starlette_gcp_logging-0.0.2-py3-none-any.whl Python 3 none any Details for starlette_gcp_logging-0.0.2-py3-none-any.whl

Release files / starlette_gcp_logging-0.0.2-py3-none-any.whl

Download URL starlette_gcp_logging-0.0.2-py3-none-any.whl
Size 13.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
37867038b788ab63325a551c327fb5c3967bf772987553249be2f7a60b4b44b7
BLAKE2b-256 checksum
How to use checksums
9948fa393b9ac2f44a24c2bb83a9751d44d86f73c90e9e6c1baa2dcc9ae32f01
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

Security for starlette-gcp-logging 0.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

1 file in this release has been uploaded with provenance metadata.

Attestation for starlette_gcp_logging-0.0.2-py3-none-any.whl

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page