kernel0707
kernel0707

Reputation: 394

What is a clean way of setting description for dynamic metrics in Micrometer?

This may sound silly and may have a really simple answer however I haven't found any yet.

Micrometer has Builder classes for each of the metric type of the following form:

<MetricType>.builder(<metricName>).description(<description>).tags(<tagsArray>).register(<meterRegistry>)

Lets take Counter metrics as an example. It can be incremented as follows:

<meterRegistry>.Counter(<counterName>, <Pairs of labels and their values>).increment()

The problem arises when you want to set the description. If the values of labels are dynamic in nature i.e the values are decided at run time or if the possible combination of labels is too many like 10+ then how are you supposed to set the description?

The only solution I could think of is to store the description somewhere when starting the metric service. Then create a wrapper around the increment method. Now, when the user calls the wrapper, call the register method (with the stored description) and then the increment method. And afaik the register method creates a new metric if it doesn't exist otherwise returns the existing metric.

The same problem could be easily resolved in Prometheus as they have a register method that lets you set all the labels (i.e keys only) and the description. The increment method then only requires the values of those labels.

Does micrometer provide similar functionality? Creating a wrapper for a simple application seems highly inconvenient. And considering that micrometer is already a wrapper around Prometheus (and other instrumenting services), I don't want to create a wrapper around it as well.

If the question doesn't make sense the following example may help:

Let's consider a Counter metric requests with label type whose value can be alpha, beta, gamma with the description being tracks requests from clients

Then as a solution I can add the following line in register function called when starting the metric service.

Counter.builder("requests")
    .description("tracks requests from clients)
    .tags("type", "alpha")
    .register(meterRegistry);

And then wherever I want to use this metric I can run the following code:

meterRegistry.Counter("request", "type", <valueOfType>).increment()

My concern is that in this solution I am initiating only the alpha type which doesn't seem right since that is the only type. Another option would be to register all 3 types, however, this is assuming all the possible values are known during runtime. If the values are discovered at runtime, this solution fails completely and now you have to use the solution with wrappers as described above or forget description altogether.

Upvotes: 4

Views: 3024

Answers (2)

Marco Dalla Santa
Marco Dalla Santa

Reputation: 361

I was struggle with your same problem, so I found this helpful post, but... I didn't liked this solution.

I think te better approach is a placeholder one, like this. So maybe define a HierarchicalNameMapper that will replace tag placeholder when tag keys match it, it's good choice.

So now the problem of dynamic tagging meters seems to be solved, but the description still remains a problem. So I didn't use this solution too.

My final solution was to store Counters Gauges and other Meters in maps and retrieving by Ids (Name and tags). The description was provided during measurements, where I lookup the map and if a metric with that Id already exists will be incremented (or decremented) otherwise will be created with a description based on a meaningful metric name and tag keys-values.

    /**
     * Increment the {@link Counter}  with input tags. If it does not exists it will be created.
     */
    public void increment(String description, String unit, Tags tags) throws MetricsException {
            this.counter(description, unit, tags).increment();
    }

    /**
     * Search for a counter with input tags. If it does not exists it will be created.
     */
    private Counter counter(String description, String unit, Tags tags) {
        final String counterId = name.concat(tags.toString());

        Counter counter = counters.get(counterId);
        if (counter == null) {

            counter = Counter.builder(name)
                    .description(description)
                    .baseUnit(unit)
                    .tags(tags)
                    .register(registry);

            counters.put(counterId, counter);
        }

        return counter;
    }

Where:
registry is MeterRegistry field value
counters is a Map<String, Counter>

If you are struggling about memory usage, I used a Java instrumentation agent to inspect the Map. This one. An entry of the map is like 1KB, so you should have like 1000 tag-names combination to use 1MB :)

Upvotes: 1

RUARO Thibault
RUARO Thibault

Reputation: 2850

I came across a similar problem, when trying to work with Counters and multiple value for a single Tag.

Here is my solution:

@RestController
public class HelloController {

    private final Counter.Builder counter;
    private MeterRegistry registry;

    public HelloController(MeterRegistry registry) {
        this.counter = Counter.builder("counter").description("A description");
        this.registry = registry;
    }

    @GetMapping("/count")
    public String count1() {
        counter.tag("tag2", "value1").register(registry).increment();
// I don't provide more description here, but I need to use .register('registry') at each call.
        return "Count";
    }

    @GetMapping("/count2")
    public String count2() {
        counter.tag("tag2", "value2").register(registry).increment();
        return "Count";
    }

}
  • Note : I have to call counter...register(registry) in order to have a single description, and multiple value for tags.

Upvotes: 3

Related Questions