Wahrscheinlich stellt heute niemand mehr die Frage, warum es notwendig ist, die Metriken von Diensten zu sammeln. Der nächste logische Schritt besteht darin, ein Alerting für die gesammelten Metriken einzurichten, das über jede Abweichung in den Daten in die von Ihnen gewünschten Kanäle (E-Mail, Slack, Telegram) informiert. Im Online-Hotelbuchungsdienst fließen alle Metriken unserer Dienste in InfluxDB und werden in Grafana angezeigt, wo auch das grundliegende Alerting eingerichtet ist. Für Aufgaben wie 'etwas berechnen und damit vergleichen' verwenden wir Kapacitor.

Kapacitor ist Teil des TICK-Stacks, der in der Lage ist, Metriken aus InfluxDB zu verarbeiten. Er kann mehrere Messungen miteinander verbinden (join), aus den gewonnenen Daten etwas Nützliches berechnen, das Ergebnis zurück in InfluxDB schreiben und Alerts an Slack/Telegram/E-Mail senden.
Der gesamte Stack hat eine tolle und detaillierte , aber es gibt immer nützliche Dinge, die in den Handbüchern nicht ausdrücklich aufgeführt sind. In diesem Artikel habe ich beschlossen, eine Reihe solcher nützlicher, nicht offensichtlicher Tipps zusammenzustellen (die grundlegende Syntax von TICKscript ist beschrieben ) und zu zeigen, wie man sie anhand einer unserer Aufgaben anwenden kann.
Los geht's!
Float & int, Berechnungsfehler
Ein absolut typisches Problem, das durch einen Cast gelöst wird:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Verwendung von default()
Wenn ein Tag/Feld nicht ausgefüllt ist, treten Berechnungsfehler auf:
|default()
.tag('status', 'empty')
.field('value', 0)
Fill beim Join (inner vs outer)
Standardmäßig werden bei einem Join Punkte ohne Daten (inner) verworfen.
Bei fill('null') wird ein outer join durchgeführt, nach dem default() ausgeführt werden muss, um leere Werte zu füllen:
var data = res1
|join(res2)
.as('res1', 'res2')
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
Hier gibt es jedoch eine Nuance. Wenn in dem obigen Beispiel eine der Serien (res1 oder res2) leer ist, wird die endgültige Serie (data) ebenfalls leer sein. Dazu gibt es mehrere Tickets auf GitHub (, , ) – wir warten auf Fixes und leiden ein wenig.
Verwendung von Bedingungen in Berechnungen (if in lambda)
|eval(lambda: if("value" > 0, true, false)
Die letzten fünf Minuten aus der Pipeline für den Zeitraum
Zum Beispiel müssen Sie die Werte der letzten fünf Minuten mit der Vorwoche vergleichen. Sie können zwei Datensätze in zwei separaten Batches nehmen oder einen Teil der Daten aus einem größeren Zeitraum extrahieren:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Eine Alternative für die letzten fünf Minuten könnte die Verwendung des Knotens BarrierNode sein, der die Daten vor der angegebenen Zeit abschneidet:
|barrier()
.period(5m)
Beispiele für die Verwendung von Go-Vorlagen in Nachrichten
Vorlagen entsprechen dem Format aus dem Paket , hier sind einige häufig auftretende Aufgaben.
if-else
Ordnung schaffen, ohne die Leute mit unnötigem Text zu triggern:
|alert()
...
.message(
'{{ if eq .Level "OK" }}Es ist jetzt in Ordnung{{ else }}Chef, alles ist kaputt{{end}}'
)
Zwei Dezimalstellen in der Nachricht
Verbesserung der Lesbarkeit der Nachricht:
|alert()
...
.message(
'Der aktuelle Wert ist {{ index .Fields "value" | printf "%0.2f" }}'
)
Entfaltung von Variablen in der Nachricht
Wir geben in der Nachricht mehr Informationen aus, um die Frage „Warum schreit es?“ zu beantworten:
var warnAlert = 10
|alert()
...
.message(
'Heute liegt der Wert unter ' + string(warnAlert) + '%'
)
Eindeutige Identifikation des Alarms
Nützlich, wenn es in den Daten mehr als eine Gruppe gibt, ansonsten wird nur ein Alarm generiert:
|alert()
...
.id('{{ index .Tags "myname" }}\/{{ index .Tags "myfield" }}')
Benutzerdefinierte Handler
In einer langen Liste von Handlern gibt es exec, das es ermöglicht, ein eigenes Skript mit übergebenen Parametern (stdin) auszuführen – Kreativität pur!
Einer unserer benutzerdefinierten Handler ist ein kleines Python-Skript zum Versenden von Benachrichtigungen in Slack.
Zunächst wollten wir ein Bild aus Grafana, das durch Authentifizierung geschützt ist, in der Nachricht senden. Danach – OK in den Thread des vorherigen Alarms aus derselben Gruppe schreiben, und nicht als separate Nachricht. Noch etwas später – die häufigste Fehlermeldung der letzten X Minuten in die Nachricht einfügen.
Ein separates Thema – die Verbindung zu anderen Diensten und irgendwelche Aktionen, die durch den Alarm initiiert werden (nur wenn Ihre Überwachung gut genug funktioniert).
Beispiel für die Beschreibung eines Handlers, wobei slack_handler.py unser selbstgeschriebenes Skript ist:
topic: slack_graph
id: slack_graph.alert
match: level() != INFO AND changed() == TRUE
kind: exec
options:
prog: \/sbin\/slack_handler.py
args: ["-c", "CHANNELID", "--graph", "--search"]
Wie debuge ich?
Variante mit Ausgabe in das Log
|log()
.level("error")
.prefix("something")
Ansehen (cli): kapacitor -url :9092 logs lvl=error
Variante mit httpOut
Zeigt die Daten im aktuellen Pipeline an:
|httpOut('something')
Ansehen (get): :9092\/kapacitor\/v1\/tasks\/task_name\/something
Ausführungsdiagramm
- Jede Aufgabe gibt einen Ausführungsbaum mit nützlichen Zahlen im Format zurück .
- Wir nehmen den Block .
- Fügen wir in den Viewer ein, .
Wo man auch ansonsten Timestamp in influxdb bei rückwärtsgeschriebener Aufzeichnung erhalten kann
Zeitstempel in InfluxDB bei Rückschreibung
Zum Beispiel richten wir einen Alert für die Anzahl der Anfragen pro Stunde (groupBy(1h)) ein und möchten den ausgelösten Alert in influxdb protokollieren (um das Vorhandensein eines Problems in einem Diagramm in grafana ansprechend darzustellen).
influxDBOut() wird im Timestamp den Wert time aus dem Alert schreiben, entsprechend wird der Punkt im Diagramm früher/später aufgezeichnet als der Alert einging.
Wenn Genauigkeit erforderlich ist: Umgehen wir dieses Problem, indem wir einen benutzerdefinierten Handler aufrufen, der die Daten mit dem aktuellen Timestamp in influxdb schreibt.
docker, build und deployment
Beim Start lädt kapacitor möglicherweise Aufgaben, Templates und Handler aus dem im Konfigurationsblock [load] angegebenen Verzeichnis.
Für die korrekte Erstellung einer Aufgabe sind folgende Dinge notwendig:
- Dateiname – wird in id/name des Skripts umgewandelt
- Typ – stream/batch
- dbrp – Schlüsselwort zur Angabe, in welcher Datenbank + Policy das Skript arbeitet (dbrp „supplier“.„autogen“)
Wenn in irgendeiner batch-Aufgabe die Zeile mit dbrp fehlt, weigerte sich der gesamte Dienst zu starten und schreibt dies ehrlich ins Log.
Im chronograf sollte diese Zeile dagegen nicht vorhanden sein, da sie über die Schnittstelle nicht akzeptiert wird und einen Fehler anzeigt.
Hack beim Bauen des Containers: Dockerfile schlägt mit -1 fehl, wenn es Zeilen mit /\/\.+dbrp gibt, was sofort hilft, den Fehler beim Bauen zu verstehen.
join eins zu viele
Beispielaufgabe: Wir müssen den 95. Perzentil der Betriebszeit des Dienstes über eine Woche nehmen und jede Minute der letzten 10 Minuten mit diesem Wert vergleichen.
Ein join eins zu viele ist nicht möglich, last/mean/median über eine Gruppe von Punkten verwandeln den Knoten in einen stream, es wird ein Fehler «cannot add child mismatched edges: batch -> stream» zurückgegeben.
Das Ergebnis des batchs, als Variable im lambda-Ausdruck, wird ebenfalls nicht eingesetzt.
Es gibt die Möglichkeit, die benötigten Zahlen aus dem ersten Batch in eine Datei über udf zu speichern und diese Datei über sideload zu laden.
Was haben wir damit gelöst?
Wir haben etwa 100 Hotelanbieter, von denen jeder mehrere Verbindungen haben kann, die wir als Kanäle bezeichnen. Es gibt ungefähr 300 dieser Kanäle, und jeder Kanal kann ausfallen. Von allen aufgezeichneten Metriken überwachen wir die Fehlerquote (requests und errors).
Warum nicht grafana?
Alerts zu Fehlern, die in grafana eingestellt sind, haben mehrere Nachteile. Einige sind kritisch, andere können je nach Situation ignoriert werden.
Grafana kann keine Berechnungen zwischen Messungen durchführen + Alerting, und wir benötigen die Rate (requests-errors)/requests.
Fehler sehen bedrohlich aus:

Und weniger bedrohlich, wenn man die erfolgreichen Anfragen betrachtet:

Okay, wir können die Rate vor dem Grafana-Service vorläufig berechnen, und in einigen Fällen wäre das ausreichend. Aber nicht in unserem, da für jeden Kanal ein bestimmtes Verhältnis als "normal" angesehen wird und die Alarme auf statischen Werten basieren (wir schauen mit den Augen, ändern sie, wenn zu oft ein Alarm ausgelöst wird).
Das sind Beispiele für "normal" für verschiedene Kanäle:


Lassen wir den vorherigen Punkt beiseite und nehmen wir an, dass bei allen Anbietern das "normale" Bild ähnlich aussieht. Nun sieht alles gut aus, können wir uns mit Alarmen in Grafana zufriedengeben?
Können wir, aber wir möchten nicht, denn wir müssen eine der Varianten wählen:
a) zahlreiche Diagramme für jeden Kanal separat erstellen (und sie mühsam pflegen)
b) ein Diagramm mit allen Kanälen behalten (und uns in bunten Linien und konfigurierten Alarmen verlieren)

Wie haben wir es gemacht?
Wieder einmal gibt es ein gutes Startbeispiel in der Dokumentation (), man kann einen Blick darauf werfen oder es als Grundlage für ähnliche Aufgaben verwenden.
Was wir letztendlich gemacht haben:
- Zusammenführung von zwei Serien über mehrere Stunden, Gruppierung nach Kanälen;
- Füllung der Serien nach Gruppen, wenn keine Daten vorlagen;
- Vergleich des Medians der letzten 10 Minuten mit den vorherigen Daten;
- Wir schreien, wenn wir etwas entdeckt haben;
- Wir schreiben die berechneten Raten und aufgetretenen Alarme in influxdb;
- Wir senden eine nützliche Nachricht in Slack.
Meiner Meinung nach ist es uns gelungen, alles, was wir uns gewünscht haben, (und sogar ein wenig mehr mit benutzerdefinierten Handlern) sehr schön umzusetzen.
Auf github.com kann man sehen und des erhaltenen Skripts.
Ein Beispiel des entstandenen Codes:
dbrp "supplier"."autogen"
var name = 'requests.rate'
var grafana_dash = 'pczpmYZWU/mydashboard'
var grafana_panel = '26'
var period = 8h
var todayPeriod = 10m
var every = 1m
var warnAlert = 15
var warnReset = 5
var reqQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."requests"'
var errQuery = 'SELECT sum("count") AS value FROM "supplier"."autogen"."errors"'
var prevErr = batch
|query(errQuery)
.period(period)
.every(every)
.groupBy(1m, 'channel', 'supplier')
var prevReq = batch
|query(reqQuery)
.period(period)
.every(every)
.groupBy(1m, 'channel', 'supplier')
var rates = prevReq
|join(prevErr)
.as('req', 'err')
.tolerance(1m)
.fill('null')
// füllen Sie Werte mit Nullen, wenn sie nicht vorhanden waren
|default()
.field('err.value', 0.0)
.field('req.value', 0.0)
// if in lambda: berechnen Sie die Rate, nur wenn Fehler vorhanden waren
|eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
.as('rate')
// speichern Sie die berechneten Werte in Influx
rates
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('rates')
// wählen Sie Daten der letzten 10 Minuten, berechnen Sie den Median
var todayRate = rates
|where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) warnAlert)
.warnReset(lambda: ("prev.median" - "today.median") < warnReset)
.flapping(0.25, 0.5)
.stateChangesOnly()
// sammeln Sie in der Nachricht den Link zum Diagramm des Grafana-Dashboards
.message(
'{{ .Level }}: {{ index .Tags "channel" }} err/req Verhältnis ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}Es ist jetzt in Ordnung{{ else }}
'+string(todayPeriod)+' Median ist {{ index .Fields "today.median" | printf "%0.2f" }}%, im Vergleich zum vorherigen '+string(period)+' ist {{ index .Fields "prev.median" | printf "%0.2f" }}%{{ end }}
http://grafana.ostrovok.in/d/'+string(grafana_dash)+
'?var-supplier={{ index .Tags "supplier" }}&var-channel={{ index .Tags "channel" }}&panelId='+string(grafana_panel)+'&fullscreen&tz=UTC0300'
)
.id('{{ index .Tags "name" }} / {{ index .Tags "channel" }}')
.levelTag('level')
.messageField('message')
.durationField('duration')
.topic('slack_graph')
// "today.median" als "value" duplizieren, auch die restlichen Felder des Alarms in Influx schreiben (keep)
trigger
|eval(lambda: "today.median")
.as('value')
.keep()
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('alerts')
.tag('alertName', name)
Und was kommt dabei heraus?
Kapacitor kann hervorragend Monitoring-Alerts mit vielen Gruppierungen durchführen, zusätzliche Berechnungen basierend auf bereits gespeicherten Metriken ausführen, benutzerdefinierte Aktionen durchführen und Skripte (udf) starten.
Die Einstiegshürde ist nicht sehr hoch – probieren Sie es aus, wenn Grafana oder andere Tools Ihren Bedürfnissen nicht voll und ganz gerecht werden.
Quelle: habr.com
