Skip to content
Draft
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
3 changes: 2 additions & 1 deletion src/rius/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .observe import observe
from .semconv import SpanKind
from .session import session
from .spans import Observation, start_as_current_span, start_span
from .spans import Observation, current_trace_id, start_as_current_span, start_span

__all__ = [
"Generation",
Expand All @@ -20,6 +20,7 @@
"SpanKind",
"__version__",
"build_span_exporter",
"current_trace_id",
"get_tracer",
"init",
"observe",
Expand Down
23 changes: 22 additions & 1 deletion src/rius/spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

``start_generation`` / ``start_as_current_generation`` are the LLM-specialized
equivalents.

``current_trace_id`` is the read side of the same context: it reports the trace
the caller is already running inside, without creating anything.
"""

from __future__ import annotations
Expand All @@ -20,7 +23,7 @@
from typing import Any

from opentelemetry import trace
from opentelemetry.trace import Span
from opentelemetry.trace import Span, format_trace_id

from . import __version__
from ._serde import serialize
Expand Down Expand Up @@ -131,3 +134,21 @@ def start_as_current_span(
observation = Observation(span)
_configure(observation, kind, input)
yield observation


def current_trace_id() -> str | None:
"""Return the trace id of the caller's active trace, or ``None`` if untraced.

Reads the ambient OpenTelemetry context, so it reports whatever trace the
caller is running inside — the enclosing ``start_as_current_span`` /
``@observe`` scope, or a context propagated in from upstream — not a trace
this SDK created. The id is the standard 32-character lowercase hex form,
ready to hand to the platform API or embed in a link.

``None`` means there is genuinely no active trace (no span in context, or a
sampled-out placeholder), rather than the all-zeros invalid id.
"""
span_context = trace.get_current_span().get_span_context()
if not span_context.is_valid:
return None
return format_trace_id(span_context.trace_id)
36 changes: 34 additions & 2 deletions tests/test_spans.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re

import pytest
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.trace import StatusCode
from opentelemetry.trace import INVALID_SPAN, StatusCode, format_trace_id, use_span

from rius import start_as_current_span, start_span
from rius import current_trace_id, start_as_current_span, start_span
from rius.semconv import SpanKind

# --- context manager: start_as_current_span ---
Expand Down Expand Up @@ -74,3 +76,33 @@ def test_manual_span_is_not_current(exported_spans: InMemorySpanExporter) -> Non
obs.end()
spans = {s.name: s for s in exported_spans.get_finished_spans()}
assert spans["inner"].parent is None # manual span is not activated as current


# --- ambient trace id: current_trace_id ---


def test_current_trace_id_inside_span_matches_span_context(
exported_spans: InMemorySpanExporter,
) -> None:
with start_as_current_span("op"):
trace_id = current_trace_id()
span = exported_spans.get_finished_spans()[0]
assert trace_id == format_trace_id(span.context.trace_id)
assert re.fullmatch(r"[0-9a-f]{32}", trace_id or "")


def test_current_trace_id_nested_span_reports_the_enclosing_trace() -> None:
with start_as_current_span("outer"):
outer = current_trace_id()
with start_as_current_span("inner"):
inner = current_trace_id()
assert inner == outer


def test_current_trace_id_outside_any_span_is_none() -> None:
assert current_trace_id() is None


def test_current_trace_id_ignores_the_invalid_span() -> None:
with use_span(INVALID_SPAN):
assert current_trace_id() is None