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]:
...namedefaults to the function’s qualified name.kinddefaults toSpanKind.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 ininput.value/output.value. Values are serialized with areprfallback 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:
| Attribute | Source |
|---|---|
gen_ai.request.model | model= argument |
gen_ai.provider.name | provider= argument |
gen_ai.request.<param> | each key of model_parameters |
gen_ai.input.messages / gen_ai.output.messages | set_input() / set_output() |
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens | set_usage() |
gen_ai.response.model | set_response_model() |
gen_ai.response.finish_reasons | set_finish_reasons() |
gen_ai.operation.name | operation= 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
useron input,assistanton output) - OpenAI-style dicts (
{"role", "content"}) are converted, includingtool_callsarrays androle: "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
- Auto-instrumentation: let the SDK create generations for you.
- Advanced features: sampling and privacy controls over what you just captured.