Engineering

So verwalten Sie Incidents mit ServiceNow und Elasticsearch

Willkommen zurück! Im ersten Teil dieser Reihe haben wir die bidirektionale Kommunikation zwischen ServiceNow und Elasticsearch eingerichtet. Die meisten der dazu notwendigen Schritte waren in ServiceNow auszuführen. Jetzt werden wir in Elasticsearch und Kibana weiterarbeiten. Am Ende dieses Blogposts haben Sie zwei äußerst leistungsfähige Anwendungen, die miteinander interagieren und das Incident-Management zu einem Kinderspiel machen – oder aber zumindest wesentlich einfacher, als Sie es bisher kannten!

Wie immer bei Elasticsearch-Projekten erstellen wir Indizes mit Zuordnungen, die auf unsere Anforderungen zugeschnitten sind. Für dieses Projekt benötigen wir Indizes, die die folgenden Daten enthalten können:

  • servicenow-incident-updates: In diesem Index werden alle Informationen gespeichert, die von ServiceNow per „Push“-Aktualisierung an Elasticsearch gesendet werden.
  • app_uptime_summary: In diesem Index wird gespeichert, wie lange jede einzelne Anwendung bisher online gewesen ist. Diese Momentaufnahme soll Ihnen die Arbeit erleichtern.
  • app_incident_summary: In diesem Index werden die Zahl der Incidents, die Uptime sowie der aktuelle MTBF-Wert (Mean Time Between Failure) für jede einzelne Anwendung gespeichert.

Die letzten beiden Indizes sind Hilfsindizes, die verhindern sollen, dass bei jeder Aktualisierung des Canvas-Workpads, das wir in Teil 3 erstellen werden, etliche komplizierte Logiken ausgeführt werden müssen. Sie werden durch den Einsatz von Transformationen kontinuierlich für uns aktualisiert werden.

Erstellen der drei Indizes

Im Folgenden werde ich erläutern, wie Sie die einzelnen Indizes erstellen können. Beachten Sie dabei, dass Sie bei Verwendung anderer Namen als der hier angegebenen möglicherweise Ihre ServiceNow-Konfiguration anpassen müssen.

Index „servicenow-incident-updates“

Gemäß den Best Practices werden wir einen Indexalias und anschließend eine Index-Lifecycle-Management(ILM)-Richtlinie einrichten. Außerdem werden wir eine Indexvorlage erstellen, damit auf jeden Index, der künftig von unserer ILM-Richtlinie erstellt wird, dieselbe Zuordnung angewendet wird. Sobald im Index 50 GB gespeichert wurden, erstellt unsere ILM-Richtlinie einen neuen Index; der Index wird nach Ablauf eines Jahres gelöscht. Dank der Nutzung eines Indexalias können wir bei der Erstellung eines neuen Index problemlos auf diesen verweisen, ohne dazu unsere ServiceNow-Geschäftsregel aktualisieren zu müssen. 

# ILM-Richtlinie erstellen 
PUT _ilm/policy/servicenow-incident-updates-policy 
{ 
  "policy": { 
    "phases": { 
      "hot": {                       
        "actions": { 
          "rollover": { 
            "max_size": "50GB" 
          } 
        } 
      }, 
      "delete": { 
        "min_age": "360d",            
        "actions": { 
          "delete": {}               
        } 
      } 
    } 
  } 
}
# Indexvorlage erstellen 
PUT _template/servicenow-incident-updates-template 
{ 
  "index_patterns": [ 
    "servicenow-incident-updates*" 
  ], 
  "settings": { 
    "number_of_shards": 1, 
    "index.lifecycle.name": "servicenow-incident-updates-policy",       
    "index.lifecycle.rollover_alias": "servicenow-incident-updates"     
  }, 
  "mappings": { 
    "properties": { 
      "@timestamp": { 
        "type": "date", 
        "format": "yyyy-MM-dd HH:mm:ss" 
      }, 
      "assignedTo": { 
        "type": "keyword" 
      }, 
      "description": { 
        "type": "text", 
        "fields": { 
          "keyword": { 
            "type": "keyword", 
            "ignore_above": 256 
          } 
        } 
      }, 
      "incidentID": { 
        "type": "keyword" 
      }, 
      "state": { 
        "type": "keyword" 
      }, 
      "app_name": { 
        "type": "keyword" 
      }, 
      "updatedDate": { 
        "type": "date", 
        "format": "yyyy-MM-dd HH:mm:ss" 
      }, 
      "workNotes": { 
        "type": "text" 
      } 
    } 
  } 
}
# Anfänglich erstellten Index bootstrappen und Alias erstellen 
PUT servicenow-incident-updates-000001 
{ 
  "aliases": { 
    "servicenow-incident-updates": { 
      "is_write_index": true 
    } 
  } 
}

Indizes „app_uptime_summary“ und „app_incident_summary“

Da diese beiden Indizes komplett entity-zentrisch sind, muss ihnen keine ILM-Richtlinie zugeordnet sein. Der Grund: Wir haben immer nur ein Dokument pro Anwendung, die wir überwachen. Zum Erstellen der Indizes geben wir die folgenden Befehle aus:

PUT app_uptime_summary 
{ 
  "mappings": { 
    "properties": { 
      "hours_online": { 
        "type": "float" 
      }, 
      "app_name": { 
        "type": "keyword" 
      }, 
      "up_count": { 
        "type": "long" 
      }, 
      "last_updated": { 
        "type": "date" 
      } 
    } 
  } 
}
PUT app_incident_summary 
{ 
  "mappings": { 
    "properties" : { 
        "hours_online" : { 
          "type" : "float" 
        }, 
        "incident_count" : { 
          "type" : "integer" 
        }, 
        "app_name" : { 
           "type" : "keyword" 
        }, 
        "mtbf" : { 
          "type" : "float" 
        } 
      } 
  } 
}

Einrichten der beiden Transformationen

Transformationen sind relativ neu im Elastic Stack – und unglaublich nützlich. Mit ihrer Hilfe können bestehende Indizes in eine entity-zentrische Übersicht verwandelt werden, die sich hervorragend für Analytics-Zwecke und zur Gewinnung neuer Erkenntnisse eignet. Zudem bieten Transformationen auch Performance-Vorteile, was häufig übersehen wird. So können wir zum Beispiel eine kontinuierliche Transformation nutzen, um uns die MTBF-Werte für die einzelnen Anwendungen in einem uns genehmen Takt berechnen zu lassen, statt den MTBF-Wert für jede Anwendung durch Abfragen und Aggregationen berechnen zu müssen, was recht kompliziert werden würde. Und dieser Takt kann gern auch minütlich sein. Ohne Transformationen würde der MTBF-Wert bei jeder Aktualisierung der Canvas-Workpad-Ansicht durch eine Person einmal neu berechnet werden. Wenn wir also 50 Leute haben, die das Workpad nutzen, und das Aktualisierungsintervall für die Workpad-Ansicht 30 Sekunden beträgt, würde die teure Abfrage pro Minute 100 Mal laufen müssen (was etwas übertrieben sein dürfte). Das würde Elasticsearch in den meisten Fällen zwar nicht aus der Fassung bringen, aber dennoch möchte ich von diesem tollen neuen Feature profitieren, das mir das Leben viel einfacher macht.

Wir werden zwei Transformationen erstellen: 

  • calculate_uptime_hours_online_transform: Diese Transformation berechnet, wie lange jede Anwendung online gewesen ist und reagiert hat. Sie nutzt dazu die Uptime-Daten aus Heartbeat. Die Ergebnisse werden im Index app_uptime_summary gespeichert. 
  • app_incident_summary_transform: Diese Transformation kombiniert die ServiceNow-Daten mit den Uptime-Daten, die durch die zuvor erwähnte Transformation bereitgestellt werden (ja … das klingt für mich ganz nach einem Join). Sie nimmt die Uptime-Daten und ermittelt, wie viele Incidents für jede Anwendung verzeichnet wurden und wie lange die Anwendung online gewesen ist. Anhand dieser Metriken wird dann der MTBF-Wert ermittelt. Der dabei entstehende Index heißt app_incident_summary.

Transformation „calculate_uptime_hours_online_transform“

PUT _transform/calculate_uptime_hours_online_transform 
{ 
  "source": { 
    "index": [ 
      "heartbeat*" 
    ], 
    "query": { 
      "bool": { 
        "must": [ 
          { 
            "match_phrase": { 
              "monitor.status": "up" 
            } 
          } 
        ] 
      } 
    } 
  }, 
  "dest": { 
    "index": "app_uptime_summary" 
  }, 
  "sync": { 
    "time": { 
      "field": "@timestamp", 
      "delay": "60s" 
    } 
  }, 
  "pivot": { 
    "group_by": { 
      "app_name": { 
        "terms": { 
          "field": "monitor.name" 
        } 
      } 
    }, 
    "aggregations": { 
      "@timestamp": { 
        "max": { 
          "field": "@timestamp" 
        } 
      }, 
      "up_count": { 
        "value_count": { 
          "field": "monitor.status" 
        } 
      }, 
      "hours_online": { 
        "bucket_script": { 
          "buckets_path": { 
            "up_count": "up_count" 
          }, 
          "script": "(params.up_count * 60.0) / 3600.0" 
        } 
      } 
    } 
  }, 
  "description": "Calculate the hours online for each thing monitored by uptime" 
}

Transformation „app_incident_summary_transform“ 

PUT _transform/app_incident_summary_transform 
{ 
  "source": { 
    "index": [ 
      "app_uptime_summary", 
      "servicenow*" 
    ] 
  }, 
  "pivot": { 
    "group_by": { 
      "app_name": { 
        "terms": { 
          "field": "app_name" 
        } 
      } 
    }, 
    "aggregations": { 
      "incident_count": { 
        "cardinality": { 
          "field": "incidentID" 
        } 
      }, 
      "hours_online": { 
        "max": { 
          "field": "hours_online", 
          "missing": 0 
        } 
      }, 
      "mtbf": { 
        "bucket_script": { 
          "buckets_path": { 
            "hours_online": "hours_online", 
            "incident_count": "incident_count" 
          }, 
          "script": "(float)params.hours_online / (float)params.incident_count" 
        } 
      } 
    } 
  }, 
  "description": "Calculates the MTBF for apps by using the output from the calculate_uptime_hours_online transform", 
  "dest": { 
    "index": "app_incident_summary" 
  }, 
  "sync": { 
    "time": { 
      "field": "@timestamp", 
      "delay": "1m" 
    } 
  } 
}

Jetzt sorgen wir dafür, dass beide Transformationen ausgeführt werden:

POST _transform/calculate_uptime_hours_online_transform/_start 
POST _transform/app_incident_summary_transform/_start

Uptime-Alerts zur Erstellung von Tickets in ServiceNow

Abgesehen davon, dass wir das Canvas-Workpad noch attraktiver machen können, besteht der letzte noch ausstehende Schritt darin, in ServiceNow ein Ticket für den Systemausfall zu erstellen, sofern es nicht bereits eines dafür gibt. Zu diesem Zweck erstellen wir mithilfe von Watcher einen Alert. Für diesen Alert gelten die im Folgenden umrissenen Schritte. Er wird einmal pro Minute ausgeführt. Eine Übersicht über alle Uptime-Felder finden Sie in der Heartbeat-Dokumentation.

1. Überprüfen, welche Anwendungen in den letzten 5 Minuten ausgefallen sind

Das ist einfach. Wir sehen nach, welche Heartbeat-Ereignisse in den letzten 5 Minuten (Filter „range“ für die Suche innerhalb eines Bereichs) down (Filter „term“ für die Suche nach einem Begriff) gewesen sind, gruppiert nach dem Feld monitor.name (Begriff-Aggregation). Dieses Feld monitor.name folgt dem Elastic Common Schema (ECS) und ist synonym mit dem Wert in unserem Feld für den Anwendungsnamen. Ermöglicht wird all dies durch den down_check-Input im Watcher unten.

2. Abrufen der Top-20-Tickets für jede Anwendung und der letzten Aktualisierung für jedes Ticket

Jetzt wird es etwas komplexer. Wir durchsuchen unsere ingestierten ServiceNow-Daten, die dank unserer ServiceNow-Geschäftsregel automatisch ingestiert wurden. Der Input existing_ticket_check nutzt mehrere Aggregationen. Die erste besteht darin, über die Begriff-Aggregation „apps“ alle Anwendungen zu gruppieren. Anschließend gruppieren wir dann für jede Anwendung mithilfe einer Begriff-Aggregation namens incidents die ServiceNow-Ticket-Incident-IDs. Schließlich rufen wir für jeden Incident bei jeder Anwendung den neuesten Zustand ab, indem wir die Aggregation „top_hits“ verwenden und zur Sortierung das Feld @timestamp heranziehen. 

3. Zusammenführen der beiden Feeds und Prüfen, ob Tickets erstellt werden müssen

Zu diesem Zweck nutzen wir die Payload-Transformation „script“. Kurz gesagt prüft diese, was gerade „down“ ist, indem sie sich immer wieder den down_check-Output ansieht und ermittelt, ob es für die jeweilige Anwendung ein offenes Ticket gibt. Wenn es kein aktuelles Ticket gibt, gleich ob neu oder mit dem Status „Warten“ („on hold“), fügt sie die Anwendung einer Liste hinzu, die zurück- und an die Aktionsphase weitergegeben wird. 

Diese Payload-Transformation führt eine ganze Reihe von Prüfungen aus, um sicherzustellen, dass auch die oben umrissenen Randfälle berücksichtigt werden, z. B. das Erstellen eines Tickets, wenn die Anwendung noch keinerlei Incident-Historie hat. Das Ergebnis dieser Transformation ist ein Array von Anwendungsnamen.

4. Erstellen des Tickets in ServiceNow, sofern noch nicht vorhanden

Wir nutzen die Webhook-Aktion, um mithilfe der ServiceNow-REST-API das Ticket in ServiceNow zu erstellen. Zu diesem Zweck kommt der Parameter foreach zum Einsatz, damit alle Anwendungsnamen aus dem Array oben verarbeitet werden. Anschließend wird für jede Anwendung die Webhook-Aktion ausgeführt. Voraussetzung ist, dass es mindestens eine Anwendung gibt, die ein Ticket benötigt. Bitte vergewissern Sie sich, dass Sie die richtigen Anmeldeinformationen und den richtigen Endpoint für ServiceNow festlegen.

PUT _watcher/watch/e146d580-3de7-4a4c-a519-9598e47cbad1 
{ 
  "trigger": { 
    "schedule": { 
      "interval": "1m" 
    } 
  }, 
  "input": { 
    "chain": { 
      "inputs": [ 
        { 
          "down_check": { 
            "search": { 
              "request": { 
                "body": { 
                  "query": { 
                    "bool": { 
                      "filter": [ 
                        { 
                          "range": { 
                            "@timestamp": { 
                              "gte": "now-5m/m" 
                            } 
                          } 
                        }, 
                        { 
                          "term": { 
                            "monitor.status": "down" 
                          } 
                        } 
                      ] 
                    } 
                  }, 
                  "size": 0, 
                  "aggs": { 
                    "apps": { 
                      "terms": { 
                        "field": "monitor.name", 
                        "size": 100 
                      } 
                    } 
                  } 
                }, 
                "indices": [ 
                  "heartbeat-*" 
                ], 
                "rest_total_hits_as_int": true, 
                "search_type": "query_then_fetch" 
              } 
            } 
          } 
        }, 
        { 
          "existing_ticket_check": { 
            "search": { 
              "request": { 
                "body": { 
                  "aggs": { 
                    "apps": { 
                      "aggs": { 
                        "incidents": { 
                          "aggs": { 
                            "state": { 
                              "top_hits": { 
                                "_source": "state", 
                                "size": 1, 
                                "sort": [ 
                                  { 
                                    "@timestamp": { 
                                      "order": "desc" 
                                    } 
                                  } 
                                ] 
                              } 
                            } 
                          }, 
                          "terms": { 
                            "field": "incidentID", 
                            "order": { 
                              "_key": "desc" 
                            }, 
                            "size": 1 
                          } 
                        } 
                      }, 
                      "terms": { 
                        "field": "app_name", 
                        "size": 100 
                      } 
                    } 
                  }, 
                  "size": 0 
                }, 
                "indices": [ 
                  "servicenow*" 
                ], 
                "rest_total_hits_as_int": true, 
                "search_type": "query_then_fetch" 
              } 
            } 
          } 
        } 
      ] 
    } 
  }, 
  "transform": { 
   "script": """ 
      List appsNeedingTicket = new ArrayList();  
      for (app_heartbeat in ctx.payload.down_check.aggregations.apps.buckets) { 
        boolean appFound = false;  
        List appsWithTickets = ctx.payload.existing_ticket_check.aggregations.apps.buckets;  
        if (appsWithTickets.size() == 0) {  
          appsNeedingTicket.add(app_heartbeat.key);  
          continue;  
        }  
        for (app in appsWithTickets) {   
          boolean needsTicket = false;  
          if (app.key == app_heartbeat.key) {  
            appFound = true;  
            for (incident in app.incidents.buckets) {  
              String state = incident.state.hits.hits[0]._source.state;  
              if (state == 'Resolved' || state == 'Closed' || state == 'Canceled') {  
                appsNeedingTicket.add(app.key);  
              }  
            }  
          }  
        }  
        if (appFound == false) {  
          appsNeedingTicket.add(app_heartbeat.key);  
        }  
      }  
      return appsNeedingTicket;  
      """ 
  }, 
  "actions": { 
    "submit_servicenow_ticket": { 
      "condition": { 
        "script": { 
          "source": "return ctx.payload._value.size() > 0" 
        } 
      }, 
      "foreach": "ctx.payload._value", 
      "max_iterations": 500, 
      "webhook": { 
        "scheme": "https", 
        "host": "dev94721.service-now.com", 
        "port": 443, 
        "method": "post", 
        "path": "/api/now/table/incident", 
        "params": {}, 
        "headers": { 
          "Accept": "application/json", 
          "Content-Type": "application/json" 
        }, 
        "auth": { 
          "basic": { 
            "username": "admin", 
            "password": "REDACTED" 
          } 
        }, 
       "body": "{'description':'{{ctx.payload._value}} Offline','short_description':'{{ctx.payload._value}} Offline','caller_id': 'elastic_watcher','impact':'1','urgency':'1', 'u_application':'{{ctx.payload._value}}'}", 
        "read_timeout_millis": 30000 
      } 
    } 
  }, 
  "metadata": { 
    "xpack": { 
      "type": "json" 
    }, 
    "name": "ApplicationDowntime Watcher" 
  } 
}

Fazit

Damit sind wir am Ende des zweiten Blogposts in dieser Reihe angelangt. Wir haben in dieser Folge ein paar allgemeine Konfigurationen für Elasticsearch vorgenommen und so für die Erstellung der Indizes und der ILM-Richtlinie gesorgt. Außerdem haben wir die Transformationen erstellt, die für jede Anwendung die Werte für Uptime und MTBF berechnen, sowie den Watcher, der unsere Uptime-Daten überwacht. Die wichtigste Erkenntnis an dieser Stelle ist, dass der Watcher zunächst prüft, ob es bereits ein ServiceNow-Ticket gibt, und gegebenenfalls eines in ServiceNow erstellt, sobald er bemerkt, dass etwas ausgefallen ist.

Sie möchten das Ganze bei sich nachverfolgen? Am einfachsten geht das mit Elastic Cloud. Entweder Sie melden sich bei der Elastic Cloud-Konsole an oder Sie nutzen unser Angebot, das Ganze 14 Tage lang kostenlos auszuprobieren. Sie können die oben beschriebenen Schritte mit Ihrer vorhandenen ServiceNow-Instanz nachvollziehen oder eine persönliche Entwickler-Instanz einrichten.

Falls Sie neben ServiceNow-Daten auch weitere Quellen, wie GitHub oder Google Drive, durchsuchen möchten, können Sie den integrierten ServiceNow-Connector in Elastic Workplace Search verwenden. Workplace Search bietet ein zentralisiertes Sucherlebnis für Ihre Teams mit relevanten Ergebnissen aus sämtlichen Inhaltsquellen. Auch diese Lösung können Sie im Rahmen der Elastic Cloud-Probeversion verwenden.

Wir haben jetzt etwas, das uneingeschränkt funktioniert. Mir persönlich fehlt jedoch noch ein wenig Attraktivität im Erscheinungsbild. Daher werden wir uns im dritten Teil dieser Reihe ansehen, wie Sie mithilfe von Canvas ein attraktives Frontend für die Präsentation aller dieser Daten erstellen können und wie Sie die anderen erwähnten Metriken, wie MTTA und MTTR, berechnen. Bis dahin!