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)
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."
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.
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_;
};
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_;
};
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;
}
Metrics class, reducing boilerplate in your application code.Metrics class for unit testing.Metrics class can be designed to handle such scenarios gracefully.I hope this approach will help you.
c++ prometheus
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With