Feat: export GCP metrics via OTLP to telemetry.googleapis.com instead of the Cloud Monitoring exporter - #395
Open
AmaadMartin wants to merge 8 commits into
Open
Feat: export GCP metrics via OTLP to telemetry.googleapis.com instead of the Cloud Monitoring exporter#395AmaadMartin wants to merge 8 commits into
AmaadMartin wants to merge 8 commits into
Conversation
added 3 commits
July 31, 2026 08:32
Replaces @google-cloud/opentelemetry-cloud-monitoring-exporter with the gRPC OTLP metric exporter pointed at the Google Cloud Telemetry API, matching adk-python's _get_gcp_otlp_metric_exporter. The Cloud Monitoring exporter hard-depends on googleapis (110.6 MB unpacked), which every @google/adk consumer downloaded whether or not they ever enabled Cloud Monitoring. Dropping it removes googleapis and googleapis-common outright and takes the nested google-auth-library v9 copies from six to three.
Adds eight cases covering the Telemetry API endpoint, the 5s periodic reader, the metrics-disabled path, ADC metadata minting, both credential failure shapes, degradation when no auth client is available, and the absence of credential I/O at construction time. The metadata cases drive the real gRPC CallCredentials returned by combineChannelCredentials, so they fail on empty metadata rather than on the choice of factory function. Existing cases keep their assertions; the shared auth double only gains a getClient stub, which the metrics path now needs.
Stubs only Application Default Credentials, so the real @opentelemetry/exporter-metrics-otlp-grpc package and the real PeriodicExportingMetricReader have to compose. Shuts the reader down in a finally so its 5s export timer cannot outlive the test.
AmaadMartin
force-pushed
the
feat/otlp-gcp-metric-exporter
branch
from
July 31, 2026 15:34
e56bd3c to
ae6b57b
Compare
added 5 commits
July 31, 2026 12:20
The Telemetry API takes the destination project as the gcp.project_id resource attribute, not as an exporter argument, so swapping the Cloud Monitoring exporter (which was handed projectId directly) for OTLP left the exported payload with nothing to route on. getGcpResource() only ran the GCP detector, which yields cloud.account.id on GCE and nothing at all off Google Cloud -- so `adk web --otel_to_cloud` on a workstation would have retried a rejected export every five seconds in silence. getGcpResource now takes the project id, matching adk-python's get_gcp_resource(project_id=None), and the dev CLI passes the value resolved from Application Default Credentials. Two further attributes Managed Service for Prometheus documents as required and rejects points without: - serviceInstanceIdDetector supplies service.instance.id, which backs the `instance` label and has no other source off Google Cloud. - envDetector makes OTEL_RESOURCE_ATTRIBUTES reach the payload at all, which is the only way to supply `location` off Google Cloud and the documented override adk-python gets from OTELResourceDetector.
setupTelemetry(true) is the only production caller of getGcpResource, and nothing covered it: dropping the project id argument there still compiled and still passed every suite. These cases assert the project resolved from ADC reaches getGcpResource and that the resulting resource reaches maybeSetOtelProviders, and that the cloud path is skipped entirely when --otel_to_cloud is off.
resolveProjectId and getGcpProjectId were the same function written twice, differing only in who owned the GoogleAuth. Collapse them into one exported function with a defaulted auth parameter, which also gives callers an injection point for non-default credentials. The getGcpResource assertion checked detector arity, not order, so it passed for any three detectors in any arrangement -- it could not catch a regression in the precedence its doc comment promises. Both resource modules are automocked, so the test and the source resolve the same detector objects and identity comparison pins the order.
The defaulted auth parameter is public API, and nothing caught a version that ignored it: the existing cases mock the GoogleAuth constructor, so building a fresh instance inside the function is indistinguishable from using the argument. Spying on one instance makes the two resolvable projects differ, so an ignored argument fails.
getGcpAuthClient was nine lines whose whole body converted a rejection to undefined for a single caller. `auth.getClient().catch(() => undefined)` infers the same AuthClient | undefined, so the narrowing, the warning and the degrade-but-keep-tracing contract are untouched, and it still catches only getClient() -- a throw from createGcpMetricReader cannot be misreported as a credentials failure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
Problem:
@google/adkdepends on@google-cloud/opentelemetry-cloud-monitoring-exporter@^0.21.0for GCP metric export. That package itself is tiny (npm view @google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0 dist.unpackedSize→53133, 53 KB) but it hard-depends ongoogleapis: "^137.0.0", which is 110.6 MB unpacked (npm view googleapis@137.1.0 dist.unpackedSize→110624690). Its compiledbuild/src/monitoring.jsreaches in withrequire("googleapis/build/src/apis/monitoring"). Every consumer of@google/adkdownloaded that subtree whether or not they ever constructed a Cloud Monitoring exporter — the exporter is only reachable throughgetGcpExporters({enableMetrics: true}), i.e. the--otel_to_cloudCLI flag, which defaults tofalse(dev/src/cli/cli.ts:130-133).In the lockfile, that exporter was the only dependent of
googleapis, andgoogleapisthe only dependent ofgoogleapis-common; between them they carried three of the six nestedgoogle-auth-library@9.15.1copies.Solution: Export GCP metrics with a direct gRPC OTLP exporter pointed at the Google Cloud Telemetry API (
https://telemetry.googleapis.com) instead, and drop the Cloud Monitoring exporter.This is a cross-language parity change.
adk-pythonalready made OTLP-to-telemetry.googleapis.comits default GCP metric exporter:src/google/adk/telemetry/google_cloud.pydefines_get_gcp_otlp_metric_exporter, whose docstring reads "Returns a raw OTLP push metric exporter to telemetry.googleapis.com. This is the default GCP metric exporter (over Cloud Monitoring)." It targets_DEFAULT_TELEMETRY_METRICS_ENDPOINT = "https://telemetry.googleapis.com/v1/metrics"and wraps it in aPeriodicExportingMetricReaderatMIN_EXPORT_INTERVAL_MS = 5000.0(_agent_engine_metric_exporter.py:162). Verified by reading those files atgoogle/adk-pythonmain, not inferred from the task description.adk-jswas the outlier.Concretely:
core/src/telemetry/google_cloud.tsbuildsnew OTLPMetricExporter({url, credentials})from@opentelemetry/exporter-metrics-otlp-grpcinside the existingPeriodicExportingMetricReader, with two new module-private constants (TELEMETRY_ENDPOINT,METRIC_EXPORT_INTERVAL_MS; the latter replaces the inline5000and keeps parity withMIN_EXPORT_INTERVAL_MS).core/package.jsondrops@google-cloud/opentelemetry-cloud-monitoring-exporterand adds@opentelemetry/exporter-metrics-otlp-grpc@^0.205.0(28.7 KB unpacked, matching the0.205.0line the repo already pins for the other OTLP exporters) plus@grpc/grpc-js@^1.14.4, which becomes a direct dependency because the new code importscredentialsandMetadatafrom it rather than relying on it transitively.getGcpResourcenow takes the destination project and adds two resource detectors — see Resource identity below, which is the non-obvious half of this change.core/src/telemetry/setup.tsand the exportedgetGcpExporters/OTelHookscontract are unchanged.@opentelemetry/exporter-metrics-otlp-httpstays —setup.tsstill uses it for the genericOTEL_EXPORTER_OTLP_*env-var path.Resource identity: why this is not just an exporter swap
The Cloud Monitoring exporter was constructed as
new MetricExporter({projectId})— the destination project was an exporter argument. OTLP has no such argument: the Telemetry API takes the destination project as thegcp.project_idresource attribute on the exported payload. The collector config Google publishes at stackdriver/docs/otlp-metrics/deploy-collector says so outright: "When sending telemetry to the GCP OTLP endpoint, thegcp.project_idresource attribute is required to be set to your project ID." All three official SDK samples set it viaOTEL_RESOURCE_ATTRIBUTESin their run instructions.Swapping the exporter alone would therefore have shipped a silent regression.
getGcpResource()ran only the GCP detector, which yieldscloud.account.idon GCE and nothing at all off Google Cloud — soadk web --otel_to_cloudon a workstation, the primary dev workflow, would have retried a rejected export every five seconds with no error surfaced. I confirmed the empty payload against a realMeterProviderbefore fixing it.Three changes close it, all in
getGcpResource:getGcpResource(projectId?: string)mergesgcp.project_id. Additive, and the exact signatureadk-pythonuses (get_gcp_resource(project_id: Optional[str] = None),src/google/adk/telemetry/google_cloud.py:228, setting"gcp.project_id": project_idat lines 241/265).adk-jspreviously took no project at all — an asymmetry that was harmless while metrics went to theTimeSeriesAPI and is not harmless now.envDetector, soOTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTESreach the payload. Before this they did not:maybeSetOtelProvidersuses the passed resource verbatim, so a user could not supplylocationfrom the environment even as a workaround.adk-pythongets this fromOTELResourceDetector, and the detector ordering here reproduces its documented precedence — agcp.project_idinOTEL_RESOURCE_ATTRIBUTESoverrides the argument.serviceInstanceIdDetector, supplyingservice.instance.id. Per stackdriver/docs/reference/telemetry/v1.metrics, OTLP metrics land on theprometheus_targetmonitored resource, wherelocationandinstanceare both required and each documented "Reject the point if empty."instanceis sourced frominstance→service.instance.id→faas.instance→k8s.pod.name→host.id; off Google Cloud none of those exist, so without this the point is rejected.Parity deviation, stated explicitly (per the "know which side wins" rule):
service.instance.idis observable on the wire, where parity normally wins, andadk-pythonsets it only on its Agent Engine path. I add it unconditionally anyway, because it is a documented-required label with no other source off Google Cloud, and it stays overridable viaOTEL_RESOURCE_ATTRIBUTES. Everything else here followsadk-python.Known remaining gap, not silently reduced: off Google Cloud,
locationstill has to come fromOTEL_RESOURCE_ATTRIBUTES=location=.... No detector can infer a location off-platform, and inventing one (global) is not something I can ground in the docs — so this change makes it supplyable, which it previously was not, and documents it ongetGcpResource. On GCE/GKE/Cloud Run the GCP detector fillscloud.region/cloud.availability_zoneand nothing extra is needed.getGcpProjectId()is newly exported so the resource can actually be built with a project. It is the same ADC lookupgetGcpExportersalready performed internally; without it the newprojectIdparameter would be dead config, since neither the dev CLI nor a library user has another way to obtain the value.dev/src/utils/telemetry_utils.tsresolves it concurrently with the exporters (Promise.all) and passes it togetGcpResource.Measured effect (clean
rm -rf node_modules && npm installbefore and after, same machine):du -sh node_modulesnpm ls googleapisgoogleapis@137.1.0via the Cloud Monitoring exporter(empty)google-auth-library@9copies in the lockfilegoogleapis,googleapis-common, and their transitive@google-cloud/precise-date/url-template/ nesteduuid@9rows are gone frompackage-lock.json. The lockfile was regenerated withnpm installand is a fixed point (a second install leaves it untouched); no version fields were hand-edited. EveryresolvedURL in the six new entries points athttps://registry.npmjs.org, and each tarball was fetched anonymously from there and checked against the lockfileintegrityhash, so the lockfile installs in CI with no registry credentials.Why gRPC and not the HTTP exporter already in the dependency list
@opentelemetry/exporter-metrics-otlp-httpis already a dependency, so it looks like the cheaper option, but its publicheadersoption is typedRecord<string, string>— a static header map. The OAuth token would be frozen at construction and every export would fail once it expired. Google's own guidance says the same thing: "due to the lack of support for dynamic token refreshing in most SDK exporters, we recommend using only the gRPC OTLP exporter, not the HTTP exporters, when exporting directly from SDKs" (Google Cloud Observability, OTLP metric ingestion overview, "Protocol support").adk-pythoncan use the HTTP exporter becausegoogle.auth.transport.requests.AuthorizedSessionrefreshes for it; the JS HTTP exporter has no equivalent hook.Parity conflict resolution: the observable wire behaviour (destination host, 5000 ms interval, degrade-don't-throw semantics, the byte-identical "Cannot determine GCP Project…" warning) follows
adk-python. The transport (gRPC here, HTTP there) and all in-process conventions follow local JS/TS reality, because the JS HTTP exporter cannot refresh a token. The endpoint string also differs by necessity: gRPC targets a host (https://telemetry.googleapis.com, normalised by the exporter totelemetry.googleapis.comand dialled on the gRPC default port 443), where the Python HTTP path needs the/v1/metricssignal path.The
google-auth-libraryv10HeaderspitfallGoogle's published Node.js sample uses
credentials.createFromGoogleCredential(authClient). That call is broken against thegoogle-auth-librarymajor this repo pins, and silently so.google-auth-library@10.7.0declaresgetRequestHeaders(url?: string | URL): Promise<Headers>— a WHATWGHeadersinstance (build/src/auth/authclient.d.ts:145) — while@grpc/grpc-js@1.14.4'screateFromGoogleCredentialbuilds its metadata withfor (const key of Object.keys(headers))(build/src/call-credentials.js:68) and types its client as returningPromise<{[index: string]: string}>.Object.keys(new Headers(...))is[], so that path produces empty gRPC metadata and unauthenticated export requests — no crash, no log, just rejected exports.This change therefore bridges with
credentials.createFromMetadataGenerator+Headers.forEach, and there is awhycomment at that call site naming the mismatch.it('mints gRPC metadata from ADC request headers')pins it (see the mutation table below).Reviewed alternatives, and why they were not taken
@google-cloud/monitoring— rejected. It is a GAPIC client for the Cloud Monitoring v3 API, not an OpenTelemetryPushMetricExporter; adopting it means re-implementing the OTel→TimeSeriestranslation layer (metric-descriptor creation, monitored-resource mapping, cumulative/delta handling, histogram conversion, label sanitisation, batching). It also pullsgoogle-gax@5, which pinsgoogle-auth-libraryto an exact10.5.0and would add a new nested copy alongside the repo's10.7.0.adk-jsexporting to a different backend thanadk-python, entrenching the parity gap this change exists to close. See the collision note below.Breaking change
This is a user-visible behaviour change for anyone running with
--otel_to_cloud(or callinggetGcpExporters({enableMetrics: true})directly). No API is renamed or removed, so no deprecation shim is required.workload.googleapis.com/prefix. They now go through the Telemetry API and are stored in Managed Service for Prometheus format. Dashboards, alerting policies and SLOs built on the old names must be repointed. Querying a UTF-8 name in PromQL needs brace-and-quote syntax, e.g.{"gen_ai.client.token.usage"}.gcloud services enable telemetry.googleapis.com.adk-pythonalready makes it the default GCP metric exporter.--otel_to_clouddefaults tofalse, and no ADK documentation names the Cloud Monitoring destination (README.md,CONTRIBUTING.mdanddocs/do not mention it), so no docs are invalidated.Error handling is unchanged in spirit and slightly more forgiving: if ADC cannot produce a client while
enableMetricsis true, metrics are disabled with alogger.warnand tracing is left intact — mirroringadk-python's_get_gcp_metrics_exporter, which returnsNonerather than raising. A metrics setup failure must never take down tracing.Why the eager
getClient()probe is keptGoogleAuthexposes the samegetRequestHeaders(url?): Promise<Headers>signature asAuthClient(google-auth-library@10.7.0,build/src/auth/googleauth.d.ts:470), soauthcould be handed straight to the credential builder and thegetClient()probe dropped entirely. That would also deleteGCP_CREDENTIALS_ERROR_MESSAGEand themetricReadersaccumulator, collapsing back to a one-line ternary. It is deliberately not done, for three reasons:disables metrics but keeps tracing when the auth client is unavailable. Removing the probe deletes that test along with the behaviour.adk web --otel_to_cloudwithGOOGLE_CLOUD_PROJECTset but no ADC is exactly that case.The probe is one line —
await auth.getClient().catch(() => undefined). The.catchis scoped togetClient()rather than wrapping the block, so a throw from anywhere else in the metrics branch can never be misreported as a credentials failure.Aggregation temporality
The replaced Cloud Monitoring exporter implemented its own
selectAggregationTemporality; nothing in this diff sets one, so the generic exporter's default applies. Verified rather than assumed:chooseTemporalitySelectorFromEnvironmentin@opentelemetry/exporter-metrics-otlp-http@0.205.0(build/src/OTLPMetricExporterBase.js:54-56) defaults tocumulativewhenOTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCEis unset. Cumulative is what Prometheus-format ingestion expects — Google's OTLP overview warns the other way round, that "Delta metrics might not query properly in certain circumstances" — and it matchesadk-python, which likewise constructsOTLPMetricExporterwith no temporality preference. No override is needed, and adding one would be a parity deviation.Collision check
gh pr list --repo AmaadMartin/adk-js --state open --limit 100plusgh pr diff --name-onlyon the plausibly adjacent PRs. Findings:core/package.json,core/src/telemetry/google_cloud.ts,core/test/telemetry/google_cloud_test.tsandpackage-lock.json. It does not land this change: it keepsMetricExporterfrom@google-cloud/opentelemetry-cloud-monitoring-exporterand lazy-loads it, sogoogleapisstays in the tree for anyone who opts in, and theadk-pythonparity gap remains. It is an alternative design for the same file rather than a prerequisite, so this PR branches frommainrather than stacking on it — stacking would make an approach this change supersedes a dependency of it, and would make each PR unportable without the other. Whichever merges second will need a small rebase ingoogle_cloud.tsandcore/package.json.docs/dependency-duplication.md+ agoogle_auth_library_duplication_test) and Fix: stop shipping a second google-auth-library copy under @google-cloud/vertexai #295 (google_auth_library_dedupe_test) touch dependency-duplication docs/tests only. Neither is in this diff; if Fix: document why google-auth-library v9 stays duplicated, and pin the allowlist #336 merges, its allowlist will need updating because this change takes the nestedgoogle-auth-library@9count from six to three.No competing implementation of the OTLP metric exporter is open.
Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
core/test/telemetry/google_cloud_test.ts— the four parametrisedgetGcpExporterscases and the two project-id-failure cases keep their bodies and assertions. No existing test was deleted, skipped, or weakened. Three existing lines did have to change, all disclosed here:vi.mock('@google-cloud/opentelemetry-cloud-monitoring-exporter')becamevi.mock('@opentelemetry/exporter-metrics-otlp-grpc')— the old specifier no longer resolves.getClientstub, because the metrics path now resolves an ADC client.should detect GCP resources using gcpDetectorasserteddetectors: [expect.any(Object)], pinning "exactly one detector" — behaviour this change deliberately alters. It now assertsdetectors: [serviceInstanceIdDetector, envDetector, gcpDetector]by identity, so it pins the order thegetGcpResourcedoc comment promises rather than only the arity. An arity-only form would pass for any three detectors in any arrangement; the identity form fails on either transposition (both are in the mutation table below).Eight new cases were appended in a
getGcpExporters OTLP metric exportblock. The metadata cases deliberately drive the real gRPCCallCredentialscaptured fromcredentials.combineChannelCredentialsand call its publicgenerateMetadata, rather than asserting which factory function was used — so they fail on empty metadata, which is the actual defect.Every new test was run against mutated source and observed to fail:
core/src/telemetry/google_cloud.tsTELEMETRY_ENDPOINT→'https://example.invalid'expected 'https://example.invalid' to be 'https://telemetry.googleapis.com'METRIC_EXPORT_INTERVAL_MS→10000expected 10000 to be 5000createFromMetadataGenerator(...)→createFromGoogleCredential(authClient)expected [] to deeply equal [ 'Bearer test-token' ].catch(() => undefined)onauth.getClient()Error: no ADC(rejected instead of degrading)if (enableMetrics)→if (true)expected [ …(1) ] to deeply equal []e instanceof Error ? e : new Error(String(e))→ always re-wrapexpected Error: Error: token refresh failed to be Error: token refresh failede as Error)expected 'boom' to be an instance of ErrorgetRequestHeaderscalled eagerly increateGcpMetricReaderexpected "getRequestHeaders" to not be called at all, but actually been called 1 timesgetGcpProjectIdreturnsgetProjectId()raw instead of degradingpromise rejected "Error: no ADC" instead of resolvinggcp.project_id(the pre-fix behaviour)expected undefined to be 'test-project'envDetector(the pre-fix behaviour)expected undefined to be 'us-central1'serviceInstanceIdDetectorexpected undefined to deeply equal Any<String>getGcpResource(projectId)→getGcpResource()in the dev CLIexpected "spy" to be called with arguments: [ 'adc-project' ]getGcpProjectId()expected "spy" to be called 1 times, but got 0 timesenvDetector/gcpDetector(arity unchanged)expected "detectResources" to be called with arguments: [ { detectors: [ … ] } ]serviceInstanceIdDetector/envDetector(arity unchanged)expected "detectResources" to be called with arguments: [ { detectors: [ … ] } ]getGcpProjectIdignores itsauthargument and builds a freshGoogleAuthexpected 'ambient-project' to be 'injected-project'The last two mutations are the reason
dev/test/utils/telemetry_utils_test.tsexists.setupTelemetry(true)is the only production caller ofgetGcpResource, and dropping the project id argument there compiled cleanly and passed every existing suite — the resource wiring had no coverage at all. That gap is what let the original version of this PR ship a payload with no project on it.Coverage of
core/src/telemetry/google_cloud.tsis 100% statements / branches / functions / lines, with no coverage suppressions:Every line this change adds to
dev/src/utils/telemetry_utils.ts(167-174, 177) is covered by the new dev suite; the uncovered statements reported for that file are all pre-existing code outside this diff.Integration test —
tests/integration/telemetry/gcp_otlp_metrics_test.ts(new). Unlike the unit tests it does not mock@opentelemetry/exporter-metrics-otlp-grpcor@opentelemetry/resources, so the real packages have to compose. Two groups:PeriodicExportingMetricReader, and that no credential is minted (hence no gRPC channel opened) before aMeterProviderdrains it; the reader is shut down in afinallyso its 5 s timer cannot leak.MeterProvideris built fromgetGcpResource(...)the waymaybeSetOtelProvidersbuilds one, a counter is recorded, andreader.collect()is asserted against — so the assertions are on the resource attributes an exporter would actually transmit, not on how the resource was constructed. These pingcp.project_id,service.instance.id, theOTEL_RESOURCE_ATTRIBUTESpath forlocation, and the env-overrides-argument precedence.METADATA_SERVER_DETECTION=noneis stubbed so they run the off-Google-Cloud path deterministically and never touch the metadata server from CI.Regression suites re-run:
build_setupis the manifest regression guard — it installscoreviafile:into six fixture projects and builds/runs each. The singledev/testfailure iscli_create_test.ts:214(initialValue: 'gcloud-project'); it reproduces identically on a cleanmainworktree with no changes, so it is ambientgcloudconfig leaking into that test and is unrelated to this diff.Repository gates, all clean on the pushed commit:
npm run ts:checkfails onmaintoday with pre-existing errors in unrelated test files; none of the files in this diff appear in its output.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Install-size verification (run from a clean clone):
Live export verification (requires a GCP project; not executed in this sandbox — it has no GCP project or ADC, so a reviewer with a project should confirm it):
gcloud services enable telemetry.googleapis.com monitoring.googleapis.comgcloud auth application-default loginexport OTEL_RESOURCE_ATTRIBUTES="location=us-central1"— Managed Service for Prometheus rejects points with an emptylocation, and no detector can infer one off-platform. On GCE/GKE/Cloud Run skip this; the GCP detector supplies it.adk web --otel_to_cloudagainst a sample agent; send a few turns and wait >10 s (two export intervals).{"gen_ai.client.token.usage"}.gcloud auth application-default revoke) and confirm the process logsCannot obtain Application Default Credentials…and keeps serving, with tracing still installed, rather than crashing.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.