Promgen은 알림을 어떻게 전달할까요?

안녕하세요, Paul Traylor입니다. 저는 현재 LINE Fukuoka 개발팀에서 LINE 계열의 앱이 사용하는 수많은 서버를 모니터링하는 도구를 개발하고 기술 지원하는 업무를 맡고 있습니다. 제 주 업무 중 하나는 Promgen을 관리하는 일입니다. 참고로 Promgen은 Prometheus가 관련된 팀에게 알림을 제대로 전달할 수 있도록 관리하는 도구입니다. 오늘은 Promgen이 알림을 어떻게 전달하는지에 대한 내용을 다뤄보고자 합니다.

Prometheus와 AlertManager는 알림을 어떻게 처리할까요?

Prometheus는 라벨을 많이 활용하는데요. PromQL(Prometheus Query Language)에서나 알림을 전달하는 등 여러 곳에서 라벨을 사용합니다. 따라서 알림을 어떻게 전달하는지 이해하기 위해선 라벨을 어떻게 사용하고 있는가를 이해하는 것이 중요합니다.

자, 먼저 Nginx 알림을 예제로 한번 살펴 보겠습니다. 다음 코드의 두 번째 줄에 있는 코드는 쿼리입니다.

alert: NginxDown
expr: rate(nginx_exporter_scrape_failures_total{}[30s]) > 0
for: 5m
labels:
  severity: major

Prometheus에서 위의 쿼리를 실행하면 아래와 비슷한 결과를 받게 됩니다.

{project="Project A, service="Service 1", job="nginx", instance="one.example.com:9113"} 1
{project="Project B, service="Service 2", job="nginx", instance="two.example.com:9113"} 1

그리고 Prometheus가 알림을 발생시키면 AlertManager에게 아래와 같은 알림에 대한 상세 내용을 전달합니다.

[
  {
    "labels": {
      "alertname": "NginxDown",
      "project": "Project A",
      "service": "Service 1",
      "job": "nginx",
      "instance": "one.example.com:9113",
      "severity": "major"
    },
    "annotations": {
      "<name>": "<value>"
    },
    "startsAt": "2016-04-21T20:14:37.698Z",
    "endsAt": "2016-04-21T20:15:37.698Z",
    "generatorURL": "<generator_url>"
  },
  {
    "labels": {
      "alertname": "NginxDown",
      "project": "Project A",
      "service": "Service 1",
      "job": "nginx",
      "instance": "two.example.com:9113",
      "severity": "major"
    },
    "annotations": {
      "<name>": "<value>"
    },
    "startsAt": "2016-04-21T20:14:37.698Z",
    "endsAt": "2016-04-21T20:15:37.698Z",
    "generatorURL": "<generator_url>"
  }
]

AlertManager는 활성화된 알림을 모아서 중복 건을 처리한 뒤 최근에 발생한 알림을 묶어 일괄처리합니다. AlertManager는 이 과정에서 자신의 웹훅 노티파이어를 사용해 Promgen에 알림 메시지를 전달합니다. Promgen이 중요시하는 것은 프로젝트서비스이기 때문에, AlertManager 설정에서 프로젝트서비스는 하나의 그룹으로 묶습니다. 아래 알림 정보에서 "groupLabels"항목과 "commonLabels"항목을 보면 확인할 수 있습니다.

{
  "receiver": "promgen",
  "status": "firing",
  "alerts": [
    {
      "labels": {
        "project": "Project A",
        "service": "Service 1",
        "job": "nginx",
        "instance": "one.example.com:9113",
        "alertname": "NginxDown",
        "severity": "major"
      },
      "annotations": {
        "<name>": "<value>"
      },
      "startsAt": "2016-04-21T20:14:37.698Z",
      "endsAt": "2016-04-21T20:15:37.698Z",
      "generatorURL": "<generator_url>"
    },
    {
      "labels": {
        "project": "Project A",
        "service": "Service 1",
        "job": "nginx",
        "alertname": "NginxDown",
        "instance": "two.example.com:9113",
        "severity": "major"
      },
      "annotations": {
        "<name>": "<value>"
      },
      "startsAt": "2016-04-21T20:14:37.698Z",
      "endsAt": "2016-04-21T20:15:37.698Z",
      "generatorURL": "<generator_url>"
    }
  ],
  "groupLabels": {
    "project": "Project A",
    "service": "Service 1",
    "job": "nginx",
    "alertname": "NginxDown",
  },
  "commonLabels": {
    "project": "Project A",
    "service": "Service 1",
    "job": "nginx",
    "alertname": "NginxDown",
  },
  "commonAnnotations": {},
  "externalURL": "alertmanager.example.com",
  "version": "3",
  "groupKey": 12345
}

다음으로 Promgen은 이 알림과 연결된 프로젝트와 서비스가 무엇인지 확인하기 위해 "commonAnnotations" 항목을 확인합니다. Promgen은 쿼리 안에 "commonLabels"를 추가하여 자신만의 주석을 추가하는데요. 이를 통해 Promgen 안에 내장된 객체들과 프로젝트, 서비스를 연결합니다.

Promgen이 메시지 전달에 실패하면?

Promgen이 메시지 전달에 실패하면 첫 번째로 확인해봐야 하는 것이 바로 라벨입니다. aggregation 쿼리를 작성하다 보면 종종 예상치 못했던 방법으로 라벨들이 변경될 수 있기 때문입니다. 앞서 본 예제와 다른 알림 규칙을 통해 살펴보겠습니다.

alert: ExportersDown
expr: count(up==0) > 5
for: 5m
labels:
  severity: major
annotations:
  summary: '{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minute.'

Prometheus가 위의 쿼리를 실행하면 아래와 같은 결과를 돌려줍니다. 단 하나의 값이 반환되었지요?

{} 123

PromQL에 익숙하지 않은 여러분을 위해 우리가 본 쿼리를 SQL로 작성하면 다음과 같습니다.

SELECT COUNT(*)
  FROM table0
 WHERE up = 0

Prometheus가 우리가 작성한 쿼리를 실행하고 알림을 생성하면, AlertManager는 아래와 같은 알림 상세 내용을 받게 됩니다.

[
  {
    "labels": {
      "alertname": "ExportersDown",
      "severity": "major"
    },
    "annotations": {
      "summary": " of job  has been down for more than 5 minute."
    },
    "startsAt": "2016-04-21T20:14:37.698Z",
    "endsAt": "2016-04-21T20:15:37.698Z",
    "generatorURL": "<generator_url>"
  }
]

생성된 알림이 서비스 정보나 프로젝트 정보를 포함하고 있지 않음을 확인할 수 있습니다. 이렇게 되면 Promgen에게 보내는 알림 정보에서 알림 전달을 위한 경로 정보가 빠져버립니다. 이런 상황을 대비하여 Prometheus에게 라벨들을 어떻게 묶을지를 쿼리를 이용하여 알려주면 중요한 라벨을 잘 유지할 수 있습니다.

count(up==0) by (service, project) > 5

비슷한 방식으로, SQL 쿼리를 작성할 때 서비스프로젝트 필드가 유지되길 원한다면, 아래와 같이 group by 절을 쿼리에 추가해야 합니다.

SELECT SERVICE, PROJECT, COUNT(*)
  FROM table0
 WHERE up = 0
 GROUP BY SERVICE, PROJECT

다만, 쿼리를 수정할 수 없는 상황이거나 알림을 다른 팀에게 전달하고 싶을 때가 있을 텐데요. 이를 위해 알림 규칙 자체에 전달받을 대상을 명시적으로 나타내야 할 때가 있습니다. 이럴 때는 다음의 예제와 같이 메시지를 전달받을 서비스를 명시하면 됩니다. 이렇게 하면 Prometheus는 PromQL에 대한 결과에서 라벨을 가져와 우리가 규칙에 명시적으로 정의한 라벨로 업데이트합니다.

alert: ExportersDown
expr: count(up==0) > 5
for: 5m
labels:
  severity: major
  service: Operations # Explicitly set the service we want to route our messages to
annotations:
  summary: '{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minute.'

왜 내 알림이 다른 팀으로 잘못 전달됐을까요?

이번 예시로는 장비 설정이 잘못되었을 때를 위한 알림과 카디널리티 문제(라벨에 고유 값이 너무 많이 있는 문제)를 대비한 알림을 정의해 보겠습니다.

alert: SuddenChangeInMetricsCount
expr: abs(scrape_samples_scraped - scrape_samples_scraped offset 1d > 1000)
for: 1h
labels:
  severity: warning

정확한 임계 값에 대한 얘기는 접어두고, 다른 문제를 살펴보고자 합니다. 우리의 원래 의도는 운영팀에 문제가 있다고 알리는 것이었습니다. 하지만 위와 같이 규칙을 작성한다면 알림은 운영팀이 아닌 개발자들에게도 바로 전달되어 버립니다. 오직 운영팀에만 알리고 싶다면(우리 개발자들의 메일함을 조금이라도 가볍게 하기 위해서라도) 서비스프로젝트 라벨 모두를 덮어써야 합니다.

alert: SuddenChangeInMetricsCount
expr: abs(scrape_samples_scraped - scrape_samples_scraped offset 1d > 1000)
for: 1h
labels:
  project: observation
  service: operations
  severity: warning

위와 같이 프로젝트서비스 라벨 모두를 덮어써야만 운영팀이 문제를 확인할 수 있을 때까지 개발자들은 알림을 받지 않도록 할 수 있습니다.

맺으며

이상으로 Promgen이 알림을 전달하는 방법에 대해 말씀드렸습니다.

읽어주셔서 감사합니다.

Related Post