Skip to content

Bounded and asynchronous log emission: cap Google write latency, add a background emitter, and agent-native stdout format #22

Description

@anth-volk

Problem

The google_cloud_logging destination emits synchronously on the caller's request path via Logger.log_struct, and the underlying WriteLogEntries call carries google-cloud-logging's defaults: a 60-second retry deadline and 60-second timeout. Consequences for consumers (diagnosed in the policyengine-household-api, which promises customers ≤30s full runtimes — see PolicyEngine/policyengine-household-api#1596/#1597):

  • Healthy path: every request pays 2–3 synchronous Logging API round-trips (~15–60ms).
  • Degraded path: during a Cloud Logging incident, each emission can block up to 60s before it counts as a strike against the 3-strike destination breaker — so the first requests of each fresh process can stall 60–180s, violating downstream latency promises even though the fail-open design (added in Disable a log destination after repeated consecutive emit failures #21) prevents outright failure.

Standard practice separates the synchronous log call (capture context, enqueue — microseconds, cannot block or raise) from asynchronous emission (network I/O off the caller's thread, batched, bounded, best-effort). Python stdlib ships QueueHandler/QueueListener for exactly this, and google-cloud-logging's own CloudLoggingHandler defaults to BackgroundThreadTransport — though its queue is unbounded (queue.Queue(0)), which is why this issue proposes a bounded implementation instead of adopting it.

Proposal (one feature branch, gated commits)

  1. Bounded Google writes (safety floor, default behavior change 60s → 2s): replace log_struct with hand-built entries written via the gapic layer with retry=None, timeout=<config, default 2.0s> (OBSERVABILITY_GOOGLE_LOG_TIMEOUT_SECONDS), keeping client.logger() for full_name/default_resource, with a fail-open fallback to plain write_entries on non-gapic transports. Also exposes emit_batch (one bounded call for N entries).
  2. Agent-native structured stdout (OBSERVABILITY_STDOUT_FORMAT=google): map payloads onto the special keys Cloud Run's log agent promotes natively (severity, time, logging.googleapis.com/trace|spanId|labels), enabling full-fidelity ingestion with zero in-process network emission on platforms with an agent.
  3. BackgroundEmitDestination: bounded deque (default 1000, drop-newest with counted drops), lazy daemon worker (pid-aware for fork safety), batch drain preferring emit_batch, consecutive-batch-failure trip that flows through the existing manager breaker/stdout-fallback path unchanged, flush(deadline)/close()/restart() lifecycle with lazy atexit registration.
  4. Config + wiring + facade: OBSERVABILITY_LOG_EMIT_MODE=sync|async (default sync), queue/batch/flush knobs; manager wraps non-stdout destinations in async mode (stdout is the fallback and is never wrapped); runtime.shutdown() flushes log destinations; public flush_observability()/restart_observability() (the latter required by consumers whose runtimes restore from memory snapshots — Modal — since threads do not survive snapshot restore).
  5. Docs: README env-var documentation, loss semantics, snapshot/fork consumer guidance, runbook kill-switches (OBSERVABILITY_LOG_DESTINATIONS=stdout, OBSERVABILITY_LOG_EMIT_MODE=sync).

Compatibility contract

Everything defaults off (sync, plain): existing consumers see no behavior change except the bounded 2s write timeout, which is strictly safer. All existing fail-open invariants from #21 (per-destination guards, DESTINATION_FAILURE_LIMIT, stdout fallback) are preserved and covered by the new tests.

Consumer adoption (platform-split defaults, Modal snapshot hook, GCP Log Router sink) happens separately in policyengine-household-api after this releases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions