Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
959e752
feat: Add OpenTelemetry environment variable and options configuratio…
chalmerlowe Jun 22, 2026
071ae3c
feat(observability): add base OpenTelemetry span enricher interceptor
chalmerlowe Jun 22, 2026
626ed58
test(observability): add test-only environment variable overrides and…
chalmerlowe Jun 22, 2026
c867011
feat(observability): simplify options resolver to tracing-only
chalmerlowe Jun 24, 2026
3f6ba78
feat(observability): remove OtelSpanEnricher from current PR
chalmerlowe Jun 24, 2026
35f0736
test(o11y): add coverage for environment overrides and fallback
chalmerlowe Jun 24, 2026
fa5d234
test(o11y): improve test isolation in environment overrides
chalmerlowe Jun 24, 2026
c19700e
test(o11y): eliminate 'traces' references and improve test readability
chalmerlowe Jul 6, 2026
5aedd86
refactor(o11y): add license header, reorganize variables and improve …
chalmerlowe Jul 6, 2026
4e49506
chore(o11y): apply black formatting fixes to tests
chalmerlowe Jul 6, 2026
4e950c1
feat(otel): align resolve_feature_flags with strict LLD requirements
chalmerlowe Jul 14, 2026
c6fe8a6
test(o11y): add comprehensive tests for generic feature resolver
chalmerlowe Jul 15, 2026
391fe4c
feat(o11y): refactor resolve_feature_flags to be generic and strictly…
chalmerlowe Jul 15, 2026
0f92c35
style(o11y): apply black formatting to options.py and test_options.py
chalmerlowe Jul 15, 2026
2fb0057
docs(o11y): revise terminology to remove 'gate' metaphor
chalmerlowe Jul 16, 2026
57014ab
fix(o11y): add warning for malformed environment variables
chalmerlowe Jul 16, 2026
1c17863
refactor(o11y): make _has_provider more robust by using getattr
chalmerlowe Jul 16, 2026
8dd6175
test(o11y): add missing tests for experimental path without provider
chalmerlowe Jul 16, 2026
9fc0088
style(o11y): apply black formatting to test_options.py
chalmerlowe Jul 16, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .options import (
clear_test_env_overrides,
resolve_feature_flags,
set_test_env_override,
)

__all__ = [
"resolve_feature_flags",
"set_test_env_override",
"clear_test_env_overrides",
]
149 changes: 149 additions & 0 deletions packages/google-api-core/google/api_core/observability/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# -*- 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.
#

"""Observability environment variable and client options resolution helpers."""

import os
import warnings
from typing import Any, Dict, Optional, Union

# Allowed truthy and falsy patterns for environment variables
_TRUTHY_VALUES = ("y", "yes", "t", "true", "on", "1")
_FALSY_VALUES = ("n", "no", "f", "false", "off", "0")

# Test-only overrides for environment variables.
# This is intended ONLY for unit/integration testing to prevent mutating
# os.environ.
_TEST_ENV_OVERRIDES: Dict[str, bool] = {}


def _strtobool(val: str) -> Optional[bool]:
"""Convert a string representation of truth to a boolean."""
clean_val = val.lower().strip()
if not clean_val:
return None
if clean_val in _TRUTHY_VALUES:
return True
if clean_val in _FALSY_VALUES:
return False
raise ValueError(f"Invalid truth value: {val!r}")


def set_test_env_override(name: str, value: Optional[bool]) -> None:
"""Sets a test-only override for a specific environment variable.

This is intended ONLY for unit/integration testing to prevent mutating
os.environ.
"""
if value is None:
_TEST_ENV_OVERRIDES.pop(name, None)
else:
_TEST_ENV_OVERRIDES[name] = value


def clear_test_env_overrides() -> None:
"""Clears all test-only overrides."""
_TEST_ENV_OVERRIDES.clear()


def _get_env_bool(name: str) -> Optional[bool]:
"""Retrieve the boolean value of an environment variable."""
if name in _TEST_ENV_OVERRIDES:
return _TEST_ENV_OVERRIDES[name]

val = os.getenv(name)
if val is None:
return None
try:
return _strtobool(val)
except ValueError as e:
warnings.warn(f"Ignored invalid value for {name}: {e}", RuntimeWarning)
return None


def _has_provider(
client_options: Optional[Union[Dict[str, Any], Any]], provider_key: str
) -> bool:
"""Checks if a specific provider key is present and not None in client_options."""
if client_options is None:
return False

if isinstance(client_options, dict):
return client_options.get(provider_key) is not None

return getattr(client_options, provider_key, None) is not None


def resolve_feature_flags(
env_var: str,
provider_key: str,
client_options: Optional[Union[Dict[str, Any], Any]] = None,
) -> bool:
"""Determines if a feature is enabled based on environment variables and client options.

Behavior depends on whether the `env_var` name contains "EXPERIMENTAL":

- **Experimental Path** (env_var contains "EXPERIMENTAL"):
Strict control. Requires the environment variable to be explicitly 'true'.
If a programmatic provider is passed but the environment variable is not 'true',
raises ValueError (Fail Fast).

- **GA Path** (env_var does not contain "EXPERIMENTAL"):
Standard precedence. Enabled if a programmatic provider is passed,
otherwise falls back to the environment variable value.

Args:
env_var: The name of the environment variable controlling this feature.
provider_key: The key in client_options/attributes for the programmatic provider.
client_options: A dictionary or object containing client configuration.

Returns:
bool: True if the feature is resolved to enabled, False otherwise.

Raises:
ValueError: If a provider is provided for an experimental feature without enabling the experimental environment variable.
"""

# Check for programmatic feature provider
has_provider = _has_provider(client_options, provider_key)

# Read environment variable
env_var_setting = _get_env_bool(env_var)

# EXPERIMENTAL PATH:
# Resolution Hierarchy:
# 1. EXPERIMENTAL Environment Variable
# 2. Fail Fast if Provider present but EXPERIMENTAL Environment Variable is not enabled
if "EXPERIMENTAL" in env_var:
# Fail Fast if provider present but experimental environment variable is not enabled
if env_var_setting is not True and has_provider:
raise ValueError(
f"Experimental feature requires {env_var} to be set to 'true' to use programmatic providers."
)

return bool(env_var_setting)

# GENERAL AVAILABILITY PATH:
# Resolution Hierarchy:
# 1. Programmatic Provider
# 2. Environment Variable

# Check Programmatic Provider
if has_provider:
return True

# Check Environment Variable
return bool(env_var_setting)
3 changes: 3 additions & 0 deletions packages/google-api-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies = [
"proto-plus >= 1.25.0, < 2.0.0; python_version >= '3.13'",
"google-auth >= 2.14.1, < 3.0.0",
"requests >= 2.33.0, < 3.0.0",
"opentelemetry-api >= 1.27.0, < 2.0.0",
]
dynamic = ["version"]

Expand Down Expand Up @@ -94,4 +95,6 @@ filterwarnings = [
"ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning",
# Remove once https://github.com/grpc/grpc/issues/35086 is fixed (and version newer than 1.60.0 is published)
"ignore:There is no current event loop:DeprecationWarning",
# Ignore external OpenTelemetry/importlib.metadata SelectableGroups warning
"ignore:.*SelectableGroups dict interface is deprecated:DeprecationWarning",
]
1 change: 1 addition & 0 deletions packages/google-api-core/testing/constraints-3.10.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ requests==2.33.0
grpcio==1.41.0
grpcio-status==1.41.0
proto-plus==1.24.0
opentelemetry-api==1.27.0
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ grpcio==1.41.0
grpcio-status==1.41.0
proto-plus==1.24.0
aiohttp==3.13.4
opentelemetry-api==1.27.0
Loading
Loading