Wie läuft’s denn so? Monitoring mit MicroProfile Metrics

In meinem Blog-Eintrag „Alles gesund? Health checking mit MicroProfile Health“ habe ich dargestellt, wie einfach man den Gesundheitszustand einer Anwendung auf Basis von MicroProfile Health veröffentlichen kann. Und wenn man nun nicht nur wissen möchte, ob ein Service läuft, sondern mehr Details über den Service-Zustand erfahren möchte? Dann unterstützt uns ein weiterer Baustein: MicroProfile Metrics.

Telemetriedaten out of the box

Über den Rest-Endpoint /metrics/base werden ohne weiteren Programmieraufwand Basis-Telemetriedaten zur Verfügung gestellt:

{
  "memory.maxHeap": 8342470656,
  "memory.usedHeap": 259349504,
  "gc.G1 Young Generation.count": 28,
  "cpu.systemLoadAverage": 0.6,
  "thread.count": 111,
  ...
}

Die Daten werden in JSON geliefert, wenn man sie mit diesem Media Type anfordert (z. B. curl -H "Accept: application/json" localhost:8080/metrics/base). Mit dem Default-Media-Type text/plain werden die Daten passend für die Verarbeitung in Prometheus geliefert.

Der Endpoint /metrics/vendor ist für herstellerspezifische Messdaten aus der genutzten Plattform vorgesehen.

Anwendungsspezifische Metriken

EE-Anwendungen können weitere Messdaten bereitstellen. So kann man bspw. Methoden von CDI Beans mit der Annotation @Gauge versehen:

  @Gauge(name = "answerToLifeUniverseAndEverything", absolute = true, unit = MetricUnits.NONE)
  public long getAnswerToLifeUniverseAndEverything() {
    return 42;
  }

Die derart zur Verfügung gestellten Werte werden im Endpoint /metrics/application veröffentlicht:

{
  "answerToLifeUniverseAndEverything": 42
}

Neben dieser Möglichkeit, eigene Messwerte zu berechnen und zu veröffentlichen, kann man auch Telemetriedaten durch das Framework aggregieren lassen:

  @GET
  @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
  @Timed(name = "personList", absolute = true)
  public List getAll() {
    return this.personRepository.findAll();
  }

@Timed bewirkt hier, dass die Ausführungszeiten der annotierten Methode gemessen und zu verschiedenen statistischen Werten aggregiert werden:

{
  "personList": {
    "p50": 4922800.0,
    "p75": 5160200.0,
    "p95": 1.00322E7,
    "p98": 1.00322E7,
    "p99": 1.00322E7,
    "p999": 1.00322E7,
    "min": 3218000.0,
    "mean": 5476794.800597489,
    "max": 1.00322E7,
    "stddev": 2340795.005960796,
    "count": 5,
    "meanRate": 0.6766077577220262,
    "oneMinRate": 1.0,
    "fiveMinRate": 1.0,
    "fifteenMinRate": 1.0
  }
}

Analog könnte mit @Counted ein Aufrufzähler für eine Methode etabliert werden.

Gesamt-Metrik

Der Endpoint /metrics fasst die bislang genannten Datengruppen in ein Gesamtergebnis zusammen.

Plattformen

MicroProfile Metrics wird u. a. von folgenden JEE-Servern unterstützt:

  • OpenLiberty (mit aktivierten Features mpMetrics-1.1 und monitor-1.0).
  • Payara.
  • WildFly (Achtung: WildFly hat unterschiedliche Ports für Applikations- und Management-Zugriffe, im Default 8080 und 9990. Der o. a. REST-Endpoint /metrics steht über den Management-Port zur Verfügung).

Die genannten Klassen befinden sich bspw. in der Maven-Dependency org.eclipse.microprofile:microprofile.

Demo

In https://github.com/GEDOPLAN/metrics-demo finden Sie ein Beispielprojekt mit einem REST-Service, dessen GET– und POST-Methoden so annotiert sind, dass Zeiten bzw. Aufrufzahlen gemessen werden. Der o. a. Service, der die legendäre 42 als Ergebnis liefert, ist ebenfalls im Beispielprojekt enthalten. Die Anwendung kann als WAR-File auf einen der genannten Server deployt werden. Alternativ können über vorkonfigurierte Maven-Profile Docker-Images zum direkten Ausprobieren erstellt werden.

Bis bald – vielleicht auch in einem unserer Trainings in Berlin, Bielefeld, Köln oder bei Ihnen!
https://gedoplan-it-training.de/

Transaktionssteuerung mit @Transactional

Transaktionen sind nötig, um Daten atomar, konsistent, isoliert und dauerhaft bearbeiten und speichern zu können (ACID-Prinzip). Methoden von CDI-Beans können durch eine simple Annotation mit einem aktiven Transaktionskontext versehen werden.

Transaktionsinterceptor mit dem Binding @Transactional

Schon seit der Version 1.1 von CDI steht ein global aktivierter Transaktionsinterceptor zur Verfügung. Er wird mit der Annotation @Transactional einer Methode einer CDI-Bean zugeordnet:

public class SomeService {
  ...
  @Transactional
  public void doSomethingTransactionally() {
    ...
  }

Beim Aufruf der Methode ohne aktive Transaktion wird nun eine Transaktion gestartet und nach dem Verlassen der Methode wieder geschlossen. Die Regeln für das Transaktionsende sind etwas seltsam:

  • Endet die Methode mit einem Return, wird ein Commit versucht.
  • Wirft die Methode eine sog. System Exception, resultiert dies in einem Rollback. System Exceptions sind i. W. die unchecked Exceptions (ergänzt um die RemoteException, die aber explizit kaum eine Rolle spielt).
  • Wirft die Methode eine Application Exception – das sind alle anderen Exeptions – wird ein Commit (!) versucht.

Insbesondere der letzte Punkt ist (zumindest mir) unverständlich: Für mich modellieren Exceptions immer Ausnahmesituationen, die den jeweiligen Geschäftsprozess ungültig machen. Glücklicherweise kann man mit einem Parameter der Annotation auch in diesem Fall ein Rollback ansteuern:

  @Transactional(rollbackOn=Exception.class)
  public void doSomethingTransactionally() {

Transaktionsmodus

Die Art der Transaktionssteuerung durch @Transactional kann mit dem Parameter value bestimmt werden:

  • TxType.REQUIRED ist der Default, der in den allermeisten Fällen passend ist. Beim Aufruf einer davon betroffenen Methode wird geprüft, ob bereits eine aktive Transaktion vorliegt. Wenn ja, wird diese für den Methodenaufruf genutzt. Andernfalls wird eine neue Transaktion gestartet und am Methodenende wieder beendet.
  • TxType.REQUIRES_NEW startet bei Methodenbeginn stets eine neue Transaktion, die am Methodenende wieder beendet wird. Eine ggf. bereits aktive Transaktion wird zuvor suspendiert. Dieser Modus sollte dann verwendet werden, wenn die Ergebnisse des Methodenaufrufs auch dann dauerhaft abgespeichert werden sollen, wenn die umgebende Transaktoin zurückgerollt wird. Beispiel: Auditing/Protokollierung von Methodenaufrufen.
  • TxType.MANDATORY verlangt eine aktive Transaktion bei Methodenbeginn und wirft andernfalls eine TransactionRequiredException. Dieser Modus ist könnte prinzipiell für Repositories (a.k.a DAOs) interessant sein, da deren feingranulare Methoden i. A. von einem Service aufgerufen werden, der selbst transaktional ist. Leider werden Lifecycle-Methoden (@PostConstruct-annotierte Methoden etc.) nicht durch die @Transactional-Interceptoren intercepted. Häufig werden darin Repository-Methoden aufgerufen. Dann sollte für Repositories wie auch bei anderen Services TxType.REQUIRED verwendet werden.
  • Die weiteren Modi TxType.NOT_SUPPORTED, TxType.SUPPORTS und TxType.NEVER sind i. A. unbrauchbar.

Danke für’s Lesen und bis bald – vielleicht auch in einem unserer Trainings in Berlin, Bielefeld, Köln oder bei Ihnen!
https://gedoplan-it-training.de/

Alles gesund? Health checking mit MicroProfile Health

Im Bereich von Microservices und verteilten Systemen – aber nicht nur dort – ist es wichtig zu wissen, ob ein Service/System ordnungsgemäß läuft oder gestört ist. Im MicroProfile (https://microprofile.io) ist mit MicroProfile Health ein Teil enthalten, der es in Kombination mit CDI sehr einfach macht, Health Checking in EE-Anwendungen zu integrieren.

Health Check Callbacks

Die Idee von MicroProfile Health ist, dass Anwendungen auf EE-Servern oder auf Basis von MicroProfile-Frameworks ein oder mehrere Methoden zum Prüfen des Gesundheitszustandes enthalten. Der Server sammelt die Zustände bei Bedarf ein und aggregiert daraus einen Gesamtzustand, der auf dem Rest-Endpoint /health veröffentlicht wird:

GET /health

200 OK
{
  "outcome": "UP",
  "checks": [{
    "name": "Service1",
    "state": "UP",
    "data": {
      "memory": 180672240
    }
  }, {
    "name": "Service2",
    "state": "UP"
  }]
}

Die Callback-Methoden heißen – etwas uninspiriert – call und stammen aus dem Interface org.eclipse.microprofile.health.HealthCheck. Der Server sucht sich automatisch alle Klassen zusammen, die dieses Interface implementieren und mit dem CDI-Qualifier org.eclipse.microprofile.health.Health versehen sind:

@ApplicationScoped
@Health
public class HealthCheck1 implements HealthCheck {

  @Override
  public HealthCheckResponse call() {
    ...

Das Prüfergebnis können die Methoden bequem mittels Builder-Pattern zusammensetzen:

  public HealthCheckResponse call() {
    return HealthCheckResponse
        .named("Service1")
        .up()
        .withData("memory", Runtime.getRuntime().freeMemory())
        .build();

Neben up gibt es auch down sowie die Methode status, der der Gesundheitszustand als Parameter übergeben wird.

Gesamtgesundheit

Bei einem GET-Request auf den Pfad /health werden alle verfügbaren call-Methoden aus allen Anwendungen des Servers aufgerufen und zu einem Gesamtzustand kombiniert. Liefern alle Checks UP, ist das Attribut outcome im Gesamtergebnis ebenfalls UP und der Response hat den Status 200 OK. Andernfalls wird der Response mit 503 Service unavailable ausgeliefert und outcome ist darin DOWN.

Plattformen

MicroProfile Health wird u. a. von folgenden JEE-Servern unterstützt:

  • OpenLiberty (mit aktiviertem Feature mpHealth-1.0).
  • Payara.
  • WildFly (Achtung: WildFly hat unterschiedliche Ports für Applikations- und Management-Zugriffe, im Default 8080 und 9990. Der o. a. REST-Endpoint /health steht über den Management-Port zur Verfügung).

Die genannten Klassen befinden sich bspw. in der Maven-Dependency org.eclipse.microprofile:microprofile.

Demo

In https://github.com/GEDOPLAN/health-demo finden Sie ein Beispielprojekt mit zwei Health Checks, deren Ergebnis über eine Webseite beeinflusst werden kann. Die Anwendung kann als WAR-File auf einen der genannten Server deployt werden. Alternativ können über vorkonfigurierte Maven-Profile Docker-Images zum direkten Ausprobieren erstellt werden.

Ausblick

Im Beispiel wird als Zusatzinformation die Menge freien Speichers hinzugefügt, was im Kontext von Health Checking nicht grundsätzlich falsch ist, aber doch eher eine Metrik darstellt. Für deren Veröffentlichung hält MicroProfile eine weitere Teilspezifikation bereit: MicroProfile Metrics. Sie wird Gegenstand eines der nächsten Blog Posts sein – stay tuned!

Bis bald – vielleicht auch in einem unserer Trainings in Berlin, Bielefeld, Köln oder bei Ihnen!
https://gedoplan-it-training.de/

Anwendungskonfiguration mit Apache Tamaya

Nahezu jede Anwendung muss irgendwie konfiguriert werden – seien es Namen, die statt im Code fest eingebaut zu werden, aus einem File gelesen werden sollen, seien es Berechnungsparameter oder URLs. Java (EE) bietet dazu bislang keinen Standard an, so dass jeder sich „sein eigenes Süppchen kochen“ muss.

Nun zeigt sich ein Silberstreif am Horizont in Form eines API zur Konfiguration von Anwendungen, das auf der diesjährigen Java One als Bestandteil von Java EE 8 angekündigt wurde und das Ideen aus dem entsprechenden Teil von Apache DeltaSpike und von Apache Tamaya übernehmen soll. Letzteres soll im Folgenden kurz beschrieben werden.

Apache Tamaya (tamaya.incubator.apache.org) befindet sich derzeit noch in einem sehr frühen Stadium, bedient sich aber bewährter Ideen aus Apache DeltaSpike Configuration und ist somit durchaus in Grenzen bereits einsetzbar.

Apache Tamaya ist modular aufgebaut. Der Core-Anteil wird immer benötigt. Darüber hinaus gibt es diverse Extensions wie bspw. zur Bereitstellung von Konfigurationswerten per CDI Injection. Für Maven-Projekte sind dies die folgenden Dependencies:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-core</artifactId>
  <version>${version.tamaya}</version>
</dependency>
<dependency>
  <groupId>org.apache.tamaya.ext</groupId>
  <artifactId>tamaya-cdi</artifactId>
  <version>${version.tamaya}</version>
</dependency>

Derzeit (Dez. 2016) ist die aktuelle Version 0.2-incubating.

Konfigurationswerte können mit einem einfachen API abgeholt werden:

String javaVendor 
  = ConfigurationProvider.getConfiguration()
                         .getOrDefault("java.vendor", "unknown");

ConfigurationProvider.getConfiguration() liefert dabei das Einstiegsobjekt vom Typ Configuration, das neben der oben genutzten Methode getOrDefault auch solche anbietet, die den Wert in einen anderen Typ als String gewandelt liefern.

In CDI Beans kann zudem per Injektion auf die Werte zugegriffen werden:

@Inject
@Config(defaultValue = "unknown")
String companyName;

@Inject
@Config
int answerToLifeUniverseAndEverything;

@Inject
@Config("java.version")
String javaVersion;

Apache Tamaya holt die Werte per Default aus System Properties, Environment Entries und der Datei META-INF/javaconfiguration.properties im Classpath. Es können weitere Property Sources registriert werden und auch die Suchreihenfolge manipuliert werden.

Weitergehende Informationen finden sich auf der Homepage des Projektes (tamaya.incubator.apache.org). Aufgrund des aktuellen Projektstands ist aber noch viel im Fluss.

Ein Demo-Projekt zum Anschauen und Ausprobieren gibt es hier: github.com/GEDOPLAN/config-demo.

Java EE mit Java 8 verschönern!

J2EE war recht komplex und unübersichtlich, was viele durchaus zu Recht zu anderen Plattformen getrieben hat. Seit Java EE 5/6/7 ist das Thema aber deutlich verbessert: Der Fokus liegt nun eindeutig auf der Unterstützung der Entwickler und nicht mehr, wie zuvor, auf der für den Betrieb notwendigen Technik.

Durch Java 8 läßt sich nun noch eine weitere Verschönerung erreichen. Damit werden Java-EE-Anwendungen deutlich strukturierter!

Das folgende kleine Java-8-Programm tut den Trick:

package de.gedoplan.beantrial;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;

public class SourceCleaner
{

  public static void main(String[] args) throws IOException
  {
    for (String filename : args)
    {
      Files
        .lines(Paths.get(filename))
        .flatMapToInt(line -> line.codePoints())
        .mapToObj(i -> Character.valueOf((char) i))
        .collect(Collectors.groupingBy(x -> x, Collectors.summingInt(x -> 1)))
        .entrySet()
        .stream()
        .map(e -> new String(new char[e.getValue()]).replace('\0', e.getKey()))
        .forEach(System.out::println);
    }
  }
}

Aus einer unübersichtlichen Quelle wie der folgenden

package de.gedoplan.seminar.cdi.demo.intercept.interceptor;

import java.io.Serializable;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;

import org.apache.commons.logging.Log;

/**
 * Interceptor-Implementierung zu {@link TransactionRequired}.
 * 
 * @author dw
 */
@TransactionRequired
@Interceptor
public class TransactionRequiredInterceptor implements Serializable
{
  private static final long serialVersionUID = 1L;

  @Inject
  Log                       logger;

  @Inject
  UserTransaction           userTransaction;

  /**
   * Interceptor-Arbeitsmethode.
   * 
   * @param invocationContext InvocationContext
   * @return Returnwert
   * @throws Exception bei Fehlern
   */
  @AroundInvoke
  public Object manageTransaction(InvocationContext invocationContext) throws Exception
  {
    // Falls schon eine TX aktiv, Methode direkt aufrufen
    if (isTransactionActive())
    {
      return invocationContext.proceed();
    }

    // TX beginnen
    if (this.logger.isDebugEnabled())
    {
      this.logger.debug("Begin tx");
    }
    this.userTransaction.begin();

    try
    {
      // Methode aufrufen
      Object result = invocationContext.proceed();

      // TX committen
      if (this.logger.isDebugEnabled())
      {
        this.logger.debug("Commit tx");
      }
      this.userTransaction.commit();

      return result;
    }
    catch (Exception e) // CHECKSTYLE:IGNORE Allgemeine Exception ist hier OK
    {
      // TX zurückrollen
      if (this.logger.isDebugEnabled())
      {
        this.logger.debug("Rollback tx");
      }
      try
      {
        this.userTransaction.rollback();
      }
      catch (Throwable ignore)
      {
      }

      throw e;
    }

  }

  // Ist die TX aktiv?
  private boolean isTransactionActive() throws SystemException
  {
    return this.userTransaction.getStatus() == Status.STATUS_ACTIVE;
  }
}

wird die folgende, übersichliche und auch kürzere Form:

                                                                                                                                                                                                                                                                                                                                                                                                        
""""""
(((((((((((((((((((((
)))))))))))))))))))))
**************
,
--
...................................................
//////////////////
1
:
;;;;;;;;;;;;;;;;;;;;;;;;
====
?
@@@@@@@@@@
AAAAAAAA
B
CCCCCCCCCCC
DDDD
EEEEEEEEEEEEE
FF
G
H
IIIIIIIIIIIIIIIIII
KK
LLLL
MM
N
OOOO
RRRRRR
SSSSSSSSSS
TTTTTTTTTTTTTTTTTTTTTTT
UUUU
VV
XXXXX
Y
_
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbb
ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
ddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
fffffffff
ggggggggggggggggggggggggggggggggggggg
hhhhhhhhhhhhhhhhhhhhhhhhh
iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
jjjjjjjjjjjjjj
kkkkkkkkkk
llllllllllllllllllllllllllllllllllllllll
mmmmmmmmmmmmmmmmmmmmmmmmmmmmm
nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
ppppppppppppppppppppppppppppppppppppp
qqq
rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt
uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
vvvvvvvvvvvvvvvvvvvvvvv
wwwwwww
xxxxxxxxxxxxxxxxxxxxxxx
yyyy
zzzz
{{{{{{{{{{{{
ü
}}}}}}}}}}}}

Probieren Sie’s aus. Es funktioniert übrigens nicht nur für Java-Quellen, sondern auch für JavaScript, HTML, ja sogar für C#!