Skip to Content
RiusSDKTracing your code

Tracing your code

The Rius SDK gives you three levels of control: the @observe decorator for whole functions, generic spans for arbitrary steps, and generations for LLM calls.

The @observe decorator

Decorate any function to trace each call as a span:

import glassflow from glassflow import SpanKind @glassflow.observe def plan(query: str) -> str: ... @glassflow.observe(name="rerank", kind=SpanKind.RETRIEVER, capture_output=False) def rerank(docs: list[str]) -> list[str]: ...
  • name defaults to the function’s qualified name.
  • kind defaults to SpanKind.CHAIN; it sets the span taxonomy (AGENT, LLM, TOOL, RETRIEVER, EMBEDDING, CHAIN).
  • capture_input / capture_output (default true) record the arguments and return value as JSON in input.value / output.value. Values are serialized with a repr fallback and truncated at 8192 characters.
  • Exceptions are recorded on the span with ERROR status and always re-raised.

Sync functions, async def functions, generators, and async generators are all supported. For generators the span covers the whole iteration, from first call to exhaustion, and the tracing context is attached only around each step, so it never leaks into the code consuming the generator between yields.

Generic spans

For steps that are not a single function, open spans directly. There are two lifecycles:

# Context manager: becomes the current span, children nest under it, # exceptions are recorded, and it ends automatically. with glassflow.start_as_current_span("retrieve", kind=SpanKind.RETRIEVER) as obs: obs.set_input(query) docs = search(query) obs.set_output([d.id for d in docs]) # Manual: parented to the current span at creation, but NOT made current. # You own the lifetime and must call end(); exceptions are not auto-recorded. obs = glassflow.start_span("background-job") try: obs.set_attribute("job.id", job_id) ... finally: obs.end()

Both return an Observation handle with set_input(), set_output(), set_attribute(), update(input=..., output=...), and end().

Use the manual form when the span outlives a lexical scope, for example a span that ends in a callback. Use the context-manager form for everything else.

Generations (LLM calls)

Generations are spans with LLM semantics. They emit OpenTelemetry GenAI attributes that Rius uses for model analytics and cost computation:

with glassflow.start_as_current_generation( "chat gpt-4o", model="gpt-4o", provider="openai", model_parameters={"temperature": 0.2}, ) as gen: gen.set_input(messages) response = client.chat.completions.create(model="gpt-4o", messages=messages) gen.set_output(response.choices[0].message.content) gen.set_response_model(response.model) gen.set_usage( input_tokens=response.usage.prompt_tokens, output_tokens=response.usage.completion_tokens, ) gen.set_finish_reasons(response.choices[0].finish_reason)

start_generation() is the manual-lifecycle twin, with the same semantics as start_span().

The attributes emitted:

AttributeSource
gen_ai.request.modelmodel= argument
gen_ai.provider.nameprovider= argument
gen_ai.request.<param>each key of model_parameters
gen_ai.input.messages / gen_ai.output.messagesset_input() / set_output()
gen_ai.usage.input_tokens / gen_ai.usage.output_tokensset_usage()
gen_ai.response.modelset_response_model()
gen_ai.response.finish_reasonsset_finish_reasons()
gen_ai.operation.nameoperation= argument (default chat)

Send token usage, never cost: Rius computes cost server-side from model pricing, so your instrumentation stays free of pricing tables.

Streaming responses

When you stream, total duration stops being a useful latency signal: a long generation usually just means a long answer. The number streaming users feel is time to first token, and the SDK records it with one call from your streaming loop:

gen = glassflow.start_generation("chat gpt-4o", model="gpt-4o", provider="openai") gen.set_input(messages) chunks = [] stream = client.chat.completions.create(model="gpt-4o", messages=messages, stream=True) for chunk in stream: gen.record_first_token() # idempotent; call it unconditionally per chunk if chunk.choices and chunk.choices[0].delta.content: chunks.append(chunk.choices[0].delta.content) gen.set_output("".join(chunks)) gen.end()

record_first_token() marks the moment the first chunk arrives (as a gen_ai.first_token span event). Only the first call records anything, so guarding it yourself is unnecessary. Rius derives time to first token from the event, which unlocks per-model TTFT percentiles and inter-token latency in your analytics.

Requires glassflow-ai 0.5.0 or newer. If you use the openai auto-instrumentation instead of manual generations, streamed OpenAI calls get a first-token marker automatically.

Message formats

set_input() and set_output() normalize whatever you pass into the GenAI message shape ({"role", "parts": [...]}):

  • a bare string becomes a single text part (role user on input, assistant on output)
  • OpenAI-style dicts ({"role", "content"}) are converted, including tool_calls arrays and role: "tool" responses
  • multimodal content lists are converted part by part
  • anything else is serialized to a JSON text part

You do not need to pre-shape messages; pass what your client library gives you.

Next steps

Last updated on