Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/google-api-core/google/api_core/_otel_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Copyright 2026 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.
#

"""OpenTelemetry helpers for resolving and instantiating interceptors."""

from typing import Any, Optional

from google.api_core import _feature_gating_helpers
from google.api_core.client_options import ClientOptions


def get_otel_grpc_interceptor(
client_options: Optional[ClientOptions | dict[str, Any]] = None,
env_var: str = "GOOGLE_CLOUD_PYTHON_TRACING_ENABLED",
) -> Optional[Any]:
"""Checks feature flags, attempts to import OTel, and returns the interceptor.

This helper centralizes the logic for checking environment variables,
programmatic configuration (via ClientOptions or dict), and soft-importing
the `opentelemetry-instrumentation-grpc` package.

Args:
client_options: The client options object or dictionary, potentially holding a tracer_provider.
env_var: The environment variable to check for enablement.

Returns:
Optional[Any]: An OpenTelemetry gRPC client interceptor instance if enabled
and installed, otherwise None.
"""
is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags(
env_var=env_var,
feature_key="tracer_provider",
configuration=client_options,
)

if is_tracing_enabled:
try:
import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]

if isinstance(client_options, dict):
tracer_provider = client_options.get("tracer_provider")
else:
tracer_provider = getattr(client_options, "tracer_provider", None)

return otel_grpc.client_interceptor(tracer_provider=tracer_provider)
except ImportError:
# Failed open if OTel is not installed but feature was requested.
# We might want to warn here, but for now we follow the "silent fail open" pattern
# or let the user handle it if they care.
pass

return None
4 changes: 4 additions & 0 deletions packages/google-api-core/google/api_core/client_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ class ClientOptions(object):
`googleapis.com`. If both `api_endpoint` and `universe_domain` are set,
then `api_endpoint` is used as the service endpoint. If `api_endpoint` is
not specified, the format will be `{service}.{universe_domain}`.
tracer_provider (Optional[object]): The OpenTelemetry tracer provider to use
for tracing. If not set, the global tracer provider will be used.

Raises:
ValueError: If both ``client_cert_source`` and ``client_encrypted_cert_source``
Expand All @@ -117,6 +119,7 @@ def __init__(
api_key: Optional[str] = None,
api_audience: Optional[str] = None,
universe_domain: Optional[str] = None,
tracer_provider: Optional[object] = None,
):
if credentials_file is not None:
warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning)
Expand All @@ -136,6 +139,7 @@ def __init__(
self.api_key = api_key
self.api_audience = api_audience
self.universe_domain = universe_domain
self.tracer_provider = tracer_provider

def __repr__(self) -> str:
return "ClientOptions: " + repr(self.__dict__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from re import match

import pytest

from google.api_core import client_options

from ..helpers import warn_deprecated_credentials_file
Expand All @@ -30,6 +29,7 @@ def get_client_encrypted_cert():


def test_constructor():
mock_tracer_provider = object()
with warn_deprecated_credentials_file():
options = client_options.ClientOptions(
api_endpoint="foo.googleapis.com",
Expand All @@ -42,6 +42,7 @@ def test_constructor():
],
api_audience="foo2.googleapis.com",
universe_domain="googleapis.com",
tracer_provider=mock_tracer_provider,
)

assert options.api_endpoint == "foo.googleapis.com"
Expand All @@ -54,6 +55,7 @@ def test_constructor():
]
assert options.api_audience == "foo2.googleapis.com"
assert options.universe_domain == "googleapis.com"
assert options.tracer_provider is mock_tracer_provider


def test_constructor_with_encrypted_cert_source():
Expand Down Expand Up @@ -162,6 +164,7 @@ def test_repr():
"scopes",
"api_key",
"api_audience",
"tracer_provider",
]
)
options = client_options.ClientOptions(api_endpoint="foo.googleapis.com")
Expand Down
105 changes: 105 additions & 0 deletions packages/google-api-core/tests/unit/test_otel_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright 2026 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 sys
from unittest import mock

from google.api_core import _otel_helpers
from google.api_core.client_options import ClientOptions


def test_get_otel_grpc_interceptor_disabled(monkeypatch):
monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "false")
interceptor = _otel_helpers.get_otel_grpc_interceptor()
assert interceptor is None


def test_get_otel_grpc_interceptor_enabled_otel_missing(monkeypatch):
monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "true")
# Simulate OTel not being installed by blocking imports
monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None)

interceptor = _otel_helpers.get_otel_grpc_interceptor()
assert interceptor is None


def test_get_otel_grpc_interceptor_enabled_otel_installed(monkeypatch):
monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "true")

mock_otel = mock.Mock()
mock_otel_grpc = mock_otel.instrumentation.grpc
mock_interceptor = mock.Mock()
mock_otel_grpc.client_interceptor.return_value = mock_interceptor

monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel)
monkeypatch.setitem(
sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation
)
monkeypatch.setitem(
sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc
)

interceptor = _otel_helpers.get_otel_grpc_interceptor()
assert interceptor is mock_interceptor
mock_otel_grpc.client_interceptor.assert_called_once_with(tracer_provider=None)


def test_get_otel_grpc_interceptor_enabled_via_config(monkeypatch):
# Tracing enabled via config (tracer_provider is set)
mock_tracer_provider = object()
options = ClientOptions(tracer_provider=mock_tracer_provider)

mock_otel = mock.Mock()
mock_otel_grpc = mock_otel.instrumentation.grpc
mock_interceptor = mock.Mock()
mock_otel_grpc.client_interceptor.return_value = mock_interceptor

monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel)
monkeypatch.setitem(
sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation
)
monkeypatch.setitem(
sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc
)

interceptor = _otel_helpers.get_otel_grpc_interceptor(client_options=options)
assert interceptor is mock_interceptor
mock_otel_grpc.client_interceptor.assert_called_once_with(
tracer_provider=mock_tracer_provider
)


def test_get_otel_grpc_interceptor_enabled_via_dict_config(monkeypatch):
# Tracing enabled via dict config
mock_tracer_provider = object()
options = {"tracer_provider": mock_tracer_provider}

mock_otel = mock.Mock()
mock_otel_grpc = mock_otel.instrumentation.grpc
mock_interceptor = mock.Mock()
mock_otel_grpc.client_interceptor.return_value = mock_interceptor

monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel)
monkeypatch.setitem(
sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation
)
monkeypatch.setitem(
sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc
)

interceptor = _otel_helpers.get_otel_grpc_interceptor(client_options=options)
assert interceptor is mock_interceptor
mock_otel_grpc.client_interceptor.assert_called_once_with(
tracer_provider=mock_tracer_provider
)
Loading