Metrics API#

The Carbonite observability framework provides a comprehensive metrics API for instrumenting applications with counters, histograms, and other measurement types. The API is designed around the OpenTelemetry metrics specification and provides both a convenient static API and a lower-level dynamic API.

Overview#

Metrics allow you to measure and track numerical data about your application’s behavior over time. Common use cases include:

  • Counting events (requests processed, errors encountered)

  • Measuring distributions (request latencies, response sizes)

  • Tracking resource usage (active connections, memory consumption)

The metrics API provides several instrument types:

  • Counter: Monotonically increasing values (e.g. total requests)

  • UpDownCounter: Values that can increase or decrease (e.g. active connections)

  • Histogram: Distribution of values (e.g. request latencies)

  • Gauge: Recording arbitrary measurements (e.g. current CPU temperature)

  • Observable Instruments: Sampled via callback rather than directly recorded

Dynamic API#

The dynamic API in omni/observability/IMetrics.h provides a lower-level, more dynamic approach to metrics. Unlike the static API where different instruments are represented by different C++ types, here they are represented by objects.

This interface is useful when:

  • Instrument names need to be determined at runtime

  • You need more dynamic control over instrument creation

  • You’re interfacing with non-C++ code or dynamic languages

Meters#

You create a meter by calling createMeter() with explicit name, version, and schema URL.

The schema URL parameter helps observability backends understand the structure and conventions used in your telemetry data, enabling consumers to interpret them correctly. An example of such standardization is the metric with name http.server.duration, which should be a histogram that uses seconds as the unit of measurement, and shoud include attributes http.method, http.status_code, http.route, server.address, and server.port.

#include <omni/observability/IMetrics.h>

carb::ObjectPtr<omni::observability::IMeter> meter = 
    observability->createMeter("MyServiceMeter", "1.0.0", "https://opentelemetry.io/schemas/1.21.0");

Instruments#

You create instruments using namespace-scope factory functions. Here’s an example with a counter:

// Create a counter instrument
carb::ObjectPtr<omni::observability::ICounter<int64_t>> requestCounter = 
    omni::observability::createCounter<int64_t>(meter, "RequestCount", "Total HTTP requests", "requests");

// Increment the counter
void handleRequest()
{
    omni::observability::add(requestCounter, 1); // Qualification is optional (ADL)
}

The same name, description, and unit concepts apply as in the static API.

Null Safety: The namespace-scope functions (omni::observability::createCounter, add, record, etc.) are null-safe. If you pass a null meter to a create* function, it returns a null instrument pointer. If you pass a null instrument pointer to functions like add or record, they become no-ops.

Important: Multiple instrument objects created with the same name, type, value type, unit, and description refer to the same underlying metric:

carb::ObjectPtr<omni::observability::ICounter<int64_t>> counter1 = 
    omni::observability::createCounter<int64_t>(meter, "RequestCount", "Total requests", "requests");
carb::ObjectPtr<omni::observability::ICounter<int64_t>> counter2 = 
    omni::observability::createCounter<int64_t>(meter, "RequestCount", "Total requests", "requests");

omni::observability::add(counter1, 5);
omni::observability::add(counter2, 3);
// Both calls affect the same underlying counter, which now has a value of 8

Attributes#

The dynamic API and the static API use the same system for specifying attributes:

#include <omni/observability/IMetrics.h>

struct HTTPMethod { carb::cpp::string_view value; };
struct StatusCode { int value; };

carb::ObjectPtr<omni::observability::IMeter> meter = 
    observability->createMeter("MyServiceMeter");

carb::ObjectPtr<omni::observability::ICounter<int64_t>> requestCounter = 
    omni::observability::createCounter<int64_t>(meter, "RequestCount", "Total HTTP requests", "requests");

void handleRequest(const std::string& method, int status)
{
    omni::observability::add(requestCounter, 1, HTTPMethod{method}, StatusCode{status});
}

Other Instrument Types#

UpDownCounter#

An up/down counter measures values that can go up or down over time, such as active connections, queue size, or memory usage. Unlike regular counters which are monotonically increasing, up/down counters can have negative deltas and their values can decrease.

carb::ObjectPtr<omni::observability::IUpDownCounter<int64_t>> activeConnections = 
    omni::observability::createUpDownCounter<int64_t>(meter, "ActiveConnections", "Currently active connections", "connections");

void onConnectionOpened()
{
    omni::observability::add(activeConnections, 1); // Qualification is optional (ADL)
}

void onConnectionClosed()
{
    omni::observability::add(activeConnections, -1);
}

Histogram#

A histogram measures the distribution of values over time, such as request latencies, response sizes, or processing times. It provides statistical summaries including count, sum, etc.

carb::ObjectPtr<omni::observability::IHistogram<double>> requestDuration = 
    omni::observability::createHistogram<double>(meter, "RequestDuration", "HTTP request duration", "ms");

void onRequestComplete(double durationMs)
{
    omni::observability::record(requestDuration, durationMs); // Qualification is optional (ADL)
}
Configuring Histogram Buckets#

Histograms use bucket boundaries to organize measurements into ranges. By default, OpenTelemetry uses a predefined set of buckets, but you can configure custom buckets using OpenTelemetry SDK Views before creating histogram instruments.

How Bucket Configuration Works#

Generated bucket lists are applied to instruments through the OpenTelemetry SDK Views mechanism:

  1. Generate buckets - Use a bucket generator function to create a vector of bucket boundaries

  2. Create a View - Register the bucket list with registry->AddView(), specifying an instrument selector (name and type patterns) that determines which instruments receive these buckets

  3. Automatic application - When instruments are subsequently created and match the selector patterns, they automatically receive the configured buckets. Views must be registered before instruments are created—registering views after instrument creation has no effect on already-created instruments.

Multiple Bucket Lists#

Yes, you can register multiple bucket lists for different instrument patterns. Each call to registry->AddView() creates a new rule. Views are evaluated in the order they are added, and the first matching view applies its configuration to the instrument.

The following example shows configuring multiple bucket lists:

#include <omni/observability/HistogramBuckets.h>
#include <opentelemetry/sdk/metrics/view/view_registry.h>

namespace otel_sdk = opentelemetry::sdk::metrics;

// During SDK initialization, configure histogram buckets using Views
void configureHistogramViews(otel_sdk::ViewRegistry* registry)
{
    // Rule 1: Use extended latency buckets for duration measurements (most common)
    auto latencyBuckets = omni::observability::extendedLatencyBucketGenerator();
    registry->AddView(
        otel_sdk::InstrumentSelectorBuilder()
            .SetName("*.duration")  // Apply to all histograms with names ending in ".duration"
            .SetType(otel_sdk::InstrumentType::kHistogram)
            .Build(),
        otel_sdk::MeterSelectorBuilder().Build(),
        otel_sdk::ViewBuilder()
            .SetAggregation(std::make_shared<otel_sdk::ExplicitBucketHistogramAggregationConfig>(
                latencyBuckets))
            .Build()
    );

    // Rule 2: Use gRPC size buckets for message/payload size measurements
    auto sizeBuckets = omni::observability::gRPCSizeBucketGenerator();
    registry->AddView(
        otel_sdk::InstrumentSelectorBuilder()
            .SetName("*.size")  // Apply to all histograms with names ending in ".size"
            .SetType(otel_sdk::InstrumentType::kHistogram)
            .Build(),
        otel_sdk::MeterSelectorBuilder().Build(),
        otel_sdk::ViewBuilder()
            .SetAggregation(std::make_shared<otel_sdk::ExplicitBucketHistogramAggregationConfig>(
                sizeBuckets))
            .Build()
    );

    // Rule 3: Use count buckets for message count measurements
    auto countBuckets = omni::observability::gRPCCountBucketGenerator();
    registry->AddView(
        otel_sdk::InstrumentSelectorBuilder()
            .SetName("*.count")  // Apply to all histograms with names ending in ".count"
            .SetType(otel_sdk::InstrumentType::kHistogram)
            .Build(),
        otel_sdk::MeterSelectorBuilder().Build(),
        otel_sdk::ViewBuilder()
            .SetAggregation(std::make_shared<otel_sdk::ExplicitBucketHistogramAggregationConfig>(
                countBuckets))
            .Build()
    );
}
Available Bucket Generators#
  1. extendedLatencyBucketGenerator() - Recommended for latency/duration measurements (most common)

    • Returns 40 buckets spanning 0.1ms to 1000s (16+ minutes)

    • Optimized for typical application latencies with fine granularity for fast operations

  2. gRPCLatencyBucketGenerator() - For gRPC call latency measurements

    • Returns 41 buckets spanning 0.01μs to 100s

    • Based on gRPC specification with additional sub-millisecond precision

  3. gRPCSizeBucketGenerator() - For message/payload size measurements

    • Returns 14 buckets spanning 0 bytes to 4GB

    • Based on gRPC recommendations for size histograms

  4. gRPCCountBucketGenerator() - For count-based measurements

    • Returns 18 buckets with exponential distribution (powers of 2)

    • Range: 0 to 65536

  5. linearBucketGenerator(start, end, step) - Configurable linear buckets

    • Creates evenly spaced buckets with specified step size

    • Example: linearBucketGenerator(0.0, 100.0, 10.0) creates {0, 10, 20, …, 100}

  6. linearBucketGeneratorFixedBucketCount(start, end, num_buckets) - Fixed-count linear buckets

    • Divides a range into a specified number of equally-sized buckets

    • Example: linearBucketGeneratorFixedBucketCount(0.0, 100.0, 10) creates 10 evenly-spaced buckets

  7. exponentialBucketGenerator(start, end, factor) - Configurable exponential buckets

    • Creates exponentially spaced buckets with specified multiplication factor

    • Example: exponentialBucketGenerator(1.0, 128.0, 2.0) creates {1, 2, 4, 8, 16, 32, 64, 128}

Important Notes:

  • Views must be configured before instruments are created. Adding views after instrument creation won’t affect already-created instruments—only future instruments will match against the new view.

  • Bucket boundaries should match the measurement unit (e.g., use seconds for latency, bytes for size)

  • More buckets provide higher precision but increase memory usage, cardinality, and bandwidth usage

  • The bucket boundaries define upper bounds; an additional implicit bucket captures all values above the highest boundary

  • When multiple views match an instrument, the first matching view’s configuration is used

For complete examples and API details, see the documentation in omni/observability/HistogramBuckets.h.

Gauge#

A gauge records arbitrary point-in-time measurements, such as current temperature, memory usage, or CPU utilization. Gauges represent the current value at the moment of observation.

carb::ObjectPtr<omni::observability::IGauge<double>> cpuTemperature = 
    omni::observability::createGauge<double>(meter, "CpuTemperature", "Current CPU temperature", "Cel");

void onTemperatureUpdate(double temperature)
{
    omni::observability::record(cpuTemperature, temperature); // Qualification is optional (ADL)
}

Observable Instruments#

Observable instruments are sampled periodically via callbacks rather than being updated directly by application code. They are useful for metrics like memory usage, CPU usage, or queue depth that you want to sample rather than record every change.

Unlike regular instruments where you call add() or record() to update values, observable instruments use a callback function that is invoked automatically by the metrics system. Your callback receives an InstrumentObserver object on which you call observe() to report the current value.

Observable Counter#

#include <omni/observability/IMetrics.h>

struct QueueStats
{
    std::atomic<int64_t> totalProcessed{0};
};

// Callback function
void observeQueueStats(void* context, omni::observability::InstrumentObserver<int64_t>& observer)
{
    QueueStats* stats = static_cast<QueueStats*>(context);
    observer.observe(stats->totalProcessed.load());
}

QueueStats stats;
carb::ObjectPtr<omni::observability::IObservableInstrument> observableCounter =
    omni::observability::createObservableCounter<int64_t>(
        meter, observeQueueStats, &stats, "QueueProcessed", "Total items processed", "items");

The callback is invoked periodically by the metrics collection system. You can report different values with different attributes:

struct HTTPMethod { carb::cpp::string_view value; };
struct StatusCode { int value; };

void observeRequestCounts(void* context, omni::observability::InstrumentObserver<int64_t>& observer)
{
    // Report multiple observations with different attributes
    observer.observe(100, HTTPMethod{"GET"}, StatusCode{200});
    observer.observe(50, HTTPMethod{"POST"}, StatusCode{200});
    observer.observe(5, HTTPMethod{"GET"}, StatusCode{404});
}

Observable UpDownCounter#

Observable up/down counters work the same way but can report values that increase or decrease:

void observeActiveConnections(void* context, omni::observability::InstrumentObserver<int64_t>& observer)
{
    ConnectionManager* mgr = static_cast<ConnectionManager*>(context);
    observer.observe(mgr->getActiveConnectionCount());
}

ConnectionManager connectionMgr;
carb::ObjectPtr<omni::observability::IObservableInstrument> observableUpDown =
    omni::observability::createObservableUpDownCounter<int64_t>(
        meter, observeActiveConnections, &connectionMgr, "ActiveConnections", "Currently active connections", "connections");