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
This commit is contained in:
Kacper Jawoszek
2025-09-19 09:59:58 -07:00
committed by Copybara-Service
parent ccd0e12b42
commit f157b2ee4c
4 changed files with 209 additions and 4 deletions
+1
View File
@@ -45,6 +45,7 @@ dependencies = [
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0, <2.0.0",
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
"opentelemetry-exporter-otlp-proto-http>=1.36.0",
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
"opentelemetry-sdk>=1.31.0, <=1.37.0",
"pydantic>=2.0, <3.0.0", # For data validation/models
+41 -4
View File
@@ -41,6 +41,7 @@ from fastapi.websockets import WebSocketDisconnect
from google.genai import types
import graphviz
from opentelemetry import trace
import opentelemetry.sdk.environment_variables as otel_env
from opentelemetry.sdk.trace import export as export_lib
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace import SpanProcessor
@@ -272,10 +273,14 @@ def _setup_telemetry(
otel_to_cloud: bool = False,
internal_exporters: Optional[list[SpanProcessor]] = None,
):
# TODO - remove the condition and else branch here once
# maybe_set_otel_providers is no longer experimental.
# TODO - remove the else branch here once maybe_set_otel_providers is no
# longer experimental.
if otel_to_cloud:
_setup_telemetry_experimental(internal_exporters=internal_exporters)
_setup_gcp_telemetry_experimental(internal_exporters=internal_exporters)
elif _otel_env_vars_enabled():
_setup_telemetry_from_env_experimental(
internal_exporters=internal_exporters
)
else:
# Old logic - to be removed when above leaves experimental.
tracer_provider = TracerProvider()
@@ -284,7 +289,19 @@ def _setup_telemetry(
trace.set_tracer_provider(tracer_provider=tracer_provider)
def _setup_telemetry_experimental(
def _otel_env_vars_enabled() -> bool:
return any([
os.getenv(endpoint_var)
for endpoint_var in [
otel_env.OTEL_EXPORTER_OTLP_ENDPOINT,
otel_env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
otel_env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
otel_env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
]
])
def _setup_gcp_telemetry_experimental(
internal_exporters: list[SpanProcessor] = None,
):
from ..telemetry.setup import maybe_set_otel_providers
@@ -316,7 +333,27 @@ def _setup_telemetry_experimental(
maybe_set_otel_providers(
otel_hooks_to_setup=otel_hooks_to_add, otel_resource=otel_resource
)
_setup_instrumentation_lib_if_installed()
def _setup_telemetry_from_env_experimental(
internal_exporters: list[SpanProcessor] = None,
):
from ..telemetry.setup import maybe_set_otel_providers
otel_hooks_to_add = []
if internal_exporters:
from ..telemetry.setup import OTelHooks
# Register ADK-specific exporters in trace provider.
otel_hooks_to_add.append(OTelHooks(span_processors=internal_exporters))
maybe_set_otel_providers(otel_hooks_to_setup=otel_hooks_to_add)
_setup_instrumentation_lib_if_installed()
def _setup_instrumentation_lib_if_installed():
# Set instrumentation to enable emitting OTel data from GenAISDK
# Currently the instrumentation lib is in extras dependencies, make sure to
# warn the user if it's not installed.
+60
View File
@@ -16,6 +16,7 @@ from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
import os
from typing import Optional
from opentelemetry import _events
@@ -25,12 +26,16 @@ from opentelemetry import trace
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs import LogRecordProcessor
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
import opentelemetry.sdk.environment_variables as otel_env
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import MetricReader
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import OTELResourceDetector
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import SpanProcessor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from ..utils.feature_decorator import experimental
@@ -50,6 +55,14 @@ def maybe_set_otel_providers(
"""Sets up OTel providers if hooks for a given telemetry type were
passed.
Additionally adds generic OTLP exporters based on following env variables:
OTEL_EXPORTER_OTLP_ENDPOINT
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
See https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
for how they are used.
If a provider for a specific telemetry type was already globally set -
this function will not override it or register more exporters.
@@ -65,6 +78,9 @@ def maybe_set_otel_providers(
if otel_resource is None:
otel_resource = _get_otel_resource()
# Add generic OTel exporters based on OTel env variables.
otel_hooks_to_setup.append(_get_otel_exporters())
span_processors = []
metric_readers = []
log_record_processors = []
@@ -115,3 +131,47 @@ def _get_otel_resource() -> Resource:
# The OTELResourceDetector populates resource labels from
# environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
return OTELResourceDetector().detect()
def _get_otel_exporters() -> OTelHooks:
span_processors = []
if os.getenv(otel_env.OTEL_EXPORTER_OTLP_ENDPOINT) or os.getenv(
otel_env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
):
span_processors.append(_get_otel_span_exporter())
metric_readers = []
if os.getenv(otel_env.OTEL_EXPORTER_OTLP_ENDPOINT) or os.getenv(
otel_env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
):
metric_readers.append(_get_otel_metrics_exporter())
log_record_processors = []
if os.getenv(otel_env.OTEL_EXPORTER_OTLP_ENDPOINT) or os.getenv(
otel_env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
):
log_record_processors.append(_get_otel_logs_exporter())
return OTelHooks(
span_processors=span_processors,
metric_readers=metric_readers,
log_record_processors=log_record_processors,
)
def _get_otel_span_exporter() -> SpanProcessor:
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
return BatchSpanProcessor(OTLPSpanExporter())
def _get_otel_metrics_exporter() -> MetricReader:
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
return PeriodicExportingMetricReader(OTLPMetricExporter())
def _get_otel_logs_exporter() -> LogRecordProcessor:
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
return BatchLogRecordProcessor(OTLPLogExporter())
+107
View File
@@ -0,0 +1,107 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from unittest import mock
from google.adk.telemetry.setup import maybe_set_otel_providers
import pytest
@pytest.fixture
def mock_os_environ():
initial_env = os.environ.copy()
with mock.patch.dict(os.environ, initial_env, clear=False) as m:
yield m
@pytest.mark.parametrize(
"env_vars, should_setup_trace, should_setup_metrics, should_setup_logs",
[
(
{"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "some-endpoint"},
True,
False,
False,
),
(
{"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "some-endpoint"},
False,
True,
False,
),
(
{"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "some-endpoint"},
False,
False,
True,
),
(
{
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "some-endpoint",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "some-endpoint",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "some-endpoint",
},
True,
True,
True,
),
(
{"OTEL_EXPORTER_OTLP_ENDPOINT": "some-endpoint"},
True,
True,
True,
),
],
)
def test_maybe_set_otel_providers(
env_vars: dict[str, str],
should_setup_trace: bool,
should_setup_metrics: bool,
should_setup_logs: bool,
monkeypatch: pytest.MonkeyPatch,
mock_os_environ, # pylint: disable=unused-argument,redefined-outer-name
):
"""
Test initializing correct providers in setup_otel
when providing OTel env variables.
"""
# Arrange.
for k, v in env_vars.items():
os.environ[k] = v
trace_provider_mock = mock.MagicMock()
monkeypatch.setattr(
"opentelemetry.trace.set_tracer_provider",
trace_provider_mock,
)
meter_provider_mock = mock.MagicMock()
monkeypatch.setattr(
"opentelemetry.metrics.set_meter_provider",
meter_provider_mock,
)
logs_provider_mock = mock.MagicMock()
monkeypatch.setattr(
"opentelemetry._logs.set_logger_provider",
logs_provider_mock,
)
# Act.
maybe_set_otel_providers()
# Assert.
# If given telemetry type was enabled,
# the corresponding provider should be set.
assert trace_provider_mock.call_count == (1 if should_setup_trace else 0)
assert meter_provider_mock.call_count == (1 if should_setup_metrics else 0)
assert logs_provider_mock.call_count == (1 if should_setup_logs else 0)