Swiftide tracing with Future AGI
Trace a Swiftide (Rust) agent in Future AGI Observe with Swiftide's own tracing spans, the Rust OpenTelemetry SDK and a standard OpenTelemetry Collector. Agent, model and tool spans with span types, model names and token counts; message content off by default.
Trace a Swiftide agent in Future AGI Observe: each agent.query call becomes one trace with typed agent, model and tool spans, the model name and the token counts of every model call.
Swiftide instruments itself with Rust tracing spans and records the model, token usage, input and output as structured fields. Your agent exports these spans with the Rust OpenTelemetry SDK to an OpenTelemetry Collector on the same host. The Collector maps the fields to Future AGI’s span attributes, removes message content and forwards the result to Future AGI. Your Future AGI keys live only in the Collector.
This guide uses Swiftide 0.33.0 (Rust 1.97 or newer) with an OpenAI-compatible model endpoint, and OpenTelemetry Collector Contrib (otelcol-contrib) 0.164.0.
Set up
Add the dependencies
[dependencies]
swiftide = { version = "=0.33.0", features = ["swiftide-agents", "openai"] }
# Record model, usage, input and output fields on Swiftide's spans. Adds no exporter.
swiftide-agents = { version = "=0.33.0", features = ["langfuse"] }
swiftide-integrations = { version = "=0.33.0", features = ["langfuse"] }
async-openai = { version = "=0.41.3", default-features = false }
anyhow = "1"
schemars = "1"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-opentelemetry = "=0.34.0"
opentelemetry = { version = "=0.33.1", default-features = false, features = ["trace"] }
opentelemetry_sdk = { version = "=0.33.1", default-features = false, features = ["trace"] }
opentelemetry-otlp = { version = "=0.33.1", default-features = false, features = ["trace", "http-proto", "reqwest-blocking-client", "reqwest-rustls"] } The langfuse feature of swiftide-agents and swiftide-integrations makes Swiftide record model, usage, input and output fields on its spans. It adds no exporter and sends nothing to Langfuse. Do not enable the langfuse feature of the swiftide crate itself: that one adds Swiftide’s Langfuse exporter.
Keep tracing-opentelemetry and the three opentelemetry crates on matching releases: tracing-opentelemetry 0.34 is built for opentelemetry 0.33, and a mismatch fails to compile.
Start the Collector
Install OpenTelemetry Collector Contrib (the core distribution has no transform or filter processor) and save this configuration as collector.yaml:
# Run on the same host as the agent. The agent sends raw Swiftide fields to this receiver.
receivers:
otlp:
protocols:
http:
endpoint: 127.0.0.1:4318
processors:
# Only spans from the swiftide-agent tracer are forwarded. Spans with links are dropped.
filter/scope:
error_mode: propagate
traces:
span:
- 'instrumentation_scope.name != "swiftide-agent"'
- 'Len(span.links) > 0'
# Map Swiftide's structured fields to Future AGI attributes and drop everything else.
transform/swiftide:
error_mode: propagate
trace_statements:
# Span types.
- context: span
statements:
- keep_keys(span.attributes, ["langfuse.type", "step_count"])
- set(span.attributes["fi.span.kind"], "CHAIN")
- set(span.attributes["fi.span.kind"], "AGENT") where IsMatch(span.name, "^agent\\.")
- set(span.attributes["fi.span.kind"], "TOOL") where IsMatch(span.name, "^tool\\.")
- set(span.attributes["fi.span.kind"], "LLM")
where span.attributes["langfuse.type"] == "GENERATION"
# Model name and token counts of each model call.
- context: spanevent
conditions:
- span.attributes["fi.span.kind"] == "LLM"
statements:
- set(span.attributes["llm.model_name"], spanevent.attributes["langfuse.model"])
where spanevent.attributes["langfuse.model"] != nil
- set(spanevent.cache["usage"], ParseJSON(spanevent.attributes["langfuse.usage"]))
where IsString(spanevent.attributes["langfuse.usage"])
and spanevent.attributes["langfuse.usage"] != ""
- set(span.attributes["llm.token_count.prompt"],
Int(spanevent.cache["usage"]["prompt_tokens"]))
where spanevent.cache["usage"]["prompt_tokens"] != nil
- set(span.attributes["llm.token_count.completion"],
Int(spanevent.cache["usage"]["completion_tokens"]))
where spanevent.cache["usage"]["completion_tokens"] != nil
- set(span.attributes["llm.token_count.total"],
Int(spanevent.cache["usage"]["total_tokens"]))
where spanevent.cache["usage"]["total_tokens"] != nil
# Inputs and outputs, only when FI_CAPTURE_CONTENT=true.
- context: spanevent
conditions:
- '"${env:FI_CAPTURE_CONTENT:-false}" == "true"'
statements:
- set(span.attributes["input.value"], spanevent.attributes["langfuse.input"])
where IsString(spanevent.attributes["langfuse.input"])
and spanevent.attributes["langfuse.input"] != ""
- set(span.attributes["output.value"], spanevent.attributes["langfuse.output"])
where IsString(spanevent.attributes["langfuse.output"])
and spanevent.attributes["langfuse.output"] != ""
# Keep mapped attributes; clear error status messages and strip backtraces.
- context: span
statements:
- keep_keys(span.attributes, ["fi.span.kind", "step_count", "llm.model_name",
"llm.token_count.prompt", "llm.token_count.completion",
"llm.token_count.total", "input.value", "output.value"])
- replace_pattern(span.attributes["input.value"], "(?s)Stack backtrace:.*", "")
where span.attributes["input.value"] != nil
- replace_pattern(span.attributes["output.value"], "(?s)Stack backtrace:.*", "")
where span.attributes["output.value"] != nil
- set(span.status.message, "")
- set(span.trace_state, "")
- keep_keys(instrumentation_scope.attributes, [])
- keep_keys(resource.attributes, ["service.name"])
- set(resource.attributes["project_name"], "${env:FI_PROJECT_NAME}")
- set(resource.attributes["project_type"], "observe")
# Swiftide's log events carry message text, so none are forwarded.
filter/events:
error_mode: propagate
traces:
spanevent:
- 'true'
batch:
timeout: 1s
exporters:
otlp_http/futureagi:
traces_endpoint: ${env:FI_TRACES_ENDPOINT:-https://api.futureagi.com/tracer/v1/traces}
headers:
X-Api-Key: ${env:FI_API_KEY}
X-Secret-Key: ${env:FI_SECRET_KEY}
compression: none
service:
telemetry:
logs:
level: error
metrics:
level: none
pipelines:
traces:
receivers: [otlp]
processors: [filter/scope, transform/swiftide, filter/events, batch]
exporters: [otlp_http/futureagi] export FI_API_KEY="YOUR_API_KEY" FI_SECRET_KEY="YOUR_SECRET_KEY" FI_PROJECT_NAME=swiftide-agent
# Self-hosted: export FI_TRACES_ENDPOINT="<your Future AGI URL>/tracer/v1/traces"
otelcol-contrib --config collector.yaml
| Variable | Value |
|---|---|
FI_API_KEY, FI_SECRET_KEY | Both keys. The Collector sends them as the X-Api-Key and X-Secret-Key headers; one of them alone is rejected. |
FI_PROJECT_NAME | The Observe project the traces go to. |
FI_TRACES_ENDPOINT | Optional. Defaults to https://api.futureagi.com/tracer/v1/traces. |
FI_CAPTURE_CONTENT | Optional. true sends prompts, answers, tool arguments and tool results. See Privacy. |
Keep the receiver on 127.0.0.1. The agent sends Swiftide’s raw fields, including message text, to the Collector, and the Collector is where they are removed.
Export Swiftide's spans to the Collector
fn main() -> Result<()> {
// Raw fields go only to the local Collector, never directly to Future AGI.
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_endpoint("http://127.0.0.1:4318/v1/traces")
.build()?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(
Resource::builder()
.with_service_name("swiftide-agent")
.build(),
)
.build();
tracing_subscriber::registry()
.with(EnvFilter::new(
"info,swiftide_agents=debug,swiftide_integrations=debug",
))
.with(
tracing_opentelemetry::layer()
.with_tracer(provider.tracer("swiftide-agent"))
.with_location(false)
.with_threads(false),
)
.try_init()?;
let question = std::env::args()
.nth(1)
.unwrap_or_else(|| "What is the weather in Paris?".into());
let runtime = tokio::runtime::Runtime::new()?;
let outcome = runtime.block_on(run_agent(question));
drop(runtime);
// Flush even when the agent fails; keep shutdown outside the async runtime.
if let Err(err) = provider.shutdown() {
eprintln!("tracer shutdown: {err}");
}
outcome
} run_agent builds an ordinary Swiftide agent (Agent::builder().llm(&openai).tools(vec![get_weather()])...query(question)), see the complete src/main.rs.
- Filter. Swiftide records the model, usage, input and output at DEBUG level, so the filter enables DEBUG for
swiftide_agentsandswiftide_integrations. Do not enable TRACE: at that level Swiftide logs whole provider requests and responses. - Tracer name. The Collector forwards only spans from the
swiftide-agenttracer. Use the same name in both places. - Source locations.
with_location(false)andwith_threads(false)keep source file paths, which contain your home directory, and thread names off the spans. - Shutdown. The batch exporter sends spans every 5 seconds and on
provider.shutdown(). Call it before the program exits, after the async runtime has finished, or the last spans are lost.
Run the agent
Run the complete example while the Collector is running:
git clone https://github.com/future-agi/docs.git
cd docs/examples/frameworks/swiftide
export OPENAI_API_KEY="YOUR_OPENAI_KEY" MODEL=gpt-4o-mini # OPENAI_BASE_URL=... for an OpenAI-compatible endpoint
cargo run -- "What is the weather in Paris right now?" What you see in Observe
| Span | Opened by (Swiftide 0.33.0) | Type | What it carries |
|---|---|---|---|
agent.query (also agent.query_once, agent.run, agent.run_once) | Agent::query and friends | Agent | The root of the trace. With content capture on: the question as input and Swiftide’s stop reason as output. |
step | one per model turn | Chain | step_count (0-based). |
complete | the OpenAI integration’s chat completion | LLM | Model name and prompt, completion and total token counts. With content capture on: the request and the response as JSON. |
tool.<name> | each tool call | Tool | With content capture on: the arguments as input and the result as output. |
add_message, hook.<type> | each message added to the history, each hook you register | Chain | Timing only. |
- Failures. A provider error makes
complete, itsstepandagent.queryERROR, and thatcompletespan has no token counts. Swiftide recovers from a failed tool: it hands the error to the model and carries on, so thetool.<name>span is not ERROR by itself. Log at error level inside your tool (tracing::error!(...), as the example’sget_weatherdoes) to mark it. step, notstep-1. Swiftide renames each step span after it has started, andtracing-opentelemetry0.34.0 keeps the first name. Usestep_countto tell steps apart.- No sessions or users. Swiftide sets no session or user id. Each
agent.querycall is its own trace. - Streaming (the
streamandresponses_streamspans), the Responses API and other model providers are not covered by this guide.
Privacy
| Default | FI_CAPTURE_CONTENT=true | |
|---|---|---|
| Prompts, answers, tool arguments and tool results | Removed | Sent as input.value and output.value |
| Swiftide’s log lines (span events) | Removed | Removed |
| Error status messages | Removed; the ERROR status stays | Removed; the ERROR status stays |
| Tool errors included in captured messages | Removed | Sent in tool output and subsequent model input |
Rust backtraces (Stack backtrace: and everything after) | Removed | Removed |
| Any other span attribute, trace state, scope attributes | Removed | Removed |
| Resource attributes | Only service.name, project_name, project_type | Only service.name, project_name, project_type |
| Spans from other tracers, spans with links | Dropped | Dropped |
X-Api-Key, X-Secret-Key, the model provider key | Request headers only; not added to spans | Request headers only; not added to spans |
Leave FI_CAPTURE_CONTENT unset if prompts, answers, tool results or recovered tool errors may contain sensitive data. When you enable it, those messages are exported as content; clearing error status messages does not redact their contents.
Restart the Collector after you change FI_CAPTURE_CONTENT. Swiftide truncates inline data: image URLs in model requests before they reach the Collector.
Troubleshooting
- No traces in Observe. Check that the Collector is running and listening on
127.0.0.1:4318, that bothFI_API_KEYandFI_SECRET_KEYare set in the Collector’s environment, and thatFI_PROJECT_NAMEis set. The agent keeps running without a Collector and does not report the lost spans; the Collector logs its own export errors to its console. responded with HTTP Status Code 401in the Collector log. One of the two keys is missing or wrong.- Model spans show type Chain, or have no model name or token counts. The
langfusefeature is missing onswiftide-integrations, or the filter does not enable DEBUG forswiftide_integrations. swiftide-langfuseis in yourCargo.lock. Theswiftidecrate’s ownlangfusefeature is enabled somewhere in your dependency tree. Remove it for this setup; enable the feature onswiftide-agentsandswiftide-integrationsonly.- The Collector does not start because the
transformorfilterprocessor is unknown. You are running the core Collector. Use the Contrib distribution,otelcol-contrib. - The trace is missing its last spans.
provider.shutdown()was not called before exit. - Compile errors in
swiftide-integrationswith Swiftide 0.32.x (cannot find type Client in crate async_openai). Swiftide 0.32 accepts anyasync-openaifrom 0.29.6 up and now resolves to an incompatible 0.42. Upgrade to Swiftide 0.33.0, or pinasync-openai = "=0.30.1"and runcargo update -p async-openai@<the 0.4x version> --precise 0.30.1.
Questions & Discussion