# Usage metrics

> Run the usage_metrics and langfuse examples to print token counters per model from an indexing pipeline and send one model call to Langfuse.

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](/observability/metrics-and-statistics/) and [Langfuse](/observability/langfuse/) cover the reference details; this page is the end-to-end run.

## Setup

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

```sh
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:

```sh
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.

## Recorder

```rust
#[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);
    }
}

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
        );
    }

    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())))
    }

    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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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

```rust
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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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

```rust
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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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

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:

```text
(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](/observability/metrics-and-statistics/#counters).

## Langfuse traces

The `langfuse` feature flag sends traces to [Langfuse](https://langfuse.com/) through a `tracing` layer:

```sh
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`:

```sh
RUST_LOG=swiftide=debug,langfuse=debug cargo run -p swiftide-examples --example langfuse
```

```rust
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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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](/observability/langfuse/#layer-setup) explains the layer's own `LevelFilter::DEBUG` and its batching.

```rust
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`](https://github.com/bosun-ai/swiftide/blob/e377c4a2e76fe2559629ae0efdae3c6e0fb4d829/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](/observability/langfuse/#reported-data) covers what each client and agent records.

## Variations

### 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](/models/errors-backoff-and-usage/#usage-callbacks).

### Pipeline statistics

A successful indexing run logs a `Pipeline completed` event with its node counts. [Pipeline statistics](/observability/metrics-and-statistics/#pipeline-statistics) covers it and its limits.

## Gotchas

- `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](/observability/langfuse/#final-flush) shows.

## Reference

- [`GenericOpenAIBuilder`](https://docs.rs/swiftide/0.34/swiftide/integrations/openai/struct.GenericOpenAIBuilder.html) (`metric_metadata`, `on_usage`)
- [`AnthropicBuilder`](https://docs.rs/swiftide/0.34/swiftide/integrations/anthropic/struct.AnthropicBuilder.html) (`metric_metadata`)
- [`AwsBedrockBuilder`](https://docs.rs/swiftide/0.34/swiftide/integrations/aws_bedrock_v2/struct.AwsBedrockBuilder.html) (`metric_metadata`)
- [`Pipeline`](https://docs.rs/swiftide/0.34/swiftide/indexing/struct.Pipeline.html)
- [`FileLoader`](https://docs.rs/swiftide/0.34/swiftide/indexing/loaders/struct.FileLoader.html)
- [`ChunkMarkdown`](https://docs.rs/swiftide/0.34/swiftide/indexing/transformers/struct.ChunkMarkdown.html)
- [`Embed`](https://docs.rs/swiftide/0.34/swiftide/indexing/transformers/struct.Embed.html)
- [`Qdrant`](https://docs.rs/swiftide/0.34/swiftide/integrations/qdrant/struct.Qdrant.html)
- [`SimplePrompt`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.SimplePrompt.html)
- [`LangfuseLayer`](https://docs.rs/swiftide/0.34/swiftide/langfuse/struct.LangfuseLayer.html)
- [Feature flags](/reference/feature-flags/)

## Next

[Hybrid search and reranking](/cookbook/hybrid-search-and-reranking/)