Skip to content

Add replay-safe OpenTelemetry meter and logger providers; warn in Google ADK plugin - #1710

Draft
DABH wants to merge 15 commits into
mainfrom
google-adk-replay-safe-metrics
Draft

Add replay-safe OpenTelemetry meter and logger providers; warn in Google ADK plugin#1710
DABH wants to merge 15 commits into
mainfrom
google-adk-replay-safe-metrics

Conversation

@DABH

@DABH DABH commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What was changed

  • New temporalio.contrib.opentelemetry.ReplaySafeMeterProvider, mirroring the existing ReplaySafeTracerProvider: wraps a user-supplied MeterProvider and drops synchronous instrument recordings (add()/record()/set()) made from workflow code while replaying (workflow.in_workflow() and workflow.unsafe.is_replaying()). Observable instruments and all non-workflow recordings pass through untouched.
  • New temporalio.contrib.opentelemetry.ReplaySafeLoggerProvider, the same gate for the OTel logs bridge: drops Logger.emit() calls made from workflow code while replaying, so ADK's gen_ai.* log events (gen_ai.choice, gen_ai.user.message, ...) are not re-emitted on every replay. opentelemetry._logs is the import path OpenTelemetry itself documents for the logs bridge API while it is pre-GA.
  • ReplaySafeTracerProvider is now also exported from temporalio.contrib.opentelemetry (previously importable only from the private _tracer_provider module), completing the replay-safe provider trio alongside the new meter and logger providers.
  • GoogleAdkPlugin now warns at worker and replayer configuration when the global OTel meter or tracer provider is positively identified as replay-unsafe, i.e. is an instance of the public OpenTelemetry SDK provider classes (opentelemetry.sdk.metrics.MeterProvider / opentelemetry.sdk.trace.TracerProvider). Everything else — unset proxies, no-ops, unknown custom wrappers — stays silent, since a false positive is worse than a missed warning. The SDK logger provider is deliberately not checked: its class is only importable from the underscore namespace opentelemetry.sdk._logs, so there is no public type to test against yet. Warnings carry actionable install snippets and are attributed (via stacklevel) to the user's Worker(...)/Replayer(...) call.
  • The opentelemetry and lambda-worker-otel extras' floors are raised from opentelemetry-api/sdk >= 1.11.1 to >= 1.24. This aligns the declarations with reality: temporalio.contrib.opentelemetry already imports opentelemetry.util._decorator._agnosticcontextmanager (added in opentelemetry-api 1.24) on main, so the declared 1.11.1 floor has been unimportable in practice for this contrib all along (install succeeds; importing the contrib fails) (and temporalio.contrib.aws.lambda_worker.otel builds on it). With the honest floor, _Gauge (OTel's canonical exported name for the spec-experimental synchronous gauge, exported since 1.23) and the logs API import unconditionally — no lazy __getattr__ exports or guarded imports. The remaining version accommodations are forwarding newer optional kwargs (get_meter/get_logger attributes, 1.26; synchronous-instrument context, 1.28; create_histogram explicit_bucket_boundaries_advisory, 1.30) only when the caller sets them, and forwarding Logger.emit() arguments verbatim because its signature changed shape within the supported range (a single positional LogRecord through 1.37, keyword fields from 1.38).
  • Regression tests: one real execution of an ADK agent workflow (scripted BaseLlm with usage_metadata) + 3 replays via Replayer. With ReplaySafeMeterProvider, every gcp.vertex.agent instrument stays at its nonzero baseline; with ReplaySafeLoggerProvider, every gen_ai.* log event stays at its baseline; controls document the unfixed 4× inflation for both.
  • Unit tests for both providers (per-instrument/emit passthrough outside workflows, observable passthrough, delegation, OTel API version compatibility for the conditional kwargs and pre-1.38 emit signatures) and the plugin warning path (warns on SDK providers, silent on unset, replay-safe-wrapped, and unknown custom providers, stacklevel attribution through wrapping plugins).
  • README updates for both contrib/google_adk_agents and contrib/opentelemetry; CHANGELOG entries.

Why

google-adk records its metrics (gen_ai.client.token.usage, gen_ai.client.operation.duration, gen_ai.invoke_agent.*, gen_ai.execute_tool.duration) and emits its gen_ai.* log events through the process-global OTel providers, from flow code that the ADK plugin runs workflow-side (model/tool calls are activities, but the instrumentation wrapping them is not). Workflow code re-executes on every replay — cache eviction, worker restart, redeploy, max_cached_workflows=0 — so each replay re-records every measurement and re-emits every log event even though the activities resolve from history. Measured: 1 real execution + 3 replays → exactly 4× on every instrument (gen_ai.client.token.usage 4→16 observations, etc.) and 4 copies of every log event, while real activity executions stayed at 1. Replays also record near-zero bogus latency samples since activity results resolve instantly from history.

Temporal's workflow.metric_meter() is already replay-safe, but ADK bypasses it via the OTel global API; this provides the equivalent gate at the provider boundary, which fixes the entire supported google-adk range without upstream changes.

Recordings and emissions become first-execution-only, matching workflow.metric_meter() semantics.

Testing

uv run pytest tests/contrib/google_adk_agents tests/contrib/opentelemetry → 60 passed, 5 skipped (pre-existing skips: API-key-gated or marked skip-in-CI). poe lint (ruff check/format, pyright, mypy, basedpyright, pydocstyle) and poe gen-docs clean.

DABH added 3 commits July 31, 2026 15:11
Google ADK and similar libraries record OpenTelemetry metrics through
the process-global meter provider from code that runs workflow-side, so
every workflow replay re-records them. ReplaySafeMeterProvider wraps a
user-supplied MeterProvider and drops synchronous instrument recordings
made from workflow code during replay, matching the first-execution-only
semantics of workflow.metric_meter(). Observable instruments and
non-workflow recordings pass through untouched.
At worker configuration, warn when the global OpenTelemetry meter or
tracer provider is not replay-safe, pointing users at
ReplaySafeMeterProvider and create_tracer_provider. Add a regression
test proving 1 real execution + 3 replays leaves ADK metric instruments
at their nonzero baseline with ReplaySafeMeterProvider installed, plus a
control asserting the 4x inflation without it.
ReplaySafeMeterProvider unconditionally forwarded get_meter attributes
(added in opentelemetry 1.26) and create_histogram
explicit_bucket_boundaries_advisory (added in 1.30), raising TypeError
into caller code on older APIs within the supported range. Forward them
only when non-None, matching unwrapped-caller behavior.
@DABH
DABH requested review from a team as code owners July 31, 2026 20:35
@DABH
DABH requested a review from Copilot July 31, 2026 21:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds replay-safe OpenTelemetry metrics support for workflow-side instrumentation by introducing a ReplaySafeMeterProvider wrapper and integrating replay-safety warnings into the Google ADK plugin, to prevent metric inflation during Temporal workflow replays.

Changes:

  • Added temporalio.contrib.opentelemetry.ReplaySafeMeterProvider to drop synchronous metric recordings from workflow code during replay.
  • Updated GoogleAdkPlugin to warn at worker configuration time when global OTel meter/tracer providers are not replay-safe.
  • Added regression + unit tests, and updated READMEs and CHANGELOG to document the behavior and configuration.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/contrib/opentelemetry/test_meter_provider.py Unit tests for the replay-safe meter provider passthrough and compatibility behavior.
tests/contrib/google_adk_agents/test_replay_metrics.py Regression test demonstrating replay metric inflation without the wrapper and correctness with it.
tests/conftest.py Adds a fixture to reset global OTel meter provider state across tests.
temporalio/contrib/opentelemetry/README.md Documents replay-safe metrics and how to install the global wrapper.
temporalio/contrib/opentelemetry/_meter_provider.py Implements ReplaySafeMeterProvider and replay-gating wrappers for sync instruments.
temporalio/contrib/opentelemetry/init.py Exports ReplaySafeMeterProvider and ReplaySafeTracerProvider.
temporalio/contrib/google_adk_agents/README.md Documents replay behavior for ADK telemetry and recommended replay-safe global providers.
temporalio/contrib/google_adk_agents/_plugin.py Adds worker-config-time warnings for non-replay-safe global OTel providers.
CHANGELOG.md Adds an entry describing the new provider and plugin warning behavior.
Suppressed comments (1)

temporalio/contrib/google_adk_agents/_plugin.py:69

  • Add stacklevel (and ideally category) to this warning so it points at the user’s worker/client setup callsite instead of inside the plugin implementation. This makes the warning much easier to act on when it fires during worker configuration.
        warnings.warn(
            "The global OpenTelemetry TracerProvider is not replay-safe: Google ADK "
            "creates spans from workflow code, so every workflow replay will "
            "re-emit them. Install a replay-safe provider: "
            "opentelemetry.trace.set_tracer_provider("
            "temporalio.contrib.opentelemetry.create_tracer_provider())"
        )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread temporalio/contrib/google_adk_agents/_plugin.py Outdated
Comment thread temporalio/contrib/opentelemetry/_meter_provider.py
Comment thread temporalio/contrib/google_adk_agents/_plugin.py
- Guard the private _Gauge import (added in opentelemetry-api 1.23) and
  import ReplaySafeMeterProvider into the package lazily so
  temporalio.contrib.opentelemetry stays importable for tracing-only
  users on opentelemetry-api < 1.12, with an actionable error on access.
- Classify the global meter provider without a module-level private
  import; when the private proxy class cannot be imported the provider
  is unclassifiable and no warning is issued.
- Forward the context argument to sync instrument wrappers only when
  set; the parameter was added in opentelemetry-api 1.28 and older
  instruments reject it.
- Add explicit UserWarning category and stacklevel so provider warnings
  point at the user's Worker(...) call.
DABH added 2 commits August 1, 2026 03:27
ADK emits gen_ai.* log events from workflow code through the global
logger provider, so replays duplicate them like metrics and spans.
ReplaySafeLoggerProvider wraps a LoggerProvider and drops Logger.emit
during replay. The logs API only exists in opentelemetry-api >= 1.15,
so the import is guarded like the meter provider's; emit forwards
arguments verbatim since its signature changed in 1.38.
Validate the global logger provider alongside meter and tracer, and run
the validation from configure_replayer too since Replayer replays are
exactly where unsafe providers re-record telemetry. Warn only on
providers positively identified as replay-unsafe (OTel SDK providers
used directly) so custom wrappers around replay-safe providers no
longer trigger false positives, and compute the warning stacklevel
dynamically so attribution survives wrapping plugins. Add log-event
replay regression tests mirroring the metrics ones.
@DABH
DABH force-pushed the google-adk-replay-safe-metrics branch from 62120b6 to 891b68b Compare August 1, 2026 09:02
@DABH
DABH marked this pull request as draft August 3, 2026 17:00
DABH added 9 commits August 4, 2026 14:15
The de facto floor of temporalio.contrib.opentelemetry is already 1.24:
_tracer_provider.py imports opentelemetry.util._decorator's
_agnosticcontextmanager, which was added in opentelemetry-api 1.24, so the
declared 1.11.1 floor has been uninstallable in practice for this contrib
regardless of the new providers. Aligning the declaration with reality lets
the replay-safe providers import _Gauge (exported since 1.23) and the logs
API unconditionally, removing the guarded-import machinery in the package
__init__ and the subprocess-based import-absence tests. The conditional
kwarg forwards stay: get_meter/get_logger attributes arrived in 1.26,
synchronous-instrument context in 1.28, and create_histogram's
explicit_bucket_boundaries_advisory in 1.30, all above the new floor.
Logger is an ABC whose __init__ records the instrumentation scope, so the
wrapper now threads name/version/schema_url from get_logger through to the
base class instead of relying on __getattr__ delegation for that state.
Fixes the basedpyright reportMissingSuperCall error that failed lint. Also
documents that opentelemetry._logs is the import path OpenTelemetry itself
sanctions for the logs bridge API while it is pre-GA.
The plugin now warns only when the global meter or tracer provider is an
instance of the public OpenTelemetry SDK provider classes; everything else
(unset proxies, no-ops, unknown wrappers) stays silent, which drops the
private _ProxyMeterProvider/ProxyLoggerProvider imports. The SDK logger
provider is not checked because its class is only importable from the
underscore namespace opentelemetry.sdk._logs while OTel logs are pre-GA.
The opentelemetry.sdk import is guarded for the SDK-less install case, the
warning stacklevel walk uses inspect.currentframe, and the conftest OTel
global resets are documented as the isolation pattern OpenTelemetry's own
test suite uses.
@DABH DABH changed the title Add replay-safe OpenTelemetry meter provider; warn in Google ADK plugin Add replay-safe OpenTelemetry meter and logger providers; warn in Google ADK plugin Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants