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:
cargo add swiftide@0.34 --features openai,qdrant,metricscargo add metrics@0.24cargo add tokio --features macros,rt-multi-threadOther 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:
export OPENAI_API_KEY=sk-...cargo run -p swiftide-examples --example usage-metricsswiftide-examples enables many integrations, including LanceDB, Kafka, Fluvio and DuckDB, so the first build takes a while and needs their native toolchains.
Recorder
Section titled “Recorder”#[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.
Client metadata
Section titled “Client metadata”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
Section titled “Indexing pipeline”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/.
Output
Section titled “Output”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])': 356counter increment for 'Key(swiftide.usage.completion_tokens, [example = metadata, model = text-embedding-3-small])': 0counter increment for 'Key(swiftide.usage.total_tokens, [example = metadata, model = text-embedding-3-small])': 356The 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.
Langfuse traces
Section titled “Langfuse traces”The langfuse feature flag sends traces to Langfuse through a tracing layer:
cargo add swiftide@0.34 --features openai,langfusecargo add tracing anyhowcargo add tracing-subscriber --features env-filterSet 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:
RUST_LOG=swiftide=debug,langfuse=debug cargo run -p swiftide-examples --example langfuselet 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.
Variations
Section titled “Variations”Usage callbacks
Section titled “Usage callbacks”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.
Pipeline statistics
Section titled “Pipeline statistics”A successful indexing run logs a Pipeline completed event with its node counts. Pipeline statistics covers it and its limits.
Gotchas
Section titled “Gotchas”AnthropicBuilder::metric_metadatatakes anOption<HashMap<String, String>>, so wrap the map inSome. The OpenAI-compatible andAwsBedrockbuilders take the map itself.metrics::set_global_recordererrors on a second call. In tests that each install a recorder, usemetrics::with_local_recorderinstead.- 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
flushbeforemainreturns, as Final flush shows.
Reference
Section titled “Reference”GenericOpenAIBuilder(metric_metadata,on_usage)AnthropicBuilder(metric_metadata)AwsBedrockBuilder(metric_metadata)PipelineFileLoaderChunkMarkdownEmbedQdrantSimplePromptLangfuseLayer- Feature flags