diff --git a/openhands-sdk/openhands/sdk/observability/laminar.py b/openhands-sdk/openhands/sdk/observability/laminar.py index 21fb027e47..9353bd7242 100644 --- a/openhands-sdk/openhands/sdk/observability/laminar.py +++ b/openhands-sdk/openhands/sdk/observability/laminar.py @@ -84,6 +84,9 @@ def maybe_init_laminar(): To force HTTP instead of gRPC for Laminar communication: LMNR_FORCE_HTTP=true # or 1, yes, on + + To initialize only selected Laminar integrations: + LMNR_INSTRUMENTS=litellm,mcp """ if not should_enable_observability(): logger.debug( @@ -99,17 +102,30 @@ def maybe_init_laminar(): base_url = get_env("LMNR_BASE_URL") or None force_http = _get_bool_env("LMNR_FORCE_HTTP") + instruments_env = get_env("LMNR_INSTRUMENTS") + instruments = None + if instruments_env is not None: + instruments = set() + for value in map(str.strip, instruments_env.split(",")): + if not value: + continue + try: + instruments.add(Instruments(value)) + except ValueError: + logger.warning("Ignoring invalid LMNR_INSTRUMENTS value %r", value) if _is_otel_backend_laminar(): Laminar.initialize( base_url=base_url, http_port=_get_int_env("LMNR_HTTP_PORT"), grpc_port=_get_int_env("LMNR_GRPC_PORT"), + instruments=instruments, force_http=force_http, ) else: # Do not enable browser session replays for non-laminar backends Laminar.initialize( + instruments=instruments, disabled_instruments=[ Instruments.BROWSER_USE_SESSION, Instruments.PATCHRIGHT, diff --git a/tests/sdk/observability/test_laminar.py b/tests/sdk/observability/test_laminar.py index 60c1a8b254..f6834d57c2 100644 --- a/tests/sdk/observability/test_laminar.py +++ b/tests/sdk/observability/test_laminar.py @@ -274,6 +274,46 @@ def test_lmnr_force_http_passed_to_laminar(force_http_value, expected_force_http del os.environ["LMNR_FORCE_HTTP"] +@pytest.mark.parametrize("is_laminar_backend", [True, False]) +@pytest.mark.parametrize( + ("instruments_env", "expected", "invalid"), + [ + ("litellm,mcp", {"litellm", "mcp"}, None), + ("litellm,,unsupported,", {"litellm"}, "unsupported"), + ], +) +def test_lmnr_instruments_passed_to_laminar( + is_laminar_backend, instruments_env, expected, invalid, caplog +): + """LMNR_INSTRUMENTS limits Laminar to the selected integrations.""" + from lmnr import Instruments + + with patch.dict( + os.environ, + { + "LMNR_PROJECT_API_KEY": "test-key", + "LMNR_INSTRUMENTS": instruments_env, + }, + ): + with ( + patch("lmnr.Laminar") as mock_laminar, + patch( + "openhands.sdk.observability.laminar._is_otel_backend_laminar", + return_value=is_laminar_backend, + ), + ): + mock_laminar.is_initialized.return_value = False + from openhands.sdk.observability.laminar import maybe_init_laminar + + maybe_init_laminar() + + assert mock_laminar.initialize.call_args.kwargs["instruments"] == { + Instruments(value) for value in expected + } + if invalid: + assert f"Ignoring invalid LMNR_INSTRUMENTS value '{invalid}'" in caplog.text + + # --------------------------------------------------------------------------- # Cross-context root-span propagation # ---------------------------------------------------------------------------