Skip to content
Merged
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
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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()`.

Expand Down
4 changes: 3 additions & 1 deletion docs/sdk-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) |
Expand Down
4 changes: 2 additions & 2 deletions src/harness_sdk/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/harness_sdk/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
},
'gen_ai': {
'enabled_frameworks': [],
'payload_capture_enabled': True,
'payload_capture_enabled': False,
'payload_evaluation_enabled': True,
},
'plugins': {
Expand Down
39 changes: 32 additions & 7 deletions src/harness_sdk/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/harness_sdk/plugins/builtin/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 []

Expand Down
2 changes: 1 addition & 1 deletion test/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions test/env_test.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 20 additions & 3 deletions test/instrumentation/test_opt_in_gating.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)


# --------------------------------------------------------------------------- #
Expand Down
Loading