Engineering

Cómo realizar la gestión de incidentes con ServiceNow y Elasticsearch

¡Bienvenido nuevamente! En el último blog configuramos la comunicación bidireccional entre ServiceNow y Elasticsearch. Dedicamos la mayor parte del tiempo a ServiceNow, pero a partir de ahora, trabajaremos en Elasticsearch y Kibana. Al finalizar este blog, tendrás estas dos aplicaciones poderosas trabajando juntas para que la gestión de incidentes sea fácil. Al menos, mucho más fácil de lo que estás acostumbrado.

Como con todos los proyectos de Elasticsearch, crearemos índices con mapeos que se adecúen a tus necesidades. Para este proyecto, necesitamos índices que puedan contener los datos siguientes:

  • Actualizaciones de incidentes de ServiceNow: esto almacena toda la información que llega de ServiceNow a Elasticsearch. Este es el índice al que ServiceNow envía las actualizaciones.
  • Resumen de tiempo de actividad de las aplicaciones para facilidad de uso: aquí se almacenará la cantidad total de horas que cada aplicación ha estado en línea. Considera esto como un estado intermedio de los datos para facilidad de uso.
  • Resumen de incidentes de las aplicaciones: aquí se almacenará la cantidad de incidentes que ha tenido cada aplicación, el tiempo de actividad y el MTBF (tiempo promedio entre fallas) de cada aplicación.

Los últimos dos índices son índices asistentes para no necesitar tener un montón de lógica complicada ejecutándose cada vez que actualicemos el panel de trabajo de Canvas que crearemos en la parte 3. Se actualizarán solos continuamente mediante el uso de transformaciones.

Creación de los tres índices

Para crear los índices, usarás la guía siguiente. Ten en cuenta que si no usas los mismos nombres que a continuación, es posible que debas hacer ajustes en tu configuración de ServiceNow.

servicenow-incident-updates

Siguiendo las mejores prácticas, configuraremos un alias de índice y después una política de gestión de ciclo de vida del índice (ILM). También crearemos una plantilla de índice para que el mismo mapeo se aplique a cualquier índice futuro creado por nuestra política de ILM. Nuestra política de ILM creará un índice nuevo una vez que se almacenen 50 GB de datos en el índice y lo borrará después de 1 año. Se usará un alias de índice para que podamos señalar fácilmente al índice nuevo cuando se cree sin actualizar nuestra regla comercial de ServiceNow. 

# Crear la política de ILM 
PUT _ilm/policy/servicenow-incident-updates-policy 
{ 
  "policy": { 
    "phases": { 
      "hot": {                       
        "actions": { 
          "rollover": { 
            "max_size": "50GB" 
          } 
        } 
      }, 
      "delete": { 
        "min_age": "360d",            
        "actions": { 
          "delete": {}               
        } 
      } 
    } 
  } 
}
# Crear la plantilla de índice 
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" 
      } 
    } 
  } 
}
# Arrancar el índice inicial y crear el alias 
PUT servicenow-incident-updates-000001 
{ 
  "aliases": { 
    "servicenow-incident-updates": { 
      "is_write_index": true 
    } 
  } 
}

app_uptime_summary y app_incident_summary

Como ambos índices se centran en la entidad, no es necesario que tengan asociada una política de ILM. Esto se debe a que tendremos solo un documento por aplicación a la que monitoreamos. Para crear los índices, debes emitir los comandos siguientes:

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

Configuración de las dos transformaciones

Las transformaciones son una adición reciente e increíblemente útil al Elastic Stack. Proporcionan la capacidad de convertir índices existentes en un resumen centrado en entidad, lo cual es excelente para analíticas e información nueva. Un beneficio de las transformaciones que suele pasarse por alto son los beneficios de rendimiento. Por ejemplo, en lugar de intentar calcular el MTBF de cada aplicación mediante búsqueda y agregación (que se volvería bastante complicado), podemos tener una transformación continua que lo calcule por nosotros con la cadencia que prefieras. Por ejemplo, cada minuto. Sin las transformaciones, se calcularía una vez por cada actualización que cada persona haga en el panel de trabajo de Canvas. Es decir, si 50 personas usan el panel de trabajo con un intervalo de actualización de 30 segundos, ejecutamos la búsqueda costosa 100 veces por minuto (lo cual parece un poco excesivo). Si bien esto no sería un problema para Elasticsearch en la mayoría de los casos, queremos aprovechar esta fantástica función nueva que facilita mucho la vida.

Crearemos dos transformaciones: 

  • calculate_uptime_hours_online_transform: calcula la cantidad de horas que cada aplicación ha estado en línea y con capacidad de respuesta. Lo hace con los datos de tiempo de actividad de Heartbeat. Almacenará estos resultados en el índice app_uptime_summary
  • app_incident_summary_transform: combina los datos de ServiceNow con los datos de tiempo de actividad que provienen de la transformación antes mencionada (sí, parece un tipo de datos join). Esta transformación tomará los datos de tiempo de actividad y calculará la cantidad de incidentes que ha tenido cada aplicación, presentará la cantidad de horas que ha estado en línea y por último calculará el MTBF según esas dos métricas. El índice resultante se llamará 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" 
    } 
  } 
}

Asegurémonos ahora de que ambas transformaciones se estén ejecutando:

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

Alertas de tiempo de actividad para crear tickets en ServiceNow

Además de hacer un buen panel de trabajo de Canvas, el paso final para cerrar el bucle es realmente crear un ticket en ServiceNow si todavía no existe uno para esta interrupción. Para hacerlo, usaremos Watcher para crear una alerta. Los pasos de esta alerta están descritos a continuación. Para que conozcas el contexto, se ejecuta cada minuto. Puedes ver todos los campos de tiempo de actividad en la documentación de Heartbeat.

1. Comprueba para ver cuáles aplicaciones estuvieron inactivas en los últimos 5 minutos

Esta es la parte simple. Obtendremos todos los eventos de Heartbeat con el estado down (filtro de término) en los últimos 5 minutos (filtro de rango) agrupados por monitor.name (agregación de términos). Este campo monitor.name está siguiendo Elastic Common Schema (ECS) y será sinónimo al valor en nuestro campo de nombre de la aplicación. Todo esto se logrará con la entrada down_check en el observador a continuación.

2. Obtén los veinte tickets principales de cada aplicación y la actualización más reciente de cada ticket

Esto marca la tendencia de la línea de complejidad. Buscaremos en todos nuestros datos de ServiceNow ingestados que se ingestan automáticamente debido a nuestra regla comercial de ServiceNow. La entrada existing_ticket_check usa varias agregaciones. La primera es agrupar todas las aplicaciones juntas a través de la agregación de términos “apps”. Para cada app agrupamos las ID de incidentes de tickets de ServiceNow usando una agregación de términos llamada incidents. Por último, de cada incidente encontrado en cada aplicación, obtendremos el estado más reciente usando la agregación top_hits ordenada según el campo @timestamp

3. Fusiona las dos fuentes y ve si es necesario crear algún ticket

Para lograrlo, usamos la transformación de carga de script. En pocas palabras, comprueba qué está inactivo iterando la salida down_check y después comprueba si esa aplicación en particular tiene un ticket abierto. Si no hay ningún ticket actualmente en curso, nuevo o en espera, agrega la aplicación a una lista que se devuelve y pasa a la fase de acción. 

Esta transformación de carga hace bastantes comprobaciones en medio de esto para detectar los casos extremos descritos a continuación, como crear un ticket si la app nunca antes ha tenido antecedentes de incidentes. La salida de esta transformación es una matriz de nombres de aplicaciones.

4. Si es nueva, crea el ticket en ServiceNow

Usamos la acción de webhook para crear el ticket en ServiceNow usando su API REST. Para hacerlo, usa el parámetro foreach para iterar por los nombres de aplicaciones de la matriz anterior y después ejecuta la acción de webhook para cada uno de ellos. Solo hará esto si hay una o más aplicaciones que necesitan un ticket. Asegúrate de configurar el endpoint y las credenciales correctas para 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" 
  } 
}

Conclusión

Aquí terminamos la segunda parte de este proyecto. En esta sección creamos algunas configuraciones generales de Elasticsearch para que los índices y la política de ILM existan. En esta sección también creamos las transformaciones que calculan el tiempo de actividad y MTBF de cada aplicación además de Watcher que monitorea nuestros datos de tiempo de actividad. Lo clave de esto que debemos tener en cuenta es que si Watcher detecta que algo se inactiva, primero comprueba si hay un ticket y, si no, crea uno en ServiceNow.

¿Te interesa seguirlo? La forma más sencilla de hacerlo es usar Elastic Cloud. Inicia sesión en la consola de Elastic Cloud o regístrate para una prueba gratuita de 14 días. Puedes seguir los pasos anteriores con tu instancia de ServiceNow existente o activar una instancia de desarrollador personal.

Además, si estás interesado en buscar datos de ServiceNow junto con otras fuentes como GitHub, Google Drive y más, Elastic Workplace Search tiene un conector ServiceNow prediseñado. Workplace Search proporciona una experiencia de búsqueda unificada para tus equipos, con resultados relevantes en todas tus fuentes de contenido. También se incluye en tu prueba de Elastic Cloud.

En este punto todo es funcional. Sin embargo, no podríamos decir que es agradable visualmente. Así que, en la tercera y última sección de este proyecto veremos cómo puedes usar Canvas para crear un front-end atractivo que represente todos estos datos y también calcularemos las otras métricas mencionadas, como MTTA, MTTR y más. ¡Nos vemos allí!