Skip to content

DarkWanderer/metrics-cpp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

53e6704 · Jan 5, 2025
Jan 5, 2025
Jan 16, 2023
Jan 3, 2025
Sep 29, 2024
Mar 17, 2024
Sep 23, 2023
Jan 5, 2025
Jan 5, 2025
Jan 5, 2025
Sep 29, 2024
Sep 29, 2024
Sep 29, 2024
Jan 16, 2023
Oct 11, 2024
Mar 26, 2024
Sep 29, 2024
Jan 5, 2025

Repository files navigation

metrics-cpp

Build Readiness

A low-footprint, high-performance C++ metrics library implementing commonly used metric classes - Counter, Gauge, Histogram, Summary - in idiomatic and thread-safe faction

Design goals

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

Key features

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

Limitations & compromises

  • 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

Readiness

Feature Readiness
Core API GA
Serialization: JSON icon
Serialization: Prometheus icon
Sink: Statsd UDP icon
Sink: Statsd TCP icon
Sink: PushGateway icon
Sink: Prometheus HTTP icon

Performance

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

Usage examples

Quickstart

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

Standalone metrics

Counter c1;
auto c2 = c1; // Another object shares same underlying metric
c2++;
cout << c1.value(); // 1

Working with registry

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

Serialization

auto registry = createRegistry();
auto gauge = registry->getGauge("my_gauge", {{"some", "label"}});
auto p = serializePrometheus(registry);
auto j = serializeJson(registry);
auto s = serializeStatsd(registry);

Timers

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

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