diff --git a/AGENTS.md b/AGENTS.md index ad162e7..5a86d70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ RUN_SDK_INTEGRATION_TESTS=1 ./scripts/run-unit-tests.sh ## Environment variable naming -All SDK config uses the `HARNESS_` prefix. `HA_`, `AT_`, and `TA_` are legacy aliases accepted for backwards compatibility. When a setting is defined under multiple prefixes, precedence is `HARNESS_` > `HA_` > `AT_` > `TA_`. Resolution lives in `src/harness_sdk/env.py` (`get_env_value`, `is_env_var_present`, `is_harness_flag_enabled`). +All SDK config uses the `HARNESS_` prefix. `HA_`, `AT_`, and `TA_` are legacy aliases accepted for backwards compatibility. When a setting is defined under multiple prefixes, precedence is `HARNESS_` > `HA_` > `AT_` > `TA_`. Resolution lives in `src/harness_sdk/env.py` (`get_env_value`, `is_env_flag_enabled`, `is_harness_flag_enabled`). Key variables: | Variable | Purpose | @@ -34,12 +34,12 @@ Key variables: | `HARNESS_REPORTING_COMPRESSION` | `gzip` or empty | | `HARNESS_CONTROL_PLUGINS` | Comma-separated control plugin names | | `HARNESS_OBSERVABILITY_PLUGINS` | Comma-separated observability plugin names | -| `HARNESS_ENABLE_CONSOLE_SPAN_EXPORTER` | Set to any value to dump spans to stdout | +| `HARNESS_ENABLE_CONSOLE_SPAN_EXPORTER` | `true` to dump spans to stdout (default: off) | | `HARNESS_CONFIG_FILE` | Path to YAML config file (overrides env) | -| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | Capture LLM prompt/response payloads | +| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | Capture LLM prompt/response payloads (default: off) | | `HARNESS_GEN_AI_PAYLOAD_EVALUATION_ENABLED` | Run control plugins on GenAI spans | -### Instrumentation opt-in (strict `HARNESS_` prefix, no legacy aliases) +### Instrumentation opt-in (`HARNESS_` or `HA_` prefix; `HARNESS_` wins — no `AT_`/`TA_` aliases) Instrumentation is opt-in: `Agent().instrument()` instruments nothing unless a flag below is set to `true`. Categorization and gating live in `src/harness_sdk/instrumentation/instrumentation_definitions.py` (`is_library_enabled`, `is_api_instrumentation_enabled`, `any_ai_provider_enabled`), enforced in `Agent.instrument()`. diff --git a/docs/sdk-quickstart.md b/docs/sdk-quickstart.md index 461157a..28107c0 100644 --- a/docs/sdk-quickstart.md +++ b/docs/sdk-quickstart.md @@ -62,6 +62,8 @@ flag is on only when its value is `true`. | `HARNESS_ENABLE_AI_GOOGLE_GENAI` | Google GenAI (Gemini / Vertex AI) | | `HARNESS_ENABLE_AI_MCP` | Model Context Protocol | +Each flag also accepts the `HA_` alias (e.g. `HA_ENABLE_API=true`); when both are set, the `HARNESS_` value wins. `AT_`/`TA_` aliases are not supported for these flags. + ```bash # Example: HTTP/API instrumentation plus LiteLLM export HARNESS_ENABLE_API=true @@ -145,7 +147,7 @@ Agent().instrument(skip_libraries=["requests"]) | Variable | Effect | |---|---| -| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | `false` to omit prompt/response bodies from spans | +| `HARNESS_GEN_AI_PAYLOAD_CAPTURE_ENABLED` | `true` to capture prompt/response bodies in spans (default: off) | | `HARNESS_SPAN_ATTRIBUTES` | extra attributes on every span, e.g. `env=prod,team=ai` | | `HARNESS_OBSERVABILITY_PLUGINS` | `builtin_span_attributes` to keep instrumentation but disable the SDK's own OTLP exporter (if you already export spans yourself) | | `HARNESS_ENABLED` | `false` to disable the SDK entirely (no code change) | diff --git a/src/harness_sdk/agent_init.py b/src/harness_sdk/agent_init.py index 8ff94b6..c1f51f1 100644 --- a/src/harness_sdk/agent_init.py +++ b/src/harness_sdk/agent_init.py @@ -17,7 +17,7 @@ from opentelemetry.sdk.resources import Resource from harness_sdk import constants from harness_sdk.config import config_pb2 -from harness_sdk.env import is_env_var_present +from harness_sdk.env import is_env_flag_enabled from harness_sdk.otlp_reporting import ( compression_type_to_otlp_grpc, compression_type_to_otlp_http, @@ -43,7 +43,7 @@ def apply_config(self, agent_config): self._config = agent_config self.init_trace_provider() self.init_propagation() - if is_env_var_present("ENABLE_CONSOLE_SPAN_EXPORTER"): + if is_env_flag_enabled("ENABLE_CONSOLE_SPAN_EXPORTER"): self.set_console_span_processor() def init_trace_provider(self) -> None: diff --git a/src/harness_sdk/config/default.py b/src/harness_sdk/config/default.py index 561510f..00f6379 100644 --- a/src/harness_sdk/config/default.py +++ b/src/harness_sdk/config/default.py @@ -43,7 +43,7 @@ }, 'gen_ai': { 'enabled_frameworks': [], - 'payload_capture_enabled': True, + 'payload_capture_enabled': False, 'payload_evaluation_enabled': True, }, 'plugins': { diff --git a/src/harness_sdk/env.py b/src/harness_sdk/env.py index e84d53f..e179847 100644 --- a/src/harness_sdk/env.py +++ b/src/harness_sdk/env.py @@ -14,15 +14,40 @@ def get_env_value(target_key): return None -def is_env_var_present(target_key): - """Return True if the key is set under any supported prefix (presence check).""" - return any(f"{prefix}{target_key}" in os.environ for prefix in _PREFIXES) +def is_env_flag_enabled(target_key): + """Boolean SDK flag under any supported prefix (HARNESS_ > HA_ > AT_ > TA_). + + Returns True only when the resolved value is case-insensitively 'true'. + """ + value = get_env_value(target_key) + return value is not None and value.strip().lower() == "true" def is_harness_flag_enabled(env_var_name): - """Strict opt-in flag under the HARNESS_ prefix only (no legacy aliases). + """Opt-in flag under HARNESS_ or HA_ (HARNESS_ wins when both are set). - Returns True only when the value is present and case-insensitively 'true'. + Enable flags never existed under AT_/TA_, so only those two prefixes are + honored. Returns True only when the resolved value is present and + case-insensitively 'true'. """ - value = os.environ.get(env_var_name) - return value is not None and value.strip().lower() == "true" + for key in _flag_keys(env_var_name): + if key in os.environ: + return os.environ[key].strip().lower() == "true" + return False + + +def is_enable_flag_present(env_var_name): + """Presence check for an opt-in flag under HARNESS_ or HA_.""" + return any(key in os.environ for key in _flag_keys(env_var_name)) + + +_FLAG_PREFIXES = ("HARNESS_", "HA_") + + +def _flag_keys(env_var_name): + suffix = ( + env_var_name[len("HARNESS_"):] + if env_var_name.startswith("HARNESS_") + else env_var_name + ) + return [f"{prefix}{suffix}" for prefix in _FLAG_PREFIXES] diff --git a/src/harness_sdk/instrumentation/instrumentation_definitions.py b/src/harness_sdk/instrumentation/instrumentation_definitions.py index e6fddbb..57b7419 100644 --- a/src/harness_sdk/instrumentation/instrumentation_definitions.py +++ b/src/harness_sdk/instrumentation/instrumentation_definitions.py @@ -4,7 +4,7 @@ from importlib import metadata as importlib_metadata from harness_sdk.custom_logger import get_custom_logger -from harness_sdk.env import is_harness_flag_enabled +from harness_sdk.env import is_enable_flag_present, is_harness_flag_enabled FLASK_KEY = 'flask' DJANGO_KEY = 'django' @@ -75,7 +75,7 @@ def is_library_enabled(library_key, enabled_ai_frameworks=None): """Decide whether a supported library should be instrumented based on opt-in env flags.""" if library_key in AI_LIBRARY_ENV_FLAGS: env_flag = AI_LIBRARY_ENV_FLAGS[library_key] - if env_flag in os.environ: + if is_enable_flag_present(env_flag): return is_harness_flag_enabled(env_flag) configured_frameworks = { _normalize_library_name(name) diff --git a/src/harness_sdk/plugins/builtin/pipeline.py b/src/harness_sdk/plugins/builtin/pipeline.py index 6149060..e665500 100644 --- a/src/harness_sdk/plugins/builtin/pipeline.py +++ b/src/harness_sdk/plugins/builtin/pipeline.py @@ -6,7 +6,7 @@ from harness_sdk.agent_init import AgentInit from harness_sdk.custom_logger import get_custom_logger -from harness_sdk.env import is_env_var_present +from harness_sdk.env import is_env_flag_enabled from harness_sdk.excluded_by_attribute_span_processor import ExcludeByAttributeSpanProcessor from harness_sdk.db_control_span_processor import DbControlSpanProcessor from harness_sdk.gen_ai_payload_scrub_span_processor import GenAiPayloadScrubSpanProcessor @@ -25,7 +25,7 @@ def on_init(self, config: Any) -> None: self._agent_init = AgentInit(config) def create_span_processors(self, config: Any) -> List[SpanProcessor]: - if is_env_var_present("ENABLE_CONSOLE_SPAN_EXPORTER"): + if is_env_flag_enabled("ENABLE_CONSOLE_SPAN_EXPORTER"): self._agent_init.set_console_span_processor() return [] diff --git a/test/config/test_config.py b/test/config/test_config.py index 04c45f3..7a807d8 100644 --- a/test/config/test_config.py +++ b/test/config/test_config.py @@ -65,7 +65,7 @@ def test_sdk_default_config(): gen_ai = traceable_config.gen_ai assert Config().enabled_ai_frameworks == [] assert gen_ai.enabled.value is False - assert gen_ai.payload_capture_enabled.value is True + assert gen_ai.payload_capture_enabled.value is False assert gen_ai.payload_evaluation_enabled.value is True assert len(traceable_config.span_attributes) == 0 diff --git a/test/env_test.py b/test/env_test.py new file mode 100644 index 0000000..fe23574 --- /dev/null +++ b/test/env_test.py @@ -0,0 +1,57 @@ +"""Boolean SDK flag resolution: is_env_flag_enabled across prefix aliases.""" +import os + +import pytest + +from harness_sdk.env import is_env_flag_enabled + +_KEY = "ENABLE_CONSOLE_SPAN_EXPORTER" +_PREFIXES = ("HARNESS_", "HA_", "AT_", "TA_") + + +@pytest.fixture(autouse=True) +def clear_flag(): + for prefix in _PREFIXES: + os.environ.pop(f"{prefix}{_KEY}", None) + yield + for prefix in _PREFIXES: + os.environ.pop(f"{prefix}{_KEY}", None) + + +def test_unset_flag_is_disabled(): + assert is_env_flag_enabled(_KEY) is False + + +def test_true_enables_flag(): + os.environ[f"HARNESS_{_KEY}"] = "true" + assert is_env_flag_enabled(_KEY) is True + + +def test_explicit_false_disables_flag(): + os.environ[f"HARNESS_{_KEY}"] = "false" + assert is_env_flag_enabled(_KEY) is False + + +def test_arbitrary_value_does_not_enable_flag(): + os.environ[f"HARNESS_{_KEY}"] = "1" + assert is_env_flag_enabled(_KEY) is False + os.environ[f"HARNESS_{_KEY}"] = "yes" + assert is_env_flag_enabled(_KEY) is False + + +def test_empty_value_does_not_enable_flag(): + os.environ[f"HARNESS_{_KEY}"] = "" + assert is_env_flag_enabled(_KEY) is False + + +def test_legacy_aliases_enable_flag(): + for prefix in ("HA_", "AT_", "TA_"): + os.environ[f"{prefix}{_KEY}"] = "true" + assert is_env_flag_enabled(_KEY) is True + del os.environ[f"{prefix}{_KEY}"] + + +def test_harness_prefix_wins_over_legacy_alias(): + os.environ[f"HARNESS_{_KEY}"] = "false" + os.environ[f"HA_{_KEY}"] = "true" + assert is_env_flag_enabled(_KEY) is False diff --git a/test/instrumentation/test_opt_in_gating.py b/test/instrumentation/test_opt_in_gating.py index 90a59f7..df5f71d 100644 --- a/test/instrumentation/test_opt_in_gating.py +++ b/test/instrumentation/test_opt_in_gating.py @@ -82,8 +82,19 @@ def test_flag_requires_exact_true_value(): assert is_api_instrumentation_enabled() is True -def test_ai_flags_have_no_legacy_aliases(): +def test_ai_flags_accept_ha_alias(): os.environ["HA_ENABLE_AI_OPENAI"] = "true" + assert is_library_enabled(OPENAI_KEY) is True + assert any_ai_provider_enabled() is True + + +def test_ai_flag_harness_prefix_wins_over_ha(): + os.environ["HARNESS_ENABLE_AI_OPENAI"] = "false" + os.environ["HA_ENABLE_AI_OPENAI"] = "true" + assert is_library_enabled(OPENAI_KEY) is False + + +def test_ai_flags_have_no_at_ta_aliases(): os.environ["AT_ENABLE_AI_OPENAI"] = "true" os.environ["TA_ENABLE_AI_OPENAI"] = "true" try: @@ -93,13 +104,19 @@ def test_ai_flags_have_no_legacy_aliases(): os.environ.pop(legacy, None) -def test_api_flag_has_no_legacy_aliases(): +def test_api_flag_accepts_ha_alias(): os.environ["HA_ENABLE_API"] = "true" + assert is_api_instrumentation_enabled() is True + + +def test_api_flag_has_no_at_ta_aliases(): os.environ["AT_ENABLE_API"] = "true" + os.environ["TA_ENABLE_API"] = "true" try: assert is_api_instrumentation_enabled() is False finally: - os.environ.pop("AT_ENABLE_API", None) + for legacy in ("AT_ENABLE_API", "TA_ENABLE_API"): + os.environ.pop(legacy, None) # --------------------------------------------------------------------------- #