Engenharia

Como executar o gerenciamento de incidentes com o ServiceNow e o Elasticsearch

Bem-vindo(a) de volta! No nosso último post, falamos sobre a comunicação bidirecional entre o ServiceNow e o Elasticsearch. Passamos a maior parte do tempo no ServiceNow, mas, daqui para a frente, vamos trabalhar no Elasticsearch e no Kibana. Ao final do post, você terá essas duas aplicações trabalhando juntas em prol de um gerenciamento de incidentes tranquilo. Ou pelo menos muito mais fácil do que era antes.

Assim como com todos os projetos do Elasticsearch, criaremos índices com mapeamentos de acordo com as suas necessidades. Para este projeto, precisaremos de índices que possam segurar os seguintes dados:

  • Atualizações de incidentes do ServiceNow: armazena todas as informações que vêm do ServiceNow para o Elasticsearch. Esse é o índice para o qual o ServiceNow manda as atualizações.
  • Resumo do tempo de funcionamento da aplicação para facilitar o uso: armazenará a quantidade total de horas em que cada aplicação esteve online. Considere esse um estado intermediário para a facilidade de uso.
  • Resumo do incidente da aplicação: armazenará a quantidade de incidentes que cada aplicação teve, o tempo de funcionamento de cada aplicação e o tempo médio entre falhas (MTBF) para cada aplicação.

Os dois últimos índices são índices ajudantes para que não precisemos ter muita lógica complicada sendo executada a cada vez que atualizamos o workpad do Canvas que criaremos na parte 3. Eles serão atualizados de forma contínua para nós através das transformações.

Crie os três índices

Para criar os índices, basta seguir as orientações abaixo. Se você não usar os mesmos nomes que usamos abaixo, talvez precise fazer ajustes na instalação do ServiceNow.

servicenow-incident-updates

Para seguir as práticas recomendadas, configuraremos um alias do índice e depois uma política de gestão de ciclo de vida de índices (ILM). Também criaremos um modelo de índice para que o mesmo mapeamento possa ser aplicado a quaisquer futuros índices criados pela nossa política de ILM. Nossa política de ILM criará um novo índice quando 50GB de dados forem armazenados dentro do índice e depois excluirá esse índice após 1 ano. Um alias do índice será usado para que possamos facilmente apontar para o novo índice quando ele for criado sem atualizar a nossa regra de negócios do ServiceNow. 

# Crie a 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": {}               
        } 
      } 
    } 
  } 
}
# Crie o modelo de índices 
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" 
      } 
    } 
  } 
}
# Inicialize o primeiro índice e crie o alias 
PUT servicenow-incident-updates-000001 
{ 
  "aliases": { 
    "servicenow-incident-updates": { 
      "is_write_index": true 
    } 
  } 
}

app_uptime_summary e app_incident_summary

Como esses dois índices são centrados na entidade, não precisam ter uma política de ILM associada a eles. Isso acontece porque sempre teremos só um documento por aplicação que estamos monitorando. Para criar os índices, use os comandos abaixo:

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

Configure as duas transformações

As transformações são muito úteis e uma novidade no Elastic Stack. Com elas, você consegue converter índices existentes em um resumo centrado em entidade, o que é ótimo para analítica e novos insights. Um dos benefícios que não são muito valorizados nas transformações são os benefícios de desempenho. Por exemplo, em vez de tentar calcular o MTBF para cada aplicação por consulta e agregação (o que seria bem complicado), podemos ter uma transformação contínua que calcule isso para nós em uma cadência à nossa escolha. A cada minuto, se você desejar. Sem as transformações, eles seriam calculados uma vez a cada atualização manual do workpad do Canvas. Ou seja, se tivermos 50 pessoas usando o workpad com um intervalo de atualização de 30 segundos, executamos a consulta cara 100 vezes por minuto (o que parece excessivo). Embora isso não seja um problema para o Elasticsearch em muitos casos, quero aproveitar esse novo recurso incrível, que facilita muito a vida.

Vamos criar duas transformações: 

  • calculate_uptime_hours_online_transform: calcula o número de horas que cada aplicação ficou online e responsiva. Ela faz isso utilizando os dados de tempo de funcionamento do Heartbeat. E armazena esses resultados no índice app_uptime_summary
  • app_incident_summary_transform: combina os dados do ServiceNow com os dados de tempo de funcionamento que vieram da transformação de que falamos (parece um join, não é mesmo?). Essa transformação pega os dados de tempo de funcionamento para entender quantos incidentes cada aplicação teve, quantas horas ela ficou online e, por fim, calcular o MTBF com base nessas duas métricas. O resultado é um índice chamado 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" 
    } 
  } 
}

Agora, vamos garantir que as duas transformações estejam sendo executadas:

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

Alertas de tempo de funcionamento para criar tíquetes no ServiceNow

Além de fazer um belo workpad do Canvas, essa última etapa, para fechar o ciclo, é também a criação de um tíquete no ServiceNow (se já não existir um para essa interrupção do serviço). Para fazer isso, usaremos o Watcher, que cria um alerta. Esse alerta tem as etapas que estão descritas abaixo. Para contexto, ele é executado a cada minuto. É possível ver todos os campos de tempo de funcionamento na documentação do Heartbeat.

1. Veja quais aplicações estiveram inativas nos últimos 5 minutos

Essa é a parte simples. Vamos pegar todos os eventos de inatividade (filtro de termo) do Heartbeat nos últimos 5 minutos (filtro de alcance) agrupados pelo monitor.name (agregação de termos). Este campo monitor.name está seguindo o Elastic Common Schema (ECS) e será sinônimo do valor em nosso campo do nome da aplicação. Tudo isso será feito com a entrada down_check no Watcher abaixo.

2. Obtenha os 20 principais tíquetes para cada aplicação e a atualização mais recente para cada tíquete

Isso modifica a linha de complexidade. Vamos fazer uma busca nos nossos dados do ServiceNow ingeridos automaticamente pela nossa regra de negócio do ServiceNow. A entrada existing_ticket_check usa várias agregações. A primeira é agrupar todas as aplicações através da agregação de termos dos apps. Depois, para cada app, agrupamos as IDs de incidente de tíquete do ServiceNow usando uma agregação de termos chamada incidentes Por fim, para cada incidente encontrado para cada aplicação, obteremos o estado mais atual usando a agregação top_hits ordenada pelo campo @timestamp

3. Mescle os dois feeds e veja se é necessário criar algum tíquete

Para fazer isso, usamos a transformação de carga de script. Resumidamente, ela verifica o que está inativo usando a saída down_check e depois verifica se aquela aplicação específica tem um tíquete aberto. Se não houver um tíquete aberto, novo ou em pausa, ele adiciona a aplicação a uma lista que é retornada e passada para a próxima fase da ação. 

Esta transformação de carga faz diversas verificações nesse meio tempo para pegar os casos limítrofes de que falamos abaixo, como criar um tíquete se o app não tiver nenhum histórico de incidente. A saída dessa transformação é uma faixa de nomes de aplicações.

4. Se for um alerta novo, crie o tíquete no ServiceNow

Usamos o webhook ação para criar o tíquete no ServiceNow usando a REST API. Para fazer isso, ela usa o parâmetro foreach para repetir os nomes dos aplicativos acima e executa ação do webhook para cada uma. Ele só fará isso se houver uma ou mais aplicações que precisem de um tíquete. Verifique se você definiu as credenciais e o endpoint corretos para o 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" 
  } 
}

Conclusão

Aqui, concluímos a segunda parte deste projeto. Nesta parte, criamos algumas configurações gerais para o Elasticsearch para que existam índices e política de ILM. Também criamos as transformações que calculam o tempo de funcionamento e o MTBF para cada aplicação, além do Watcher, que monitora os nossos dados de tempo de funcionamento. O mais importante de se notar aqui é que, se o Watcher perceber algum problema, primeiro, ele verificará se existe um tíquete e, caso não exista, ele criará um no ServiceNow.

Quer acompanhar? O jeito mais fácil é usar o Elastic Cloud. Faça login no console Elastic Cloud ou inscreva-se para uma avaliação gratuita de 14 dias. Siga as orientações acima na sua instância do ServiceNow ou teste uma instância de desenvolvedor pessoal.

E, se quiser fazer buscas nos dados do ServiceNow junto com outras fontes como GitHub, Google Drive e muito mais, o Elastic Workplace Search tem um conector do ServiceNow pré-criado. O Workplace Search oferece uma experiência de busca unificada para as suas equipes, com resultados relevantes para todas as suas fontes de conteúdo. Ele também está incluído na versão de avaliação do Elastic Cloud.

Agora, está tudo funcionando! Mas não dá para dizer que está muito interessante do ponto de vista visual. Por isso, na terceira e última parte deste projeto, vamos ver como usar o Canvas para criar um frontend visualmente interessante para representar esses dados e calcular as outras métricas de que falamos aqui, como MTTA, MTTR e muito mais. Até !