How do you create custom metrics using the OpenTelemetry Metrics API (counters, histograms, gauges)?
Quick Answer
OpenTelemetry provides three primary metric instrument types: Counters (monotonically increasing values like request counts), Histograms (distribution of measurements like latency), and Gauges (point-in-time values like queue depth). You create instruments via a Meter instance, record measurements with attributes, and the SDK periodically exports aggregated data to backends.
Detailed Answer
Think of OpenTelemetry metrics instruments like different tools in a workshop. A counter is like a tally clicker that only goes up, counting events as they happen. A histogram is like a measuring tape that records each measurement and automatically sorts them into buckets for percentile analysis. A gauge is like a thermometer that reports the current temperature at any given moment. Choosing the wrong instrument is like using a thermometer to count cars passing by. It technically produces a number, but the number is meaningless for your purpose.
Counters are the most common instrument type, used for values that only increase over time, such as the total number of HTTP requests handled, bytes transferred, or errors encountered. OpenTelemetry provides two counter variants: Counter (synchronous) where you explicitly call add() in your code, and ObservableCounter (asynchronous) where you register a callback that the SDK invokes at collection time. In the payments-api, you would use a synchronous counter to count processed payments, incrementing it each time a payment completes. The counter value itself is not very useful on its own; you typically apply a rate function in your backend (like rate() in Prometheus) to compute payments per second. UpDownCounters are a variant that can both increase and decrease, useful for tracking values like active connections or items in a queue where the count fluctuates.
Histograms record the distribution of measurements, making them ideal for latency, request sizes, and other values where you care about percentiles rather than averages. When you record a value with a histogram, the SDK assigns it to a bucket based on configurable boundaries. The default bucket boundaries in OpenTelemetry are [0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000] milliseconds. For the checkout-service, you might configure custom boundaries like [10, 50, 100, 200, 500, 1000, 2000, 5000] to get better resolution in the latency range you care about. Exponential histograms are a newer feature that automatically adjust bucket boundaries based on the data, providing high accuracy without manual boundary configuration.
Gauges represent point-in-time values that can go up or down arbitrarily, like CPU utilization, memory usage, queue depth, or active sessions. OpenTelemetry primarily supports gauges through the ObservableGauge (asynchronous) instrument, where a callback function returns the current value at collection time. This design prevents the common mistake of setting a gauge value in a loop and exporting stale data. For the inventory-sync service, you might use a gauge to report the current number of items awaiting synchronization, with the callback querying the actual queue depth at each collection interval.
In production, the choice of instrument type affects how backends store and query the data. Counters are stored as cumulative values and require rate() to be useful in queries. Histograms generate multiple time series per instrument (one per bucket boundary plus sum and count), so cardinality management is critical. If your histogram has 10 buckets and you record with 5 attribute dimensions, each with 10 possible values, you generate 10 * 10^5 = 1,000,000 time series, which will overwhelm most backends. Attributes should be low-cardinality: use http.method (GET, POST, PUT, DELETE) and http.status_code (200, 400, 500), not request_id or user_id. The OpenTelemetry SDK supports Views, which let you override default aggregation settings per instrument, such as changing histogram bucket boundaries or dropping specific attributes before export.
A common gotcha is using a gauge when you need a counter. If you want to count the total number of payments processed, use a counter, not a gauge that you increment manually. Counters handle process restarts correctly because backends compute rate over the cumulative value, while a manually incremented gauge loses its value on restart. Another pitfall is not configuring histogram bucket boundaries for your specific use case. The default boundaries assume millisecond-scale latency, but if your checkout-service responds in microseconds or takes minutes for batch operations, the default buckets provide zero resolution in the ranges that matter.