Counters
Counters report a single metric: a count. The Counter
interface lets you increment by a fixed amount, which must be positive.
Never count something you can time with a Timer or summarize with a DistributionSummary ! Both Timer and DistributionSummary always publish a count of events in addition to other measurements.
|
When building graphs and alerts off of counters, you should generally be most interested in measuring the rate at which some event occurs over a given time interval. Consider a simple queue. You could use counters to measure various things, such as the rate at which items are being inserted and removed.
It is tempting, at first, to conceive of visualizing absolute counts rather than a rate, but the absolute count is usually both a function of the rapidity with which something is used and the longevity of the application instance under instrumentation. Building dashboards and alerts of the rate of a counter per some interval of time disregards the longevity of the app, letting you see aberrant behavior long after the application has started.
Be sure to read through the timer section before jumping into using counters, as timers record a count of timed events as part of the suite of metrics that go into timing. For those pieces of code you intend to time, you do NOT need to add a separate counter. |
The following code simulates a real counter whose rate exhibits some perturbation over a short time window:
Normal rand = ...; // a random generator
MeterRegistry registry = ...
Counter counter = registry.counter("counter"); (1)
Flux.interval(Duration.ofMillis(10))
.doOnEach(d -> {
if (rand.nextDouble() + 0.1 > 0) { (2)
counter.increment(); (3)
}
})
.blockLast();
1 | Most counters can be created off of the registry itself with a name and, optionally, a set of tags. |
2 | A slightly positively biased random walk. |
3 | This is how you interact with a counter. You could also call counter.increment(n) to increment by more than one in a single operation. |
A fluent builder for counters on the Counter
interface itself provides access to less frequently used options, such as
base units and description. You can register the counter as the last step of its construction by calling register
:
Counter counter = Counter
.builder("counter")
.baseUnit("beans") // optional
.description("a description of what this counter does") // optional
.tags("region", "test") // optional
.register(registry);
The @Counted
Annotation
The micrometer-core
module contains a @Counted
annotation that frameworks can use to add counting support to either specific types of methods such as those serving web request endpoints or, more generally, to all methods.
Micrometer’s Spring Boot configuration does not recognize @Counted on arbitrary methods.
|
Also, an incubating AspectJ aspect is included in micrometer-core
. You can use it in your application either through compile/load time AspectJ weaving or through framework facilities that interpret AspectJ aspects and proxy targeted methods in some other way, such as Spring AOP. Here is a sample Spring AOP configuration:
@Configuration
public class CountedConfiguration {
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
Applying CountedAspect
makes @Counted
usable on any arbitrary method in an AspectJ proxied instance, as the following example shows:
@Service
public class ExampleService {
@Counted
public void sync() {
// @Counted will record the number of executions of this method
...
}
@Async
@Counted
public CompletableFuture<?> async() {
// @Counted will record the number of executions of this method
return CompletableFuture.supplyAsync(...);
}
}
@MeterTag on Method Parameters
To support using the @MeterTag
annotation on method parameters, you need to configure the @CountedAspect
to add the CountedMeterTagAnnotationHandler
.
ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]";
// Example of a ValueExpressionResolver that uses Spring Expression Language
ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver();
// Setting the handler on the aspect
countedAspect.setMeterTagAnnotationHandler(
new CountedMeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver));
Let’s assume that we have the following interface.
interface MeterTagClassInterface {
@Counted
void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test);
@Counted
void getAnnotationForTagValueExpression(
@MeterTag(key = "test", expression = "'hello' + ' characters'") String test);
@Counted
void getAnnotationForArgumentToString(@MeterTag("test") Long param);
@Counted
void getMultipleAnnotationsForTagValueExpression(
@MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2",
expression = "'value2: ' + value2") DataHolder param);
}
When its implementations would be called with different arguments (remember that the implementation needs to be annotated with @Counted
annotation too), the following counters would be created:
// Example for returning <toString()> on the parameter
service.getAnnotationForArgumentToString(15L);
assertThat(registry.get("method.counted").tag("test", "15").counter().count()).isEqualTo(1);
// Example for calling the provided <ValueResolver> on the parameter
service.getAnnotationForTagValueResolver("foo");
assertThat(registry.get("method.counted")
.tag("test", "Value from myCustomTagValueResolver [foo]")
.counter()
.count()).isEqualTo(1);
// Example for calling the provided <ValueExpressionResolver>
service.getAnnotationForTagValueExpression("15L");
assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1);
// Example for using multiple @MeterTag annotations on the same parameter
// @MeterTags({ @MeterTag(...), @MeterTag(...) }) can be also used
service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe"));
assertThat(registry.get("method.counted")
.tag("value1", "value1: zxe")
.tag("value2", "value2: qwe")
.counter()
.count()).isEqualTo(1);
Function-tracking Counters
Micrometer also provides a more infrequently used counter pattern that tracks a monotonically increasing function (a function that stays the same or increases over time but never decreases). Some monitoring systems, such as Prometheus, push cumulative values for counters to the backend, but others publish the rate at which a counter is incrementing over the push interval. By employing this pattern, you let the Micrometer implementation for your monitoring system choose whether to rate-normalize the counter, and your counter remains portable across different types of monitoring systems.
Cache cache = ...; // suppose we have a Guava cache with stats recording on
registry.more().counter("evictions", tags, cache, c -> c.stats().evictionCount()); (1)
1 | evictionCount() is a monotonically increasing function that increments with every cache eviction from the beginning of its life. |
The function-tracking counter, in concert with the monitoring system’s rate normalizing functionality (whether this is an artifact of the query language or the way data is pushed to the system), adds a layer of richness on top of the cumulative value of the function itself. You can reason about the rate at which the value is increasing, whether that rate is within an acceptable bound, is increasing or decreasing over time, and so on.
Micrometer cannot guarantee the monotonicity of the function for you. By using this signature, you assert its monotonicity based on what you know about its definition. |
A fluent builder for function counters on the FunctionCounter
interface itself provides access to less frequently used options, such as base units and description. You can register the counter as the last step of its construction by calling register(MeterRegistry)
:
MyCounterState state = ...;
FunctionCounter counter = FunctionCounter
.builder("counter", state, state -> state.count())
.baseUnit("beans") // optional
.description("a description of what this counter does") // optional
.tags("region", "test") // optional
.register(registry);