Guida rapida al micrometro

1. Introduzione

Micrometro fornisce una semplice facciata sui client di strumentazione per numerosi sistemi di monitoraggio diffusi. Attualmente supporta i seguenti sistemi di monitoraggio: Atlas, Datadog, Graphite, Ganglia, Influx, JMX e Prometheus.

In questo articolo, introdurremo l'utilizzo di base di Micrometer e la sua integrazione con Spring.

Per semplicità, prenderemo Micrometer Atlas come esempio per dimostrare la maggior parte dei nostri casi d'uso.

2. Dipendenza da Maven

Per cominciare, aggiungiamo la seguente dipendenza al pom.xml :

 io.micrometer micrometer-registry-atlas 0.12.0.RELEASE 

L'ultima versione può essere trovata qui.

3. MeterRegistry

In Micrometer, un MeterRegistry è il componente principale utilizzato per registrare i contatori. Possiamo iterare sul registro e approfondire le metriche di ogni metro, per generare una serie temporale nel back-end con combinazioni di metriche e relativi valori di dimensione.

La forma più semplice del registro è SimpleMeterRegistry . Ma nella maggior parte dei casi, dovremmo utilizzare un MeterRegistry progettato esplicitamente per il nostro sistema di monitoraggio; per Atlas, è AtlasMeterRegistry .

CompositeMeterRegistry consente di aggiungere più registri. Fornisce una soluzione per pubblicare simultaneamente le metriche dell'applicazione su vari sistemi di monitoraggio supportati.

Possiamo aggiungere qualsiasi MeterRegistry necessario per caricare i dati su più piattaforme:

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

C'è un supporto del registro globale statico in Micrometer: Metrics.globalRegistry . Inoltre, viene fornito un set di builder statici basati su questo registro globale per generare contatori in Metrics :

@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. Tag e misuratori

4.1. Tag

Un identificatore di un contatore è costituito da un nome e tag. Si suggerisce di seguire una convenzione di denominazione che separa le parole con un punto, per garantire la portabilità dei nomi delle metriche su più sistemi di monitoraggio.

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

I tag possono essere utilizzati per affettare la metrica per ragionare sui valori. Nel codice sopra, page.visitors è il nome del contatore, con age = 20s come tag. In questo caso, il contatore ha lo scopo di contare i visitatori della pagina con età compresa tra 20 e 30 anni.

Per un sistema di grandi dimensioni, possiamo aggiungere tag comuni a un registro, supponiamo che le metriche provengano da una regione specifica:

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

4.2. Counter

Un contatore segnala semplicemente un conteggio su una proprietà specificata di un'applicazione. Possiamo costruire un contatore personalizzato con il builder fluente o il metodo di supporto di qualsiasi 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);

Come visto dallo snippet sopra, abbiamo provato a diminuire il contatore di uno, ma possiamo solo incrementarlo monotonicamente di un importo positivo fisso.

4.3. Timer

Per misurare le latenze o la frequenza degli eventi nel nostro sistema, possiamo utilizzare i timer . Un timer riporterà almeno il tempo totale e il conteggio degli eventi di serie temporali specifiche.

Ad esempio, possiamo registrare un evento dell'applicazione che può durare diversi secondi:

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));

Per registrare eventi di lunga durata, utilizziamo 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. Valutare

Un indicatore mostra il valore corrente di un metro.

Diverso da altri metri, Indicatori dovrebbe riferire solo i dati se osservato. Gli indicatori possono essere utili durante il monitoraggio delle statistiche di cache, raccolte, ecc .:

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. DistributionSummary

La distribuzione degli eventi e un semplice riepilogo sono forniti da 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());

Inoltre, DistributionSummary e Timers possono essere arricchiti da quantili:

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

Nello snippet sopra, tre indicatori con tag quantile = 0.3 , quantile = 0.5 e quantile = 0.95 saranno disponibili nel registro, indicando i valori al di sotto dei quali rientrano rispettivamente il 95%, 50% e 30% delle osservazioni.

Per vedere questi quantili in azione, aggiungiamo i seguenti record:

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);

Poi possiamo verificare estraendo i valori in quei tre quantile Calibri :

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)));

Inoltre, Micrometro supporta anche gli istogrammi:

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

Simile ai quantili, dopo aver aggiunto diversi record, possiamo vedere che l'istogramma gestisce il calcolo abbastanza bene:

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

In questo articolo, abbiamo introdotto la metrica per facciate Micrometro. Estraendo e supportando più sistemi di monitoraggio con una semantica comune, lo strumento semplifica il passaggio tra le diverse piattaforme di monitoraggio.

Come sempre, il codice di implementazione completo di questo articolo può essere trovato su Github.