엔지니어링

ServiceNow와 Elasticsearch로 문제 관리를 수행하는 방법

다시 오신 것을 환영합니다! 지난 블로그에서는 ServiceNow와 Elasticsearch 간의 양방향 통신을 설정했습니다. 대부분의 시간을 ServiceNow에서 작업하며 보냈지만, 여기서부터는 Elasticsearch와 Kibana에서 작업하게 됩니다. 이 글이 끝날 때쯤에는 이 두 개의 강력한 애플리케이션을 함께 작동시켜 수월하게 문제 관리를 할 수 있게 될 것입니다. 아니면 적어도 지금까지 해오신 것보다는 훨씬 더 쉬워집니다!

모든 Elasticsearch 프로젝트와 마찬가지로, 필요에 맞는 매핑이 포함된 인덱스를 만들겠습니다. 이 프로젝트의 경우, 다음 데이터를 저장할 수 있는 인덱스가 필요합니다.

  • ServiceNow 문제 업데이트: 여기에는 ServiceNow에서 Elasticsearch로 전송되는 모든 정보가 저장됩니다. 이것은 ServiceNow에서 업데이트를 푸시하는 인덱스입니다.
  • 사용 편의성을 위한 애플리케이션 가동 시간 요약: 여기에는 각 애플리케이션이 온라인에 접속한 총 시간이 저장됩니다. 이것을 사용 편의성을 위한 중간 데이터 상태로 간주합니다.
  • 애플리케이션 문제 요약: 여기에는 각 애플리케이션에 대해 발생한 문제 수, 각 애플리케이션의 가동 시간 및 현재 MTBF(실패 간 평균 시간)가 저장됩니다.

마지막 두 인덱스는 도우미 인덱스입니다. 따라서 3부에서 만들 Canvas 워크패드를 새로 고칠 때마다 복잡한 로직을 많이 실행되지 않아도 됩니다. 이 인덱스는 데이터 트랜스폼의 사용을 통해 지속적으로 업데이트됩니다.

세 가지 인덱스 생성

인덱스를 만들려면 다음 지침을 사용합니다. 아래에서 사용하는 것과 동일한 이름을 사용하지 않는 경우, ServiceNow 설정을 조정해야 할 수도 있습니다.

servicenow-incident-updates

모범 사례에 따라 인덱스 별칭을 설정한 다음 인덱스 수명 주기 관리(ILM) 정책을 설정하겠습니다. 또한 ILM 정책에 의해 생성된 향후 인덱스에 동일한 매핑이 적용되도록 인덱스 템플릿을 생성할 것입니다. Elastic의 ILM 정책은 50GB의 데이터가 인덱스에 저장되면 새 인덱스를 생성하게 되고, 그 후 1년이 지나면 삭제합니다. 인덱스 별칭이 사용되므로 ServiceNow 비즈니스 규칙을 업데이트하지 않고도 새 인덱스를 생성할 때 인덱스를 쉽게 지칭할 수 있습니다. 

# ILM 정책 생성 
PUT _ilm/policy/servicenow-incident-updates-policy 
{ 
  "policy": { 
    "phases": { 
      "hot": {                       
        "actions": { 
          "rollover": { 
            "max_size": "50GB" 
          } 
        } 
      }, 
      "delete": { 
        "min_age": "360d",            
        "actions": { 
          "delete": {}               
        } 
      } 
    } 
  } 
}
# 인덱스 템플릿 생성 
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" 
      } 
    } 
  } 
}
# 초기 인덱스를 부트스트랩하고 별칭 생성 
PUT servicenow-incident-updates-000001 
{ 
  "aliases": { 
    "servicenow-incident-updates": { 
      "is_write_index": true 
    } 
  } 
}

app_uptime_summary & app_incident_summary

이 두 인덱스는 모두 엔티티 중심이기 때문에 ILM 정책을 관련시킬 필요가 없습니다. 이는 모니터링 중인 애플리케이션당 하나의 문서만 보유하게 되기 때문입니다. 인덱스를 생성하려면 다음 명령을 실행합니다.

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

두 데이터 트랜스폼 설정

데이터 트랜스폼은 Elastic Stack에 최근에 추가된 매우 유용한 기능입니다. 또한 기존 인덱스를 엔티티 중심 요약으로 변환할 수 있는 기능을 제공하므로 분석새로운 인사이트에 적합합니다. 데이터 트랜스폼의 종종 간과되는 이점은 성능상의 이점입니다. 예를 들어 쿼리 및 집계(매우 복잡해짐)로 각 애플리케이션에 대한 MTBF를 계산하는 대신, 선택한 속도에 따라 지속적인 데이터 트랜스폼을 통해 MTBF를 계산할 수 있습니다. 예를 들어, 매분을 선택할 수 있습니다. 데이터 트랜스폼이 없으면 각 사용자가 Canvas 워크패드에서 수행하는 모든 새로 고침에 대해 한 번씩 계산됩니다. 즉, 새로 고침 간격이 30초인 워크패드를 사용하는 사용자가 50명일 경우 분당 100번씩 비싼 쿼리를 실행합니다. (좀 지나친 감이 있죠.) 대부분의 경우 Elasticsearch에는 문제가 되지 않겠지만, 개인적으로는 한결 쉽고 편안하게 해주는 이 멋진 새로운 기능을 이용하고 싶습니다.

이제 두 가지 데이터 트랜스폼을 만들겠습니다. 

  • calculate_uptime_hours_online_transform: 각 애플리케이션이 온라인 상태이고 응답한 시간을 계산합니다. Heartbeat의 가동 시간 데이터를 활용하여 이 작업을 수행합니다. 이 결과는 app_uptime_summary 인덱스에 저장됩니다. 
  • app_incident_summary_transform: ServiceNow 데이터를 앞서 언급한 데이터 트랜스폼의 가동 시간 데이터와 결합합니다. (그렇습니다...약간 join처럼 들리죠.) 이 데이터 트랜스폼은 가동 시간 데이터를 사용하여 각 애플리케이션의 문제 횟수를 파악하고 온라인에 접속한 시간이 얼마나 되는지 파악한 후 최종적으로 이 두 가지 메트릭을 기반으로 MTBF를 계산합니다. 결과 인덱스를 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" 
    } 
  } 
}

이제 두 데이터 트랜스폼이 모두 실행 중인지 확인하겠습니다.

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

ServiceNow에서 티켓을 생성하기 위한 가동 시간 경보

보기 좋은 Canvas 워크패드를 만드는 것 외에도, 루프를 닫는 마지막 단계는 이러한 중단에 대한 티켓이 아직 존재하지 않는 경우 ServiceNow에서 티켓을 생성하는 것입니다. 이를 위해 Watcher를 사용하여 경보를 생성하려고 합니다. 이 경보에는 아래에 설명된 단계가 있습니다. 컨텍스트에서는 매분마다 실행됩니다. Heartbeat 설명서에서 모든 가동 시간 필드를 볼 수 있습니다.

1. 지난 5분 동안 어떤 애플리케이션이 중단되었는지 확인

이것은 간단한 작업입니다. 지난 5분 동안(범위 필터) down(용어 필터)된 모든 Heartbeat 이벤트를 monitor.name(용어 집계)으로 그룹화하여 가져올 것입니다. 이 monitor.name 필드는 Elastic Common Schema(ECS)를 따르며, 애플리케이션 이름 필드의 값과 동의어가 됩니다. 이 모든 것은 아래 Watcher의 down_check 입력을 통해 이루어집니다.

2. 각 애플리케이션의 상위 티켓 20장을 얻고 각 티켓에 대한 최신 업데이트 받기

이것은 복잡도의 작업 과정을 따라가게 됩니다. ServiceNow 비즈니스 규칙으로 인해 자동으로 수집되는 수집된 ServiceNow 데이터를 검색하겠습니다. existing_ticket_check 입력은 여러 개의 집계를 사용합니다. 첫 번째는 "apps" 용어 집계를 통해 모든 애플리케이션을 그룹화하는 것입니다. 그런 다음 각 앱에 대해 incidents라는 용어 집계를 사용하여 ServiceNow 티켓 문제 ID를 그룹화합니다. 마지막으로, 각 애플리케이션에 대해 발견된 각 문제에 대해 @timestamp 필드를 기준으로 정렬된 top_hits 집계를 사용하여 최신 상태를 확인합니다. 

3. 두 피드를 병합하여 티켓을 생성해야 하는지 확인

이를 위해 스크립트 페이로드 트랜스폼을 사용합니다. 즉, down_check 출력에 대해 반복하여 중단된 내용을 확인한 다음 해당 애플리케이션에 현재 진행 중인 티켓이 있는지 확인합니다. 현재 진행 중이거나 새 티켓 또는 보류 중인 티켓이 없는 경우, 반환되어 작업 단계로 전달되는 목록에 애플리케이션을 추가합니다. 

이러한 페이로드 트랜스폼은 이전에 앱에 문제 기록이 없는 경우 티켓을 생성하는 것과 같이 아래에 요약한 엣지 케이스를 파악하기 위해 많은 확인을 수행합니다. 이 트랜스폼의 출력은 애플리케이션 이름의 배열입니다.

4. 새로운 문제인 경우 ServiceNow에서 티켓 생성

REST API를 사용하여 ServiceNow에서 티켓을 생성하기 위해 Webhook 작업을 사용합니다. 이렇게 하려면 foreach 매개 변수를 사용하여 위의 배열에서 애플리케이션 이름에 대해 반복한 다음 각 매개 변수에 대해 Webhook 작업을 실행합니다. 티켓이 필요한 애플리케이션이 하나 이상 있는 경우에만 이 작업을 수행합니다. 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" 
  } 
}

결론

여기서 이 프로젝트의 두 번째 편을 마칩니다. 이 섹션에서는 인덱스 및 ILM 정책이 존재하도록 Elasticsearch를 위한 몇 가지 일반적인 구성을 만들어 보았습니다. 이 섹션 내에서 또한 각 애플리케이션의 가동 시간과 MTBF를 계산하는 데이터 트랜스폼과 가동 시간 데이터를 모니터링하는 Watcher도 만들었습니다. 여기서 주목해야 할 중요한 사항은 Watcher가 먼저 무언가 문제가 있다는 것을 알아차린 경우, 우선 티켓이 존재하는지 확인하고, 없으면 ServiceNow에서 티켓을 만든다는 것입니다.

직접 한 번 해보고 싶으세요? 가장 쉬운 방법은 Elastic Cloud를 사용하는 것입니다. Elastic Cloud 콘솔에 로그인하거나 14일 무료 체험판에 등록하시면 됩니다. 기존 ServiceNow 인스턴스로 위의 단계를 따르거나 개인 개발자 인스턴스를 스핀업하실 수 있습니다.

또한 GitHub, Google Drive 등과 같은 다른 소스와 함께 ServiceNow 데이터를 검색하려는 경우, Elastic Workplace SearchServiceNow 커넥터가 미리 빌드되어 있습니다. Workspace Search는 모든 컨텐츠 소스에서 관련 결과를 포함하여 팀에게 통합 검색 환경을 제공합니다. 또한 Elastic Cloud 체험판에도 포함되어 있습니다.

현재 모든 것이 잘 작동하고 있습니다! 하지만, 시각적으로 매력적이라고 보긴 좀 어렵습니다. 따라서, 이 프로젝트의 세 번째이자 마지막 섹션에서는 Canvas를 사용하여 이 모든 데이터를 표현할 수 있는 멋진 프런트엔드를 생성하는 방법과 MTTA, MTTR 등과 같은 기타 메트릭을 계산하는 방법에 대해 알아보겠습니다. 다음 섹션에서 뵙겠습니다!