Francisco Dorado
Francisco Dorado Software Architect at sngular.com in Seville. Specialised in backend technologies based in the Java ecosystem. Currently working on Microservices using Spring Framework and AWS Cloud technologies

Undertow metrics with Spring Actuator

Undertow metrics with Spring Actuator

This post is intended to show a possible solution to provide metrics of Undertow with Spring Actuator (Micrometer). Undertow provides its own metric collector, but we have encountered one issue where the values of these metrics are not being updated correctly because when we make a request the collector is incrementing the counter in two units instead of one. Consequently, we have created our own solution based on Undertow metrics collector where we providing next metrics:

  • Number of requests
  • Total number of error requests
  • The longest request duration in time
  • The shortest request duration in time

Defining and registering a metrics handler

Undertow provides a class MetricsHandler with all the information about the requests. We will create a wrapper about this metrics handler and register it to the handler process in Undertow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class UndertowMetricsHandlerWrapper implements HandlerWrapper {

    private MetricsHandler metricsHandler;

    @Override
    public HttpHandler wrap(HttpHandler handler) {
        metricsHandler = new MetricsHandler(handler);
        return metricsHandler;
    }

    public MetricsHandler getMetricsHandler() {
        return metricsHandler;
    }
}

For register the HandlerWrapper you must define a Bean of type UndertowDeploymentInfoCustomizer setting the handler.

1
2
3
4
5
6
7
@Bean
UndertowDeploymentInfoCustomizer undertowDeploymentInfoCustomizer(
    UndertowMetricsHandlerWrapper undertowMetricsHandlerWrapper) {

    return deploymentInfo -> 
        deploymentInfo.addOuterHandlerChainWrapper(undertowMetricsHandlerWrapper);
}

The MeterBinder

The next step is connect all MeterRegistry of our application to MetricsHandler. This operation should be executed when the application is ready because is the moment where the MetricsHandler is initialized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class UndertowMeterBinder implements ApplicationListener<ApplicationReadyEvent> {

    private final UndertowMetricsHandlerWrapper undertowMetricsHandlerWrapper;

    public UndertowMeterBinder(UndertowMetricsHandlerWrapper undertowMetricsHandlerWrapper) {
        this.undertowMetricsHandlerWrapper = undertowMetricsHandlerWrapper;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        bindTo(applicationReadyEvent.getApplicationContext().getBean(MeterRegistry.class));
    }

    public void bindTo(MeterRegistry meterRegistry) {
        bind(meterRegistry, undertowMetricsHandlerWrapper.getMetricsHandler());
    }

    // ...
}

Now, we can add the binder methods to our class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void bind(MeterRegistry registry, MetricsHandler metricsHandler) {
    bindTimer(registry, "undertow.requests", "Number of requests", metricsHandler,
            m -> m.getMetrics().getTotalRequests(), m2 -> m2.getMetrics().getMinRequestTime());
    bindTimeGauge(registry, "undertow.request.time.max", "The longest request duration in time", metricsHandler,
            m -> m.getMetrics().getMaxRequestTime());
    bindTimeGauge(registry, "undertow.request.time.min", "The shortest request duration in time", metricsHandler,
            m -> m.getMetrics().getMinRequestTime());
    bindCounter(registry, "undertow.request.errors", "Total number of error requests ", metricsHandler,
            m -> m.getMetrics().getTotalErrors());

}

private void bindTimer(MeterRegistry registry, String name, String desc, MetricsHandler metricsHandler,
                       ToLongFunction<MetricsHandler> countFunc, ToDoubleFunction<MetricsHandler> consumer) {
    FunctionTimer.builder(name, metricsHandler, countFunc, consumer, TimeUnit.MILLISECONDS)
            .description(desc).register(registry);
}

private void bindTimeGauge(MeterRegistry registry, String name, String desc, MetricsHandler metricResult,
                           ToDoubleFunction<MetricsHandler> consumer) {
    TimeGauge.builder(name, metricResult, TimeUnit.MILLISECONDS, consumer).description(desc)
            .register(registry);
}

private void bindCounter(MeterRegistry registry, String name, String desc, MetricsHandler metricsHandler,
                         ToDoubleFunction<MetricsHandler> consumer) {
    FunctionCounter.builder(name, metricsHandler, consumer).description(desc)
            .register(registry);
}

The project

In [1] it is available the project to test this metrics. We have used JMX Meter Binder and you can check the results using jconsole

JConsole

References

[1] Link to the project in Github

comments powered by Disqus