Skip to content

Usage metrics

After this recipe you have two examples from the swiftide repository running on your machine. usage_metrics.rs prints the token counters of an indexing pipeline, labelled with the model and your own metadata. langfuse.rs sends one model call, with its input, output and token usage, to Langfuse. Metrics and pipeline statistics and Langfuse cover the reference details; this page is the end-to-end run.

Token counters sit behind the metrics feature flag. The recorder comes from the metrics crate. In your own crate, add:

Terminal window
cargo add swiftide@0.34 --features openai,qdrant,metrics
cargo add metrics@0.24
cargo add tokio --features macros,rt-multi-thread

Other clients that emit counters need their own feature flag as well: anthropic, aws-bedrock, ollama or groq.

To run the example itself, clone the swiftide repository, set OPENAI_API_KEY, and start Qdrant on localhost:6334 (or set QDRANT_URL). Then run from the repository root:

Terminal window
export OPENAI_API_KEY=sk-...
cargo run -p swiftide-examples --example usage-metrics

swiftide-examples enables many integrations, including LanceDB, Kafka, Fluvio and DuckDB, so the first build takes a while and needs their native toolchains.

#[derive(Clone, Debug)]
struct PrintHandle(Key);
impl CounterFn for PrintHandle {
fn increment(&self, value: u64) {
println!("counter increment for '{}': {}", self.0, value);
}
fn absolute(&self, value: u64) {
println!("counter absolute for '{}': {}", self.0, value);
}
}
19 collapsed lines
impl GaugeFn for PrintHandle {
fn increment(&self, value: f64) {
println!("gauge increment for '{}': {}", self.0, value);
}
fn decrement(&self, value: f64) {
println!("gauge decrement for '{}': {}", self.0, value);
}
fn set(&self, value: f64) {
println!("gauge set for '{}': {}", self.0, value);
}
}
impl HistogramFn for PrintHandle {
fn record(&self, value: f64) {
println!("histogram record for '{}': {}", self.0, value);
}
}
#[derive(Debug)]
struct PrintRecorder;
impl Recorder for PrintRecorder {
fn describe_counter(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString) {
println!(
"(counter) registered key {} with unit {:?} and description {:?}",
key_name.as_str(),
unit,
description
);
}
17 collapsed lines
fn describe_gauge(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString) {
println!(
"(gauge) registered key {} with unit {:?} and description {:?}",
key_name.as_str(),
unit,
description
);
}
fn describe_histogram(&self, key_name: KeyName, unit: Option<Unit>, description: SharedString) {
println!(
"(histogram) registered key {} with unit {:?} and description {:?}",
key_name.as_str(),
unit,
description
);
}
fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
Counter::from_arc(Arc::new(PrintHandle(key.clone())))
}
7 collapsed lines
fn register_gauge(&self, key: &Key, _metadata: &Metadata<'_>) -> Gauge {
Gauge::from_arc(Arc::new(PrintHandle(key.clone())))
}
fn register_histogram(&self, key: &Key, _metadata: &Metadata<'_>) -> Histogram {
Histogram::from_arc(Arc::new(PrintHandle(key.clone())))
}
}
fn init_print_logger() {
metrics::set_global_recorder(PrintRecorder).unwrap()
}

Source: examples/usage_metrics.rs#L91-L170

Swiftide emits its counters through the metrics facade from the metrics-rs project. The facade sends them to whichever Recorder you install, and without one every counter goes nowhere. PrintRecorder is the basic example from metrics-rs: it prints every increment. The example’s main calls init_print_logger() before it builds the client, because a counter emitted before the recorder exists is lost. In an application you install an exporter crate in its place, such as a Prometheus exporter.

let metric_metadata = HashMap::from([("example".to_string(), "metadata".to_string())]);
let openai_client = integrations::openai::OpenAI::builder()
.default_embed_model("text-embedding-3-small")
.default_prompt_model("gpt-4.1-nano")
// Metadata will be added to every metric
.metric_metadata(metric_metadata)
.build()?;

Source: examples/usage_metrics.rs#L30-L36

Each entry in metric_metadata becomes a label on every counter this client emits, next to the model label Swiftide adds. Give each client a label for the tenant, feature or environment it serves, and your metrics backend can sum per label. The method only exists with the metrics feature flag.

indexing::Pipeline::from_loader(FileLoader::new(".").with_extensions(&["md"]))
.then_chunk(ChunkMarkdown::from_chunk_range(10..512))
.then_in_batch(Embed::new(openai_client.clone()).with_batch_size(10))
.then_store_with(
Qdrant::builder()
.batch_size(50)
.vector_size(1536)
.collection_name("swiftide-examples-metrics")
.build()?,
)
.run()
.await?;

Source: examples/usage_metrics.rs#L40-L51

Each batch of 10 chunks is one embedding call, and each call increments all three counters once. vector_size(1536) must match text-embedding-3-small. FileLoader walks the directory with the ignore crate, so it skips hidden and gitignored paths such as target/.

The example keeps the output of one run, embedding with text-embedding-3-small, in a comment at the end of main. These are its first lines:

(counter) registered key swiftide.usage.prompt_tokens with unit None and description "token usage for the prompt"
(counter) registered key swiftide.usage.completion_tokens with unit None and description "token usage for the completion"
(counter) registered key swiftide.usage.total_tokens with unit None and description "total token usage"
counter increment for 'Key(swiftide.usage.prompt_tokens, [example = metadata, model = text-embedding-3-small])': 356
counter increment for 'Key(swiftide.usage.completion_tokens, [example = metadata, model = text-embedding-3-small])': 0
counter increment for 'Key(swiftide.usage.total_tokens, [example = metadata, model = text-embedding-3-small])': 356

The three registered key lines appear once, on the first emit. After that, every embedding call prints three increment lines: prompt, completion and total. Embeddings have no completion, so completion_tokens grows by 0. The three counters and the clients that emit them are listed in Counters.

The langfuse feature flag sends traces to Langfuse through a tracing layer:

Terminal window
cargo add swiftide@0.34 --features openai,langfuse
cargo add tracing anyhow
cargo add tracing-subscriber --features env-filter

Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY, or their fallbacks LANGFUSE_INIT_PROJECT_PUBLIC_KEY and LANGFUSE_INIT_PROJECT_SECRET_KEY. LangfuseLayer::default() panics when a key is missing or empty. LANGFUSE_URL defaults to http://localhost:3000. The example also needs OPENAI_API_KEY:

Terminal window
RUST_LOG=swiftide=debug,langfuse=debug cargo run -p swiftide-examples --example langfuse
let fmt_layer = tracing_subscriber::fmt::layer()
.compact()
.with_target(false)
.boxed();
let langfuse_layer = swiftide::langfuse::LangfuseLayer::default()
.with_filter(LevelFilter::DEBUG)
.boxed();
let registry = tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(vec![fmt_layer, langfuse_layer]);
registry.init();

Source: examples/langfuse.rs#L28-L41

The EnvFilter sits on the registry itself, so it filters for every layer, the Langfuse layer included. Without RUST_LOG it lets only errors through, and Langfuse receives no spans. That is why the run command sets RUST_LOG. Layer setup explains the layer’s own LevelFilter::DEBUG and its batching.

let openai = swiftide::integrations::openai::OpenAI::builder()
.default_prompt_model("gpt-5")
.build()
.unwrap();
let paris = openai
.prompt("What is the capital of France?".into())
.await?;
println!("The capital of France is {paris}");

Source: examples/langfuse.rs#L54-L63

With the langfuse feature flag, the client’s prompt call opens a span of type GENERATION. Langfuse shows that generation with the model, the prompt, the answer and the token usage. Reported data covers what each client and agent records.

When you want the numbers in your own code rather than in a metrics backend, set on_usage or on_usage_async on the client builder. The callback receives the Usage of each call and needs no feature flag. See Usage callbacks.

A successful indexing run logs a Pipeline completed event with its node counts. Pipeline statistics covers it and its limits.

  • AnthropicBuilder::metric_metadata takes an Option<HashMap<String, String>>, so wrap the map in Some. The OpenAI-compatible and AwsBedrock builders take the map itself.
  • metrics::set_global_recorder errors on a second call. In tests that each install a recorder, use metrics::with_local_recorder instead.
  • The Langfuse layer does not send its last partial batch on exit. A short program can end before it leaves. Keep a clone of the layer and call flush before main returns, as Final flush shows.

Hybrid search and reranking