From c6fa8b0adc121268255ddbc6ca759feda7562f70 Mon Sep 17 00:00:00 2001 From: Shimada666 <649940882@qq.com> Date: Sat, 8 Aug 2026 20:24:03 +0800 Subject: [PATCH 1/2] feat(observability): allow selecting Laminar instruments Co-authored-by: openhands --- .../openhands/sdk/observability/laminar.py | 11 +++++++ tests/sdk/observability/test_laminar.py | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/openhands-sdk/openhands/sdk/observability/laminar.py b/openhands-sdk/openhands/sdk/observability/laminar.py index 21fb027e47..b6229ec6c0 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,25 @@ 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 = ( + {Instruments(value.strip()) for value in instruments_env.split(",")} + if instruments_env + else None + ) 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..565b863c4b 100644 --- a/tests/sdk/observability/test_laminar.py +++ b/tests/sdk/observability/test_laminar.py @@ -274,6 +274,36 @@ 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]) +def test_lmnr_instruments_passed_to_laminar(is_laminar_backend): + """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": "litellm,mcp", + }, + ): + 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.LITELLM, + Instruments.MCP, + } + + # --------------------------------------------------------------------------- # Cross-context root-span propagation # --------------------------------------------------------------------------- From 6425b2c0c9563ec7cd8bf4993121125cc9322888 Mon Sep 17 00:00:00 2001 From: Shimada666 <649940882@qq.com> Date: Mon, 10 Aug 2026 17:49:37 +0800 Subject: [PATCH 2/2] fix(observability): ignore invalid Laminar instruments Co-authored-by: openhands --- .../openhands/sdk/observability/laminar.py | 15 ++++++++++----- tests/sdk/observability/test_laminar.py | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/openhands-sdk/openhands/sdk/observability/laminar.py b/openhands-sdk/openhands/sdk/observability/laminar.py index b6229ec6c0..9353bd7242 100644 --- a/openhands-sdk/openhands/sdk/observability/laminar.py +++ b/openhands-sdk/openhands/sdk/observability/laminar.py @@ -103,11 +103,16 @@ 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 = ( - {Instruments(value.strip()) for value in instruments_env.split(",")} - if instruments_env - else None - ) + 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( diff --git a/tests/sdk/observability/test_laminar.py b/tests/sdk/observability/test_laminar.py index 565b863c4b..f6834d57c2 100644 --- a/tests/sdk/observability/test_laminar.py +++ b/tests/sdk/observability/test_laminar.py @@ -275,7 +275,16 @@ def test_lmnr_force_http_passed_to_laminar(force_http_value, expected_force_http @pytest.mark.parametrize("is_laminar_backend", [True, False]) -def test_lmnr_instruments_passed_to_laminar(is_laminar_backend): +@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 @@ -283,7 +292,7 @@ def test_lmnr_instruments_passed_to_laminar(is_laminar_backend): os.environ, { "LMNR_PROJECT_API_KEY": "test-key", - "LMNR_INSTRUMENTS": "litellm,mcp", + "LMNR_INSTRUMENTS": instruments_env, }, ): with ( @@ -299,9 +308,10 @@ def test_lmnr_instruments_passed_to_laminar(is_laminar_backend): maybe_init_laminar() assert mock_laminar.initialize.call_args.kwargs["instruments"] == { - Instruments.LITELLM, - Instruments.MCP, + Instruments(value) for value in expected } + if invalid: + assert f"Ignoring invalid LMNR_INSTRUMENTS value '{invalid}'" in caplog.text # ---------------------------------------------------------------------------