Technique

La gestion des incidents avec ServiceNow et Elasticsearch

Bienvenue à nouveau ! Dans l'article précédent, nous avons vu comment configurer une communication bidirectionnelle entre ServiceNow et Elasticsearch. Pour cela, nous avons passé la majeure partie de notre temps dans ServiceNow. Maintenant, c'est au tour d'Elasticsearch et de Kibana ! À la fin de cet article, ces deux applications puissantes fonctionneront de pair pour faciliter la gestion des incidents. Simple comme bonjour !

Comme dans tous les projets Elasticsearch, nous allons créer des index avec des mappings adaptés à nos besoins. Dans le cas présent, nous avons besoin d'index pouvant contenir les données suivantes :

  • Mises à jour des incidents ServiceNow : il s'agit de stocker toutes les informations venant de ServiceNow dans Elasticsearch. C'est l'index dans lequel ServiceNow place les mises à jour.
  • Résumé de la disponibilité des applications pour vous faciliter la tâche : il s'agit de stocker le nombre total d'heures pendant lesquelles chaque application a été connectée. Considérez ce résumé comme un état de données intermédiaire qui vous est fourni pour vous faciliter la tâche.
  • Résumé des incidents des applications : il s'agit de stocker le nombre d'incidents rencontrés par chaque application, la disponibilité de chaque application et le MTBF (temps moyen entre les défaillances) actuel de chaque application.

Les deux derniers index sont des index d'aide. Aussi, nous n'avons pas besoin d'exécuter toute une logique compliquée à chaque fois que nous actualisons la présentation Canvas que nous allons créer dans le 3e article. Ces index seront mis à jour de façon continue grâce à des transformations.

Création des trois index

Pour créer les index, nous allons procéder comme suit. Attention : si vous n'utilisez pas les mêmes noms que ceux indiqués ci-dessous, vous devrez peut-être apporter des ajustements à votre configuration ServiceNow.

servicenow-incident-updates

Conformément aux bonnes pratiques, nous allons configurer un alias d'index et une politique de gestion du cycle de vie (ILM). Nous allons également définir un modèle d'index qui nous permettra d'appliquer le même mapping à chaque futur index qui sera créé dans le cadre de notre politique ILM. Dès qu'un volume de 50 Go de données sera stocké dans un index, un nouvel index sera créé en vertu de notre politique ILM, puis sera supprimé au bout d'un an. Nous nous servirons également d'un alias d'index pour créer un chemin d'accès vers le nouvel index au moment de sa création, sans qu'il soit nécessaire de mettre à jour notre règle métier ServiceNow. 

# Création de la politique ILM 
PUT _ilm/policy/servicenow-incident-updates-policy 
{ 
  "policy": { 
    "phases": { 
      "hot": {                       
        "actions": { 
          "rollover": { 
            "max_size": "50GB" 
          } 
        } 
      }, 
      "delete": { 
        "min_age": "360d",            
        "actions": { 
          "delete": {}               
        } 
      } 
    } 
  } 
}
# Création du modèle d'index 
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" 
      } 
    } 
  } 
}
# Démarrage de l'index initial et création de l'alias 
PUT servicenow-incident-updates-000001 
{ 
  "aliases": { 
    "servicenow-incident-updates": { 
      "is_write_index": true 
    } 
  } 
}

app_uptime_summary et app_incident_summary

Étant donné que ces deux index sont centrés sur les entités, il n'est pas nécessaire d'y associer une politique ILM. En effet, nous aurons seulement un document par application que nous monitorons. Pour créer les index, exécutez les commandes suivantes :

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" 
        } 
      } 
  } 
}

Configuration des deux transformations

Récemment ajoutées à la Suite Elastic, les transformations présentent une grande utilité. Elles permettent de convertir des index existants en un résumé centré sur les entités, ce qui offre un atout significatif en matière d'analyse et de découverte d'informations exploitables. Autre avantage souvent négligé : leur contribution au niveau des performances. Par exemple, au lieu d'essayer de calculer le MTBF de chaque application au moyen d'une recherche et d'une agrégation (ce qui serait assez compliqué), une transformation continue le fait pour nous, à la fréquence de notre choix. Et même à chaque minute, si vous le souhaitez ! Sans les transformations, le MTBF serait calculé à chaque actualisation de la présentation Canvas. Ce qui signifie que, si 50 personnes utilisaient la présentation avec un intervalle d'actualisation de 30 secondes, la recherche serait effectuée 100 fois par minute (ce qui fait beaucoup, vous en conviendrez). Même si dans la plupart des cas, cela ne poserait pas de problème à Elasticsearch, nous souhaitons néanmoins tirer profit de cette incroyable fonctionnalité qui nous rend la vie bien plus facile.

Nous allons créer deux transformations : 

  • calculate_uptime_hours_online_transform : calcule le nombre d'heures pendant lequel chaque application a été connectée et réactive. Pour cela, cette transformation se base sur les données de disponibilité issues de Heartbeat. Les résultats sont stockés dans l'index app_uptime_summary
  • app_incident_summary_transform : combine les données ServiceNow avec les données de disponibilité venant de la transformation précédente (oui, ça ressemble un peu à un type join). Cette transformation traite les données de disponibilité et détermine le nombre d'incidents rencontrés par chaque application, les confronte au nombre d'heures pendant lequel une application a été connectée, et pour finir, calcule le MTBF en fonction de ces deux indicateurs. L'index qui en résulte s'appellera app_incident_summary.

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" 
}

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" 
    } 
  } 
}

Vérifions maintenant que les deux transformations fonctionnent :

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

Alertes de disponibilité pour créer des tickets dans ServiceNow

Pour boucler la boucle, il ne nous reste plus qu'à créer un ticket dans ServiceNow s'il n'y en aucun pour cet échec et réaliser une jolie présentation Canvas. Nous allons donc nous servir d'un Watcher pour créer une alerte. Procédons comme indiqué ci-dessous. À noter : l'exécution se fait à chaque minute. Tous les champs de disponibilité sont détaillés dans la documentation Heartbeat.

1. Déterminez les applications qui ont été en échec au cours des cinq dernières minutes.

Ça, c'est la partie facile. Pour identifier les événements Heartbeat concernés, appliquez un filtrage sur le terme down, ainsi qu'un filtrage de plage sur les cinq dernières minutes, et effectuez un regroupement par monitor.name (agrégation de termes). Étant donné que le champ monitor.name applique la spécification Elastic Common Schema (ECS), ce sera donc un synonyme de la valeur indiquée dans notre champ de nom d'application. L'entrée down_check du Watcher ci-dessous nous permet d'obtenir les résultats escomptés.

2. Identifiez les 20 premiers tickets de chaque application et, pour chacun d'eux, obtenez la dernière mise à jour.

Là, on peut dire que les choses se corsent. Nous allons effectuer une recherche sur les données ServiceNow automatiquement ingérées conformément à notre règle métier ServiceNow. L'entrée existing_ticket_check utilise plusieurs agrégations. Nous regroupons d'abord toutes les applications à l'aide de l'agrégation de termes "apps". Ensuite, pour chaque application, nous regroupons les ID des incidents des tickets ServiceNow à l'aide d'une agrégation de termes incidents. Enfin, pour chaque incident qu'une application a rencontré, nous cherchons à en obtenir le dernier état à l'aide de l'agrégation top_hits en fonction du champ @timestamp

3. Fusionnez les deux flux et vérifiez s'il faut créer des tickets.

Pour cela, nous utilisons une transformation de la charge utile du script. Pour faire court, cette transformation détermine les applications qui ont été en échec en itérant sur la sortie down_check, puis vérifie si un ticket a été ouvert pour ces applications. S'il n'y a pas de ticket en cours, nouveau ou en suspens pour ces applications, la transformation ajoute ces dernières à une liste qui est renvoyée et traitée pour poursuivre la procédure. 

Cette transformation de la charge utile effectue quelques vérifications au cours de la procédure pour repérer les cas limites indiqués ci-dessous, comme la création d'un ticket si l'application n'a jamais eu d'historique d'incidents jusque-là. La sortie de cette transformation est un ensemble de noms d'applications.

4. En cas de nouvelle alerte, si aucun ticket n'existe, créez-le dans ServiceNow.

L'action de webhook permet de créer le ticket dans ServiceNow à l'aide de l'API REST. L'API se sert du paramètre foreach pour itérer sur les noms d'applications de l'ensemble susmentionné, puis exécute l'action de webhook pour chacun d'eux. Elle ne procèdera ainsi que s'il y a au moins une application ayant besoin d'un ticket. Veillez à définir les informations d'identification et le point de terminaison appropriés pour ServiceNow.

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" 
  } 
}

Conclusion

Nous voilà arrivés au terme de ce 2e article. Nous avons mis en place une configuration générale pour Elasticsearch afin que les index et la politique ILM existent. Nous avons également créé des transformations qui calculent la disponibilité et le MTBF de chaque application, ainsi qu'un Watcher qui monitore nos données de disponibilité. Ce qu'il faut retenir ici, c'est que si le Watcher constate un échec, il vérifie d'abord si un ticket existe, et dans le cas contraire, il en crée un dans ServiceNow.

Envie de tester par vous-même ? La manière la plus simple de procéder est d'utiliser Elastic Cloud. Connectez-vous à la console Elastic Cloud ou inscrivez-vous pour un essai gratuit de 14 jours. Vous pouvez suivre les étapes ci-dessus sur votre instance ServiceNow ou lancer une instance de développeur personnelle.

De plus, si vous souhaitez effectuer une recherche parmi les données de ServiceNow avec d'autres sources comme GitHub, Google Drive, etc., Elastic Workplace Search possède un connecteur ServiceNow intégré. Workplace Search offre une expérience de recherche unifiée à vos équipes, avec des résultats pertinents pour l'ensemble des sources de contenu. Et la bonne nouvelle, c'est qu'il est inclus dans votre essai Elastic Cloud.

À ce stade, tout est fonctionnel ! Mais d'un point de vue visuel, on ne peut pas dire que ce soit très attrayant. C'est pourquoi, dans le 3e article de cette série, nous allons voir comment utiliser Canvas pour créer un joli tableau de bord pour présenter toutes ces données, ainsi que pour calculer les autres indicateurs mentionnés, tels que le MTTA, le MTTR, et plus encore. On se retrouve ici ! À tout de suite !