prometheus-cpp icon indicating copy to clipboard operation
prometheus-cpp copied to clipboard

default registry support present or not?

Open dark-slayer opened this issue 6 years ago • 12 comments
trafficstars

How do we let a metric family be added to a default registry? If we do not call Register(registry) on an object of say GaugeBuilder (if that helps to add it to default registry), then how will we able to call Add() fn later to be able to add metrics to the family as Add function can be called only on a Family<Gauge> object, which is returned by Register(registry) fn?

dark-slayer avatar Jun 27 '19 04:06 dark-slayer

I'm assuming that by default registry you mean a static global object? We don't provide one because we think that global variables are a bad idea. That being said, nothing prevents you from creating one and using it in the same way as the local one created in the usage example.

jupp0r avatar Jun 27 '19 04:06 jupp0r

Was wondering this as well, this is from the prometheus docs: There MUST be a default CollectorRegistry, the standard metrics MUST by default implicitly register into it with no special work required by the user.

What's best practice for sharing an exposer and registry across different parts of a project? Or is it expected that every class would create it's own registry? Was also wondering how I should keep track of families. Should I just re-register the same metric name, track the metric names globally, or do it at the class level? Is it just as bad as having a global to have public methods in a Prometheus singleton? Was looking at some other libraries, and they'll panic if you register the same metric name twice with a registry, but as far as I can tell that works fine with cpp.

mkenigs avatar Jul 11 '19 17:07 mkenigs

All excellent questions.

There MUST be a default CollectorRegistry

Due to the nature of C++, it's not a good way for this library to provide a global variable for this that fits everybodys use case. Think DLL loading, dependencies between static variables, etc.

What's best practice for sharing an exposer and registry across different parts of a project?

Create an Exposer close to main(). Inject it into your subcomponents, they create a registry and register it with the exposer.

Was also wondering how I should keep track of families. Should I just re-register the same metric name, track the metric names globally, or do it at the class level?

If two pieces of code should share the same family, I usually inject a reference families into their constructors instead of/in addition to the registry.

Was looking at some other libraries, and they'll panic if you register the same metric name twice with a registry, but as far as I can tell that works fine with cpp.

You are right, PR #286 is hopefully fixing this soon. With multiple registries, you have the additional problem that they might have conflicts with each other when metrics are combined on collection.

In general, I'm curious if colliding metrics is an actual issue in somebodys codebase. You should be able to avoid this by namespacing in your metrics names (ie network_ for everything related to the network component of your server). Phrased differently, how can you even know what part of your code a specific metrics belongs to if they are disorganized enough to cause conflicts?

jupp0r avatar Jul 11 '19 17:07 jupp0r

Thanks, that's very helpful!

Due to the nature of C++, it's not a good way for this library to provide a global variable for this that fits everybodys use case. Think DLL loading, dependencies between static variables, etc.

Create an Exposer close to main(). Inject it into your subcomponents, they create a registry and register it with the exposer.

For a basic use case, is there any reason it's helpful to have each subcomponent create it's own registry? I think I'll just end up doing this every single time:

auto registry = std::make_shared<Registry>();
exposer.RegisterCollectable(registry);

Or of course I could just pass a registry instead of passing the exposer.

Instead of that boilerplate, I'd rather have a per exposer (not global) default CollectorRegistry and just call Register(exposer). Don't know if that's incompatible with use cases where you need multiple registries, but seems like that it would somewhat provide what the docs talk about without requiring globals.

mkenigs avatar Jul 11 '19 20:07 mkenigs

For a basic use case, is there any reason it's helpful to have each subcomponent create it's own registry?

You can also inject the registry. The benefit of injecting the exposer is that - for complex use cases -components are little more flexible in terms of the number of registries they want to create, pass on to subcomponents, etc.

Instead of that boilerplate, I'd rather have a per exposer (not global) default CollectorRegistry and just call Register(exposer). Don't know if that's incompatible with use cases where you need multiple registries, but seems like that it would somewhat provide what the docs talk about without requiring globals.

That's an interesting idea I haven't thought about before. I'm open to a PR for this, especially as it wouldn't break the current API.

jupp0r avatar Jul 11 '19 20:07 jupp0r

Cool if I get a chance I'll take a look at it. Thanks!

mkenigs avatar Jul 11 '19 20:07 mkenigs

If two pieces of code should share the same family, I usually inject a reference families into their constructors instead of/in addition to the registry.

It should be very very rare that two pieces of code share a metric name. Almost always that means that the metric should be moved up the call stack. A metric name is fundamentally a pointer to a single specific bit of code, so should generally live and be scoped at file level.

Inject it into your subcomponents, they create a registry and register it with the exposer.

What if a subcomponent is via a transitive dependency, which you have no knowledge of? The default registry exists partially for that reason, globals aren't always bad - just often bad.

brian-brazil avatar Aug 06 '19 15:08 brian-brazil

It should be very very rare that two pieces of code share a metric name. Almost always that means that the metric should be moved up the call stack. A metric name is fundamentally a pointer to a single specific bit of code, so should generally live and be scoped at file level.

I agree, injecting references to the actual metrics is the most common case.

What if a subcomponent is via a transitive dependency, which you have no knowledge of? The default registry exists partially for that reason, globals aren't always bad - just often bad.

Because I have no knowledge of it, I cannot easily provide lifetime guarantees around a default registry for it. Is the subcomponent loaded at runtime via dlopen()? Does it outlive main()? I don't think it's a good idea for a library to prescribe a model for this in C++, it should be the applications concern.

jupp0r avatar Aug 06 '19 18:08 jupp0r

It doesn't matter how long the subcomponent lives, its metrics will remain for the lifetime of the process. The only place you'd need to be careful here is with custom collectors, as they might reference things that no longer exist.

brian-brazil avatar Aug 06 '19 18:08 brian-brazil

It doesn't matter how long the subcomponent lives

It does, it can live longer than the registry singleton and then crash on shutdown when accessing metrics. Static initialization/destruction order in C++ is non-deterministic. It's surprisingly hard to implement global variables, especially if you are a library and know nothing about the executable.

jupp0r avatar Aug 06 '19 19:08 jupp0r

True, I'd try to arrange things so it's never destructed. The process is about to die anyway.

brian-brazil avatar Aug 07 '19 09:08 brian-brazil

I'd try to arrange things so it's never destructed

That triggers tools like LeakSanitizer, which would make unit/integration test suites fail.

jupp0r avatar Aug 08 '19 03:08 jupp0r