/ DEVOPS, MICROMETER, METRICS, SPRING BOOT

Metrics with Spring Boot 2.0 - Counters and gauges

Last week, I wrote about how one could migrate metrics from Spring Boot 1.5 to Spring Boot 2.0.

This week, it’s time to check the different metrics available in Spring Boot 2.0 and how to create them.

Meter

There are 4 main types of metrics available:

  1. Counter
  2. Gauge
  3. Timer
  4. Distribution summary
To keep the post readable in one piece, it will be limited to Counter and Gauge

All metrics inherit from the base Meter class. A Meter provides basic measurement storage capabilities. As per the documentation:

A named and dimensioned producer of one or more measurements

Here’s a simplified class diagram:

Meter class diagram

Counter

A Counter is a specialized Meter that can be incremented by a positive number (0 included - it can stay the same). For example, the number of pages views can be modeled as a counter, as it can only go up.

Creating a counter is quite straightforward, it all starts from the MeterRegistry. A MeterRegistry bean is provided by Spring Boot, so it only needs to be injected.

@Bean
fun simpleCounter(registry: MeterRegistry) = registry.counter("simple.counter")

There’s another option available: all of Meter children interfaces embed a fluent builder. This builder has a register(MeterRegistry) method.

The following snippet is equivalent to the above one:

@Bean
fun simpleCounter2(registry: MeterRegistry) = Counter
    .builder("simple.counter2")
    .register(registry)
The builder function is in general more powerful as it provides more options

An alternative to Counter is FunctionCounter, though they are un-related to one another interface-wise. FunctionCounter allows to track a monotonically increasing function - a function which value can only increase (or stay the same) over time.

class TrackedObject {

    var value: Double = 0.0
        get() {
            field += 1
            return field
        }
}

@Bean
fun tracked() = TrackedObject()

@Bean
fun simpleFunctionCounter(registry: MeterRegistry,
                           tracked: TrackedObject) = registry
            .more()
            .counter("simple.functioncounter", arrayListOf(), tracked) { it.value }
@Bean
fun simpleFunctionCounter2(registry: MeterRegistry,
                            tracked: TrackedObject) = FunctionCounter
            .builder("simple.functioncounter2", tracked) { it.value }
            .register(registry)
}

Micrometer only keeps a weak reference to the object parameter (the TrackedObject above). Unless the application code keeps a strong reference to it - just like in the above snippet where it’s part of the application context, it will be garbage collected. In general, it’s good.

In this snippet, it means that if tracked is not injected, but the method creates a new TrackedObject() instance, the metric will be lost pretty soon.

Both above will create a meter that will increase by 1 after being queried. Obviously, this is not super useful. However, function counters can be bound to interesting metrics:

  • Page views
  • Cache eviction
  • etc.
@Bean
fun guavaCache(registry: MeterRegistry, guava: GuavaCacheMetrics) = FunctionCounter
        .builder("cache.evictions", guava) { it.evictionCount().doubleValue() }
        .register(registry)

The Counter class hierarchy looks like the following:

Counter class diagram

Gauge

While a counter must always go up, gauges have no such constraint: a gauge outputs a value, unrelated from one call to the next.

To create a gauge, use the builder from Gauge.

@Bean
fun random() = SecureRandom()

@Bean
fun simpleGauge(registry: MeterRegistry, random: Random) = Gauge
        .builder("simple.gauge", random)
        { it.nextInt(100).toDouble() }
        .register(registry)

This creates a gauge that will randomly output a value.

Static methods on the MeterRegistry do create a gauge object but do not return a reference to it, but to the parameter object instead. It allows to "manually" change the value of a gauge:

val atomic = ref<MeterRegistry>().gauge("atomic.gauge", AtomicLong())
atomic?.getAndAdd(5)
atomic?.getAndDecrement()
atomic?.getAndSet(1)

On the flip side, it makes it impossible to register beans using those methods.

Micrometer offers a dedicate gauge to track time values, the TimeGauge. In addition to the value from Gauge, it adds a TimeUnit.

@Bean
fun simpleTimeGauge(registry: MeterRegistry, random: Random) = TimeGauge
        .builder("simple.timegauge", random, TimeUnit.SECONDS)
        { it.nextInt(100).toDouble() }
        .register(registry)

The Gauge class hierarchy is as follows:

Gauge class diagram

Conclusion

In this post, we covered counters and gauges. Also, Micrometer offers alternative flavors of them, such as function counters and time gauges. The main different between counters and gauges is that the former’s value can only increase, while the latter’s value is unrelated from one data point to another.

To go further:
Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Software Architect with 15 years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with narrower interests like Software Quality, Build Processes and Rich Internet Applications. Currently working for an eCommerce solution vendor leader. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More