A low-footprint, high-performance C++ metrics library implementing commonly used metric classes - Counter, Gauge, Histogram, Summary - in idiomatic and thread-safe faction
The design goals of this library are the following:
- Be as lightweight as possible - all operations on Counter, Gauge, Histogram are lock-free using atomic operations
- Allow to think of instrumenting first and exposition later
- Provide easy to use API
- Provides commonly used metric classes
- A number of out-the-box optimizations
- all metrics except Summary are lock-free
- Labels are optimized for cache locality (vector instead of std::map; make sure to use a compiler which takes advantage of SSO)
- Minimized locking for operations in Registry
- Various methods of serialization
- Prometheus
- JSON/JSONL
- statsd
- Cross-platform (built for Windows, Ubuntu, MacOS)
- Due to limited number of locks employed, there is no strong consistency guarantee between different metrics
- If a particular thread changes two counters and serialization happens in the middle, you may see a value for one counter increasing but not for the other - until the next time metrics are collected. Hence, care must be taken when creating alerts based on metrics differential
- For same reason, histogram 'sum' may be out of sync with total count - skewing the average value with ⅟n asymptotic upper bound
- It's not possible to remove metrics from a
Registry
- conceptually shared with Prometheus - Boost::accumulators do not correctly work under MacOS, which prevents Summary class from working there - more throrough investigation pending
Feature | Readiness |
---|---|
Core API | |
Serialization: JSON | |
Serialization: Prometheus | |
Sink: Statsd UDP | |
Sink: Statsd TCP | |
Sink: PushGateway | |
Sink: Prometheus HTTP |
Performance of metrics is comparable to atomic
primitives - even with pointer indirection
Run on (24 X 3700 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x12)
L1 Instruction 32 KiB (x12)
L2 Unified 512 KiB (x12)
L3 Unified 32768 KiB (x2)
-----------------------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------------------
BM_Reference_AtomicIncrement 1.50 ns 1.51 ns 497777778
BM_CounterIncrement 1.34 ns 1.34 ns 560000000
BM_GaugeSet 1.84 ns 1.84 ns 407272727
BM_Histogram2Observe 4.24 ns 4.20 ns 160000000
BM_Histogram5Observe 4.70 ns 4.60 ns 149333333
BM_Histogram10Observe 5.37 ns 5.47 ns 100000000
BM_SummaryObserve 9.13 ns 9.21 ns 74666667
BM_RegistryGet 39.2 ns 39.2 ns 17920000
BM_RegistryGetLabels 138 ns 138 ns 4977778
The library API was designed to provide a low barrier for entry:
auto metrics = createRegistry();
metrics->getCounter( "birds", {{ "kind", "pigeon" }} )++;
metrics->getCounter( "birds", {{ "kind", "sparrow" }} )+=10;
metrics->getGauge( "tiredness" ) += 1.5;
cout << "The library supports outputting metrics in Prometheus format:" << endl << serializePrometheus(*metrics) << endl;
cout << "And in JSON format:" << endl << serializeJsonl(*metrics) << endl;
For further information on using library via CMake, see this sample
Counter c1;
auto c2 = c1; // Another object shares same underlying metric
c2++;
cout << c1.value(); // 1
Registry
is a class representing grouping of metrics within the application. Usually you would have a single registry
per application or application domain. You can create metrics from within the registry:
auto registry = createRegistry();
auto gauge = registry->getGauge("my_gauge", {{"some", "label"}});
gauge = 10.0;
Or add existing metrics with a key:
Gauge gauge;
gauge = 5;
auto registry = createRegistry();
registry->add("my_gauge", {{"some", "label"}}, gauge);
cout << registry->getGauge("my_gauge", {{"some", "label"}}).value(); // 5
The recommended pattern is to instrument low-level code using standalone metrics and then add the needed metrics to a registry
instance - this way, you can track same metrics under different names in different contexts
auto registry = createRegistry();
auto gauge = registry->getGauge("my_gauge", {{"some", "label"}});
auto p = serializePrometheus(registry);
auto j = serializeJson(registry);
auto s = serializeStatsd(registry);
Histogram histogram({1., 2., 5., 10.});
for (auto file: files)
{
Timer<std::chrono::seconds> timer(histogram); // Adds an observation to the histogram on scope exit
process_file(file);
}
Sinks can be created explicitly by type or from a URL. In latter case, specific sink type is derived from URL schema, e.g. statsd+udp
or pushgateway+http
// Set this value in config
std::string url = "statsd+udp://localhost:1234";
auto registry = createRegistry();
auto gauge = registry->getGauge("my_gauge", {{"some", "label"}});
gauge = 5.0;
auto sink = createOnDemandSink(url);
if (sink)
sink->send(registry);