Guía rápida de micrómetro

1. Introducción

Micrometer proporciona una fachada simple sobre los clientes de instrumentación para varios sistemas de monitoreo populares. Actualmente, es compatible con los siguientes sistemas de monitorización: Atlas, Datadog, Graphite, Ganglia, Influx, JMX y Prometheus.

En este artículo, presentaremos el uso básico de Micrometer y su integración con Spring.

En aras de la simplicidad, tomaremos Micrometer Atlas como ejemplo para demostrar la mayoría de nuestros casos de uso.

2. Dependencia de Maven

Para empezar, agreguemos la siguiente dependencia al pom.xml :

 io.micrometer micrometer-registry-atlas 0.12.0.RELEASE 

La última versión se puede encontrar aquí.

3. MeterRegistry

En Micrómetro, un MeterRegistry es el componente principal utilizado para registrar medidores. Podemos iterar sobre el registro y ampliar las métricas de cada medidor para generar una serie de tiempo en el backend con combinaciones de métricas y sus valores de dimensión.

La forma más simple del registro es SimpleMeterRegistry . Pero en la mayoría de los casos, deberíamos usar un MeterRegistry diseñado explícitamente para nuestro sistema de monitoreo; para Atlas, es AtlasMeterRegistry .

CompositeMeterRegistry permite agregar varios registros. Proporciona una solución para publicar métricas de aplicaciones en varios sistemas de monitoreo compatibles simultáneamente.

Podemos agregar cualquier MeterRegistry necesario para cargar los datos en múltiples plataformas:

CompositeMeterRegistry compositeRegistry = new CompositeMeterRegistry(); SimpleMeterRegistry oneSimpleMeter = new SimpleMeterRegistry(); AtlasMeterRegistry atlasMeterRegistry = new AtlasMeterRegistry(atlasConfig, Clock.SYSTEM); compositeRegistry.add(oneSimpleMeter); compositeRegistry.add(atlasMeterRegistry);

Hay un soporte de registro global estático en Micrometer: Metrics.globalRegistry . Además, se proporciona un conjunto de constructores estáticos basados ​​en este registro global para generar medidores en Métricas :

@Test public void givenGlobalRegistry_whenIncrementAnywhere_thenCounted() { class CountedObject { private CountedObject() { Metrics.counter("objects.instance").increment(1.0); } } Metrics.addRegistry(new SimpleMeterRegistry()); Metrics.counter("objects.instance").increment(); new CountedObject(); Optional counterOptional = Metrics.globalRegistry .find("objects.instance").counter(); assertTrue(counterOptional.isPresent()); assertTrue(counterOptional.get().count() == 2.0); }

4. Etiquetas y medidores

4.1. Etiquetas

Un identificador de un medidor consta de un nombre y etiquetas. Se sugiere que deberíamos seguir una convención de nomenclatura que separe las palabras con un punto, para ayudar a garantizar la portabilidad de los nombres de métricas en múltiples sistemas de monitoreo.

Counter counter = registry.counter("page.visitors", "age", "20s");

Las etiquetas se pueden utilizar para dividir la métrica para razonar sobre los valores. En el código anterior, page.visitors es el nombre del medidor, con age = 20s como etiqueta. En este caso, el contador está destinado a contar los visitantes de la página con edades comprendidas entre los 20 y los 30 años.

Para un sistema grande, podemos agregar etiquetas comunes a un registro, digamos que las métricas son de una región específica:

registry.config().commonTags("region", "ua-east");

4.2. Mostrador

Un contador informa simplemente un recuento de una propiedad específica de una aplicación. Podemos construir un contador personalizado con el constructor fluido o el método auxiliar de cualquier MetricRegistry :

Counter counter = Counter .builder("instance") .description("indicates instance count of the object") .tags("dev", "performance") .register(registry); counter.increment(2.0); assertTrue(counter.count() == 2); counter.increment(-1); assertTrue(counter.count() == 2);

Como se ve en el fragmento anterior, intentamos disminuir el contador en uno, pero solo podemos incrementar el contador monótonamente en una cantidad positiva fija.

4.3. Temporizadores

Para medir latencias o frecuencia de eventos en nuestro sistema, podemos usar Timers . Un temporizador informará al menos el tiempo total y el recuento de eventos de series temporales específicas.

Por ejemplo, podemos registrar un evento de aplicación que puede durar varios segundos:

SimpleMeterRegistry registry = new SimpleMeterRegistry(); Timer timer = registry.timer("app.event"); timer.record(() -> { try { TimeUnit.MILLISECONDS.sleep(1500); } catch (InterruptedException ignored) { } }); timer.record(3000, MILLISECONDS); assertTrue(2 == timer.count()); assertTrue(4510 > timer.totalTime(MILLISECONDS) && 4500 <= timer.totalTime(MILLISECONDS));

Para registrar eventos de larga duración, usamos LongTaskTimer :

SimpleMeterRegistry registry = new SimpleMeterRegistry(); LongTaskTimer longTaskTimer = LongTaskTimer .builder("3rdPartyService") .register(registry); long currentTaskId = longTaskTimer.start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException ignored) { } long timeElapsed = longTaskTimer.stop(currentTaskId); assertTrue(timeElapsed / (int) 1e9 == 2);

4.4. Calibre

Un medidor muestra el valor actual de un medidor.

A diferencia de otros medidores, los medidores solo deben informar datos cuando se observan. Los medidores pueden ser útiles al monitorear estadísticas de caché, colecciones, etc.

SimpleMeterRegistry registry = new SimpleMeterRegistry(); List list = new ArrayList(4); Gauge gauge = Gauge .builder("cache.size", list, List::size) .register(registry); assertTrue(gauge.value() == 0.0); list.add("1"); assertTrue(gauge.value() == 1.0);

4.5. Distribución Resumen

Distribución de eventos y un simple resumen es proporcionado por DistributionSummary :

SimpleMeterRegistry registry = new SimpleMeterRegistry(); DistributionSummary distributionSummary = DistributionSummary .builder("request.size") .baseUnit("bytes") .register(registry); distributionSummary.record(3); distributionSummary.record(4); distributionSummary.record(5); assertTrue(3 == distributionSummary.count()); assertTrue(12 == distributionSummary.totalAmount());

Además, DistributionSummary y Timers se pueden enriquecer con cuantiles:

SimpleMeterRegistry registry = new SimpleMeterRegistry(); Timer timer = Timer.builder("test.timer") .quantiles(WindowSketchQuantiles .quantiles(0.3, 0.5, 0.95) .create()) .register(registry);

En el fragmento anterior, tres medidores con etiquetas cuantiles = 0.3 , cuantiles = 0.5 y cuantiles = 0.95 estarán disponibles en el registro, indicando los valores por debajo de los cuales caen el 95%, 50% y 30% de las observaciones, respectivamente.

Para ver estos cuantiles en acción, agreguemos los siguientes registros:

timer.record(2, TimeUnit.SECONDS); timer.record(2, TimeUnit.SECONDS); timer.record(3, TimeUnit.SECONDS); timer.record(4, TimeUnit.SECONDS); timer.record(8, TimeUnit.SECONDS); timer.record(13, TimeUnit.SECONDS);

Luego, podemos verificar extrayendo valores en esos tres medidores de cuantiles :

List quantileGauges = registry.getMeters().stream() .filter(m -> m.getType().name().equals("Gauge")) .map(meter -> (Gauge) meter) .collect(Collectors.toList()); assertTrue(3 == quantileGauges.size()); Map quantileMap = extractTagValueMap(registry, Type.Gauge, 1e9); assertThat(quantileMap, allOf( hasEntry("quantile=0.3",2), hasEntry("quantile=0.5", 3), hasEntry("quantile=0.95", 8)));

Además, Micrometer también admite histogramas:

DistributionSummary hist = DistributionSummary .builder("summary") .histogram(Histogram.linear(0, 10, 5)) .register(registry);

De manera similar a los cuantiles, después de agregar varios registros, podemos ver que el histograma maneja el cálculo bastante bien:

Map histograms = extractTagValueMap(registry, Type.Counter, 1.0); assertThat(histograms, allOf( hasEntry("bucket=0.0", 0), hasEntry("bucket=10.0", 2), hasEntry("bucket=20.0", 2), hasEntry("bucket=30.0", 1), hasEntry("bucket=40.0", 1), hasEntry("bucket=Infinity", 0)));

Generally, histograms can help illustrate a direct comparison in separate buckets. Histograms can also be time scaled, which is quite useful for analyzing backend service response time:

SimpleMeterRegistry registry = new SimpleMeterRegistry(); Timer timer = Timer .builder("timer") .histogram(Histogram.linearTime(TimeUnit.MILLISECONDS, 0, 200, 3)) .register(registry); //... assertThat(histograms, allOf( hasEntry("bucket=0.0", 0), hasEntry("bucket=2.0E8", 1), hasEntry("bucket=4.0E8", 1), hasEntry("bucket=Infinity", 3)));

5. Binders

The Micrometer has multiple built-in binders to monitor the JVM, caches, ExecutorService and logging services.

When it comes to JVM and system monitoring, we can monitor class loader metrics (ClassLoaderMetrics), JVM memory pool (JvmMemoryMetrics) and GC metrics (JvmGcMetrics), thread and CPU utilization (JvmThreadMetrics, ProcessorMetrics).

Cache monitoring (currently, only Guava, EhCache, Hazelcast, and Caffeine are supported) is supported by instrumenting with GuavaCacheMetrics, EhCache2Metrics, HazelcastCacheMetrics, and CaffeineCacheMetrics. And to monitor log back service, we can bind LogbackMetrics to any valid registry:

new LogbackMetrics().bind(registry);

The usage of above binders are quite similar to LogbackMetrics and are all rather simple, so we won’t dive into further details here.

6. Spring Integration

Spring Boot Actuator provides dependency management and auto-configuration for Micrometer. Now it's supported in Spring Boot 2.0/1.x and Spring Framework 5.0/4.x.

We'll need the following dependency (the latest version can be found here):

 io.micrometer micrometer-spring-legacy 0.12.0.RELEASE 

Without any further change to existing code, we have enabled Spring support with the Micrometer. JVM memory metrics of our Spring application will be automatically registered in the global registry and published to the default atlas endpoint: //localhost:7101/api/v1/publish.

There're several configurable properties available to control metrics exporting behaviors, starting with spring.metrics.atlas.*. Check AtlasConfig to see a full list of configuration properties for Atlas publishing.

If we need to bind more metrics, only add them as @Bean to the application context.

Say we need the JvmThreadMetrics:

@Bean JvmThreadMetrics threadMetrics(){ return new JvmThreadMetrics(); }

As for web monitoring, it's auto-configured for every endpoint in our application, yet manageable via a configuration property: spring.metrics.web.autoTimeServerRequests.

The default implementation provides four dimensions of metrics for endpoints: HTTP request method, HTTP response code, endpoint URI, and exception information.

When requests are responded, metrics relating to request method (GET, POST, etc.) will be published in Atlas.

With Atlas Graph API, we can generate a graph to compare the response time for different methods:

By default, response codes of 20x, 30x, 40x, 50x will also be reported:

We can also compare different URIs :

or check exception metrics:

Note that we can also use @Timed on the controller class or specific endpoint methods to customize tags, long task, quantiles, and percentiles of the metrics:

@RestController @Timed("people") public class PeopleController { @GetMapping("/people") @Timed(value = "people.all", longTask = true) public List listPeople() { //... } }

Based on the code above, we can see the following tags by checking Atlas endpoint //localhost:7101/api/v1/tags/name:

["people", "people.all", "jvmBufferCount", ... ]

Micrometer also works in the function web framework introduced in Spring Boot 2.0. Metrics can be enabled by filtering the RouterFunction:

RouterFunctionMetrics metrics = new RouterFunctionMetrics(registry); RouterFunctions.route(...) .filter(metrics.timer("server.requests"));

Metrics from the data source and scheduled tasks can also be collected. Check the official documentation for more details.

7. Conclusion

En este artículo, presentamos el Micrómetro métrico de fachada. Al abstraerse y admitir múltiples sistemas de monitoreo bajo una semántica común, la herramienta facilita el cambio entre diferentes plataformas de monitoreo.

Como siempre, el código de implementación completo de este artículo se puede encontrar en Github.