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

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