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
Static API (Recommended)#
The static API in omni/observability/Metrics.h provides the most convenient way to add metrics to your application.
This API is called static because individual objects like meters and instruments are identified by a user-defined C++ type. Defining the type defines the instrument, and all functionality is accessible via static member functions.
Meters#
A meter represents a logical grouping of related metrics, typically corresponding to a library or a component:
// Define a meter with version and schema URL
struct MyServiceMeter : omni::observability::Meter
{
constexpr static char version[] = "1.0.0";
constexpr static char schemaUrl[] = "https://example.com/schema";
};
// Or define a simple meter using defaults
struct MyServiceMeter : omni::observability::Meter
{
};
Each meter has:
Name: By default, automatically derived from the type name (e.g.,
MyNamespace::HTTPMeterbecomes"MyNamespace/HTTPMeter")Version: Optional version string (defaults to empty)
Schema URL: Optional URL pointing to the metric schema (defaults to empty)
A meter does not have any functionality, it simply defines the name, version and the schema URL.
Instruments#
An instrument is a specific metric that can be recorded. Here’s an example of defining a counter instrument:
// Define an instrument
struct RequestCount : omni::observability::Instrument
{
constexpr static char description[] = "Total number of HTTP requests processed";
constexpr static char unit[] = "requests";
};
// Define a counter from the meter and instrument types
using HTTPRequestCounter = omni::observability::Counter<MyServiceMeter, RequestCount>;
// Increment the counter
void handleRequest()
{
HTTPRequestCounter::add(1);
}
Each instrument has:
Name: Automatically derived from the type name (e.g.,
MyNamespace::RequestCountbecomes"MyNamespace/RequestCount"). This default naming can be overridden with a staticnamememberDescription: Human-readable description of what is being measured (required)). This description must be detailed enough to understand the purpose of the metric and a suggestion about how it could be interpreted. This must be written for an audience of developers and data analysts, and should be clear enough to be scraped as a docstring.
Unit: Optional unit of measurement following UCUM standard (e.g., “ms”, “bytes”, “requests”)
Value Type: The numeric type for measurements (defaults to
uint64_tforCounterandHistogram,int64_tforUpDownCounter, etc.)
To specify a different value type, add it as a template parameter:
// Counter with double values
using ResponseTime = omni::observability::Counter<MyServiceMeter, RequestCount, double>;
Attributes#
Any combination of attributes can be passed to instrument functions like add() for counters. Here’s an example:
#include <omni/observability/Metrics.h>
// Define attribute types
struct HTTPMethod { carb::cpp::string_view value; };
struct StatusCode { int value; };
// Define meter and instrument
struct MyServiceMeter : omni::observability::Meter
{
};
struct RequestCount : omni::observability::Instrument
{
constexpr static char description[] = "Total HTTP requests";
constexpr static char unit[] = "requests";
};
using HTTPRequestCounter = omni::observability::Counter<MyServiceMeter, RequestCount>;
void handleRequest(const std::string& method, int status)
{
// Increment counter with HTTPMethod and StatusCode:
HTTPRequestCounter::add(1, HTTPMethod{method}, StatusCode{status});
}
The example above uses simple C++ structs with a data member called value to define attributes. Alternatively, attributes can be specified in an initializer list, where the name of the attribute is provided as a string literal rather than deduced automatically from the struct name:
void handleRequest(const std::string& method, int status)
{
// Increment counter with HTTPMethod and StatusCode:
HTTPRequestCounter::add(1, {"HTTPMethod", method}, {"StatusCode", status});
}
The struct-based approach reflects the different attributes in the C++ type system, which provides a degree of safety (for example it prevents bugs arising from attribute names being misspelled).
Why use attributes#
Attributes provide additional context for each measurement, allowing you to slice and aggregate metrics by different dimensions. For example, an HTTP request counter might use attributes for HTTP method (GET, POST, etc.) and status code (200, 404, 500, etc.).
Each combination of attribute values creates a separate measurement. For instance, if you have a counter with attributes for HTTP method and status code, each combination like HTTPMethod{"GET"}, StatusCode{200}, HTTPMethod{"POST", StatusCode{404}, etc., allocates its own counter value that is tracked independently.
Without attributes, we would have to define many different counters “by hand”, which would be cumbersome. In addition, attributes are recognized and supported on the collection/display side, which enables automatic filtering and aggregation.
Cardinality Warning: Be careful with attribute cardinality (the number of unique attribute combinations). High cardinality can lead to:
Excessive memory consumption
Performance degradation
Difficulty analyzing data
Avoid using attributes with unbounded or very large value sets (e.g., user IDs, request IDs, timestamps). Instead, use attributes with a small, well-defined set of values (e.g., HTTP methods, status code ranges, etc).
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");