Gerade haben wir wieder eine schöne API gebaut. Jetzt fehlen nur noch ein paar Nutzer, die sie auch verwenden. Wie das funktioniert, haben wir ja schon mal gespielt. Und damit die API auch für unsere Consumer funktioniert, haben wir noch mal fix überflogen, was wir beachten müssen, damit sie unsere API zumindest nicht hassen:
Beim Design haben wir uns die allergrößte Mühe gegeben. Wir nutzen Fehler-Codes und geben auch hilfreiche Error Responses zurück – finden wir. Ziemlich schnell sind die Services hinter unserer API auch. Das haben wir getestet. Alles hat auf Anhieb super funktioniert, wir sind zufrieden und die API ist veröffentlicht – die Party kann starten!
Da kommt plötzlich ein Spielverderber und fragt uns, ob wir unsere API denn auch monitoren. Monitoring?
Ja, haben wir. Wenn auf dem Server der Speicher knapp wird oder die Festplatte vollläuft, werden wir informiert und können reagieren. Unsere Infrastruktur haben wir im Griff!
Der Spielverderber lässt nicht locker. Und unsere schönen Server interessieren ihn eigentlich überhaupt nicht. Stattdessen will er noch immer wissen, ob wir unsere API monitoren. Warum? Es ist doch alles auf den verschiedensten Levels getestet – vom Unit-Test bis zum System-Integrations- und End-to-End-Test. Das hat alles geklappt und war erfolgreich. Und wie wir sehen, funktioniert die API ja offensichtlich. Das reicht doch!
Und was passiert, wenn ein externes System, von dem wir abhängen, plötzlich nicht mehr richtig funktioniert? Oh! Äh, ja, stimmt. In dem Fall hätten wir ein Problem! Wir können zwar auf Resilience-Patterns zurückgreifen, um Mitigationsstrategien umzusetzen, doch ganz spurlos werden Ausfälle oder Fehler anderer Systeme nicht an uns vorbeigehen. Na gut, dann setzen wir vielleicht doch noch ein Monitoring auf: Die externen APIs haben ja bestimmt einen Health- oder Status-Endpunkt. Die rufen wir einfach alle in unserer Applikation regelmäßig auf, aggregieren die Ergebnisse und unser eigener Health-Endpunkt gibt nur “OK” zurück, wenn es allen unseren Freunden auch gut geht. Unseren Status-Endpunkt nehmen wir dann ins Monitoring mit auf. Wenn sich dann etwas ändert, bekommen wir das mit. Fertig! Das reicht doch dann wirklich!
Ähm … Nein. Das reicht eher nicht, wirklich nicht. Schon im Oktober 2018 machten API-Aufrufe 83 Prozent des Internet-Traffics aus. Der überwiegende Teil dieses Traffics geht auf Cloud-Anwendungen und digitale Transformation zurück. Das heißt, was unsere API und andere APIs tun, ist ziemlich wichtig für unsere Kunden – und deren Kunden, Menschen. Wenn doch etwas schiefgeht, sollten wir das wirklich wissen wollen – bevor der Kunde anruft, weil unsere zusammengefassten Status-Informationen möglicherweise doch nicht der Weisheit letzter Schluss waren: Sie machen lediglich eine Aussage über die Verfügbarkeit unserer API, geben aber keine Auskunft über Fehlersituationen, die beim Aufruf der API entstehen. Davon abgesehen können wir auch mit ziemlicher Sicherheit davon ausgehen, dass unser Kunde Besseres zu tun hat, als den ganzen Tag zu schauen, ob die API, die wir für ihn gebaut haben, richtig funktioniert (wenn wir das schon nicht tun). Und vermutlich nutzt er auch andere Endpunkte als den Status-Endpunkt. Ruft er uns also an, heißt das entweder, dass wir alle gemeinsam Glück hatten und er zufällig festgestellt hat, dass etwas nicht so wie erwartet funktioniert, oder dass er selbst auch angerufen wurde.
Angesichts der Reichweite und Verbreitung von APIs betrachten wir es als ziemlich sichere Annahme, dass die Kunden unseres Kunden nicht nur ihn angerufen haben sondern ihr Problem auch publik und ihrem Ärger Luft gemacht haben. Schnell springen andere auf den Zug auf und ein Shitstorm wird vom Zaun gebrochen. Irgendwann folgen die Medien – und die Konkurrenten unseres Kunden könnten sich kaum eine bessere Werbung vorstellen. Die werden sich freuen!
Letzte Woche waren mit Facebook, Instagram und WhatsApp drei große Dienste aufgrund einer Änderung der Router-Konfiguration nicht verfügbar. Schon im Februar war der Facebook-Messenger nicht verfügbar, ein Großteil der Nutzer in Großbritannien hatte Probleme beim Empfangen von Nachrichten, andere konnten sich gar nicht erst einloggen. Ebenfalls im Februar hat LinkedIn seine Nutzer mit einem generischen “An error has occurred” begrüßt, eine Konfigurationsänderung war auch hier schuld. Im Dezember letzten Jahres waren gleich mehrere Google-Dienste nicht erreichbar, einschließlich Youtube und Gmail, weil ein Authentifizierungsservice nicht funktioniert hat. Und als im November die Amazon Web Services einen Ausfall hatten, war das halbe Internet kaputt.
Es ist ein bisschen wie mit dem berühmten “This should never happen”-Kommentar am Catch-Block für eine Exception, die für uns unvorstellbar war, bei der es völlig unlogisch ist, dass sie geworfen werden könnte. Und dann tritt genau dieser Fall doch ein. Insbesondere mit verteilten Anwendungen in der Cloud, mit wechselseitigen und externen Abhängigkeiten sind die möglichen Ursachen für Ausfälle vielfältig und kaum zu überblicken.
Okay, überzeugt. Wir sind tatsächlich gut beraten, insbesondere das erwartungsgemäße Verhalten unserer API kontinuierlich im Blick zu behalten. Welche Möglichkeiten bieten sich uns dafür?
Infrastruktur-Monitoring
Eigentlich wollten wir ja die API monitoren, aber reicht es nicht auch aus, wenn wir den Status und die Metriken aller beteiligten Komponenten überwachen? Wenn die Server für meine Applikation laufen, die Datenbanken erreichbar sind und es dem API Gateway gut geht, können wir doch davon ausgehen, dass auch die API funktioniert.
Hmm … Nein. An dem Punkt waren wir schon mal. Die API ist mehr als ihre Teile. Funktioniert die Infrastruktur nicht, wie sie soll, können wir zwar sicher davon ausgehen, dass unsere API nicht funktionieren wird, umgekehrt gilt das jedoch nicht. Fehlerfreie Infrastruktur ist kein Garant für eine fehlerfreie API. Mit dem Infrastruktur-Monitoring sichern wir also zunächst nur die elementare Grundvoraussetzung ab und stellen fest, ob die API überhaupt funktionieren kann.
Für den wirklichen Einblick fehlt da noch was … Also, next!
Passives Monitoring
Wir legen uns einfach mal auf die Lauer: Wir integrieren einen Agenten in die Anwendung, die die API bereitstellt, und überwachen, was so vor sich geht.
Unser Agent registriert eingehende Anfragen und die dazugehörigen Antworten und bemerkt Exceptions. Er zeichnet auf, wie lange die Bearbeitung der Anfragen dauert und mit welchem Status sie schließlich beantwortet werden. All diese Informationen werden auch an ein Monitoring-Tool übermittelt, so dass wir über die Zeit feststellen können, wenn sich die Dinge verändern:
Nimmt die Zahl der Anfragen zu? Dauert die Bearbeitung immer länger? Treten vermehrt Fehler auf, nehmen sie sogar überproportional zu?
Dann ist möglicherweise etwas im Busch. Irgendwas stimmt da nicht. Wenn wir in der glücklichen Situation sind, dass die Logs unserer Anwendung im gleichen Tool wie die Informationen des Agenten gesammelt werden, können wir uns auf die Suche nach Querverbindungen machen und dem unerwarteten Verhalten auf den Grund gehen.
Wir sind also definitiv schlauer als vorher und in der Lage, anhand des Live-Traffics der API das erwartungsgemäße Verhalten zu überwachen. Unser Problem ist gelöst! Jedenfalls bemerken wir immerhin schon mal, wenn wir eines haben. Fertig!
Moment. Nicht so schnell!
Exceptions in unserer Anwendung und auch Error Responses können zwar bedeuten, dass wir ein Problem haben, es ist aber nicht gesagt, dass wir auch tatsächlich eins haben. Es gehören immer zwei dazu, mindestens. Es ist zwar gut, dass wir vor unserer eigenen Tür kehren wollen und damit rechnen, dass bei uns etwas nicht so funktioniert, wie es soll. Auf der anderen Seite gibt es jedoch auch noch die Nutzer unserer API. Und niemand garantiert uns, dass sie sich so verhalten, wie wir uns das vorgestellt haben und wünschen.
Tatsächlich sind die möglichen Fehler vielfältig, die wir idealerweise an den HTTP-Statuscodes – definiert in HTTP 1.1 (RFC 2616) – erkennen können. Sechs verschiedene 5xx Statuscodes signalisieren Fehler auf unserer (Server-)Seite und dreimal so viele 4xx Statuscodes zeigen an, dass das Problem wohl eher auf der Seite des Nutzers liegt. Entweder ist das HTTP-Protokoll bei der Einordnung und Beschreibung von Client-Fehlern also deutlich einfallsreicher – oder es ist doch nicht so unwahrscheinlich, dass die “Schuld” eher beim Konsumenten unserer API zu suchen ist, da irgendetwas mit seiner Anfrage nicht zu stimmen scheint oder wir sie nicht bearbeiten können.
Doch halt, wir wollen ja niemandem die Schuld in die Schuhe schieben. Wir wollen, dass unsere API ihren Nutzern einen Mehrwert bietet. Stellen wir also vermehrt Client-”Fehler” fest, kann das auch einfach ein Indiz dafür sein, dass sie die API gern anders verwenden würden oder unsere Dokumentation missverstanden haben. Abgesehen von Angriffen auf unsere API mit dem Ziel, unseren Systemen zu schaden, gehen wir nicht davon aus, dass mut- und böswillige Requests gesendet werden. Wir glauben an das Gute im Client! Und wenn wir neben dem API-Monitoring auch auf API-Management setzen, kennen wir die Clients und Konsumenten unserer API. Das heißt, wir können bei Auffälligkeiten aus dem Monitoring proaktiv auf sie zugehen, die Situation klären und gemeinsam an der API (oder am Verständnis) arbeiten.
Aber was ist denn eigentlich, wenn wir keine Anfragen bekommen – egal, ob “gut” oder “böse” – wenn es keinen Live-Traffic gibt, den wir überwachen können? Ist dann alles in Ordnung?
Nicht unbedingt. Wenn gar keine Request kommen, kann das bedeuten, dass sich schlicht und ergreifend niemand für unsere (richtig funktionierende) API interessiert. Im Sinne der Überwachung erwartungsgemäßer Funktion wäre das zwar gut, dennoch aber zumindest sehr, sehr traurig. Gleichzeitig können ausbleibende Anfragen auf der anderen Seite ein Indiz dafür sein, dass zwar jemand unsere API nutzen möchte, aber nicht kann, weil irgendwo anders etwas nicht funktioniert, sozusagen vor unserer Tür. Das bekommen wir einfach nicht mit – und das ist dann doch sehr ärgerlich!
Unser Ziel war ja sicherzustellen, dass unsere API für die Konsumenten funktioniert. Dazu gehört mehr als nur zu erfahren, wenn Anfragen fehlschlagen.
Synthetisches Monitoring
Für ein kontinuierliches Monitoring benötigen wir also auch kontinuierlich Anfragen. Synthetisches Monitoring ist hier die Lösung.
Wenn wir lediglich schauen wollen, ob unsere API grundsätzlich erst mal erreichbar ist, genügt hier im einfachsten Fall der regelmäßige Aufruf eines Health- oder Status-Check-Endpunktes unserer API. Werten wir die HTTP-Statuscodes der Antworten aus, können wir damit schon mal erkennen, ob die API überhaupt für andere (von außen) erreichbar ist.
Was ist jedoch mit möglicherweise noch vorhandenen Backend-Services zu unserem Backend-Service? Unsere API kann ja verfügbar sein – und trotzdem nicht richtig funktionieren.
Natürlich könnte der Health Check unserer API die Health Checks aller Backends aggregieren und selbst nur dann ein “Mir gehts gut!” verkünden, wenn es allen anderen auch gut geht. Doch wo hört das auf? Backends des Backends könnten Backends haben – und sehr wahrscheinlich werden wir niemals alle aufrufen können oder dürfen, geschweige denn sie überhaupt kennen. Dieser Ansatz hat also definitiv auch seine Grenzen – oder eben gerade keine.
Was könnten wir also sonst noch tun, wenn ein Health Check nicht genügt?
Wenn wir schon regelmäßig Requests zu einem Status-Endpunkt unserer API senden, können wir das genauso mit anderen Endpunkten machen und auch mit mehreren verschiedenen, vielleicht besonders wichtigen oder kritischen. Mit anderen Worten: Synthetisches Monitoring erlaubt es uns, das Verhalten eines “echten” Nutzers zu simulieren.
Wir rufen die verschiedenen API-Endpunkte tatsächlich so auf, wie wir es von unseren Konsumenten annehmen oder erwarten. Wenn wir Glück haben, haben wir dabei alles in der Hand: Die Requests kennen wir schonmal, das ist klar. Kennen wir darüber hinaus auch das Backend mit seiner Logik – und die Daten –, wissen wir außerdem genau, wie eine korrekte und erwartungsgemäße Antwort auszusehen hat. Das können wir dann ebenfalls prüfen.
An dieser Stelle spielen die Backend-Backend-Backends und andere möglicherweise vorhandene Komponenten im Grunde keine Rolle mehr. Unsere API wird zur völligen Black Box und wir verifizieren lediglich, dass sie sich auf Ebene der Geschäftslogik korrekt verhält. Enthält der normale Userflow mehr als einen Request, bedeutet das, dass wir nicht nach einem Request stoppen, sondern den kompletten Business-Flow monitoren.
Doch halt, eine Herausforderung bleibt: In den seltensten Fällen sind die Daten, auf die unsere API Zugriff gewährt, unveränderlich und statisch. Sicherlich greifen wir nicht nur lesend zu, wenn wir komplette Business-Flows abbilden. Wir werden neue Daten anlegen, bestehende Daten verändern, vielleicht auch Daten löschen. All das ist möglicherweise nicht in jedem produktiven System einfach so möglich, oder die erwarteten Antworten für das Monitoring können sich ändern und sind damit nicht mehr ganz so erwartbar. Darüber werden wir uns Gedanken machen müssen, bevor wir ein Monitoring aufsetzen. Testdaten, der Zugriff darauf und der Umgang damit sind immer wieder ein spannendes Thema!
Eine weitere Herausforderung im Zusammenhang mit dem Monitoring von Business-Flows in Produktivumgebungen können potenzielle Konflikte mit Reportings und Analysen sein. Sicherlich gibt es auf Business-Seite auch (berechtigtes) Interesse daran zu messen, wie viele Aufrufe – das könnten beispielsweise Bestellungen sein – über unsere API ausgelöst wurden. Wenn wir dabei nicht aufpassen und die Monitoring-Anfragen herausfiltern, sieht das zwar auf den ersten Blick aus, als würde die API sehr oft und mit schöner Regelmäßigkeit genutzt werden, es steht jedoch kein “echter” Nutzer dahinter.
Andererseits erlauben uns die regelmäßig wiederholten Anfragen über Metriken wie die Antwortzeit Trends zu identifizieren und Anomalien zu erkennen. Wir müssen also genau schauen, welche Daten wofür genutzt und zur Verfügung gestellt werden.
Apropos Metriken …
Wenn wir über weitere Metriken sprechen, können wir uns noch einmal kurz an das Infrastruktur-Monitoring oder das passive Monitoring erinnern: Wenn wir nämlich die Black Box aufbrechen und doch noch einmal tiefer in die Anwendungslogik einsteigen, bieten uns verschiedene Werkzeuge (beispielsweise Prometheus oder Datadog) die Möglichkeit eigene Metriken zu definieren, die dann im Monitoring Berücksichtigung finden.
Ungeachtet der sehr verschiedenen Ansätze, die beide Tools verfolgen, ermöglichen uns unsere eigenen Metriken relativ unabhängig von äußeren Umständen, Geschäftsprozesse innerhalb der Anwendung mit beliebiger Granularität und Ausprägung zu überwachen: Wir können Zähler, Histogramme oder Werte definieren, die sowohl steigen als auch fallen können. Eigentlich ist das jedoch schon wieder ein ganz anderes Thema.
Wir können auch Monitoring-Agents in Frontend-Anwendungen integrieren und auf diese Weise nachvollziehen, wie sich die Benutzer unserer Anwendung durch die Webseite bewegen und an welchen Stellen sie auf Probleme stoßen oder welche Funktionalität sie gar nicht nutzen. Doch auch das ist ein zu weites Feld …
Take-away
Es gibt vielfältige Möglichkeiten für das Monitoring von Infrastruktur, APIs und Anwendungen. Alle haben unterschiedliche Möglichkeiten und Limitierungen. Nicht immer müssen wir von allen Gebrauch machen, doch so ganz ohne Monitoring fahren wir mit Sicherheit nicht gut. Ein Blindflug ist nie zu empfehlen.
Nicht außer Acht gelassen werden sollte andererseits, dass Monitoring uns zwar eine Menge Mehrwert bietet, gerade synthetisches Monitoring oft allerdings ebenso aufwändig zu implementieren ist: Wir benötigen gesonderte Test-User und -daten in produktiven Systemen und Monitoring-Aufrufe müssen gegebenenfalls aus Reporting-Metriken gefiltert werden, um fachliche Auswertungen nicht zu erschweren oder gar unmöglich zu machen.
Wir sollten uns also in jedem Fall Gedanken darüber machen, wie wir sinnvoll, mit angemessenem Aufwand und vor allem Nutzen prüfen können, ob unsere Systeme so funktionieren, wie sie sollen.
Vertrauen ist zwar gut, doch Kontrolle ist hier definitiv besser!