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

Cargo.toml toml
[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:

collector.yaml 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
VariableValue
FI_API_KEY, FI_SECRET_KEYBoth keys. The Collector sends them as the X-Api-Key and X-Secret-Key headers; one of them alone is rejected.
FI_PROJECT_NAMEThe Observe project the traces go to.
FI_TRACES_ENDPOINTOptional. Defaults to https://api.futureagi.com/tracer/v1/traces.
FI_CAPTURE_CONTENTOptional. 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

src/main.rs rust
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_agents and swiftide_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-agent tracer. Use the same name in both places.
  • Source locations. with_location(false) and with_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

SpanOpened by (Swiftide 0.33.0)TypeWhat it carries
agent.query (also agent.query_once, agent.run, agent.run_once)Agent::query and friendsAgentThe root of the trace. With content capture on: the question as input and Swiftide’s stop reason as output.
stepone per model turnChainstep_count (0-based).
completethe OpenAI integration’s chat completionLLMModel name and prompt, completion and total token counts. With content capture on: the request and the response as JSON.
tool.<name>each tool callToolWith 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 registerChainTiming only.
  • Failures. A provider error makes complete, its step and agent.query ERROR, and that complete span has no token counts. Swiftide recovers from a failed tool: it hands the error to the model and carries on, so the tool.<name> span is not ERROR by itself. Log at error level inside your tool (tracing::error!(...), as the example’s get_weather does) to mark it.
  • step, not step-1. Swiftide renames each step span after it has started, and tracing-opentelemetry 0.34.0 keeps the first name. Use step_count to tell steps apart.
  • No sessions or users. Swiftide sets no session or user id. Each agent.query call is its own trace.
  • Streaming (the stream and responses_stream spans), the Responses API and other model providers are not covered by this guide.

Privacy

DefaultFI_CAPTURE_CONTENT=true
Prompts, answers, tool arguments and tool resultsRemovedSent as input.value and output.value
Swiftide’s log lines (span events)RemovedRemoved
Error status messagesRemoved; the ERROR status staysRemoved; the ERROR status stays
Tool errors included in captured messagesRemovedSent in tool output and subsequent model input
Rust backtraces (Stack backtrace: and everything after)RemovedRemoved
Any other span attribute, trace state, scope attributesRemovedRemoved
Resource attributesOnly service.name, project_name, project_typeOnly service.name, project_name, project_type
Spans from other tracers, spans with linksDroppedDropped
X-Api-Key, X-Secret-Key, the model provider keyRequest headers only; not added to spansRequest 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 both FI_API_KEY and FI_SECRET_KEY are set in the Collector’s environment, and that FI_PROJECT_NAME is 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 401 in 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 langfuse feature is missing on swiftide-integrations, or the filter does not enable DEBUG for swiftide_integrations.
  • swiftide-langfuse is in your Cargo.lock. The swiftide crate’s own langfuse feature is enabled somewhere in your dependency tree. Remove it for this setup; enable the feature on swiftide-agents and swiftide-integrations only.
  • The Collector does not start because the transform or filter processor 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-integrations with Swiftide 0.32.x (cannot find type Client in crate async_openai). Swiftide 0.32 accepts any async-openai from 0.29.6 up and now resolves to an incompatible 0.42. Upgrade to Swiftide 0.33.0, or pin async-openai = "=0.30.1" and run cargo update -p async-openai@<the 0.4x version> --precise 0.30.1.
Was this page helpful?

Questions & Discussion