Launch Week 02 wrapped — explore all five launches

Manage Trace Context

Create spans, update traces and spans, and set trace properties manually.

Overview

For most apps, init() is all you need. It automatically traces supported providers and frameworks, including the relationships between their calls.

Use the APIs on this page when you need more control over that trace:

  • Add spans for your own functions, tools, and application steps.
  • Set trace-level details such as input, output, users, and threads.
  • Add data to a specific span.
  • Keep auto-instrumented framework calls attached to the right trace.
  • Supply trace details before the first span starts.

The sections below show how to make these changes without replacing the instrumentation that init() already provides.

Create Spans

Auto-instrumentation captures everything the framework integrations know about, but it can't know where your request begins or which of your own functions count as tools. To mark those boundaries you create a span yourself, either around a whole function or around a specific block of code.

Around a function

from confident_trace import init, span

init()

@span(type="retriever")
def retriever(query: str):
    return retrieve(query)

@span("llm_app", type="agent")
def llm_app(query: str):
    context = retriever(query)
    return generate(query, context)

@span takes an optional positional name (defaulting to the function name), a type, and any type-specific or shared fields up front — for example @span(type="llm", model="gpt-4o"). It works on sync and async functions alike.

Around a block of code

You don't have to wrap an entire function. When you can't change a function's definition, or only want part of it to show up, trace just that block with the same arguments:

from confident_trace import span

def generate(prompt: str) -> str:
    with span("generate", type="llm", model="gpt-4o"):
        return call_llm(prompt)

span() works as a sync or async context manager (with / async with) as well as a decorator. Everything that applies to @span applies here too.

Each span nests under whatever is currently active. If nothing is active it becomes the root of a new trace, so the outermost span you create in a request handler is usually the trace itself. The type tells Confident AI how to render the span and which type-specific fields it accepts — see span types for the full list.

Update Span Properties

update_span() / updateSpan() writes span-level fields to the span that is current at the moment you call it. Reach for it when a step's default I/O isn't what you want to see, or when you have step-specific data like retrieved chunks or token counts:

from confident_trace import span, update_span

@span(type="retriever")
def retriever(query: str):
    chunks = retrieve(query)
    update_span(input=query, output=chunks, retrieval_context=chunks)
    return chunks

There's one update_span() for every span type. It accepts the shared fields (name, input, output, metadata, retrieval_context, context, expected_output, tools_called, expected_tools) plus the LLM-only fields (model, provider, token counts, and per-token costs) — the LLM fields only take effect on an llm span.

Both update helpers need an active, recording span to write to. Outside of any span, or after the span has ended, they silently do nothing. That's why a trace context must wrap an auto-instrumented call rather than an update helper being called after the call returns.

Update Trace Properties

When init() already instruments a framework call, you don't need to create a span just to add details to its trace. Open a trace context around the call instead. It doesn't create a span; it supplies fields to the trace that the instrumented call starts:

from langchain_openai import ChatOpenAI
from confident_trace import trace_context

model = ChatOpenAI(model="gpt-4o")

def chat(message: str, user_id: str, thread_id: str):
    with trace_context(user_id=user_id, thread_id=thread_id, input=message):
        return model.invoke(message)

The context works with with or async with. Every instrumented call inside it receives the fields you provide.

A trace context is best for details you know before the call starts, such as the user, thread, input, environment, tags, and metadata. Calls using the same thread ID are grouped into the same thread while each call still creates its own trace.

You'll use this same scoped pattern later to drop tracing for selected requests and to route traces dynamically to different projects.

Update from Inside a Span

If you've already created a custom span, adding a trace context around it would be redundant: the span has already started the trace. Update that active trace directly instead. This also lets you set values you only know after the work finishes, such as its final output:

from confident_trace import span, update_trace

@span("llm_app", type="agent")
def llm_app(query: str, user_id: str):
    context = retriever(query)
    res = generate(query, context)
    update_trace(
        input=query,
        output=res,
        user_id=user_id,
        tags=["production"],
        metadata={"app_version": "1.2.3"},
    )
    return res

The trace update helper always targets the root span of the current trace, so you can call it from any nested span. Use it for:

You can call it as many times as you like; later calls override earlier ones field by field, and fields you leave out stay as they were. By contrast, a trace context supplies defaults: it fills fields that haven't already been set and doesn't overwrite existing values. Compound values such as tags, metadata, and thread are never merged between the two.

Which One Should I Use?

You want to…Use
Mark where a request, tool, or retrieval step begins and ends@span / span() / withSpan()
Add known trace details without creating a spantrace_context() / traceContext()
Set or replace trace details from inside an active spanupdate_trace() / updateTrace()
Set a step's I/O, retrieval context, or LLM token usageupdate_span() / updateSpan()
Start a new trace per conversation turnturn()

For an already-instrumented framework call, start with a trace context. If you intentionally add a custom span (or turn()) around the request, update the active trace from inside that span instead.

Next Steps

Now that you know how to shape the trace context, give your spans meaning with types and set the I/O that Confident AI evaluates on.

Ready to monitor AI in production?Connect traces, alerts, dashboards, and evals in one production workflowBook a demo

Last updated on

Built byConfident AI