1# -*- coding: utf-8 -*-
2# Copyright 2026 Google LLC
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17"""Observability environment variable and client options resolution helpers."""
18
19import os
20import warnings
21from typing import Any
22
23# We import ClientOptions for type hinting
24from google.api_core.client_options import ClientOptions
25
26# Allowed truthy and falsy patterns for environment variables
27_TRUTHY_VALUES = ("y", "yes", "t", "true", "on", "1")
28_FALSY_VALUES = ("n", "no", "f", "false", "off", "0")
29
30
31class FeatureGatingError(ValueError):
32 """Raised when feature gating resolution fails or is misconfigured."""
33
34 pass
35
36
37def _strtobool(val: str) -> bool | None:
38 """Convert a string representation of truth to a boolean."""
39 clean_val = val.lower().strip()
40 if not clean_val:
41 return None
42 if clean_val in _TRUTHY_VALUES:
43 return True
44 if clean_val in _FALSY_VALUES:
45 return False
46 raise ValueError(f"Invalid truth value: {val!r}")
47
48
49def _get_env_bool(name: str) -> bool | None:
50 """Retrieve the boolean value of an environment variable."""
51 val = os.getenv(name)
52 if val is None:
53 return None
54 try:
55 return _strtobool(val)
56 except ValueError as e:
57 warnings.warn(f"Ignored invalid value for {name}: {e}", RuntimeWarning)
58 return None
59
60
61def _has_feature_key(
62 *, configuration: ClientOptions | dict[str, Any] | None, feature_key: str
63) -> bool:
64 """Checks if a specific feature key is present and not None in configuration."""
65 if configuration is None:
66 return False
67
68 if feature_key.startswith("__"):
69 return False
70
71 if isinstance(configuration, dict):
72 return configuration.get(feature_key) is not None
73
74 return getattr(configuration, feature_key, None) is not None
75
76
77def resolve_feature_flags(
78 *,
79 env_var: str,
80 feature_key: str,
81 configuration: ClientOptions | dict[str, Any] | None = None,
82) -> bool:
83 """Determines if a feature is enabled based on environment variables and configuration.
84
85 Behavior depends on whether the `env_var` name contains "EXPERIMENTAL":
86
87 - **Experimental Path** (env_var contains "EXPERIMENTAL"):
88 Strict control. Requires the environment variable to be explicitly 'true'.
89 If a programmatic feature key is passed but the environment variable is not 'true',
90 raises FeatureGatingError (Fail Fast).
91
92 - **GA Path** (env_var does not contain "EXPERIMENTAL"):
93 Standard precedence. Enabled if a programmatic feature key is passed,
94 otherwise falls back to the environment variable value.
95
96 Args:
97 env_var: The name of the environment variable controlling this feature.
98 feature_key: The key in configuration/attributes for the programmatic configuration.
99 configuration: Optional. A dictionary or object containing client configuration.
100
101 Returns:
102 bool: True if the feature is resolved to enabled, False otherwise.
103
104 Raises:
105 FeatureGatingError: If a feature key is provided for an experimental feature without enabling the experimental environment variable.
106 """
107
108 # Check for programmatic feature configuration
109 has_feature_key = _has_feature_key(
110 configuration=configuration, feature_key=feature_key
111 )
112
113 # Read environment variable
114 env_var_setting = _get_env_bool(env_var)
115
116 # EXPERIMENTAL PATH:
117 # Resolution Hierarchy:
118 # 1. EXPERIMENTAL Environment Variable
119 # 2. Fail Fast if Feature Key present but EXPERIMENTAL Environment Variable is not enabled
120 if "EXPERIMENTAL" in env_var:
121 # Fail Fast if feature key present but experimental environment variable is not enabled
122 if env_var_setting is not True and has_feature_key:
123 raise FeatureGatingError(
124 f"Experimental feature requires {env_var} to be set to 'true' to use programmatic configuration."
125 )
126
127 return bool(env_var_setting)
128
129 # GENERAL AVAILABILITY PATH:
130 # Resolution Hierarchy:
131 # 1. Programmatic Configuration (Feature Key)
132 # 2. Environment Variable
133
134 # Check Programmatic Configuration
135 if has_feature_key:
136 return True
137
138 # Check Environment Variable
139 return bool(env_var_setting)