Quarkus, MicroProfile und die wunderbare Welt der Metriken

Februar 2022
November 2020
TLDR: MicroProfile Metriken sind toll und nützlich, hier die Doku.

Wer bereits in den Genuss gekommen ist eine Anwendung mit Quarkus zu entwickeln, weiß wahrscheinlich um die Vorzüge, die sich in Bezug auf das Sammeln von Metriken ergeben. Die MicroProfile-Spezifikation bietet dazu eine Fülle an Möglichkeiten, die ich hier kurz vorstellen möchte.

Natürlich deckt dieser Beitrag längst nicht alle Möglichkeiten und Aspekte bezüglich MicroProfile Metriken ab, und dient eher als Einstieg mit sinnvollen Beispielen. Der Inhalt bezieht sich auf die Verwendung mit Quarkus, sollte jedoch grundlegend auch für andere MicroProfile-Implementierungen gelten. Auf die Speicherung und Darstellung der Metriken mit beispielsweise Prometheus oder Grafana gehe ich hier nicht ein.

Update zu MicroProfile 3.3

Mittlerweile unterstützt Quarkus (seit Version 1.3) auch die MicroProfile 3.3-Spezifikation, die MicroProfile Metrics in Version 2.3 mitbringt.

MicroProfile Metriken - Ein Überblick

Die Metriken im MicroProfile sind – wie viele Spezifikationen im MicroProfile – auf einfache Anwendungen ausgelegt. Wir unterteilen Metriken in drei Bereiche (Scopes):"Base"-,"Vendor"- und "Application"-Metriken.

"Base"-Metriken müssen von jeder MicroProfile-Implementierung erstellt werden."Vendor"-Metriken sind für zusätzliche Metriken der einzelnen Implementierungen gedacht und "Application"-Metriken schließlich für die Anwendung selbst.

Da ich hier auf die Möglichkeiten im Rahmen eines Quarkus-Projekts eingehen möchte, beschränke ich mich auf die"Application"-Metriken.

Weitere Informationen zur Spezifikation können (beispielsweise für die aktuelle von Quarkus unterstützte Version 2.3) hier eingesehen werden:

Spezifikation

Siehe hierzu auch eine kurze Einführung mit Beispielen für die Verwendung in Quarkus:

Einführung

MicroProfile Metriken - Ein Beispiel

Wir fangen direkt mit einem minimalen Code-Beispiel an. Dieses stellt eine REST Schnittstelle dar, die eine übergebene Liste aus Ganzzahlen sortiert:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    public Response getRandom(@QueryParam("numbers") List<Integer> values)
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values); 
        Collections.sort(numbers);
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

Annotierte Metriken

Am einfachsten setzen wir Metriken per Annotation an die gewünschten Klassen, Methoden oder Felder. Schauen wir uns ein paar einfache Metriken an.

Annotierte Metriken – Counter

Wenn wir wissen wollen, wie häufig unsere Ressource aufgerufen wird, genügt Folgendes:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    @Counted
    public Response getRandom(@QueryParam("numbers") List<Integer> values) 
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values); 
        Collections.sort(numbers); 
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

Diese und alle anderen Metriken können wir bequem per REST-Schnittstelle im OpenMetrics oder Json Format unter “/metrics/application” für alle Application-Metriken abfragen. In unserem Beispiel bekommen wir, sofern wir unsere Schnittstelle einmal aufgerufen haben, folgende Ausgabe im OpenMetrics Format:

 
    application_test_RandomResource_getRandom_total 1.0
    

Nicht so fancy, oder? Gut, neuer Versuch mit:

     
    @Counted(name = "getRandomCount", absolute = true)
    

Damit erhalten wir Folgendes:

     
    application_getRandomCount_total 1.0
    

Wir können den Namen der Metrik festlegen(name) und bestimmen, dass dieser als absoluter Name(absolute) statt des vollen Klassen-Namens angezeigt wird.

Annotierte Metriken – Timer

Mit einem Timer bekommen wir - neben der Anzahl - auch Ausführungszeiten präsentiert:

     
    @Timed(name = "getRandomTimer", absolute = true, unit = MetricUnits.MILLISECONDS)
    
    
    application_getRandomTimer_rate_per_second0.03127755194344862application_getRandomTimer_one_min_rate_per_second0.04797284870696242application_getRandomTimer_five_min_rate_per_second0.012098841350039854application_getRandomTimer_fifteen_min_rate_per_second0.004292367258391886application_getRandomTimer_min_seconds3.8485E-5application_getRandomTimer_max_seconds0.003196787application_getRandomTimer_mean_seconds2.473753023562506E-4application_getRandomTimer_stddev_seconds7.667319852774055E-4application_getRandomTimer_seconds_count4.0application_getRandomTimer_seconds{quantile="0.5"} 4.8331E-5application_getRandomTimer_seconds{quantile="0.75"} 5.6871E-5application_getRandomTimer_seconds{quantile="0.95"} 0.003196787application_getRandomTimer_seconds{quantile="0.98"} 0.003196787application_getRandomTimer_seconds{quantile="0.99"} 0.003196787application_getRandomTimer_seconds{quantile="0.999"} 0.003196787            
    

Annotierte Metriken – SimpleTimer

Ein SimpleTimer ist ein vereinfachter Timer, der nur die Anzahl und die verstrichene Zeit berücksichtigt.

Das ist insbesondere dann sinnvoll, wenn die Metriken mit Prometheus ausgewertet werden sollen. Da Prometheus viele Werte ohnehin selbst bestimmt, müssen diese nicht über die Metrik zur Verfügung gestellt werden. Siehe auch die Spezifikation und diese Diskussion auf GitHub.

     
    @SimplyTimed(name = "getRandomTimer", absolute = true, unit = MetricUnits.MILLISECONDS)
    
    
    application_getRandomTimer_total3.0application_getRandomTimer_elapsedTime_seconds0.001360847
    

Annotierte Metriken – Meter

Ein Meter dagegen zeigt die Anzahl der Aufrufe in bestimmten Zeitabschnitten an:

     
    @Metered(name = "getRandomMeter", absolute = true, unit = MetricUnits.MILLISECONDS)
    
    
    application_getRandomMeter_total 1001.0
   application_getRandomMeter_rate_per_second 25.19547222937428
   application_getRandomMeter_one_min_rate_per_second 14.747915378636803
   application_getRandomMeter_five_min_rate_per_second 3.4269890149051405
   application_getRandomMeter_fifteen_min_rate_per_second 1.294740801690123
    

Annotierte Metriken – Gauge

Mit einem Gauge ist es möglich, Rückgabewerte von Methoden für die Erzeugung von Metriken zu verwenden. Der Wert selbst kann somit programmatisch und zur Laufzeit angepasst werden. Ein gutes Beispiel dazu findet sich im Quarkus-Tutorial.

Reicht das trotzdem nicht aus, so müssen eventuell Metriken dynamisch angelegt werden.

Dynamische Metriken

Mit den vorgestellten Metriken per Annotation kann man bereits viele Szenarien abdecken, die in einem Microservice relevant sind. Jedoch ist es manchmal erforderlich, zusätzlich Metriken dynamisch und programmatisch zu sammeln. Insbesondere Metriken für spezifische Werte der Businesslogik (wie Anzahlen von Objekten) oder Metriken, die erst zur Laufzeit dynamisch registriert werden, können somit flexibel eingesetzt werden.

Dynamische Metriken - MetricRegistry

Metriken werden in einer MetricRegistry gespeichert. Eine MetricRegistry verwaltet alle Metriken samt Metadaten, wie beispielsweise Name und Beschreibung. Die eindeutige Zuordnung geschieht über die Metric-ID, die aus dem Namen und allen Tags der Metrik besteht. Ferner existieren Registries für jeden der drei Bereiche Base, Vendor und Application. Später werden wir auf die Verwendung von Tags näher eingeben.

Die entsprechende Registry lässt sich komfortabel injecten:

    
    @Inject
   @RegistryType(type = MetricRegistry.Type.APPLICATION) 
    MetricRegistry metricRegistry;
    

Da die Application Registry der Standard ist, kann sie auch ohne @RegistryType injected werden.

Dynamische Metriken – Counter

Sobald man sich eine Registry eingebunden hat, lassen sich Metriken direkt in dieser registrieren. Für das hier verwendete Beispiel wollen wir neben einem annotierten Counter zusätzliche Counter für die Status Codes 200 und 400 einführen.

Dazu gibt man Folgendes an:

    
    http200Count = metricRegistry.counter("http200Count"); 
   http400Count = metricRegistry.counter("http400Count");
    

Diese Counter können nun im Code beliebig aufgerufen werden. In unserem Beispiel sieht das so aus:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    @Counted(name = "getRandomCount", absolute = true) 
    public Response getRandom(@QueryParam("numbers") List<Integer> values) 
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values);
        if (numbers.size() == 1) { 
            http400Count.inc(); 
            return Response.status(Status.BAD_REQUEST).build(); 
        }
        Collections.sort(numbers);
        http200Count.inc();
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

Falls nur eine Zahl übergeben wird, soll ein HTTP Response Code 400 zurückgegeben werden. Außerdem wird die entsprechende Metrik hochgezählt. Entsprechend wird im Erfolgsfall die Metrik für den Response Code 200 hochgezählt.

Beim Aufruf der Schnittstelle, einmal mit einer Zahl und einmal mit drei Zahlen, ergibt sich folgendes Bild beim Abruf der Metriken:

    
    application_http200Count_total 1.0 
   application_getRandomCount_total 2.0 
   application_http400Count_total 1.0
    

Das ist nur ein Minimalbeispiel. Mithilfe eines Metadata-Objekts können weitere Daten wie Name und Beschreibung hinzugefügt werden. Siehe dazu auch die Spezifikation.

Dynamische Metriken – Histogramm

Wenn man eine neue Metrik ohnehin in die Application Registry aufnehmen möchte, kann man das ebenfalls bequemer per @Metric Annotation erledigen. Das möchte ich in unserem Beispiel anhand einer Metrik zeigen, die nur dynamisch verfügbar ist – dem Histogramm.

Wir registrieren unser Histogramm folgendermaßen:

    
    @Inject
   @Metric(name = "getRandomHistoram", absolute = true) 
    Histogramm getRandomHistogram;
    

Im Code verwenden wir es überdies, um die Anzahl der übergebenen Zahlen als Häufigkeiten zu messen:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    @Counted(name = "getRandomCount", absolute = true) 
    public Response getRandom(@QueryParam("numbers") List<Integer> values) 
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values);
        if (numbers.size() == 1) { 
            http400Count.inc();
            return Response.status(Status.BAD_REQUEST).build(); 
        } 
        Collections.sort(numbers);
        http200Count.inc(); 
        getRandomHistoram.update(numbers.size());
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

Ein Aufruf mit viermal zwei Zahlen, dreimal drei Zahlen, zweimal vier Zahlen und einmal fünf Zahlen ergibt folgende Metriken:

    
    application_http200Count_total 10.0 
   application_getRandomCount_total 10.0 
   application_http400Zahl_insgesamt 0.0 
   anwendung_getRandomHistoram_min 2.0 
   anwendung_getRandomHistoram_max 5.0 
   application_getRandomHistoram_mean 3.316335141802363 
   application_getRandomHistoram_stddev 1.0588695413046927 
   anwendung_getRandomHistoram_count 10.0 
   application_getRandomHistoram{quantile="0.5"} 3.0 
   application_getRandomHistoram{Quantil="0.75"} 4.0 
   application_getRandomHistoram{Quantil="0.95"} 5.0 
   application_getRandomHistoram{Quantil="0.98"} 5.0 
   application_getRandomHistoram{Quantil="0.99"} 5.0 
   application_getRandomHistoram{Quantil="0.999"} 5.0
    

Tags

Zusätzlich zum Namen der Metrik, lassen sich Tags als Key, Value Paare definieren. Damit können wir die Metrik besser beschreiben und abgrenzen. Tags können wir sowohl bei annotierten als auch bei dynamischen Metriken hinzufügen.

Hier eine annotierte Metrik mit Tags samt Abruf der Metrik-Schnittstelle:

     
    @Counted(name = "getRandomCounter", absolute = true, tags = { "app=test", "method=get" })
    
     
    application_getRandomCounter_total{app="test",method="get"} 1.0
    

Metriken wiederverwenden

Bei annotierten Metriken ist das Wiederverwenden unter gleichem Namen und Tags (Metrik-ID) standardmäßig nicht möglich, um schwer auffindbare Fehler zu vermeiden. Dennoch kann man dies explizit ermöglichen. Dafür muss man das Feld “reusable” auf true setzen. Bei dynamischen Metriken dagegen ist das Wiederverwenden standardmäßig aktiviert, um eine Metrik an unterschiedlichen Stellen im Code ansprechen zu können.

Zur Verwendung von Namen, Tags, Metadaten und Typen sei noch Folgendes gesagt:

Wenn man eine Metrik wiederverwenden will, dann müssen Name, Tags und Type (z.B. Counter) übereinstimmen. Mehr dazu hier.

Externe Metriken

Abgesehen von selbst erstellten Metriken gibt es auch noch solche, die erst mit der Nutzung anderer Teile des MicroProfiles gesetzt werden.

Ein Beispiel dafür ist die Fault Tolerance Spezifikation, die beim Einsatz von z.B. @Timeout oder @Retry automatisch entsprechende Metriken registriert. Durch dieses Zusammenspiel der einzelnen Spezifikationen kommt man in den Genuss von automatischen Metriken, ohne selbst welche anlegen zu müssen.

Ein anderesBeispiel kommt von Quarkus selbst. Dort kann man beispielsweise bei Bedarf Metriken aktivieren, wenn der eigene Service mit Datenbanken kommuniziert. Diese bekommt man also “frei Haus” geliefert. Das ist nicht nur praktisch, sondern erspart auch Implementierungsaufwand und sorgt zusätzlich dafür, dass sich nicht jeder Entwickler für die gleiche Metrik eine eigene Lösung ausdenken muss.

Ausblick – Wie es mit den Metriken weitergeht

Mit der neuen MicroProfile Version 4.0 werden entsprechend auch die Metriken in einer neuen Version 3.0 zur Verfügung stehen.

Die Änderungen fallen dabei umfangreicher aus als zwischen 2.2 und 2.3. Abgesehen vonAnpassungen an Metriken wie SimpleTimer und Timer wurde beispielsweise die Wiederverwendbarkeit von Metriken ausgebaut. Nun geht das auch standardmäßig bei annotierten Metriken (außer Gauge). Weitere Änderungen lassen sich dem Changelog entnehmen.

MicroProfile Metriken - Ein Fazit

Insgesamt bietet die Metrik-Spezifikation des MicroProfiles eine tolle und einfache Möglichkeit, den eigenen Microservice mit Metriken aufzuhübschen.

Bisher konnten wir diese schon in einigen Projekten in mehreren Microservices erfolgreich einsetzen.

Natürlich sind die vorgestellten Metriken und Möglichkeiten nur eine Auswahl. Die am Anfang erwähnte Spezifikation der Metriken samt Link gibt einen tieferen Einblick für Interessierte.

Ich hoffe, dieser Beitrag ist dabei für den ein oder anderen hilfreich! Und Respekt an alle, die bis hierhin durchgehalten haben 😉

Mehr zu technologischen Themen

Ausgewählte Beiträge

Mehr anzeigen
Kein Spam, versprochen
Erhalten Sie wertvolle Insights von unserem Expertenteam.