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:
mkdir go
export GOPATH=/root/go
export GOPATH=/root/go
export GOPATH=$(go env GOPATH)
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:
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:
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:
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:
telegram_token: XXXXX:YYYYYYYYYYY
template_path: "template.tmpl"
time_zone: "Europe/Madrid"
split_token: "|"
Mi template quedarÃa del siguiente modo:
{{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:
Comprobamos que exista:
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:
Comprobamos que haya arrancado:
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:
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:
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:
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 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
-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.
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:
La configuración del Nginx se leerá desde fuera del CT:
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:
htpasswd -c prometheusConf/nginxAlertmanager/.htpasswd admin
Arrancamos el servicio:
Comprobamos que haya arrancado:
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:
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.
[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/
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:
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.
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:
Si queremos borrar las alarmas podemos reiniciar 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}}