Skip to content

Commit 51f2846

Browse files
committed
feat: Add OpenTelemetry environment variable and options configuration helpers
1 parent 247e2ad commit 51f2846

3 files changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .options import is_signal_enabled
2+
3+
__all__ = ["is_signal_enabled"]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Observability environment variable and client options resolution helpers."""
2+
3+
import os
4+
import warnings
5+
from typing import Any, Dict, List, Optional, Union
6+
7+
# Allowed truthy and falsy patterns for environment variables
8+
_TRUTHY_VALUES = ("y", "yes", "t", "true", "on", "1")
9+
_FALSY_VALUES = ("n", "no", "f", "false", "off", "0")
10+
11+
12+
def _strtobool(val: str) -> Optional[bool]:
13+
"""Convert a string representation of truth to a boolean."""
14+
clean_val = val.lower().strip()
15+
if not clean_val:
16+
return None
17+
if clean_val in _TRUTHY_VALUES:
18+
return True
19+
if clean_val in _FALSY_VALUES:
20+
return False
21+
raise ValueError(f"Invalid truth value: {val!r}")
22+
23+
24+
def _get_env_bool(name: str) -> Optional[bool]:
25+
"""Retrieve the boolean value of an environment variable."""
26+
val = os.getenv(name)
27+
if val is None:
28+
return None
29+
try:
30+
return _strtobool(val)
31+
except ValueError:
32+
return None
33+
34+
35+
def _get_env_bool_with_dev_fallback(name: str) -> Optional[bool]:
36+
"""Retrieve the boolean value of an environment variable, checking dev/exp fallbacks first."""
37+
if name.startswith("GOOGLE_CLOUD_"):
38+
exp_name = name.replace("GOOGLE_CLOUD_", "GOOGLE_CLOUD_EXPERIMENTAL_", 1)
39+
val = _get_env_bool(exp_name)
40+
if val is not None:
41+
return val
42+
return _get_env_bool(name)
43+
44+
45+
def is_signal_enabled(
46+
service_name: str,
47+
signal_type: str,
48+
client_options: Optional[Union[Dict[str, Any], Any]] = None,
49+
default: bool = False,
50+
legacy_vars: Optional[List[str]] = None,
51+
) -> bool:
52+
"""Determines if a telemetry signal is enabled."""
53+
service_upper = service_name.upper().replace("-", "_")
54+
signal_upper = signal_type.upper()
55+
56+
# 1. Resolve Programmatic Options First
57+
if client_options is not None:
58+
options_dict = (
59+
client_options
60+
if isinstance(client_options, dict)
61+
else getattr(client_options, "__dict__", {})
62+
)
63+
option_key = f"enable_{signal_type.lower()}"
64+
provider_key = f"{signal_type.rstrip('s').lower()}_provider"
65+
66+
if options_dict.get(option_key) is not None:
67+
return bool(options_dict.get(option_key))
68+
if options_dict.get(provider_key) is not None:
69+
return True
70+
71+
# 2. Language & Service-specific
72+
val = _get_env_bool_with_dev_fallback(
73+
f"GOOGLE_CLOUD_PYTHON_{service_upper}_{signal_upper}_ENABLED"
74+
)
75+
if val is not None:
76+
return val
77+
78+
# 3. Language-wide Global
79+
val = _get_env_bool_with_dev_fallback(f"GOOGLE_CLOUD_PYTHON_{signal_upper}_ENABLED")
80+
if val is not None:
81+
return val
82+
83+
# 4. Cross-language Service-specific
84+
val = _get_env_bool_with_dev_fallback(
85+
f"GOOGLE_CLOUD_{service_upper}_{signal_upper}_ENABLED"
86+
)
87+
if val is not None:
88+
return val
89+
90+
# 5. Cross-language Global
91+
val = _get_env_bool_with_dev_fallback(f"GOOGLE_CLOUD_{signal_upper}_ENABLED")
92+
if val is not None:
93+
return val
94+
95+
# 6. Legacy Variables
96+
if legacy_vars:
97+
for legacy_var in legacy_vars:
98+
val = _get_env_bool(legacy_var)
99+
if val is not None:
100+
warnings.warn(
101+
f"Environment variable {legacy_var!r} is deprecated and will be removed "
102+
"in a future release. Please migrate to the standardized "
103+
f"GOOGLE_CLOUD_PYTHON_{service_upper}_{signal_upper}_ENABLED instead.",
104+
DeprecationWarning,
105+
stacklevel=2,
106+
)
107+
return val
108+
109+
# 7. Default Fallback
110+
return default
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import pytest
2+
3+
from google.api_core.observability import options
4+
5+
6+
@pytest.mark.parametrize(
7+
"env_vars, client_options, default_val, expected",
8+
[
9+
# Default fallback tests
10+
({}, None, False, False),
11+
({}, None, True, True),
12+
# Service-specific env var
13+
({"GOOGLE_CLOUD_PYTHON_TRANSLATE_TRACES_ENABLED": "true"}, None, False, True),
14+
({"GOOGLE_CLOUD_PYTHON_TRANSLATE_TRACES_ENABLED": "false"}, None, True, False),
15+
# Experimental fallback
16+
(
17+
{"GOOGLE_CLOUD_EXPERIMENTAL_PYTHON_TRANSLATE_TRACES_ENABLED": "true"},
18+
None,
19+
False,
20+
True,
21+
),
22+
# Precedence: Service specific overrides global
23+
(
24+
{
25+
"GOOGLE_CLOUD_PYTHON_TRACES_ENABLED": "true",
26+
"GOOGLE_CLOUD_PYTHON_TRANSLATE_TRACES_ENABLED": "false",
27+
},
28+
None,
29+
False,
30+
False,
31+
),
32+
(
33+
{
34+
"GOOGLE_CLOUD_PYTHON_TRACES_ENABLED": "false",
35+
"GOOGLE_CLOUD_PYTHON_TRANSLATE_TRACES_ENABLED": "true",
36+
},
37+
None,
38+
False,
39+
True,
40+
),
41+
# Precedence: Client options override env vars
42+
(
43+
{"GOOGLE_CLOUD_PYTHON_TRANSLATE_TRACES_ENABLED": "false"},
44+
{"enable_traces": True},
45+
False,
46+
True,
47+
),
48+
],
49+
)
50+
def test_is_signal_enabled(
51+
monkeypatch, env_vars, client_options, default_val, expected
52+
):
53+
# Setup environment variables using pytest's monkeypatch fixture
54+
for k, v in env_vars.items():
55+
monkeypatch.setenv(k, v)
56+
57+
result = options.is_signal_enabled(
58+
"translate", "traces", client_options=client_options, default=default_val
59+
)
60+
assert result is expected
61+
62+
63+
def test_legacy_var_with_warning(monkeypatch):
64+
monkeypatch.setenv("LEGACY_TRACE_VAR", "true")
65+
66+
with pytest.warns(DeprecationWarning, match="LEGACY_TRACE_VAR"):
67+
result = options.is_signal_enabled(
68+
"translate", "traces", legacy_vars=["LEGACY_TRACE_VAR"]
69+
)
70+
assert result is True

0 commit comments

Comments
 (0)