Esta pagina se ve mejor con JavaScript habilitado

PMM2 Vol1 Telegrambot + Alertmanager

 ·  🎃 kr0m

Alertmanager es el gestor de alertas de Prometheus, este se encarga de notificar las alarmas a los distintos receptores que configuremos, estos receptores pueden ser tan sencillos como enviar un email o tan complicados como llamar a una API remota, de este modo conseguiremos una integración con servicios como Telegram, Slack, Email o Xmpp entre otros.

Empezamos instalando el bot de Telegram mediante Docker como viene siendo habitual en los artículos sobre PMM.

Primero instalamos las dependencias de Go necesarias para la compilación del binario:

emerge -av dev-lang/go
mkdir go
export GOPATH=/root/go

vi .bashrc

export GOPATH=/root/go  
export GOPATH=$(go env GOPATH)
go get github.com/gin-gonic/gin
go get gopkg.in/telegram-bot-api.v4
go get golang.org/x/crypto/bcrypt

La construcción de la imagen desde Docker está rota, así que compilamos el binario de prometheus_bot en nuestro servidor y generamos la imagen de Docker:

git clone https://github.com/inCaller/prometheus_bot.git
cd prometheus_bot
sed -i ‘1d’ main.go
sed -i ‘1 i\package main’ main.go

Compilamos exactamente igual como se haría en el CT Alpine:

make clean
CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix cgo -o prometheus_bot

NOTA: Con un make compila pero luego Alpine no encuentra el binario aun existiendo:

[FATAL tini (7)] exec /prometheus_bot failed: No such file or directory

De modo que ejecutamos make clean y no make.

Por alguna razón el Dockerfile que viene por defecto arranca el bot mediante tini y deja dos procesos corriendo, lo cual genera problemas, creamos uno propio:

vi Dockerfile

FROM alpine:3.11
COPY prometheus_bot /
COPY config.yaml /
COPY template.tmpl /
RUN apk add --no-cache ca-certificates tzdata tini
USER nobody
EXPOSE 9087
ENTRYPOINT ["/prometheus_bot"]

En la configuración indicaremos el token de Telegram, el template a utilizar y la zona horaria:

vi config.yaml

telegram_token: XXXXX:YYYYYYYYYYY
template_path: "template.tmpl"
time_zone: "Europe/Madrid"
split_token: "|"

Mi template quedaría del siguiente modo:

vi template.tmpl

{{if eq .Status "firing"}}
Status: <b>{{.Status | str_UpperCase}} 🔥 </b>
{{end}}
{{if eq .Status "resolved"}}
Status: <b>{{.Status | str_UpperCase}} ✅ </b>
{{end}}
<b>Active Alert List:</b>
{{- range $val := .Alerts}}
{{if .Labels.node_name }}
<b>--> Server: {{.Labels.node_name}}</b>
{{else if .Labels.name}}
<b>--> Server: {{.Labels.name}}</b>
{{else if .Labels.instance}}
<b>--> Server: {{.Labels.instance}}</b>
{{else if .Labels.proxy}}
<b>--> Ha backend: {{.Labels.proxy}}</b>
{{else if .Labels.server}}
<b>--> Server: {{.Labels.server}}</b>
{{else -}}
<b>--> Server: Unknown</b>
{{end}}
 Problem: {{.Labels.alertname}}
{{end}}

AlertList: http://pmm.alfaexploit.com/prometheus/alerts
AlertSilence: http://pmm.alfaexploit.com:9093/#/alerts

Construimos la imagen:

docker build -t prometheus_bot .

Comprobamos que exista:

docker images

REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
prometheus_bot       latest              50046b0f988e        5 seconds ago       23.1MB

Arrancamos la imagen pero no exponemos el puerto al exterior ya que solo accederá pmm-server por la red interna de Docker:

docker run -d --network=pmm-net --name pmm-telegrambot --restart always prometheus_bot:latest

Comprobamos que haya arrancado:

docker ps

CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS                            PORTS                                      NAMES
b10cf1280f04        prometheus_bot             "prometheus_bot"   3 minutes ago       Restarting (127) 39 seconds ago                                              pmm-telegrambot

Si hacemos cambios en el template o la configuración debemos eliminar el CT viejo, regenerar la imagen y relanzarlo:

cd prometheus_bot/

vi template.tmpl

docker stop pmm-telegrambot  
docker rm pmm-telegrambot  
docker image rm prometheus_bot  
docker build -t prometheus_bot .  
docker run -d --network=pmm-net --name pmm-telegrambot --restart always prometheus_bot:latest

Si miramos los logs del CT veremos los campos de la alerta recibida, con estos campos podremos crear el template del bot con la información que necesitemos:

root@pmm-percona:# docker logs -f pmm-telegrambot

2020/04/16 14:32:17 Bot alert post: CHATID
2020/04/16 14:32:17 +------------------ A L E R T J S O N -------------------+
2020/04/16 14:32:17 {"alerts":[{"annotations":{},"endsAt":"0001-01-01T00:00:00Z","generatorURL":"http://localhost:9090/prometheus/graph?g0.expr=redis_up+%3D%3D+0\u0026g0.tab=1","labels":{"alertname":"RedisDown","instance":"kr0mtest:9121","job":"redis_exporter","node_name":"kr0mtest:9121","severity":"critical"},"startsAt":"2020-04-16T14:31:47.086Z"}],"commonAnnotations":{},"commonLabels":{"alertname":"RedisDown","instance":"kr0mtest:9121","job":"redis_exporter","node_name":"kr0mtest:9121","severity":"critical"},"externalURL":"http://4eb9bbb54554:9093","groupKey":0,"groupLabels":{},"receiver":"telegram","status":"firing","version":0}
2020/04/16 14:32:17 +-----------------------------------------------------------+

2020/04/16 14:32:17 +--------------- F I N A L M E S S A G E ---------------+
2020/04/16 14:32:17
Status: <b>FIRING 🔥</b>

<b>Active Alert List:</b>
<b>--> Server: kr0mtest:9121</b>
 Problem: RedisDown

AlertList: http://pmm.alfaexploit.com/prometheus/alerts
AlertSilence: http://pmm.alfaexploit.com:9093/#/alerts

2020/04/16 14:32:17 +-----------------------------------------------------------+
[GIN] 2020/04/16 - 14:32:17 | 200 | 82.008708ms | 172.18.0.2 | POST "/alert/CHATID"

Podemos utilizar este template como referencia:
https://github.com/inCaller/prometheus_bot/blob/master/testdata/production_example.tmpl

DEBUG
Podemos ver los logs del CT con:

docker logs -f pmm-telegrambot

Si el CT no arranca con el entrypoint indicado en el Dockerfile no podremos acceder al él, por lo tanto lo arrancamos con un bucle y accedemos para echar un vistazo y comprobar que todo esté en orden:

docker stop pmm-telegrambot
docker rm pmm-telegrambot
docker run -d --network=pmm-net --name pmm-telegrambot --restart always prometheus_bot:latest /bin/sh -c “while true; do sleep 2; done”
docker exec -it pmm-telegrambot sh

~ $ ls -la /prometheus_bot /template.tmpl /config.yaml

-rw-r--r-- 1 root root 184 Mar 31 14:06 /config.yaml
-rwxr-xr-x 1 root root 15580478 Mar 31 14:06 /prometheus_bot
-rw-r--r-- 1 root root 396 Mar 31 14:06 /template.tmpl

Ahora instalamos y configuramos Alertmanager, este utilizará el bot de Telegram y un webhook a una API en un servidor Asterisk, de este modo nos enviará un Telegram y nos llamará por teléfono.

NOTA: La mayor pega de Alertmanager es que funciona por polling no por eventos por lo tanto ni las alarmas ni los recoverys serán instantáneas.

Para conseguir el chatid del grupo de Telegram añadimos un bot al grupo:

@RawDataBot

Una vez obtenida la información necesaria lo eliminamos.

Realizamos la configuración de Alertmanager en un fichero externo, de este modo podremos reinstalar el CT sin perder el trabajo realizado.

mkdir prometheusConf

vi prometheusConf/alertmanager.conf

global:

route:

  # Default receiver por defecto: Alertmanager parece ignorar este parámetros, como lo ignora lo definimos mas abajo como un receiver vacío
  receiver: 'default'

  # No agrupes las alertas, de este modo los resolved se ven claros en Telegram
  #group_by: [node_name]

  # Tiempo de espera desde que se recibe por primera vez la alerta de grupo hasta que se envia(solo aplicable si se agrupan alertas)
  #group_wait: 30s

  # Tiempo de espera entre notificaciones de una alarma de grupo previamente recibida(solo aplicable si se agrupan alertas)
  #group_interval: 6m

  # Cada cuanto reenviar las alertas, a esto hay que sumarle el tiempo del alert de Prometheus
  # Si el alert tiene "for: 6m" y repeat_interval 1m -> la alerta se enviará cada 6+1=7m
  repeat_interval: 1m

  # Additional receivers
  routes:

  # Telegram: Solo alarmas criticas
  - match:
      severity: critical
    receiver: 'telegram'
    continue: true

  # Asterisk: Solo alarmas criticas
  - match:
      severity: critical
    receiver: 'asterisk-api'
    continue: true

inhibit_rules:
- source_match:
    alertname: 'BrokenNodeExporter'
  target_match_re:
    severity: '.*'
  equal: ['node_name']

receivers:
- name: 'default'

- name: 'telegram'
  webhook_configs:
  - url: http://pmm-telegrambot:9087/alert/CHATID
    send_resolved: true

- name: 'asterisk-api'
  webhook_configs:
  - url: http://pmm-asterisk.alfaexploit.com:4444/alertSysAdmins
    send_resolved: false

NOTA: La inhibición de alertas nos permitirá eliminar alertas basura ya que si se cae un servidor es normal que todos sus exporters fallen, le regla de inhibición indica que se debe ignorar la alerta independientemente de la severidad si el node_name coincida con otra alerta de BrokenNodeExporter. Hay algunas alertas que no pueden ser inhibidas como la caída de un nodo de un RS de Mongo, ya que los que informan de la caída son el resto de nodos del RS por lo tanto el node_name es el de estos servidores y no el del servidor caído.

No exponemos el puerto de Alertmanager al exterior ya que no tiene autenticación, lo que haremos será dejarlo en la red interna de Docker y montar un Nginx con autenticación que sí expondremos:

docker run -d --network=pmm-net --restart always --name pmm-alertmanager -v $PWD/prometheusConf/alertmanager.conf:/alertmanager.conf -v /etc/localtime:/etc/localtime:ro prom/alertmanager:latest --config.file=/alertmanager.conf

La configuración del Nginx se leerá desde fuera del CT:

mkdir prometheusConf/nginxAlertmanager

vi prometheusConf/nginxAlertmanager/pmm-alertmanager.conf

server {
    listen 80;
    listen [::]:80;
    server_name pmm.alfaexploit.com;

    location / {
        auth_basic "Restricted Content";
        auth_basic_user_file /etc/nginx/.htpasswd;
        proxy_pass http://pmm-alertmanager:9093;
    }
}

Generamos el fichero de passwords, este también se leerá desde fuera del Nginx:

emerge -av app-admin/apache-tools
htpasswd -c prometheusConf/nginxAlertmanager/.htpasswd admin

Arrancamos el servicio:

docker run --name pmm-alertmanager-nginx -d --network=pmm-net -p 9093:80 -v $PWD/prometheusConf/nginxAlertmanager/pmm-alertmanager.conf:/etc/nginx/conf.d/default.conf:ro -v $PWD/prometheusConf/nginxAlertmanager/.htpasswd:/etc/nginx/.htpasswd:ro --restart always nginx

Comprobamos que haya arrancado:

docker ps

fc4aa86409a3        nginx                      "nginx -g 'daemon of..."   22 seconds ago      Up 20 seconds                     0.0.0.0:9093->80/tcp                       pmm-alertmanager-nginx

Si queremos ver las alarmas accedemos a Alertmanager:
http://pmm.alfaexploit.com:9093/#/alerts

Como véis las alertas aparecen duplicadas, esto es debido a que la alerta machea dos receivers, si hubiesen 3 receivers con los que machee saldría triplicada, esto es un bug del Alertmanager, nosotros podemos hacer poco al respecto.

Desde esta interfaz podremos silenciar alarmas si es la misma alarma duplicada por el match de los receivers, silenciando cualquiera de ellas hará desaparecer el resto. Algo a tener en cuenta es que si silenciamos una alarma y resolvemos el problema el recovery no será enviado hasta que el silencio expire, podemos esperar hasta que expire por si mismo o ir a la interfaz web y expirarlo manualmente, de este modo enviará de forma inmediata el recovery, esto puede resultar útil si hay un grupo de sysadmins pendientes del Telegram.

Los enlaces “sources” de las alarmas mostradas en Alertmanager no funcionan ya que Prometheus debería ser arrancado con el parámetro --web.external-url pero PMM2 ya no permite editar el fichero de arranque:

docker exec -it pmm-server /bin/bash
head /etc/supervisord.d/prometheus.ini

; Managed by pmm-managed. DO NOT EDIT.

Y el servicio pmm-managed no permite el parámetro --web.external-url, si lo intentamos Prometheus no arranca.

vi /etc/supervisord.d/pmm.ini

[program:pmm-managed]
priority = 14
command =
    /usr/sbin/pmm-managed
        --prometheus-config=/etc/prometheus.yml
        --prometheus-url=http://127.0.0.1:9090/prometheus
        --postgres-name=pmm-managed
        --postgres-username=pmm-managed
        --postgres-password=pmm-managed
        --supervisord-config-dir=/etc/supervisord.d
        --web.external-url=http://pmm.alfaexploit.com/prometheus/
docker logs -f pmm-server
2020-04-16 19:22:14,688 INFO exited: pmm-managed (exit status 1; not expected)

Así que esa funcionalidad no va a funcionar si utilizamos PMM.

Las alarmas recibidas en Telegram serán de este estilo:


DEBUG
Podemos ver los logs del CT con:

docker logs -f pmm-alertmanager

Para agilizar la configuración de Alertmanager se pueden generar alertas manualmente, dejo a continuación un script desarrollado a partir del tráfico snifado mediante Ngrep.

vi generateAlarmAlertManager.py

import json
import requests
import base64
import datetime

username = 'admin'
password = 'PASSWORD'

credentials = (username + ':' + password).encode('utf-8')
base64_encoded_credentials = base64.b64encode(credentials).decode('utf-8')

headers = {
    'Authorization': 'Basic ' + base64_encoded_credentials,
    'content-type': 'application/json'
}

startsAt = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z")
endsAt = (datetime.datetime.now() + datetime.timedelta(minutes=20)).strftime("%Y-%m-%dT%H:%M:%S.000Z")

data = [{"endsAt":endsAt,"startsAt":startsAt,"generatorURL":"http://localhost:9090/prometheus/graph?g0.expr=sum+by%28node_name%29+%28up%7Bagent_type%3D%22node_exporter%22%7D+%3D%3D+0%29\u0026g0.tab=1","labels":{"alertname":"generateAlarmAlertManager","node_name":"generateAlarmAlertManagerNode","severity":"critical"}}]

data_json = json.dumps(data)
#print("Sending %s: " % data_json)
r = requests.post('http://pmm.alfaexploit.com:9093/api/v2/alerts', data=data_json, headers=headers)
print("Received code: %s" % r.status_code)

Podemos debugear el tráfico con:

ngrep -d INTERFAZ -p -q -W byline port 9093

Si queremos borrar las alarmas podemos reiniciar Alertmanager:

docker restart pmm-alertmanager

O reinsertar la misma alarma pero con un endsAt muy cercano a la hora actual.


UPDATE
El siguiente template puede resultar mas conveniente para silenciar las alarmas directamente con un link:

{{if eq .Status "firing"}}
Status: <b>{{.Status | str_UpperCase}} 🔥 </b>
{{end}}
{{if eq .Status "resolved"}}
Status: <b>{{.Status | str_UpperCase}} ✅ </b>
{{end}}

<b>Active Alert List:</b>
{{- range $val := .Alerts}}
-- label values --
{{ range $key,$val := .Labels -}}
    {{$key}} = <code>{{$val}}</code>
{{ end }}
-- label values --
{{if .Labels.node_name }}
- Server: {{.Labels.node_name}}
  Problem: {{.Labels.alertname}}
  Silence: http://pmm.alfaexploit.com:9093/#/silences/new?filter=%7Balertgroup%3D%22{{.Labels.alertgroup}}%22%2C%20alertname%3D%22{{.Labels.alertname}}%22%2C%20node_name%3D%22{{.Labels.node_name}}%22%2C%20severity%3D%22{{.Labels.severity}}%22%7D
{{else if .Labels.name}}
- Server: {{.Labels.name}}
  Problem: {{.Labels.alertname}}
  Silence: http://pmm.alfaexploit.com:9093/#/silences/new?filter=%7Balertgroup%3D%22{{.Labels.alertgroup}}%22%2C%20alertname%3D%22{{.Labels.alertname}}%22%2C%20node_name%3D%22{{.Labels.name}}%22%2C%20severity%3D%22{{.Labels.severity}}%22%7D
{{else if .Labels.instance}}
- Server: {{.Labels.instance}}
  Problem: {{.Labels.alertname}}
  Silence: http://pmm.alfaexploit.com:9093/#/silences/new?filter=%7Balertgroup%3D%22{{.Labels.alertgroup}}%22%2C%20alertname%3D%22{{.Labels.alertname}}%22%2C%20node_name%3D%22{{.Labels.instance}}%22%2C%20severity%3D%22{{.Labels.severity}}%22%7D
{{else if .Labels.proxy}}
- Ha backend: {{.Labels.proxy}}
  Problem: {{.Labels.alertname}}
  Silence: http://pmm.alfaexploit.com:9093/#/silences/new?filter=%7Balertgroup%3D%22{{.Labels.alertgroup}}%22%2C%20alertname%3D%22{{.Labels.alertname}}%22%2C%20node_name%3D%22{{.Labels.proxy}}%22%2C%20severity%3D%22{{.Labels.severity}}%22%7D
{{else if .Labels.server}}
- Server: {{.Labels.server}}
  Problem: {{.Labels.alertname}}
  Silence: http://pmm.alfaexploit.com:9093/#/silences/new?filter=%7Balertgroup%3D%22{{.Labels.alertgroup}}%22%2C%20alertname%3D%22{{.Labels.alertname}}%22%2C%20node_name%3D%22{{.Labels.server}}%22%2C%20severity%3D%22{{.Labels.severity}}%22%7D
{{else -}}
- Server: Unknown
  Problem: {{.Labels.alertname}}
{{end}}
{{end}}
Si te ha gustado el artículo puedes invitarme a un RedBull aquí