Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Usage guidelines for Prometheus C++?

Tags:

c++

prometheus

I've looked at the syntax example in prometheus-cpp and the very similar go one in the main prometheus documentation, and I'm not sure how I'm supposed to use similar code in my C++ application. Go uses a global variable to hold the counter, C++ uses a local reference within the instrumented function. Auto references means that I can't easily put counters into a wrapper, but 10 lines of overhead every time I want to increment a counter isn't acceptable.

Naively it looks like this:

void SomeClass::a_little_method() {
    auto start = get_accurate_time();

    // actual code that the method
    // uses to do whatever it does
    // in less than ten lines of code

    auto& counter_family = BuildCounter()
            .Name("little_method")
            .Help("little method execution count and duration")
            .Labels({
        {"My Application", "metrics"}
    })
    .Register(*global_registry_pointer);
    auto& call_counter = counter_family.Add({
        {"calls", "count"}
    });
    auto& execution_timer = counter_family.Add({
        {"calls", "duration"}
    });
    call_counter.Increment();
    execution_timer.Increment(get_accurate_time() - start);
}

There is much more instrumentation than code being instrumented. It gets worse as more things get instrumented, the prometheus guide "there should be a counter for every log line" means every log line gains 8 or 10 lines of prometheus verbiage. And there's two local variables created, used once, then destroyed.

Solution One: More Global Variables

Prometheus-cpp has its global "registry" object, so presumably the intent is that I just add a bunch of "counter family" globals followed by a huge pile of global "counter" variables. That means the program won't run at all if prometheus fails to initialise, but at least each counter is only set up once. At least the library of counters is all in one place so it's easy to see and organise.

Solution Two: a wrapper thread that exposes Increment() methods

I could declare all those auto reference variables in one giant method, finish the method with a "while not terminated sleep" call and run it as a thread. Then expose those local counter variables via a set of Increment methods. But this feels as though I'm working against the intent of the library author.

Solution Three: do it properly??

I really want a single line per counter increment, ideally as a method on an injectable/mockable class. Preferably with the other prometheus wrapper duration wrapper. My program should run even if prometheus isn't available or can't run for some reason (I'm not running a server thats sole purpose is to play with prometheus).

SomeClass::SomeClass(... prometheus...)
SomeClass::wrap_a_little_method() {
    prometheus.observe_duration([&]() {
        a_little_method();
    }
    prometheus.Increment(a_little_method_call_count);
}

(there's no prometheus-cpp tag and I don't have the rep to create one, sorry)

like image 391
Code Abominator Avatar asked Nov 24 '25 13:11

Code Abominator


1 Answers

To address your concerns about instrumenting your C++ application with Prometheus-cpp without adding excessive boilerplate code, you can consider a more structured approach that leverages the power of C++ classes and dependency injection.

Here's a solution that aligns with your third option: "do it properly."

Solution: Using a Metrics Wrapper Class

You can create a Metrics class that encapsulates the Prometheus-related logic. This class can provide methods for incrementing counters and observing durations, making your instrumentation code cleaner and more maintainable.

1: Define the Metrics Class

First, define a Metrics class that will handle the creation and management of Prometheus counters and timers.

#include <prometheus/counter.h>
#include <prometheus/registry.h>
#include <chrono>

class Metrics {
public:
    Metrics(prometheus::Registry* registry) : registry_(registry) {
        auto& counter_family = prometheus::BuildCounter()
            .Name("little_method")
            .Help("little method execution count and duration")
            .Labels({{"My Application", "metrics"}})
            .Register(*registry_);

        call_counter_ = &counter_family.Add({{"calls", "count"}});
        execution_timer_ = &counter_family.Add({{"calls", "duration"}});
    }

    void IncrementCallCounter() {
        call_counter_->Increment();
    }

    void ObserveDuration(std::function<void()> func) {
        auto start = std::chrono::steady_clock::now();
        func();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
        execution_timer_->Increment(duration);
    }

private:
    prometheus::Registry* registry_;
    prometheus::Counter* call_counter_;
    prometheus::Counter* execution_timer_;
};

2: Inject the Metrics Class into Your Application

Next, inject the Metrics class into your application classes. This allows you to easily mock or replace the metrics logic if needed.

class SomeClass {
public:
    SomeClass(Metrics& metrics) : metrics_(metrics) {}

    void wrap_a_little_method() {
        metrics_.ObserveDuration([&]() {
            a_little_method();
        });
        metrics_.IncrementCallCounter();
    }

private:
    void a_little_method() {
        // Actual code that the method uses to do whatever it does
    }

    Metrics& metrics_;
};

3: Initialize and Use the Metrics Class

Finally, initialize the Metrics class with the Prometheus registry and use it in your application.

int main() {
    auto registry = std::make_shared<prometheus::Registry>();
    Metrics metrics(registry.get());

    SomeClass some_class(metrics);
    some_class.wrap_a_little_method();

    // Expose the Prometheus metrics endpoint (e.g., using a web server)
    // ...

    return 0;
}

Benefits of This Approach

  1. Cleaner Code: The instrumentation logic is encapsulated within the Metrics class, reducing boilerplate in your application code.
  2. Maintainability: Centralizing the metrics logic makes it easier to manage and update.
  3. Testability: Dependency injection allows you to mock the Metrics class for unit testing.
  4. Fault Tolerance: Your application can run even if Prometheus is not available, as the Metrics class can be designed to handle such scenarios gracefully.

I hope this approach will help you.

c++ prometheus

like image 167
ᴋɪꜱʜᴏʀᴇ Avatar answered Nov 26 '25 01:11

ᴋɪꜱʜᴏʀᴇ



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!