Skip to content

Commit 2a422aa

Browse files
Zach Arnoldclaude
andcommitted
fix: patch TracerManager.init defaults to set correct OTEL service name
The previous sys.argv[0] override had no effect because Python evaluates default arguments at import time — TracerManager.init(app_name=sys.argv[0]) captured the binary path (/usr/local/bin/openhands-agent-server) when the module was first loaded. Now we patch __defaults__ directly so the correct OTEL_SERVICE_NAME is used when Laminar.initialize() calls TracerManager.init(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a406b52 commit 2a422aa

File tree

2 files changed

+108
-7
lines changed

2 files changed

+108
-7
lines changed

openhands-sdk/openhands/sdk/observability/laminar.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import sys
32
from collections.abc import Callable
43
from typing import (
54
Any,
@@ -43,12 +42,19 @@ def maybe_init_laminar():
4342
```
4443
"""
4544
if should_enable_observability():
46-
# Laminar.initialize() doesn't expose app_name but internally calls
47-
# TracerManager.init(app_name=sys.argv[0]). Override sys.argv[0]
48-
# temporarily so the OTEL service.name resource attribute is correct.
45+
# Laminar.initialize() calls TracerManager.init() without passing
46+
# app_name, so it falls back to the default: sys.argv[0]. However,
47+
# Python evaluates default arguments at *import time*, so the default
48+
# is baked in as the binary path (e.g. /usr/local/bin/openhands-agent-server)
49+
# before we can override sys.argv[0].
50+
#
51+
# Fix: patch TracerManager.init's default for app_name directly.
4952
service_name = os.environ.get('OTEL_SERVICE_NAME', 'openhands-agent-server')
50-
saved_argv0 = sys.argv[0]
51-
sys.argv[0] = service_name
53+
from lmnr.opentelemetry_lib import TracerManager
54+
55+
_orig_defaults = TracerManager.init.__defaults__
56+
# app_name is the first default (position 0)
57+
TracerManager.init.__defaults__ = (service_name,) + _orig_defaults[1:]
5258
try:
5359
if _is_otel_backend_laminar():
5460
Laminar.initialize()
@@ -62,7 +68,7 @@ def maybe_init_laminar():
6268
],
6369
)
6470
finally:
65-
sys.argv[0] = saved_argv0
71+
TracerManager.init.__defaults__ = _orig_defaults
6672
# Wrap the TracerProvider's sampler to drop health-check spans.
6773
# This is the OTEL equivalent of DD_TRACE_SAMPLING_RULES.
6874
provider = trace.get_tracer_provider()
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Test that maybe_init_laminar sets the correct OTEL service.name.
2+
3+
Regression test for the bug where Laminar's TracerManager.init captured
4+
sys.argv[0] at *import time* (the binary path, e.g.
5+
/usr/local/bin/openhands-agent-server) and ignored the OTEL_SERVICE_NAME
6+
env var.
7+
"""
8+
9+
import subprocess
10+
import sys
11+
import textwrap
12+
13+
14+
def test_service_name_uses_otel_env_var_not_argv():
15+
"""service.name should come from OTEL_SERVICE_NAME, not sys.argv[0].
16+
17+
Run in a subprocess to get a clean Python interpreter — OTEL and Laminar
18+
are singleton-heavy and impossible to reset reliably in-process.
19+
"""
20+
script = textwrap.dedent("""\
21+
import os, sys
22+
23+
# Simulate the container: argv[0] is the installed console-script path
24+
sys.argv[0] = "/usr/local/bin/openhands-agent-server"
25+
26+
os.environ["OTEL_SERVICE_NAME"] = "openhands-agent-server"
27+
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = "http://localhost:4317"
28+
29+
from openhands.sdk.observability.laminar import maybe_init_laminar
30+
maybe_init_laminar()
31+
32+
from opentelemetry import trace
33+
from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
34+
from opentelemetry.sdk.resources import SERVICE_NAME
35+
36+
provider = trace.get_tracer_provider()
37+
assert isinstance(provider, SdkTracerProvider), (
38+
f"Expected SdkTracerProvider, got {type(provider)}"
39+
)
40+
service = provider.resource.attributes.get(SERVICE_NAME)
41+
assert service == "openhands-agent-server", (
42+
f"Expected 'openhands-agent-server', got '{service}'"
43+
)
44+
# Must NOT contain the binary path
45+
assert "/usr/local/bin" not in str(service), (
46+
f"service.name leaked binary path: {service}"
47+
)
48+
print("OK")
49+
""")
50+
result = subprocess.run(
51+
[sys.executable, '-c', script],
52+
capture_output=True,
53+
text=True,
54+
timeout=30,
55+
)
56+
assert result.returncode == 0, (
57+
f'Subprocess failed:\nstdout: {result.stdout}\nstderr: {result.stderr}'
58+
)
59+
assert 'OK' in result.stdout
60+
61+
62+
def test_custom_service_name():
63+
"""A non-default OTEL_SERVICE_NAME should be respected."""
64+
script = textwrap.dedent("""\
65+
import os, sys
66+
67+
sys.argv[0] = "/usr/local/bin/openhands-agent-server"
68+
os.environ["OTEL_SERVICE_NAME"] = "my-custom-service"
69+
os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] = "http://localhost:4317"
70+
71+
from openhands.sdk.observability.laminar import maybe_init_laminar
72+
maybe_init_laminar()
73+
74+
from opentelemetry import trace
75+
from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
76+
from opentelemetry.sdk.resources import SERVICE_NAME
77+
78+
provider = trace.get_tracer_provider()
79+
assert isinstance(provider, SdkTracerProvider)
80+
service = provider.resource.attributes.get(SERVICE_NAME)
81+
assert service == "my-custom-service", (
82+
f"Expected 'my-custom-service', got '{service}'"
83+
)
84+
print("OK")
85+
""")
86+
result = subprocess.run(
87+
[sys.executable, '-c', script],
88+
capture_output=True,
89+
text=True,
90+
timeout=30,
91+
)
92+
assert result.returncode == 0, (
93+
f'Subprocess failed:\nstdout: {result.stdout}\nstderr: {result.stderr}'
94+
)
95+
assert 'OK' in result.stdout

0 commit comments

Comments
 (0)