Monitoring - JAX https://jax.de/tag/monitoring/ Java, Architecture & Software Innovation Wed, 02 Oct 2024 13:37:12 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Verdächtige Gerüche https://jax.de/blog/verdaechtige-gerueche-durch-code-smells-zum-besseren-code/ Fri, 03 Jun 2022 11:52:41 +0000 https://jax.de/?p=86883 Will man bestehenden Code verbessern, ist es nicht immer leicht, zu entscheiden, wann man mit dem Refactoring [1] anfängt und wann man es beendet. Die Idee der „Code Smells“ hilft dabei. Der Begriff beschreibt Strukturen im Code, die überarbeitet werden sollten. In diesem Artikel betrachten wir eine Sammlung wichtiger Code Smells und führen an einem Beispiel Schritt für Schritt ein Refactoring durch, durch das wir eine übelriechende Codestruktur verbessern.

The post Verdächtige Gerüche appeared first on JAX.

]]>
Die Idee der Code Smells ist über zwanzig Jahre alt, aber immer noch aktuell. Der Begriff wurde von Kent Beck geprägt und erlangte durch das Buch „Refactoring“ [2] von Martin Fowler eine weite Verbreitung. Fowler und Beck haben 22 Smells identifiziert und für deren Beseitigung einen umfangreichen Refactoring-Katalog erstellt. Laut der Software Craftsmanship Community gehört dieses Buch zu den Klassikern, die jeder Entwickler gelesen haben sollte, denn es verändere die Art und Weise, wie wir Software entwickeln [3]. Auch in dem Standardwerk „Clean Code“ von Robert C. Martin sind Code Smells ein fester Bestandteil. „Uncle Bob“ listet in seinem Buch nicht nur die Smells von Fowler und Beck auf, sondern ergänzt sie um seine eigenen Erfahrungen.

Fünf Kategorien

Die klassischen 22 Smells von Fowler und Beck werden ohne eine Kategorisierung oder Beziehung zueinander vorgestellt. Da einige Code Smells eng miteinander verwandt sind und die gleichen Symptome zeigen, haben Lassenius und Mäntylä in einer wissenschaftlichen Ausarbeitung vorgeschlagen, eine Kategorisierung einzuführen [4]. Das Ergebnis waren folgende fünf Kategorien: Bloaters, Change Preventers, Dispensables, Couplers und Object Orientation Abuser. Diese Kategorisierung ist mittlerweile sehr verbreitet und Code Smells lassen sich damit sehr einfach erklären. Abbildung 1 zeigt die fünf Kategorien mit der Einordnung der Code Smells, die wir in diesem Artikel näher betrachten werden.

Das Ziel ist es, eine Übersicht über die große Anzahl von Code Smells zu bekommen und erste Hinweise mitzunehmen, anhand welcher Kriterien wir sie jeweils wiedererkennen.

Abb. 1: Fünf Kategorien von Code Smells

Bloaters

Die Kategorie Bloaters umfasst Smells, die den Code aufblähen. Es handelt sich dabei um Strukturen, durch die der Code sehr groß und unbeweglich wird. Solche Strukturen entstehen nicht zwingend von Anfang an, sondern über eine gewisse Zeit, gerade, wenn technische Schulden nicht abgebaut werden. Ab einer bestimmten Größe des Codes wird es für Entwickler sehr schwer, Erweiterungen oder Modifikationen daran durchzuführen. Für sehr umfangreiche Codestrukturen ohne Modularität ist es schwierig bis unmöglich, sie unter Test zu bringen. Die Smells dieser Kategorie lassen sich aufgrund ihrer großen Präsenz sehr einfach erkennen.

Eine davon ist die „lange Methode“. Es ist deutlich einfacher, die Logik einer kurzen Methode als die einer langen im Kopf zu behalten. Eine Methode sollte keine 100 Zeilen und bestenfalls keine 20 Zeilen groß sein, hier waren sich Kent Beck und Robert C. Martin bereits vor zwanzig Jahren einig [5]. Ihre Empfehlung liegt bei ca. fünf Zeilen pro Methode. Fakt ist: Je kleiner die Methoden sind, desto einfacher sind sie zu verstehen und desto einfacher lassen sie sich erweitern. Dies hat sich in vielen Projekten bewährt. In der Praxis, gerade in Legacy-Systemen, kommen leider schon mal Methoden mit 1 000 Zeilen vor. Hierbei handelt es sich dann um einen sehr übelriechenden Code, der nach Verbesserung geradezu schreit.

Durch lange Methoden entstehen indirekt auch lange Parameterlisten, da die benötigten Werte auf irgendeinem Weg in die Methode gelangen müssen. Keine wirkliche Alternative ist die Verwendung globaler Daten, denn auch hier muss der Entwickler die Herkunft der Daten interpretieren und verstehen. Je länger die Parameterliste, desto höher die Komplexität. Aus Testsicht sind lange Parameterlisten sehr schwierig, denn es muss jede mögliche Konstellation mit Tests abgedeckt werden. Hier wird empfohlen, ab drei Parametern darüber nachzudenken, ob man zum Beispiel ein Parameterobjekt einführt [5].

 

 

Die „umfangreiche Klasse“, auch als „Gottobjekt“ bekannt, bezeichnet eine Klasse, die zu viel weiß oder zu viel tut. Da in so einem Konstrukt die Übersicht schnell verloren geht, schleichen sich auch gern Redundanzen ein. Ein bekanntes Prinzip, um diesem Code Smell entgegenzuwirken, ist das Single Responsibility Principle (SRP). Jede Klasse sollte nur eine Verantwortlichkeit haben.

Bei „Datenklumpen“ handelt es sich um verschiedene Datenelemente, die häufig zusammen auftreten, in Feldern, aber auch in Parameterlisten. Das Konstrukt ist meistens durch gleiche Präfixe oder Suffixe erkennbar (Abb. 2). Ein sehr einfacher Test, um Datenklumpen zu identifizieren, ist es, ein Feld aus dem Klumpen zu löschen. Wenn die Felder nicht für sich alleine stehen können, dann handelt es sich um einen Datenklumpen. Dieser Code Smell würde auch in die Kategorie „Object Orientation Abuser“ passen, denn aus jedem Datenklumpen wird eine eigene Klasse extrahiert.

Abb. 2: Datenklumpen extrahieren

Change Preventers

Code Smells dieser Kategorie verhindern bzw. verlangsamen jegliche Erweiterung von Software. Bei einer Änderung an einer Stelle ist zwingend auch eine Änderung an einer anderen Stelle notwendig. Damit erhöht sich der Aufwand von Anpassungen drastisch. Diese Kategorie enthält zwei Code Smells, die zwar eng miteinander verwandt, aber im Grunde das genaue Gegenteil voneinander sind.

Abb. 3: Change Preventers

Wenn eine Klasse oder Methode aus verschiedenen Gründen und auf verschiedene Art und Weise immer wieder geändert wird, dann handelt es sich dabei um den Code Smell „divergierende Änderungen“. Eine Klasse wird zum Beispiel an drei Stellen geändert, um eine Datenbankänderung durchzuführen. Die gleiche Klasse muss aber auch an vier ganz anderen Stellen geändert werden, um einen neuen Datentyp einzuführen. Bei diesem Code wird das Single Responsibility Principle (SRP) verletzt. Die Klasse enthält mehrere Zuständigkeiten und sollte aufgeteilt werden.

Genau das Gegenteil von divergierenden Änderungen ist die sogenannte „Chirurgie mit der Schrotflinte“ (engl. Shotgun Surgery). Hierbei müssen bei jeder Änderung zwingend auch Anpassungen an weiteren Stellen durchgeführt werden. Die Modifikationen für die Änderung sind weit verstreut, dadurch kann es äußerst aufwendig sein, alle relevanten Stellen zu identifizieren. Um diese komplexe Struktur aufzulösen, sollte man zuallerst die schlecht voneinander getrennte Programmlogik bündeln und im Anschluss in eigene Strukturen extrahieren. Denn das oberste Ziel sollte sein, eine Änderung immer nur an einer einzigen Stelle durchführen zu müssen.

Dispensables

Die Kategorie „Dispensables“ repräsentiert entbehrlichen und sinnlosen Code. Hierzu gehören Code Smells wie „redundanter Code“, „träges Element“, „spekulative Generalisierung“ und „Kommentare“. Das Fehlen dieser Konstrukte würde den Code deutlich sauberer und leichter verständlich machen.

Redundanter Code kann in verschiedenen Ausprägungen vorkommen. Wenn es sich bei der Redundanz um komplett identischen Code handelt, dann lässt sich dieser sehr einfach identifizieren und beseitigen. Mittlerweile kann auch die IDE sehr gut unterstützen, um identischen Code über das gesamte Projekt zu finden. Handelt es sich bei den Redundanzen jedoch um ähnlichen, aber nicht vollständig identischen Code, dann kann das Identifizieren und Beseitigen schon aufwendiger werden. Eines der wichtigsten Prinzipien für sauberen Code ist das DRY-Prinzip (Don’t Repeat Yourself). Die Kernaussage ist, redundanten Code zu vermeiden.

Wenn eine Klasse zu wenig tut, sollte ihre Daseinsberechtigung hinterfragt werden. Es könnte sein, dass sie in der Vergangenheit mehr Verantwortung hatte und durch eine Überarbeitung zu einem „trägen Element“ (engl. lazy class) geworden ist. Hierbei muss man konsequent sein und das Ziel haben, unnötige Komplexität zu entfernen.

Die „spekulative Generalisierung“ entsteht, wenn Entwickler mit der Prämisse „Früher oder später brauchen wir die Funktionalität“ programmieren. Es wird Funktionalität umgesetzt, die nicht verwendet wird. Dies erhöht die Komplexität des Gesamtsystems unnötig und sollte vermieden werden. Hier gelten die zwei Prinzipien: KISS – Keep it Simple, Stupid und YAGNI – You ain’t gonna need it.

Zu Kommentaren haben Fowler und Beck eine klare Meinung: Es handele sich um Deodorant für Code Smells und sei damit selbst ein solcher [2]. Wenn Entwickler übelriechenden Code schreiben und ihnen das bewusst ist, sprühen sie etwas Deodorant drüber, indem ein Kommentar dazu verfasst wird. Daher sollten Kommentare immer kritisch betrachtet werden: Was will man damit verbergen?

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Couplers

Die Code Smells dieser Kategorie führen zu einer übermäßigen Kopplung zwischen Klassen. Für die leichte Wandelbarkeit einer Software strebt man jedoch eine möglichst lose Kopplung zwischen Bausteinen an sowie eine hohe Kohäsion (starke Bindung) innerhalb eines Bausteins [6]. Enge Kopplung führt dazu, dass Änderungen in einer Klasse automatisch auch Änderungen in einer anderen Klasse verursachen.

Zu dieser Kategorie gehören Smells wie „Featureneid“, Insiderhandel, Mitteilungsketten und Vermittler. Die letzten beiden entstehen, wenn die enge Kopplung durch eine übermäßige Delegation ersetzt wird.

Abb. 4: Featureneid und Insiderhandel

Wenn eine Methode mehr an den Daten einer anderen Klasse interessiert ist als an den Daten der eigenen Klasse, dann sprechen wir von Featureneid (Abb. 4). Ein typisches Szenario ist beispielsweise, wenn eine Methode eine Berechnung durchführen will und dafür ein Dutzend Getter-Methoden einer anderen Klasse aufruft, um sich die Daten für die Berechnung zu holen. Bei so viel Neid sollte hinterfragt werden, ob die Methode in der richtigen Klasse ist.

Beim Insiderhandel tuscheln zwei Klassen hinter dem Rücken der offiziellen Schnittstelle. Diese Kopplung ist nicht auf Anhieb ersichtlich und nur schwer zu entdecken, da die Absprachen der Klassen heimlich erfolgen. Bei der Vererbung ist dieses Konstrukt oft zu beobachten, wenn die Unterklasse mehr über die Basisklasse weiß als notwendig. Daraufhin verschafft sich die Unterklasse einen Vorteil, in dem sie mit den Daten der Basisklasse arbeitet. In diesem Fall sollte die Vererbungshierarchie hinterfragt werden.

Abb. 5: Mitteilungsketten und Vermittler

Eine Mitteilungskette (Abb. 5) entsteht, wenn ein Objekt A die Daten von Objekt D benötigt und dies nur durch unnötige Navigation durch die Objekte B und C erreicht. Objekt A fragt nach Objekt B, danach das Objekt C und anschließend Objekt D mit den relevanten Daten.

A.getB().getC().getD().getTheNeededData()

Jede Änderung an den Beziehungen der beteiligten Objekte zwingt den Aufrufer ebenfalls zu einer Änderung. Durch die Refactoring-Maßnahme „Delegation verbergen“ kann die Mitteilungskette verkürzt werden. Bei Delegation verbergen muss der Aufrufer (Objekt A) nicht zwingend alle Objekte kennen, wenn zum Beispiel Objekt B die Verbindung zu Objekt C und D verbirgt.

A.getB().getTheNeededDataThroughCandD()

Somit muss bei Änderungen nicht immer der Aufrufer (Objekt A) geändert werden. Durch solch eine Maßnahme könnte aber auch der Code Smell „Vermittler“ entstehen. Wenn eine Klasse hauptsächlich aus einfachen Delegationen besteht und sonst keinen eigenen Mehrwert bietet, dann sprechen wir von einem Vermittler. Der Vermittler ist an der Stelle eine unnötige Komplexität, wir sollten versuchen, darauf zu verzichten.

Object-oriented Abuser

Diese Kategorie enthält übelriechenden Code, bei dem die objektorientierten Prinzipien falsch oder unvollständig umgesetzt wurden. Vererbung und Polymorphie sind die grundlegenden Konzepte der Objektorientierung. Wenn diese Konzepte falsch angewendet werden, so leiden darunter hauptsächlich die Wartbarkeit und Wiederverwendbarkeit. Object-oriented Abuser enthält Code Smells wie wiederholte Switch-Anweisung, alternative Klassen mit unterschiedlichen Schnittstellen und temporäres Feld.

Wenn sich Switch-Anweisungen mit identischer Steuerungslogik wiederholen, dann ist dies ein Anzeichen für das Fehlen von objektorientiertem Design. Die gleiche Fallunterscheidung anhand eines Typs ist an verschiedenen Stellen im Code verteilt. Möchte man einen weiteren Typ für die Fallunterscheidung hinzufügen, so muss man zwingend alle Switch-Anweisungen finden und dort die Erweiterung durchführen. Das ist sehr mühsam, unübersichtlich und fehleranfällig, doch das Problem lässt sich sehr elegant durch Polymorphie lösen. Wir schauen uns im nächsten Abschnitt diesen Code Smell anhand eines Beispiels etwas genauer an.

Beim ausgeschlagenen Erbe (engl. Refused Bequest) übernimmt eine Unterklasse nicht alle Daten oder Funktionen der Oberklasse, sondern unterstützt nur bestimmte Aspekte. Dazu kann es kommen, wenn die Vererbung verwendet wird, um bestimmte Aspekte der Oberklasse wiederzuverwenden, ohne dass die zwei Objekte etwas miteinander zu tun haben. Bei diesem Code Smell sollte die Vererbungshierarchie hinterfragt werden.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

 

Klassen untereinander austauschen zu können, ist ein großer Vorteil der objektorientierten Programmierung. Dies wird durch die Verwendung von Schnittstellen erreicht. Um Klassen untereinander auszutauschen, sollte man aber auch die gleichen Schnittstellen verwenden. Der Code Smell „alternative Klassen mit unterschiedlichen Schnittstellen“ kann entstehen, wenn beim Erstellen einer Klasse eine äquivalente Klasse übersehen wird. Dann wird eine ähnliche Klasse mit einer anderen Schnittstelle erzeugt. Die Schnittstellen müssen in so einem Fall vereinheitlicht werden.

Ein nur temporär genutztes Feld in einer Klasse kann zu großer Verwirrung führen. Der Entwickler muss die Fragen „Wo wird es verwendet?“ und „Warum wird es nur da verwendet?“ klären. Das erhöht unnötig die Komplexität, denn die Erwartungshaltung ist, dass ein Objekt alle seine Felder benötigt und benutzt und das nicht nur unter bestimmten Konstellationen. Ein temporäres Feld sollte am besten in eine eigene Klasse extrahiert werden und sämtlicher Code, der mit dem Feld arbeitet, sollte auch umgezogen werden.

Der Weg zu besserem Code

Das Erkennen von übelriechendem Code ist erst die halbe Miete. Uns ist dadurch zwar bewusst, welche Konstrukte überarbeitet werden sollten, doch nun stellt sich die Frage, wie wir übelriechenden Code beseitigen, ohne die Funktionalität der Software zu gefährden.

Ein Refactoring sollte immer in kleinen Schritten durchgeführt werden. Ein großes Big Bang Refactoring geht selten gut. Bei der Überarbeitung ist eine entsprechende Testabdeckung sehr wichtig. Dies kann jedoch eine Herausforderung sein, speziell im Umfeld von Legacy Code. Aber auch für Legacy-Systeme gibt es Ansätze, um die Testabdeckung aufzubauen, z. B. kann man mit der umgedrehten Testpyramide beginnen und diese mit der Zeit drehen.

Fowler und Beck haben für jeden identifizierten Code Smell eine Anleitung verfasst, welche Schritte notwendig sind, um ihn aufzulösen. Wir schauen uns weiter unten in Listing 1 exemplarisch so ein Vorgehen an. Da die Ausprägungen von Code Smells sehr unterschiedlich sein können, ist nicht immer eine Anleitung notwendig, gerade wenn es sich um einfache Smells handelt. Erfahrungsgemäß lassen sich diese bereits durch folgende toolgestützte Refactoring-Maßnahmen beseitigen:

  • Methode/Variable verschieben
  • Methode/Variable umbenennen
  • Methode/Variable extrahieren
  • Methode/Variable inline platzieren

Diese Maßnahmen werden von der Entwicklungsumgebung (IDE) unterstützt. Es lohnt sich, diese Unterstützung der IDE im Detail zu kennen und zu verwenden, denn das gibt dem Entwickler eine zusätzliche Sicherheit beim Refactoring.

Anhand des Beispiels in Listing 1 schauen wir uns den Code Smell „wiederholte Switch-Anweisungen“ etwas genauer an. Wir analysieren zum einen die übelriechenden Stellen im Code und schauen, wie wir ihn mit Hilfe der Anleitung aus dem Refactoring-Katalog [2] verbessern können. Bei dem Beispiel handelt es sich um das Parrot-Refactoring Kata [7], das in verschiedenen Programmiersprachen existiert.

Die Klasse Parrot (engl. für Papagei) enthält zwei Methoden mit den redundanten Switch-Anweisungen. Die Methode getSpeed bestimmt die Geschwindigkeit des Papageis und die Methode getPlumage den Zustand des Federkleides. Es gibt die drei Papageientypen „European“, „African“ und „Norwegian Blue“, die unterschiedliche Ausprägungen für Geschwindigkeit und Gefieder haben. Würde ein neuer Papageientyp hinzukommen, so müsste man alle Fallunterscheidungen anpassen. Dies widerspricht dem Open Closed Principle (OCP) und lässt sich mit Polymorphie sehr gut lösen.

public class Parrot {
// ...
double getSpeed(ParrotTypeEnum type) {
  switch (type) {
    case EUROPEAN:
      return getBaseSpeed();
    case AFRICAN:
      return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
    case NORWEGIAN_BLUE:
      return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
  throw new RuntimeException("Should be unreachable");
}
 
String getPlumage(ParrotTypeEnum type) {
  switch (type) {
    case EUROPEAN:
      return "average";
    case AFRICAN:
      return numberOfCoconuts > 2 ? "tired" : "average";
    case NORWEGIAN_BLUE:
      return voltage > 100 ? "scorched" : "beautiful";
  }
  throw new RuntimeException("Should be unreachable");
}
// ...
}

Bedingung durch Polymorphie ersetzen

Im Folgenden schauen wir uns in Kürze die Anleitung aus dem Refactoring-Katalog von Fowler und Beck an. Es handelt sich dabei um ein bewährtes Vorgehen zum Auflösen unseres Code Smells „wiederholte Switch-Anweisungen“.

  1. Klassen für die Polymorphie erstellen
  2. Fabrikfunktionen zur Rückgabe der richtigen Instanzen erstellen
  3. Im aufrufenden Code die Fabrikmethoden verwenden
  4. Methoden in der Unterklasse überschreiben
  5. Verschieben der bedingten Funktionen aus der Basisklasse in die Unterklassen

Das ganze Refactoring ist auf sehr kleine Schritte aufgeteilt. Nach jedem Schritt wird geprüft, ob der Code kompilierbar ist und die automatisierten Tests noch funktionieren.

Im ersten Schritt werden die Klassen für die Polymorphie erstellt. Das Ergebnis sind die Klassen EuropeanParrot, AfricanParrot und NorwegianParrot, die von der Oberklasse Parrot erben. Im zweiten Schritt werden die entsprechenden Fabrikfunktionen erstellt und im nächsten Schritt in die aufrufenden Stellen eingebunden. Die ersten drei Schritte haben nur die neue objektorientierte Klassenstruktur erzeugt. Die gesamte Logik ist weiterhin in der Oberklasse Parrot und wird von den Unterklassen verwendet. Dies hat den großen Vorteil, dass eine neue Klassenstruktur in bestehenden Code eingeführt werden kann, ohne die Funktionalität zu beeinträchtigen.

Im vierten Schritt überschreiben die Unterklassen die Methoden getSpeed und getPlumage und rufen vorerst über super (Listing 2) die Methoden der Oberklasse auf, in der noch immer die Switch-Anweisungen ausgeführt werden.

public class AfricanParrot extends Parrot {
// ...
@Override
public double getSpeed() {
  return super.getSpeed();
}
@Override
public double getPlumage() {
  return super.getPlumage();
}
}

Erst im fünften Schritt wird die Logik aus den Switch-Anweisungen in die entsprechenden Unterklassen verschoben, und das Refactoring ist fast vollbracht. Zum Schluss haben die Methoden getSpeed und getPlumage in der Parrot-Klasse keine Funktionalität mehr und können damit abstract werden. Das Ergebnis aller Schritte inklusive der Bereinigung zum Schluss zeigt Listing 3.

public abstract class Parrot {
 
  public static Parrot createEuropeanParrot() {
    return new EuropeanParrot();
  }
  public static Parrot createAfricanParrot( int numberOfCoconuts) {
    return new AfricanParrot(numberOfCoconuts);
  }
 
  public static Parrot createNorwegianBlueParrot(double voltage, 
boolean isNailed) {
    return new NorwegianBlueParrot(voltage, isNailed);
  }
 
  public abstract double getSpeed();
  public abstract double getPlumage();
  // ...
}
 
public class AfricanParrot extends Parrot {
  // ...
  @Override
  public double getSpeed() {
    return Math.max(0, getBaseSpeed() - getLoadFactor() * numberOfCoconuts);
  }
 
  @Override
  public String getPlumage() {
    return numberOfCoconuts > 2 ? "tired" : "average";
  }
  // ...
}
 
public class EuropeanParrot extends Parrot{
  // ...
  @Override
  public double getSpeed() {
    return getBaseSpeed();
  }
 
  @Override
  public String getPlumage() {
    return "average";
  }
  // ...
}
 
public class NorwegianBlueParrot extends Parrot{
  // ...
  @Override
  public double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
 
  @Override
  public String getPlumage() {
    return voltage > 100 ? "scorched" : "beautiful";
  }
  // ...
}

Dieses Vorgehen zeigt uns, wie iterativ eine komplexe Strukturänderung im Code erfolgen kann. Auch wenn die Schritte teilweise zu kleinteilig wirken, sind sie in der Praxis, wenn der Code deutlich umfangreicher ist, genau richtig. Nur durch solche kleinen Schritte und die ständige Ausführung der Tests kann ein komplexes Refactoring ohne Seiteneffekte erfolgreich durchgeführt werden.

Fazit

In diesem Artikel haben wir anhand von fünf Kategorien 18 Code Smells betrachtet. Um die schlechten Gerüche im Code wieder loszuwerden, sollte als Erstes die Testabdeckung sichergestellt werden. Weitere Sicherheit bieten toolgestützte Refactorings, mit denen bereits gute Ergebnisse erzielt werden. Bei komplexen Code Smells lohnt es sich, auf bewährtes Vorgehen zurückzugreifen. Ein Beispiel hierfür haben wir uns im Detail angeschaut.

Mit der Metapher Code Smells steht uns seit mehr als zwanzig Jahren ein Werkzeug zur Verfügung, mit dem wir schlechten Code identifizieren und bereinigen können. Wir sollten von diesen Erfahrungen profitieren und nicht alle Fehler selbst machen. Sich mit den Code Smells auseinanderzusetzen, hat zwei wesentliche Vorteile. Zum einen erkennen wir leichter die Strukturen, die überarbeitet werden sollten, und zum anderen entwickeln wir ein besseres Bewusstsein dafür, welche Fehler wir vermeiden sollten, wenn wir neuen Code schreiben. Beide Aspekte führen zu dem Ergebnis, dass die Qualität der Software besser wird.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

 

Links & Literatur

[1] https://de.wikipedia.org/wiki/Refactoring

[2] Fowler, Martin: „Refactoring: Improving the Design of Existing Code“; Addison-Wesley, 2018

[3] Mancuso, Sandro: „The Software Craftsman: Professionalism, Pragmatism, Pride“, Pearson Education, 2014

[4] Mäntylä, M. V. and Lassenius, C.: „Subjective Evaluation of Software Evolvability Using Code Smells: An Empirical Study“, Journal of Empirical Software Engineering, vol. 11, no. 3, 2006, pp. 395–431.

[5] Martin, Robert C.: „Clean Code: A Handbook of Agile Software Craftsmanship“, Prentice Hall, 2008

[6] Starke, Gernot: „Effektive Softwarearchitekturen: Ein praktischer Leitfaden“, Hanser Fachbuch, 2002

[7] https://github.com/emilybache/Parrot-Refactoring-Kata.git

The post Verdächtige Gerüche appeared first on JAX.

]]>
Alles im Blick https://jax.de/blog/alles-im-blick/ Fri, 11 Mar 2022 13:59:28 +0000 https://jax.de/?p=85794 Wie funktionieren APM Agents in der Java Virtual Machine im Detail? Was ist bei der Instrumentierung zu beachten und welche Besonderheiten von APM-Agenten muss man berücksichtigen? Und warum werden oftmals nur die bekanntesten Java-Frameworks unterstützt? Diesen Fragen gehen wir in diesem Beitrag auf den Grund.

The post Alles im Blick appeared first on JAX.

]]>
APM steht für Application Performance Management und erlaubt es als Teil der Observability, die eigene Anwendung genauer zu durchleuchten. Wie lange dauern bestimmte SQL-Abfragen? Welche Microservices oder Datenbanken werden innerhalb eines HTTP Requests abgefragt? Welcher Teil eines HTTP Requests ist der eigentliche Flaschenhals und weist die längste Antwortzeit auf? Um diese Fragen zu beantworten, reicht es nicht, Logs oder Metriken zu betrachten, sondern man muss sich die Laufzeit einzelner Methoden oder Aufrufe innerhalb der eigenen Anwendung ansehen. Hier kommt APM ins Spiel.

APM als Säule der Observability

APM gehört zum Tracing, das neben Logs und Metriken als eine der drei Säulen der Observability gilt. Dabei geht es allerdings nicht um die Daten selbst oder die Art der Datengenerierung, sondern allein um die Möglichkeit, aus vielen Signalen (Logs, Metriken, Traces, Monitoring, Health Checks) diejenigen herauszufiltern, die auf eine mögliche Einschränkung eines eigenen oder fremden Service wie Antwortzeit oder Verfügbarkeit hindeuten. Einzelne Bereiche der Observability sollten nicht isoliert betrachtet und idealerweise auch nicht mit unterschiedlichen Tools bearbeitet werden, damit man nicht nachts um drei Uhr mit mehreren Browsertabs und manueller Korrelation bei einem Ausfall eingreifen muss. Eine Kerneigenschaft des APM ist die Darstellung der Laufzeit von Komponenten der eigenen Anwendung. Die zwei wichtigsten Begriffe sind hier Transaction und Span. Eine Transaction ist eine systemübergreifende Zusammenfassung einzelner Spans, welche die Laufzeit konkreter Methoden oder Aktionen innerhalb eines Systems zusammenfassen. Eine Transaction kann sich über mehrere Systeme ziehen und beginnt bei einer Webanwendung im besten Fall im Browser des Users (Abb. 1).

Abb. 1: Transaction über mehrere Systemgrenzen (farbig gekennzeichnet)

Instrumentierung innerhalb der JVM

Bei APM-Agenten liegt der Fall etwas anders als bei Logging und Metriken, da sie in die Anwendung hineinschauen müssen und diese unter Umständen auch verändern. Bugs in diesen Agenten sind gefährlich und können sich auf die Anwendung auswirken – unabhängig von der Programmiersprache. Java hat im Gegensatz zu vielen anderen Sprachen eine standardisierte Schnittstelle zur Instrumentierung. Der durch Kompilierung erstellte Bytecode kann verändert werden und die Anwendung kann mit diesen Änderungen weiterlaufen. Um diese Veränderung so einfach wie möglich zu implementieren, gibt es Bibliotheken wie ASM oder Byte Buddy, mit denen Methodenaufrufe abgefangen werden können, um beispielsweise die Laufzeit zu messen. Wenn ich als Entwickler eines Agenten also alle Aufrufe des in der JVM eingebauten HTTP-Clients abfange und die Laufzeit sowie den Endpunkt als Teil eines Spans logge, kann ich danach einfach im APM UI sehen, wie viel Zeit diese Anfrage benötigt und ob lokales Request Caching beim Einhalten möglicher SLAs hilft. Durch Aktivieren des Agenten darf kein oder nur geringer Einfluss auf die Geschwindigkeit der Anwendung genommen werden (Overhead). Das Gleiche gilt für die Garbage Collection. Beides lässt sich nicht völlig verhindern, jedoch stark reduzieren.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Der Zeitpunkt der Instrumentierung ist unterschiedlich. Die bekannteste und beliebteste Variante ist das Setzen des Agents als Parameter beim Starten der JVM:

java -javaagent:/path/to/apm-agent.jar -jar my-application.jar

Dieser Aufruf führt die Instrumentierung aus, bevor der eigentliche Code geladen wird. Alternativ kann die Instrumentierung auch bei einer bereits laufenden JVM stattfinden.

Doch was genau bedeutet Instrumentierung eigentlich? Die Laufzeit von Methoden kann nur gemessen werden, wenn diese Methoden abgefangen werden und Code des Agenten um den eigentlichen Code herum ausgeführt wird – zum Beispiel das Anlegen eines Spans oder einer Transaction, wenn ein HTTP Request abgeschickt wird. Dieser Ansatz ist aus der aspektorientierten Programmierung bekannt. Eine Voraussetzung muss gegeben sein: Der jeweilige APM-Agent muss die Methode inklusive Signatur kennen, die instrumentiert wird. Für Java Servlets ist zum Beispiel die Methode service(HttpServletRequest req, HttpServletResponse resp) im Interface HttpServlet die wichtigste Methode, um jeden HTTP-Aufruf zu überwachen, unabhängig vom Pfad oder der HTTP-Methode. Das bedeutet gleichzeitig, dass bei einer Änderung der Methodendeklaration der Agent ebenfalls angepasst werden muss. Und hier liegt eines der größten Probleme mit dieser Form der Überwachung: Es muss sichergestellt sein, dass sowohl möglichst viele Frameworks (Spring Boot, JAX-RS, Grails, WildFly, Jetty etc.) und deren Methoden instrumentiert werden, als auch ständig überprüft wird, ob die Instrumentierung bei einem neuen Release noch funktioniert, besonders bei Major Releases. Das ist eine der großen Maintenance-Aufgaben bei der Entwicklung von APM-Agenten. Während es einige Standards im Java-Bereich gibt, die einfach zu instrumentieren sind, wie Servlets, JDBC oder JAX-RS, gibt es ebenfalls eine Menge Frameworks, die keinen solchen Standards folgen, wie beispielsweise Netty. Das heißt auch, dass jeder Agent, dessen Instrumentierung eines Frameworks nicht funktioniert hat, weil eventuell Methoden aus einer anderen Major-Version mit anderer Signatur existieren, sicherstellen muss, dass dies kein Problem darstellt.

Bisher haben wir nur APM Agents im Kontext von Java Agents erwähnt; es gibt aber noch weitere Anwendungsfälle. Ganz aktuell hat das AWS-Corretto-Team um Volker Simonis einen Agent zum Patchen der Log4Shell-Sicherheitslücke entwickelt [1]. Der Agent verhindert das Sicherheitsproblem der Remote Code Execution bei einer bereits laufenden JVM. Ein anderer Use Case sind Agents, die Sicherheitsfeatures wie das automatische Setzen von HTTP Headern oder WAF-Funktionalität bereitstellen.

 

Agents und ihre Features

Der bekannteste Standard in der Observability-Welt ist OpenTelemetry [2]. Die Idee von OpenTelemetry ist, den einen, herstellerneutralen, universellen Standard zur Verfügung zu stellen, den alle verwenden, unabhängig von der Programmiersprache. Mit dem OpenTelemetry Agent gibt es einen JVM Agent, den viele Observability-Anbieter als eigene Distribution veröffentlichen, wie zum Beispiel Lightstep oder Honeycomb. Dieser kommt bereits mit einigen Instrumentierungen für bekannte Frameworks [3].

Eine weitere Standardisierung in Zeiten von Microservices und der Möglichkeit, einen Request vom Laden der Webseite im Browser bis zur SQL-Query zu identifizieren, ist das Distributed Tracing. Um Distributed Tracing zwischen verschiedenen Programmiersprachen und Umgebungen zu implementieren, existiert die OpenTracing-Spezifikation und -Implementierung. Viele APM Agents folgen dieser Spezifikation, um Kompatibilität sicherzustellen, unter anderem Lightstep, Instana, Elastic APM, Apache Skywalking und Datadog.

Vor OpenTelemetry gab es einige wenige Agents, die aus JVM-Sicht eigentlich gar keine waren, weil sie keinerlei Instrumentierung vorgenommen, sondern lediglich Interfaces in bestimmten Frameworks implementiert haben, um Monitoringdaten auszulesen. Ich gehe davon aus, dass es über kurz oder lang nur noch Agents geben wird, die auf dem OpenTelemetry Agent basieren, und dass die Alleinstellungsmerkmale nicht im Sammeln der Daten, sondern ausschließlich in der Auswertung liegen.

Elastic APM Agent

Wenn es einen OpenTelemetry Agent gibt, wieso gibt es dann zum Beispiel auch einen Elastic-APM-spezifischen Agent? Zum einen gibt es eben doch mehr Features als einen universellen Standard. Beispiel: das Erfassen interner JVM-Metriken (Speicherverbrauch, Garbage-Collection-Statistiken) oder auch das Auffinden langsam ausführender Methoden ohne das Wissen um konkrete Methoden oder Code mit Hilfe des async-profilers [4] – eine Technologie, die bei Datadog, Elastic APM oder Pyroscope [5] verwendet wird. Des Weiteren existieren eine Menge Agenten bereits länger als die OpenTelemetry-Implementierung und bringen mehr Unterstützung für bestimmte Frameworks mit, die erst in den OpenTelemetry-Agenten portiert werden müssen.

Der Elastic Agent bietet zudem ein weiteres sehr interessantes Feature, und zwar das programmatische Konfigurieren des Agent anstatt der Verwendung des JVM-Agent-Mechanismus als Parameter beim Starten der JVM. Das bedeutet, man bindet den Agent als Dependency in den Code ein, und versucht die folgende Zeile Code beim Start der Anwendung so früh wie möglich auszuführen:

ElasticApmAttacher.attach();

Jetzt geschieht prinzipiell dasselbe wie bei der Agent-spezifischen Konfiguration: Der Agent attacht sich selbst an den laufenden Code. Diese Art der Einrichtung hat einen großen Vorteil: Die Dependency ist bereits Teil des Deployments und muss nicht als Teil des Build-Prozesses oder der Container-Image-Erstellung heruntergeladen werden. Gleichzeitig ist der Entwickler für das fortlaufende Aktualisieren verantwortlich.

Bisher ungeklärt ist die Frage, was ein APM Agent mit den erhobenen Daten eigentlich machen soll. Im Fall von Elastic APM werden diese an einen APM-Server geschickt, der sie wiederum im nächsten Schritt in einem Elasticsearch Cluster speichert. Der APM-Server kommuniziert nicht nur mit den anderen Elastic APM Agents (Node, Ruby, PHP, Go, iOS, .NET, Python), sondern kann auch Daten puffern und als Middleware für Source Mapping bei JavaScript-Anwendungen agieren.

Wie bereits erwähnt, liegt der Mehrwert weniger im Sammeln als im Auswerten von Daten. Im Fall von Elastic APM ist das unter anderem die Integration mit Machine Learning, genauer der Time Series Anomaly Detection, um automatisiert Laufzeiten von Transaktionen zu erkennen, die im Vergleich zu vorher gemessenen Ergebnissen überdurchschnittlich lange brauchen, dem automatischen Annotieren von Deployments im APM UI oder auch der Korrelation von plötzlich auftretenden Transaktionslatenzen und Fehlerraten in allen von der Anwendung generierten Logs. So wird sichergestellt, dass die Grenzen zwischen den anfangs erwähnten Observability-Säulen nicht existieren.

Programmatische Spans und Transactions

Nicht jeder Entwickler möchte ein eigenes APM-Agent-Plug-in schreiben, damit die eigene Java-Anwendung Spans und Transactions innerhalb der eigenen Geschäftslogik verwendet. Ein alle 30 Sekunden laufender Job im Hintergrund sollte als eigene Transaktion und jede der darin aufgerufenen Methoden als eigener Span konfiguriert werden. Hier gibt es zwei Möglichkeiten der Konfiguration. Entweder werden die Methodennamen über die Agentenkonfiguration angegeben oder man wählt die programmatische Möglichkeit. Ein Beispiel innerhalb von Spring Boot zeigt Listing 1.

@Component
public class MyTask {
 
  @CaptureTransaction
  @Scheduled(fixedDelay = 30000)
  public void check() {
    runFirst();
    runSecond();
  }
 
  @CaptureSpan
  public void runFirst() {
  }
 
  @CaptureSpan
  public void runSecond() {
  }
}

Die Annotation @CaptureTransaction legt eine neue Transaktion an und innerhalb dieser Transaktion werden die beiden Spans für die Methoden via @CaptureSpan angelegt. Sowohl Transaktion als auch Spans können mit einem eigenen Namen konfiguriert werden, der im UI einfacher identifiziert werden kann. Unter [6] gibt es ein GitHub Repository, das sowohl die Instrumentierung des Java-HTTP-Clients zeigt als auch das Verwenden von programmatischen Transaktionen und Spans im eigenen Java-Code.

 

Elastic APM Log Correlation

Wie erwähnt, ist es sinnvoll, Logs, Metriken und Traces miteinander zu verbinden. Wie aber kann eine bestimmte Logzeile mit einer bestimmten Transaktion verbunden werden? In Elastic APM heißt dieses Feature Log Correlation. Der erste (optionale) Schritt ist, Logdateien ins JSON-Format zu überführen. Das macht es wesentlich einfacher, weitere Felder zu den Logdaten hinzuzufügen. Eben diese Felder werden für die Korrelation benötigt. Wenn man im Agent die Option log_ecs_reformatting verwendet, werden im sogenannten MDC der jeweiligen Logger-Implementierung die Felder transaction.id, trace.id und error.id hinzugefügt, nach denen dann sowohl in Transactions und Spans als auch in einzelnen Lognachrichten gesucht werden kann. So können Lognachrichten unterschiedlichster Services miteinander korreliert und durchsucht werden; Logmeldungen eines Service sind einer konkreten eingehenden HTTP-Anfrage zuzuordnen.

Automatische Instrumentierung mit K8s

Will man Container mit Java-Anwendungen unter Kubernetes instrumentieren, kann man jederzeit die verwendeten Images/Pod-Konfigurationen anpassen und innerhalb dieser den Agent konfigurieren sowie APM-Konfigurationen einstellen, zum Beispiel APM-Endpunkte, API-Token (zum Beispiel via HashiCorp Vault [7]) oder die erwähnte Log Correlation. Es gibt eine weitere Möglichkeit, und zwar die Verwendung eines Init-Containers, der vor den eigentlichen Anwendungscontainern in einem Pod ausgeführt wird [8]. Dieser Container konfiguriert Umgebungsvariablen, die dann beim Starten des regulären Containers ausgelesen werden und somit zusätzlich den passenden JVM-Agenten starten [9]. Dieser Ansatz kann sinnvoll sein, wenn man keine Kontrolle über die erstellten Container hat oder sicherstellen möchte, dass ein Agent in einer bestimmten Version für alle Java-Anwendungen läuft.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

Distributed Tracing mit RUM

In Zeiten von Microservices und APIs ist es in vielen Systemarchitekturen wahrscheinlich, dass ein einzelner Aufruf eines Anwenders sich zu mehreren Aufrufen in der internen Architektur multiplext und mehrere Services abgefragt werden. Hier ist es besonders wichtig, verfolgen zu können, wie ein initialer Request durch die unterschiedlichen Services weitergereicht und verändert wird. Eine Transaktion kann mehrere Spans haben, die in unterschiedlichen Systemen auftreten, unter Umständen auch gleichzeitig. Hier kommt Distributed Tracing mit Hilfe von Trace IDs ins Spiel, die durch alle Requests hindurch – zum Beispiel mit Hilfe von HTTP-Headern – an die jeweiligen Spans angehängt werden und somit durch den Lebenszyklus des initialen Request rückverfolgbar sind. Ein weiterer Vorteil von Distributed Tracing ist die Möglichkeit, aus diesen Daten eine Service Map zu erstellen, da man weiß, welche Services miteinander kommunizieren (Abb. 2).

Abb. 2: Service Map, um Kommunikationsflüsse einzelner Komponenten zu visualisieren

Bei der Entwicklung von Webanwendungen ist es ebenfalls nicht ausreichend, erst an den eigenen Systemgrenzen mit dem Anlegen von Transactions und Spans zu beginnen, da man sonst keinen Überblick über die komplette Performance der eigenen Anwendung hat. Wie lange dauert das Aufbauen der Verbindung im Browser zum Webserver? Ist die Latenz hier eventuell so hoch, dass es irrelevant ist, 50 ms bei einer komplexen SQL-Query zu sparen? Um dieses Problem anzugehen, gibt es das Real User Monitoring, kurz RUM. Zum einen können Transaktionen an der richtigen Stelle begonnen werden, zum anderen werden auch Browserereignisse geloggt, um festzustellen, wie lange das initiale Rendern der Seite braucht, sodass der Anwender mit der Anwendung interagieren kann (Abb. 3).

Abb. 3: RUM-Dashboard mit Ladezeiten und Browserstatistiken

APM in der Zukunft

Das Bedürfnis, für Anwendungen eine Art Röntgengerät zu bekommen, wird in Zukunft noch zunehmen – vielleicht werden sich die Methoden etwas ändern. Zeit für einen kleinen Ausblick. In den vergangenen Jahren ist eine neue Art von Agents auf den Markt gekommen, die eine neue, sprachunabhängige Technologie verwenden: eBPF. Mit Hilfe von eBPF kann man Programme im Kernelspace laufen lassen, ohne den Kernel zu verändern oder ein Linux-Kernel-Modul laden zu müssen. Alle eBPF-Programme laufen innerhalb einer Sandbox, sodass das Betriebssystem Stabilität und Geschwindigkeit garantiert. Da eBPF Syscalls überwachen kann, ist es ein idealer Einstiegspunkt für jegliche Observability-Software. Der weitaus wichtigere Teil ist allerdings die Möglichkeit, diese Syscalls auf Methodenaufrufe in die jeweilige Programmiersprache des überwachten Programms zu übersetzen. eBPF-basierte Profiler haben generell einen geringen Overhead, da sie sehr tief im System verankert sind. Des Weiteren müssen keine Deployments angepasst werden, da diese Profiler auch innerhalb eines Kubernetes-Clusters für alle Pods konfiguriert werden können. Beispiele für diese Art von Profiler sind prodfiler [10] von Elastic, Pixie [11], Parca [12] oder Cilium Hubble [13].

Ein weiteres neues Themenfeld ist das Überwachen auf Serverless-Plattformen. Hier braucht man etwas andere Lösungen, da nicht garantiert ist, dass nach dem Verarbeiten einer Anfrage noch Rechenkapazität zur Verfügung gestellt wird. Methoden wie etwa Spans und Traces als Batch zu sammeln und periodisch an den APM-Server zu verschicken, funktionieren hier also nicht. Für AWS Lambda steht mit opentelemetry-lambda [14] ein eigenes GitHub-Projekt zur Verfügung. Die grundlegende Idee ist ein sogenannter Lambda-Layer, der diese Observability-Aufgaben übernimmt. Wenn man also in diese Art von Plattformen eintaucht, sollte man sicherstellen, dass die eigene Observability-Plattform diese Technologien unterstützt.

Ein weiterer wichtiger Baustein abseits vom Sammeln und Auswerten der Livedaten ist der Trend zu Shift Left – nicht nur in der Security. Hier bietet JfrUnit [15] von Gunnar Morning einen interessanten Ansatz aus dem Umfeld des Java Flight Recorders. Als Teil des Unit Testings werden JFR Events herangezogen, um bestimmte Constraints wie Garbage Collection, erhöhte Memory Allocation oder I/O bereits in Tests festzustellen und vor dem eigentlichen Deployment zu korrigieren.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Schlusswort

Wie überall, so ist auch in der Welt der JVM Agents für APM nicht alles rosig. Einige Agents unterstützen zum Beispiel nur die bekanntesten Web-Frameworks wie Spring oder Spring Boot bzw. auch innerhalb eines Frameworks nur synchrones Request Processing. Es gilt daher, anfangs in Ruhe mögliche Agents zu testen. Fast alle Agents sind Open Source, sodass man im Fall der Fälle auch ein eigenes Plug-in schreiben kann. Je nach Sicherheitseinstellungen der Plattform, auf der Services betrieben werden, ist es eventuell nicht erlaubt, einen Agent programmatisch an den Java-Prozess anzuhängen – zum Beispiel ist mir das bei der Digital-Ocean-Apps-Plattform, einem PaaS, nicht gelungen. Der -javaagent-Parameter innerhalb des Docker Image hat hingegen einwandfrei funktioniert.

Ein weiteres Thema, das man in den aktuellen Java-Trends wahrscheinlich schon entdeckt hat, ist GraalVM. Falls mit Hilfe der GraalVM die Anwendungen in native Binaries umgewandelt werden, existiert der Mechanismus zum Anhängen von Java Agents nicht. Das heißt nicht, dass keinerlei Instrumentierung möglich ist. Die programmatische Erstellung von Spans und Traces könnte allerdings bei einigen APM-Lösungen noch funktionieren, die nicht auf reines Bytecode Enhancement setzen. Da viele bekanntere Frameworks wie Spring und Quarkus inzwischen native Extensions und Module haben, um möglichst einfache Binaries zu erstellen, erwarte ich in den nächsten Monaten, dass auch die APM-Plattformen nachziehen werden. Quarkus hat bereits Support für OpenTracing und DataDog im native Mode.

Um es noch einmal abschließend zu wiederholen: Observability ersetzt kein Monitoring und APM ersetzt kein effizientes Entwickeln performanter Software. Viele Probleme können durch Testing, Reviews oder Pair Programming sehr viel früher im Lebenszyklus der Software gefunden werden und sind dann weitaus ökonomischer zu fixen. Wesentlich komplizierter ist das bereits bei Distributed Tracing und dessen Intersystemkommunikation, um mögliche Bottlenecks vor dem Produktionsbetrieb zu identifizieren. Nichtsdestoweniger ist ein so tiefer Einblick in die selbstgeschriebene Software, wie APM ihn bietet, von Vorteil und sollte auch genutzt werden, wenn der zusätzliche Aufwand der initialen Einrichtung einmal erledigt ist.

 

Links & Literatur

[1] https://github.com/corretto/hotpatch-for-apache-log4j2

[2] https://opentelemetry.io

[3] https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md#libraries–frameworks

[4] https://github.com/jvm-profiling-tools/async-profiler

[5] https://pyroscope.io

[6] https://github.com/spinscale/observability-java-samples

[7] https://www.vaultproject.io/

[8] https://kubernetes.io/docs/concepts/workloads/pods/init-containers/

[9] https://www.elastic.co/blog/using-elastic-apm-java-agent-on-kubernetes-k8s

[10] https://prodfiler.com

[11] https://px.dev

[12] https://www.parca.dev

[13] https://github.com/cilium/hubble

[14] https://github.com/open-telemetry/opentelemetry-lambda

[15] https://github.com/moditect/jfrunit

The post Alles im Blick appeared first on JAX.

]]>