Zum Hauptinhalt springen

Natürlich sind meine Flows getestet, fehlerfrei und laufen immer. An mir liegt es also nicht, wenn ein Fehler passiert... Aber im Ernst: Fehler können eintreten, also welche Dinge sollte ich als Vorbereitung in meine Flows einbauen?

Die Idee für diesen Artikel ist mir auf dem European Collaboration Summit in Düsseldorf gekommen – nach einem Gespräch mit Oliver Menzel, den ich schon sehr oft freitags im PowerAtelier der beiden MVPs Stefan Riedel und Tomislav Karafilov, in Miniaturform gesehen habe. Oliver und ich haben uns ausgetauscht, wie den Usern im Fehlerfall schneller geholfen werden kann. Ich habe kurz skizziert, wie wir in WeDit und auch in anderen Applikationen damit umgehen. Aus Worten wurde ein offener Laptop und das Versprechen ein .zip mit dem Flow zu ziehen. Aber warum nur für Oliver? Daher dieser Blogeintrag.

Das Verfahren habe ich ca. ab Anfang 2019 in meine Flows eingebaut – damals noch mittels Foreneinträgen recherchiert. Heute gibt es gute Blog-Artikel dazu, z.B. hier von Matthew Devaney oder hier von Richard A. Wilson.

Was wollen wir erreichen?

Das Ziel ist relativ simpel: Wir möchten Flows einheitlich erstellen, damit sie:

  • Rückmeldungen an die Apps/Flows geben können und diese für Ausgaben an den User genutzt werden können
  • Fehlermeldungen einheitlich und immer gleich verarbeiten
  • ein zentrales Logging aufbauen
  • erste Informationen über den Fehler an die Applikationsbetreuer:innen versenden
  • optional/nice-to-have: leichtes ausrollen auf viele Environments

Die fertige Solution steht unten zum Download.

Die Grundidee zum Abfangen von Fehlern - Try/Catch/Finally

Wer schon einmal programmiert hat, wird das Verfahren kennen:

  • Try - versuche eine oder mehrere Operationen auszuführen
  • Catch - fange ggf. die Fehler ab
  • Finally - führe eine sinnvolle, abschließende Operation z. B. zur Benachrichtigung aus

In Flow lässt sich dies leicht umsetzen mittels der Scopes und der Anpassung der Bedingungen, wann die Aktionen laufen sollen. Die beiden verlinkten Blog-Artikel oben beschreiben das grundsätzliche einstellen wunderbar. Ich möchte viel mehr darauf eingehen, wie unser best practices aussieht.

unser "Muss-Aufbau" eines jeden Flows

Bei uns fällt jeder Flow, der nicht so wie oben (oder leicht anders, weil komplexer, in WeDit) aussieht, durch das interne Testing. Alle Operationen müssen sich in der Aktion "SCOPE - main" befinden. Auf diese Weise können wir auch den immer selben Punkt für mehr Informationen abgreifen: den Namen "SCOPE - main". Das können gerne auch weitere Scopes sein, um eine bessere Struktur zu erhalten:

Scopes um Struktur in die Flows zu bringen

Nach dem Scope sind zwei Aktionen platziert: die Response-Aktion, die den statusCode 200 (also Erfolg) zurückgibt sowie das Scope "SCOPE - errorHandler" – dieses läuft jedoch nur im Fehlerfall. Es wird eine Response-Aktion, die den statusCode 500 (also Fehler) zurückgibt und anschließend einen Child-Flow aufruft. Dieser sorgt dann für eine entsprechende Verarbeitung des Fehlers.

Eine Kleinigkeit ist ebenfalls im Scope - Eine Terminate-Aktion:

Terminate zum erneuten Auslösen eines Fehlers

Ohne dieses Terminate würde der Flow als "Erfolg" verbucht werden, denn er wurde auch im Fehlerfall erfolgreich abgeschlossen: Das Fehler-Abfangen hat funktioniert. Daher werfe ich mittels des Terminate erneut einen Fehler, um diesen im automatischen Logging von Flow sichtbar zu machen.

Eine weitere Kleinigkeit ist in den Bedingungen verborgen, wann die Aktion "RUN CHILD" ausgelöst werden soll: Ebenfalls, wenn die Aktion "RESP - 500" übersprungen wird. Es könnte sein, dass es Gründe gibt, in denen keine Rückmeldung an eine App oder einen Flow erfolgen muss. Trotzdem soll natürlich unser errorProcessing ausgelöst werden.

An diesem Punkt können wir die Anforderung "Rückmeldungen an die Apps/Flows geben können und diese für Ausgaben an den User genutzt werden können" abhaken.

Zwischenstand

  • Rückmeldungen an die Apps/Flows geben können und diese für Ausgaben an den User genutzt werden können
  • Fehlermeldungen einheitlich und immer gleich verarbeiten
  • ein zentrales Logging aufbauen
  • erste Informationen über den Fehler an die Applikationsbetreuer:innen versenden
  • optional/nice-to-have: leichtes ausrollen auf viele Environments

Das Verarbeiten des Fehlers

Was passiert aber im Child-Flow, der uns mit den weiteren Anforderungen helfen sollte. Eigentlich nur zwei wesentliche Dinge:

  • Wegschreiben im zentralen Logging
  • Versenden einer Mail

Der Flow weist allerdings viel mehr Aktionen auf, als nur diese beiden - warum?

Übersicht des Child Flows zum Error Processing

Informationen erhalten - die Variablen

Vom aufrufenden, also dem fehlerhaften Flow können wir sehr viele Informationen bekommen - für uns haben sich die folgenden drei als sinnvoll herausgestellt:

workflow()

Über diese Variable können u.a. der Lauf (zur Erstellung eines Links zur Durchführung der Fehleranalyse), das Environment, der Name des Flows und noch weitere interessante Dinge ermittelt werden.

trigger()['outputs']['headers']

In diesem JSON finden wir u.a. den auslösenden Benutzer des Flows - aber natürlich nur, wenn es wirklich ein "physischer" Benutzer war, der den Flow ausgelöst hat. Dazu später mehr.

result('SCOPE_-_main')

In diesem JSON befinden sich die wirklichen "Fehlermeldungen" - also potenziell das, warum ein Flow scheitert. In meinem Beispiel, dass eine Division durch 0 stattfindet. Diese Variable ist auch der Grund, warum jedes Scope, in dem gerechnet wird "SCOPE - main" heißen muss: Dadurch sind die Inputs im aufrufenden Flow immer gleich.

Auf Seiten des aufrufenden Flows sieht der ganze Catch-Block also so aus:

Also nein, eigentlich sieht er so aus:

Da wir keine Objekte/Arrays als Variablen in den nicht-Premium-Aktionen und Triggern nutzen können, müssen wir eine Umwandlung vornehmen. Dummerweise passiert dasselbe Spiel auf Seiten des Child Flows in die andere Richtung.

Vielleicht kommt die Frage auf, warum habe ich eigentlich INIT und SET der Variablen auseinander gezogen? Naja, beim Setzen (in diesem Fall umwandeln eines Strings in JSON) könnte ja etwas schief gehen. Das möchte ich abfangen. Im Fall des errorProcessings etwas mit Kanonen auf Helium-Atome geschossen, aber es ist eine Angewohnheit von mir - für jeden anderen Flow ist es aus meiner Sicht ein absolutes muss (solang Microsoft nicht endlich das Initialisieren von Variablen in Scopes erlaubt).

Übrigens eine Information suche ich immer noch: Die Id der Solution, in der der Flow läuft. Wenn jemand eine Idee hat, gerne hier melden.

zentrales Logging - SharePoint-Liste

Für das Aufzeichnen der Fehler nutze ich in unserem Beispiel eine simple SharePoint-Liste. Im Fall von WeDit ist das natürlich eine Tabelle in der Azure SQL-Datenbank. Zwei Szenarien wären für mich denkbar: Soll es eine zentrale Liste geben, die mehrere Applikationen protokolliert? Soll es pro Applikation eine getrennte Liste geben?

Wenn sich für eine zentrale Liste entschieden wird, müssen sich natürlich weitere Gedanken über Berechtigungen etc. gemacht werden. Das gute direkt vorweg: Durch die Verwendung eines Child Flows werden wir eine feste Verbindung hinterlegen müssen. D. h. die Endnutzer müssen keinen Zugriff auf unsere Liste erhalten.

In meinem Fall sieht die SharePoint-Liste sehr simpel aus:

Spalten der SharePoint-Liste

Eine kleine Anmerkung: Die Liste kann generisch genutzt werden, d.h. PowerApps könnten auch in sie loggen, daher gibt es eine Spalte "errorType". Die Bedeutung der Spalten wird durch die Befüllung im Flow sicherlich deutlich klarer:

Daten in die SharePoint-Liste eintragen

Folgende Funktionen nutze ich - errorUser und errorSessionLink möchte ich kurz überspringen:

  • errorMessage: string(variables('arrWorkflowResult'))
  • errorSessionId: variables('objWorkflowData')?['run']?['name']
  • errorFunctionName: variables('objWorkflowData')?['tags']?['flowDisplayName']
  • errorData: variables('objWorkflowData')

errorUser

Dieser Ausdruck ist ein wenig länger - warum? Weil nicht unbedingt immer ein User den Flow aufruft, sondern eben auch mal ein Child Flow. Dieser wird von einer Azure Logic App aufgerufen, der User wird nicht durchgereicht. Daher muss leider eine If-Condition zum Einsatz kommen, da ansonsten ein Fehler entsteht. Das wollen wir ganz sicher im errorProcessing verhindern!

if(
    startsWith(variables('objWorkflowHeader')['User-Agent'], 'azure-logic-apps')
    , variables('objWorkflowHeader')['User-Agent']
    , variables('objWorkflowHeader')['x-ms-user-email']
)

Mittels der Prüfung, ob in dem Element "User-Agent" die azure-logic-apps stehen, wissen wir, ob es sich um einen Child Flow handelt oder nicht. Wir schreiben ansonsten die UPN des aufrufenden Benutzers in die Liste.

errorSessionLink

Der sessionLink soll in der Mail leicht klickbar sein, daher baue ich ihn zusammen, um direkt auf den fehlerhaften Run zu verweisen. In dem Blog-Artikel von Matthew Devaney ist auch nochmal genau das gut erklärt.

https://make.powerautomate.com/environments/@{variables('objWorkflowData')?['tags']?['environmentName']}/flows/@{variables('objWorkflowData')['name']}/runs/@{variables('objWorkflowData')?['run']?['name']}

Zwischenstand

  • Rückmeldungen an die Apps/Flows geben können und diese für Ausgaben an den User genutzt werden können
  • Fehlermeldungen einheitlich und immer gleich verarbeiten
  • ein zentrales Logging aufbauen
  • erste Informationen über den Fehler an die Applikationsbetreuer:innen versenden
  • optional/nice-to-have: leichtes ausrollen auf viele Environments

Nur noch die Mail senden... oder doch mehr?

Eine vermeintliche Kleinigkeit steht noch aus - der Versand einer Mail, dass ein Flow fehlgeschlagen ist - Kleinigkeit! Ein entsprechender Link, der genutzt werden kann, um auf den fehlerhaften Flow zu springen ist in der Liste erzeugt, der kann wiederum auch in der Mail genutzt werden.

Aber nehmen wir folgendes Szenario: Der Flow schlägt fehl, weiter z. B. ein Drittanbieter-Dienst gerade außer Betrieb ist. Das kann vorkommen und wird entsprechend auch im Flow per Fehlermeldung an der Aktion gemeldet. Sollen die Applikationsbetreuer in so Fällen wirklich in den Flow gehen müssen? Nein. Dafür haben wir die Variable result('SCOPE_-_main').

Diese ist jedoch ein Array, daher müssen die relevanten Datensätze herausgefiltert werden. Dafür kann der status genutzt werden, der schlicht "Failed" sein muss. Doch wie sollen mehr als eine Fehlermeldung in der Mail angezeigt werden? Sinnvoll wäre eine strukturierte Sicht als HTML-Tabelle. Genau hierfür kann die Select-Aktion genutzt werden. In dieser bereiten wir die HTML-Zeile der Fehlermeldung vor. In der ersten Spalte steht der Name der fehlgeschlagenen Aktion, in der zweiten Spalte die Fehlermeldung:

Filter und Select-Aktion, welche gleichzeitig die HTML-Tabelle vorbereitet

Ein Problem gibt es beim Auslesen Fehlermeldung. Im result-JSON stehen die Fehlermeldungen, je nach fehlgeschlagener Aktion, an verschiedenen Stellen. Das versuche ich via eines Ifs abzufangen.

Aber Moment - aus der Select-Aktion wird ein Array als Ergebnis zurückgegeben. Kann dieses überhaupt in einer Mail genutzt werden? Die Antwort ist sehr simpel: join() ohne ein Trennzeichen.

Versand der Mail

Da Microsoft leider noch nicht den "aufklapp-Bug" bei Mails und Teams-Nachrichten, die als HTML formatiert sind, gelöst hat, gibt es hier noch den Code für Copy+Paste.

<p>Hallo,<br>
    <br>
    der Flow @{variables('objWorkflowData')?['tags']?['flowDisplayName']}, gestartet vom User @{if(
    startsWith(variables('objWorkflowHeader')?['User-Agent'], 'azure-logic-apps')
    , 'Child-Flow'
    , variables('objWorkflowHeader')?['x-ms-user-email']
    )}, ist auf einen Fehler gelaufen. Unter diesem <a
        href="https://make.powerautomate.com/environments/@{variables('objWorkflowData')?['tags']?['environmentName']}/flows/@{variables('objWorkflowData')['name']}/runs/@{variables('objWorkflowData')?['run']?['name']}">Link</a>
    kann der fehlerhafte Durchgang aufgerufen werden.
</p>
<p>Folgende Fehlermeldungen gab der Flow zurück:</p>
<table>
    <tr>
        <td>Flow action</td>
        <td>Message</td>
    </tr>
    <tr>
        @{join(body('SELECT_-_getMessages'), '')}
    </tr>
</table>

Im Ergebnis bekommen wir jetzt diese Mail:

Berechtigungen für den Flow

Durch die Nutzung eines Child Flows sind wir gezwungen feste Verbindungen zu hinterlegen:

In unserem Fall ist das sehr hilfreich, da so die Benutzer keinerlei Berechtigungen auf die genutzte SharePoint-Liste oder shared Mailbox benötigen. Ein Funktionsaccount bietet sich in diesem Fall an und führt mich nahezu nahtlos zur letzten, optionalen Anforderung.

Zwischenstand

  • Rückmeldungen an die Apps/Flows geben können und diese für Ausgaben an den User genutzt werden können
  • Fehlermeldungen einheitlich und immer gleich verarbeiten
  • ein zentrales Logging aufbauen
  • erste Informationen über den Fehler an die Applikationsbetreuer:innen versenden
  • optional/nice-to-have: leichtes ausrollen auf viele Environments

Ausrollen/Nutzung in mehreren Umgebungen

In der vorliegenden Lösung habe ich den Flow zusammen mit der "Applikation" - diese besteht in meiner Demo nur aus zwei Flows, die Fehler erzeugen. Wäre das ein Konzept, dass in der Realität sinnvoll wäre? Aus meiner Sicht nein!

Der errorProcessing-Flow, die Umgebungsvariablen und Verbindungsreferenzen können in eine getrennte Lösung gepackt und einheitlich auf allen Umgebungen ausgespielt werden. Die Bereitstellung eines standardisierten errorProcessings muss auf Dauer nicht das einzige sein, was Makern im Business bereitgestellt wird. Andere Beispiele wären logging-Flows beim Start von PowerApps, um ggf. Anforderungen des Betriebsrats zu genügen oder oder oder...

Der große Vorteil: Die Maker müssen sich nicht um das Thema Catch/Try/Finally kümmern - bis auf die korrekte Einbindung innerhalb ihrer Flows. Das beschränkt sich jedoch auf die Nutzung eines Scopes namens "SCOPE - main" und die Kopie des errorHandler-Scopes aus einer Vorlage.

Alle Anforderungen wären damit umgesetzt! Oder fehlt irgendwas - gerne via LinkedIn bei mir melden!

Die Solution und ihre Einrichtung

Das .zip der unmanaged solution ist hier zu finden. Vor dem Einspielen sollte eine SharePoint-Liste auf einer entsprechenden SharePoint-Site eingerichtet werden. Beim Import sind die beiden SharePoint-Umgebungsvariablen mit dieser Seite und der Liste zu füllen. Die Verbindungen können bzw. sollten ein Funktionsaccount (Multiplexing bedenken) ausgestattet werden. Die Umgebungsvariable envTxt_errorSharedMailbox mit einer entsprechenden shared Mailbox füllen (Berechtigung für die Verbindung nicht vergessen) und los geht's!