Skip to content
Open
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
19 changes: 19 additions & 0 deletions packages/traceloop-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

Traceloop’s Python SDK allows you to easily start monitoring and debugging your LLM execution. Tracing is done in a non-intrusive way, built on top of OpenTelemetry. You can choose to export the traces to Traceloop, or to your existing observability stack.

For agent-to-agent HTTP calls, use the opt-in W3C helpers to carry the current
trace across service boundaries:

```python
from opentelemetry import trace
from traceloop.sdk import inject_trace_context, extract_trace_context

headers = inject_trace_context()
remote_context = extract_trace_context(request.headers)
tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("handle-agent-request", context=remote_context):
handle_request()
```
Comment thread
Whxuan0701 marked this conversation as resolved.

The helpers use the application's configured global propagator. OpenTelemetry
defaults to W3C Trace Context and Baggage; custom propagators passed through
`Traceloop.init(propagator=...)` remain supported.

```python
Traceloop.init(app_name="joke_generation_service")

Expand Down
36 changes: 36 additions & 0 deletions packages/traceloop-sdk/tests/test_propagation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from opentelemetry import context, trace
from opentelemetry.sdk.trace import TracerProvider

from traceloop.sdk.propagation import extract_trace_context, inject_trace_context


def test_inject_and_extract_trace_context_round_trip():
provider = TracerProvider()
tracer = provider.get_tracer("test")
carrier = {}

with tracer.start_as_current_span("parent") as span:
inject_trace_context(carrier)
extracted = extract_trace_context(carrier)
extracted_span = trace.get_current_span(extracted)

assert carrier["traceparent"].startswith("00-")
assert f"-{span.get_span_context().span_id:016x}-" in carrier["traceparent"]
assert extracted_span.get_span_context().trace_id == span.get_span_context().trace_id


def test_inject_accepts_an_existing_carrier():
carrier = {"x-request-id": "request-1"}
result = inject_trace_context(carrier)

assert result is carrier
assert result["x-request-id"] == "request-1"


def test_extract_does_not_mutate_current_context():
carrier = {}
current = context.get_current()

extracted = extract_trace_context(carrier)

assert extracted is not current
16 changes: 16 additions & 0 deletions packages/traceloop-sdk/tests/test_sdk_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from traceloop.sdk.tracing.tracing import TracerWrapper
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, BatchSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.propagate import get_global_textmap, set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator


@pytest.fixture
Expand Down Expand Up @@ -320,6 +322,20 @@ def test_use_attributes_defaults_to_true(isolated_tracer_wrapper):
assert OpenAIConfig.use_legacy_attributes is True
assert AnthropicConfig.use_legacy_attributes is True


def test_init_without_propagator_preserves_global_configuration(isolated_tracer_wrapper):
original_propagator = get_global_textmap()
configured_propagator = CompositePropagator([])
set_global_textmap(configured_propagator)

try:
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True)

assert get_global_textmap() is configured_propagator
finally:
set_global_textmap(original_propagator)


def test_both_exporter_and_processor_warns():
"""Passing both exporter and processor is a mistake — the processor already wraps
the exporter internally. We warn instead of silently dropping the exporter, and
Expand Down
4 changes: 4 additions & 0 deletions packages/traceloop-sdk/traceloop/sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
from typing import Dict
from traceloop.sdk.client.client import Client
from traceloop.sdk.associations.associations import AssociationProperty as AssociationProperty
from traceloop.sdk.propagation import (
extract_trace_context as extract_trace_context,
inject_trace_context as inject_trace_context,
)


class Traceloop:
Expand Down
19 changes: 19 additions & 0 deletions packages/traceloop-sdk/traceloop/sdk/propagation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Opt-in W3C trace-context helpers for agent-to-agent calls."""

from typing import Any, MutableMapping

from opentelemetry import propagate
from opentelemetry.context import Context


def inject_trace_context(carrier: MutableMapping[str, Any] | None = None) -> MutableMapping[str, Any]:
"""Inject the current W3C trace context into an HTTP-like carrier."""
target: MutableMapping[str, Any] = carrier if carrier is not None else {}
propagate.inject(target)
return target


def extract_trace_context(carrier: Any) -> Context:
"""Extract a remote W3C trace context without changing the current context."""
return propagate.extract(carrier)

2 changes: 1 addition & 1 deletion packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def wrapped_on_end(span):
obj.__spans_processor.on_start = obj._span_processor_on_start
obj.__tracer_provider.add_span_processor(obj.__spans_processor)

if propagator:
if propagator is not None:
set_global_textmap(propagator)

# this makes sure otel context is propagated so we always want it
Expand Down