Skip to content

Commit f157b2e

Browse files
jawoszekcopybara-github
authored andcommitted
feat(otel): support standard OTel env variables for exporter endpoints
ADK web server will automatically setup OTel providers with exporters if any of the .*_ENDPOINT variables from https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ is set. PiperOrigin-RevId: 809079453
1 parent ccd0e12 commit f157b2e

File tree

4 files changed

+209
-4
lines changed

4 files changed

+209
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies = [
4545
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
4646
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0, <2.0.0",
4747
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
48+
"opentelemetry-exporter-otlp-proto-http>=1.36.0",
4849
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
4950
"opentelemetry-sdk>=1.31.0, <=1.37.0",
5051
"pydantic>=2.0, <3.0.0", # For data validation/models

src/google/adk/cli/adk_web_server.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from google.genai import types
4242
import graphviz
4343
from opentelemetry import trace
44+
import opentelemetry.sdk.environment_variables as otel_env
4445
from opentelemetry.sdk.trace import export as export_lib
4546
from opentelemetry.sdk.trace import ReadableSpan
4647
from opentelemetry.sdk.trace import SpanProcessor
@@ -272,10 +273,14 @@ def _setup_telemetry(
272273
otel_to_cloud: bool = False,
273274
internal_exporters: Optional[list[SpanProcessor]] = None,
274275
):
275-
# TODO - remove the condition and else branch here once
276-
# maybe_set_otel_providers is no longer experimental.
276+
# TODO - remove the else branch here once maybe_set_otel_providers is no
277+
# longer experimental.
277278
if otel_to_cloud:
278-
_setup_telemetry_experimental(internal_exporters=internal_exporters)
279+
_setup_gcp_telemetry_experimental(internal_exporters=internal_exporters)
280+
elif _otel_env_vars_enabled():
281+
_setup_telemetry_from_env_experimental(
282+
internal_exporters=internal_exporters
283+
)
279284
else:
280285
# Old logic - to be removed when above leaves experimental.
281286
tracer_provider = TracerProvider()
@@ -284,7 +289,19 @@ def _setup_telemetry(
284289
trace.set_tracer_provider(tracer_provider=tracer_provider)
285290

286291

287-
def _setup_telemetry_experimental(
292+
def _otel_env_vars_enabled() -> bool:
293+
return any([
294+
os.getenv(endpoint_var)
295+
for endpoint_var in [
296+
otel_env.OTEL_EXPORTER_OTLP_ENDPOINT,
297+
otel_env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
298+
otel_env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
299+
otel_env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
300+
]
301+
])
302+
303+
304+
def _setup_gcp_telemetry_experimental(
288305
internal_exporters: list[SpanProcessor] = None,
289306
):
290307
from ..telemetry.setup import maybe_set_otel_providers
@@ -316,7 +333,27 @@ def _setup_telemetry_experimental(
316333
maybe_set_otel_providers(
317334
otel_hooks_to_setup=otel_hooks_to_add, otel_resource=otel_resource
318335
)
336+
_setup_instrumentation_lib_if_installed()
337+
338+
339+
def _setup_telemetry_from_env_experimental(
340+
internal_exporters: list[SpanProcessor] = None,
341+
):
342+
from ..telemetry.setup import maybe_set_otel_providers
343+
344+
otel_hooks_to_add = []
345+
346+
if internal_exporters:
347+
from ..telemetry.setup import OTelHooks
348+
349+
# Register ADK-specific exporters in trace provider.
350+
otel_hooks_to_add.append(OTelHooks(span_processors=internal_exporters))
351+
352+
maybe_set_otel_providers(otel_hooks_to_setup=otel_hooks_to_add)
353+
_setup_instrumentation_lib_if_installed()
354+
319355

356+
def _setup_instrumentation_lib_if_installed():
320357
# Set instrumentation to enable emitting OTel data from GenAISDK
321358
# Currently the instrumentation lib is in extras dependencies, make sure to
322359
# warn the user if it's not installed.

src/google/adk/telemetry/setup.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from dataclasses import dataclass
1818
from dataclasses import field
19+
import os
1920
from typing import Optional
2021

2122
from opentelemetry import _events
@@ -25,12 +26,16 @@
2526
from opentelemetry.sdk._events import EventLoggerProvider
2627
from opentelemetry.sdk._logs import LoggerProvider
2728
from opentelemetry.sdk._logs import LogRecordProcessor
29+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
30+
import opentelemetry.sdk.environment_variables as otel_env
2831
from opentelemetry.sdk.metrics import MeterProvider
2932
from opentelemetry.sdk.metrics.export import MetricReader
33+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
3034
from opentelemetry.sdk.resources import OTELResourceDetector
3135
from opentelemetry.sdk.resources import Resource
3236
from opentelemetry.sdk.trace import SpanProcessor
3337
from opentelemetry.sdk.trace import TracerProvider
38+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
3439

3540
from ..utils.feature_decorator import experimental
3641

@@ -50,6 +55,14 @@ def maybe_set_otel_providers(
5055
"""Sets up OTel providers if hooks for a given telemetry type were
5156
passed.
5257
58+
Additionally adds generic OTLP exporters based on following env variables:
59+
OTEL_EXPORTER_OTLP_ENDPOINT
60+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
61+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
62+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
63+
See https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
64+
for how they are used.
65+
5366
If a provider for a specific telemetry type was already globally set -
5467
this function will not override it or register more exporters.
5568
@@ -65,6 +78,9 @@ def maybe_set_otel_providers(
6578
if otel_resource is None:
6679
otel_resource = _get_otel_resource()
6780

81+
# Add generic OTel exporters based on OTel env variables.
82+
otel_hooks_to_setup.append(_get_otel_exporters())
83+
6884
span_processors = []
6985
metric_readers = []
7086
log_record_processors = []
@@ -115,3 +131,47 @@ def _get_otel_resource() -> Resource:
115131
# The OTELResourceDetector populates resource labels from
116132
# environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
117133
return OTELResourceDetector().detect()
134+
135+
136+
def _get_otel_exporters() -> OTelHooks:
137+
span_processors = []
138+
if os.getenv(otel_env.OTEL_EXPORTER_OTLP_ENDPOINT) or os.getenv(
139+
otel_env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
140+
):
141+
span_processors.append(_get_otel_span_exporter())
142+
143+
metric_readers = []
144+
if os.getenv(otel_env.OTEL_EXPORTER_OTLP_ENDPOINT) or os.getenv(
145+
otel_env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
146+
):
147+
metric_readers.append(_get_otel_metrics_exporter())
148+
149+
log_record_processors = []
150+
if os.getenv(otel_env.OTEL_EXPORTER_OTLP_ENDPOINT) or os.getenv(
151+
otel_env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
152+
):
153+
log_record_processors.append(_get_otel_logs_exporter())
154+
155+
return OTelHooks(
156+
span_processors=span_processors,
157+
metric_readers=metric_readers,
158+
log_record_processors=log_record_processors,
159+
)
160+
161+
162+
def _get_otel_span_exporter() -> SpanProcessor:
163+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
164+
165+
return BatchSpanProcessor(OTLPSpanExporter())
166+
167+
168+
def _get_otel_metrics_exporter() -> MetricReader:
169+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
170+
171+
return PeriodicExportingMetricReader(OTLPMetricExporter())
172+
173+
174+
def _get_otel_logs_exporter() -> LogRecordProcessor:
175+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
176+
177+
return BatchLogRecordProcessor(OTLPLogExporter())
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
from unittest import mock
17+
18+
from google.adk.telemetry.setup import maybe_set_otel_providers
19+
import pytest
20+
21+
22+
@pytest.fixture
23+
def mock_os_environ():
24+
initial_env = os.environ.copy()
25+
with mock.patch.dict(os.environ, initial_env, clear=False) as m:
26+
yield m
27+
28+
29+
@pytest.mark.parametrize(
30+
"env_vars, should_setup_trace, should_setup_metrics, should_setup_logs",
31+
[
32+
(
33+
{"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "some-endpoint"},
34+
True,
35+
False,
36+
False,
37+
),
38+
(
39+
{"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "some-endpoint"},
40+
False,
41+
True,
42+
False,
43+
),
44+
(
45+
{"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "some-endpoint"},
46+
False,
47+
False,
48+
True,
49+
),
50+
(
51+
{
52+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "some-endpoint",
53+
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "some-endpoint",
54+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "some-endpoint",
55+
},
56+
True,
57+
True,
58+
True,
59+
),
60+
(
61+
{"OTEL_EXPORTER_OTLP_ENDPOINT": "some-endpoint"},
62+
True,
63+
True,
64+
True,
65+
),
66+
],
67+
)
68+
def test_maybe_set_otel_providers(
69+
env_vars: dict[str, str],
70+
should_setup_trace: bool,
71+
should_setup_metrics: bool,
72+
should_setup_logs: bool,
73+
monkeypatch: pytest.MonkeyPatch,
74+
mock_os_environ, # pylint: disable=unused-argument,redefined-outer-name
75+
):
76+
"""
77+
Test initializing correct providers in setup_otel
78+
when providing OTel env variables.
79+
"""
80+
# Arrange.
81+
for k, v in env_vars.items():
82+
os.environ[k] = v
83+
trace_provider_mock = mock.MagicMock()
84+
monkeypatch.setattr(
85+
"opentelemetry.trace.set_tracer_provider",
86+
trace_provider_mock,
87+
)
88+
meter_provider_mock = mock.MagicMock()
89+
monkeypatch.setattr(
90+
"opentelemetry.metrics.set_meter_provider",
91+
meter_provider_mock,
92+
)
93+
logs_provider_mock = mock.MagicMock()
94+
monkeypatch.setattr(
95+
"opentelemetry._logs.set_logger_provider",
96+
logs_provider_mock,
97+
)
98+
99+
# Act.
100+
maybe_set_otel_providers()
101+
102+
# Assert.
103+
# If given telemetry type was enabled,
104+
# the corresponding provider should be set.
105+
assert trace_provider_mock.call_count == (1 if should_setup_trace else 0)
106+
assert meter_provider_mock.call_count == (1 if should_setup_metrics else 0)
107+
assert logs_provider_mock.call_count == (1 if should_setup_logs else 0)

0 commit comments

Comments
 (0)