Skip to main content
A Counter is a synchronous instrument that records values that only increase over time. Counters are ideal for tracking totals like request counts, bytes sent, or errors encountered.

When to Use Counter

Use a Counter when:
  • Values only increase (never decrease)
  • You’re counting discrete events
  • You want to track cumulative totals
  • Resets should only happen on application restart
Common examples:
  • HTTP requests served
  • Bytes transmitted
  • Errors encountered
  • Items processed
  • Cache hits
Do not use Counter for values that can decrease. Use UpDownCounter instead.

API Reference

pub struct Counter<T>(Arc<dyn SyncInstrument<T> + Send + Sync>);

impl<T> Counter<T> {
    pub fn add(&self, value: T, attributes: &[KeyValue]);
}
The add method records an increment to the counter. The value must be non-negative.

Creating a Counter

Counters support u64 and f64 data types:
use opentelemetry::global;

let meter = global::meter("my-app");

// u64 counter
let u64_counter = meter.u64_counter("requests_total")
    .with_description("Total number of requests")
    .with_unit("requests")
    .build();

// f64 counter
let f64_counter = meter.f64_counter("bytes_sent")
    .with_description("Total bytes sent")
    .with_unit("By")
    .build();

Recording Measurements

Use the add method to record increments:
use opentelemetry::KeyValue;

// Record a simple increment
u64_counter.add(1, &[]);

// Record with attributes
u64_counter.add(
    1,
    &[
        KeyValue::new("method", "GET"),
        KeyValue::new("status", "200"),
        KeyValue::new("endpoint", "/api/users"),
    ],
);

// Record larger increments
f64_counter.add(1024.5, &[KeyValue::new("protocol", "http")]);

Complete Example

Here’s a complete example tracking HTTP requests:
use opentelemetry::{global, KeyValue};
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_sdk::Resource;

fn init_meter_provider() -> SdkMeterProvider {
    let exporter = opentelemetry_stdout::MetricExporterBuilder::default().build();
    let provider = SdkMeterProvider::builder()
        .with_periodic_exporter(exporter)
        .with_resource(
            Resource::builder()
                .with_service_name("my-service")
                .build(),
        )
        .build();
    global::set_meter_provider(provider.clone());
    provider
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let meter_provider = init_meter_provider();
    let meter = global::meter("http-server");

    // Create a counter for HTTP requests
    let request_counter = meter
        .u64_counter("http_requests_total")
        .with_description("Total number of HTTP requests")
        .with_unit("requests")
        .build();

    // Simulate handling requests
    request_counter.add(
        1,
        &[
            KeyValue::new("method", "GET"),
            KeyValue::new("endpoint", "/api/users"),
            KeyValue::new("status", "200"),
        ],
    );

    request_counter.add(
        1,
        &[
            KeyValue::new("method", "POST"),
            KeyValue::new("endpoint", "/api/users"),
            KeyValue::new("status", "201"),
        ],
    );

    meter_provider.shutdown()?;
    Ok(())
}

Attributes and Cardinality

Attributes add dimensions to your counter, allowing you to slice the data in different ways:
counter.add(1, &[
    KeyValue::new("method", "GET"),
    KeyValue::new("endpoint", "/api/users"),
    KeyValue::new("status", "200"),
]);

counter.add(1, &[
    KeyValue::new("method", "GET"),
    KeyValue::new("endpoint", "/api/posts"),
    KeyValue::new("status", "200"),
]);
Each unique combination of attributes creates a separate time series.
Cardinality matters! Each unique attribute combination creates a new time series. Avoid high-cardinality attributes like user IDs or request IDs, as they can cause memory issues and poor query performance.

Good Attributes (Low Cardinality)

  • HTTP method (GET, POST, PUT, DELETE)
  • HTTP status code (200, 404, 500)
  • Endpoint patterns (/api/users, /api/posts)
  • Environment (production, staging)
  • Region (us-east-1, eu-west-1)

Bad Attributes (High Cardinality)

  • User IDs
  • Request IDs
  • Timestamps
  • Email addresses
  • Full URLs with query parameters

Cloning Counters

Counters implement Clone, allowing you to share them across your application:
let counter = meter.u64_counter("requests").build();
let counter_clone = counter.clone();

// Both reference the same underlying counter
counter.add(1, &[]);
counter_clone.add(1, &[]);
Clone counters rather than creating duplicates with the same name. Creating multiple counters with the same name can lower SDK performance.

Counter vs. ObservableCounter

Choose based on how you track the data:
// Use when incrementing inline with your code
let counter = meter.u64_counter("requests").build();

fn handle_request() {
    // Increment as events happen
    counter.add(1, &[KeyValue::new("endpoint", "/api/users")]);
}
See Observable Instruments for more details on asynchronous counters.

UpDownCounter Alternative

If your values can both increase and decrease, use UpDownCounter instead:
let updown_counter = meter.i64_up_down_counter("active_connections")
    .with_description("Number of active connections")
    .build();

// Can add positive or negative values
updown_counter.add(1, &[]);  // Connection opened
updown_counter.add(-1, &[]); // Connection closed

Best Practices

  1. Use descriptive names: http_requests_total is better than requests
  2. Include units: Specify units like "requests", "By" (bytes), or "ms"
  3. Keep cardinality low: Limit unique attribute combinations
  4. Reuse instruments: Clone counters instead of creating duplicates
  5. Choose the right type: Use u64 for counts, f64 for fractional values

Next Steps

Histogram

Record value distributions like request latency

Gauge

Record independent point-in-time values

Observable Instruments

Use callbacks to report measurements

Views

Customize how counters are aggregated

Build docs developers (and LLMs) love