Skip to content
Closed
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
21 changes: 20 additions & 1 deletion src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,30 @@ def _get_span_type(span: Span) -> str:
return "STANDALONE"


def _active_otel_trace_id() -> str | None:
"""Active OpenTelemetry/W3C trace id as 32-char hex, or None when OTel is absent or no span is recording."""
try:
from opentelemetry import trace as otel_trace
except ImportError:
return None
context = otel_trace.get_current_span().get_span_context()
if not context.is_valid:
return None
return format(context.trace_id, "032x")


def _resolve_trace_id(agentex_trace_id: str) -> str:
"""Prefer the ambient W3C/OTel trace id so agent spans share the request's observability trace, else keep agentex's own id."""
return _active_otel_trace_id() or agentex_trace_id


def _add_source_to_span(span: Span, env_vars: EnvironmentVariables) -> None:
if span.data is None:
span.data = {}
if isinstance(span.data, dict):
span.data["__source__"] = "agentex"
# Preserve agentex's own id so the task linkage survives when the SGP trace_id adopts the W3C id.
span.data["__agentex_trace_id__"] = span.trace_id
if env_vars.ACP_TYPE is not None:
span.data["__acp_type__"] = env_vars.ACP_TYPE
if env_vars.AGENT_NAME is not None:
Expand All @@ -77,7 +96,7 @@ def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan:
span_type=_get_span_type(span),
span_id=span.id,
parent_id=span.parent_id,
trace_id=span.trace_id,
trace_id=_resolve_trace_id(span.trace_id),
input=span.input,
output=span.output,
metadata=span.data,
Expand Down
63 changes: 63 additions & 0 deletions tests/lib/core/tracing/processors/test_sgp_tracing_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,66 @@ def test_falsy_values_restore_span_start(self, monkeypatch, val):
def test_other_values_keep_skip_enabled(self, monkeypatch, val):
monkeypatch.setenv("AGENTEX_TRACING_SKIP_SPAN_START", val)
assert self._fn()() is True


class TestResolveTraceId:
def _mod(self):
import agentex.lib.core.tracing.processors.sgp_tracing_processor as m

return m

def test_prefers_active_otel_trace_id(self, monkeypatch):
m = self._mod()
monkeypatch.setattr(m, "_active_otel_trace_id", lambda: "4bf92f3577b34da6a3ce929d0e0e4736")
assert m._resolve_trace_id("task-123") == "4bf92f3577b34da6a3ce929d0e0e4736"

def test_falls_back_to_agentex_id_without_otel(self, monkeypatch):
m = self._mod()
monkeypatch.setattr(m, "_active_otel_trace_id", lambda: None)
assert m._resolve_trace_id("task-123") == "task-123"

def test_active_otel_trace_id_none_without_span(self):
pytest.importorskip("opentelemetry")
assert self._mod()._active_otel_trace_id() is None

def test_active_otel_trace_id_returns_w3c_hex(self):
otel_trace = pytest.importorskip("opentelemetry.trace")
otel_context = pytest.importorskip("opentelemetry.context")

span_context = otel_trace.SpanContext(
trace_id=0x4BF92F3577B34DA6A3CE929D0E0E4736,
span_id=0x00F067AA0BA902B7,
is_remote=False,
trace_flags=otel_trace.TraceFlags(otel_trace.TraceFlags.SAMPLED),
)
token = otel_context.attach(otel_trace.set_span_in_context(otel_trace.NonRecordingSpan(span_context)))
try:
resolved = self._mod()._active_otel_trace_id()
finally:
otel_context.detach(token)
assert resolved == "4bf92f3577b34da6a3ce929d0e0e4736"


class TestBuildSgpSpanTraceId:
def _build(self, monkeypatch, otel_id):
import agentex.lib.core.tracing.processors.sgp_tracing_processor as m

monkeypatch.setattr(m, "_active_otel_trace_id", lambda: otel_id)
captured = {}
monkeypatch.setattr(m, "create_span", lambda **kw: captured.update(kw) or _make_mock_sgp_span())
env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None)
span = _make_span()
m._build_sgp_span(span, env)
return captured, span

def test_adopts_otel_trace_id_and_preserves_agentex_id(self, monkeypatch):
captured, span = self._build(monkeypatch, "4bf92f3577b34da6a3ce929d0e0e4736")
assert captured["trace_id"] == "4bf92f3577b34da6a3ce929d0e0e4736"
assert isinstance(span.data, dict)
assert span.data["__agentex_trace_id__"] == "trace-1"

def test_keeps_agentex_trace_id_without_otel(self, monkeypatch):
captured, span = self._build(monkeypatch, None)
assert captured["trace_id"] == "trace-1"
assert isinstance(span.data, dict)
assert span.data["__agentex_trace_id__"] == "trace-1"
Loading