Oggi non ci si chiede più perché sia necessario raccogliere le metriche dei servizi. Il passo logico successivo è configurare l'allerta sulle metriche raccolte, che notificherà qualsiasi deviazione dei dati nei canali di tua scelta (email, Slack, Telegram). Nel servizio di prenotazione online degli hotel tutte le metriche dei nostri servizi vengono inviate a InfluxDB e visualizzate in Grafana, dove è anche configurato un allerta di base. Per attività come "devo calcolare qualcosa e confrontarlo con questo", utilizziamo Kapacitor.

Kapacitor è parte della stack TICK, in grado di elaborare le metriche da InfluxDB. Può unire più misurazioni tra loro (join), calcolare cose utili dai dati ottenuti, scrivere il risultato nuovamente in InfluxDB e inviare un allerta in Slack/Telgram/email.
L'intera stack ha una documentazione fantastica e dettagliata , ma ci sono sempre cose utili che non sono esplicitamente indicate nei manuali. In questo articolo, ho deciso di raccogliere una serie di consigli utili e non così ovvi (la sintassi di base di TICKscript è descritta ) e mostrare come possono essere applicati, prendendo come esempio la soluzione di uno dei nostri problemi.
Andiamo!
float & int, errori di calcolo
Un problema assolutamente standard, risolvibile attraverso un cast:
var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))
Uso di default()
Se il tag/campo non è compilato, si verificheranno errori nei calcoli:
|default()
.tag('status', 'empty')
.field('value', 0)
fill in join (inner vs outer)
Di default, join scarta i punti in cui non ci sono dati (inner).
Utilizzando fill(‘null’), verrà eseguito un outer join, dopo il quale è necessario fare default() e riempire i valori vuoti:
var data = res1
|join(res2)
.as('res1', 'res2')
.fill('null')
|default()
.field('res1.value', 0.0)
.field('res2.value', 100.0)
C'è comunque un particolare. Se in questo esempio una delle serie (res1 o res2) è vuota, la serie finale (data) sarà anch'essa vuota. A questo proposito, ci sono alcuni ticket su GitHub (, , ) – attendiamo le correzioni e ne soffriamo leggermente.
Uso di condizioni nei calcoli (if in lambda)
|eval(lambda: if("value" > 0, true, false)
Ultimi cinque minuti dal pipeline per il periodo
Ad esempio, se hai bisogno di confrontare i valori degli ultimi cinque minuti con quelli della settimana precedente. Puoi prendere due pacchetti di dati in due batch separati o estrarre una parte dei dati da un periodo maggiore:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Un'alternativa per gli ultimi cinque minuti potrebbe essere l'uso del nodo BarrierNode, che esclude i dati prima del tempo specificato:
|barrier()
.period(5m)
Esempi di utilizzo dei template Go in message
I template seguono il formato del pacchetto , qui sotto alcuni compiti comuni.
if-else
Facciamo ordine, evitando di allertare le persone con messaggi superflui:
|alert()
...
.message(
'{{ if eq .Level "OK" }}Va bene ora{{ else }}Capo, tutto è rotto{{end}}'
)
Due cifre dopo la virgola nel message
Miglioriamo la leggibilità del messaggio:
|alert()
...
.message(
'il valore attuale è {{ index .Fields "value" | printf "%0.2f" }}'
)
Espansione delle variabili nel message
Fornendo nel messaggio più informazioni per rispondere alla domanda "Perché avvisa?"
var warnAlert = 10
|alert()
...
.message(
'Il valore di oggi è inferiore a '+string(warnAlert)+'%'
)
Identificatore unico dell'allerta
Una cosa necessaria quando ci sono più gruppi nei dati, altrimenti verrà generata solo un'allerta:
|alert()
...
.id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Handler personalizzati
In un lungo elenco di handler c'è exec, che consente di eseguire il proprio script con i parametri forniti (stdin) – solo creatività!
Uno dei nostri personalizzati è un piccolo script Python per inviare notifiche su Slack.
Inizialmente, volevamo inviare un'immagine da Grafana protetta da autorizzazione nel messaggio. Poi – scrivere OK nella discussione del precedente allerta dello stesso gruppo, anziché in un messaggio separato. Ancora un po' più tardi – aggiungere nel messaggio l'errore più comune degli ultimi X minuti.
Un argomento separato è la connessione con altri servizi e qualsiasi azione avviata dall'allerta (solo se il tuo monitoraggio funziona a dovere).
Esempio di descrizione dell'handler, dove slack_handler.py è il nostro script custom:
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"]
Come fare debug?
Opzione con output nel log
|log()
.level("error")
.prefix("qualcosa")
Guardare (cli): kapacitor -url :9092 logs lvl=error
Opzione con httpOut
Mostra i dati nel pipeline attuale:
|httpOut('qualcosa')
Guarda (get): :9092/kapacitor/v1/tasks/task_name/qualcosa
Schema di esecuzione
- Ogni task restituisce un albero di esecuzione con numeri utili in formato .
- Prendiamo il blocco .
- Incolliamolo nel viewer, .
Dove è possibile ottenere timestamp in influxdb durante la scrittura inversa
timestamp in influxdb durante la scrittura inversa
Ad esempio, stiamo configurando un alert sulla somma delle richieste per un'ora (groupBy(1h)) e vogliamo registrare l'alert avvenuto in influxdb (per mostrare visivamente il fatto che esiste un problema nel grafico in grafana).
influxDBOut() registrerà nel timestamp il valore di time dall'alert, di conseguenza, il punto sul grafico sarà registrato prima/dopo che è arrivato l'alert.
Quando è necessaria precisione: affrontiamo questo problema tramite la chiamata a un handler personalizzato, che registrerà i dati in influxdb con il timestamp corrente.
docker, build e deploy
All'avvio, kapacitor può caricare task, template e handler dalla directory specificata nella configurazione, nel blocco [load].
Per una corretta creazione del task sono necessarie le seguenti cose:
- Nome del file – si trasforma in id/nome dello script
- Tipo – stream/batch
- dbrp – keyword per indicare in quale database + policy il script sta operando (dbrp «supplier».«autogen»)
Se in un qualche task batch non ci sarà la riga con dbrp, il servizio non si avvierà e lo indicherà onestamente nel log.
In chronograf, al contrario, questa riga non deve essere presente, tramite l'interfaccia non viene accettata e genera errore.
Hack durante la costruzione del container: il Dockerfile esce con -1 se ci sono righe con //.+dbrp, il che permetterà di capire immediatamente la causa del fallimento durante la build.
join uno a molti
Esempio di task: dobbiamo prendere il 95° percentile del tempo di funzionamento del servizio per settimana, confrontando ogni minuto degli ultimi 10 con questo valore.
Non è possibile fare join uno a molti, last/mean/median su un gruppo di punti trasforma il nodo in stream, restituirà errore «cannot add child mismatched edges: batch -> stream».
Il risultato del batch, come variabile nell'espressione lambda, non viene nemmeno sostituito.
C'è un modo per salvare i numeri necessari dal primo batch in un file tramite udf e caricare questo file tramite sideload.
Cosa stavamo risolvendo con questo?
Abbiamo circa 100 fornitori di hotel, ciascuno di essi può avere diverse connessioni, chiamiamole canali. Ci sono circa 300 di questi canali, ognuno dei quali può cadere. Di tutte le metriche registrate, monitoreremo il tasso di errori (requests e errors).
Perché non grafana?
Gli alert sugli errori impostati in grafana presentano diversi svantaggi. Alcuni critici, su altri si può chiudere un occhio, a seconda della situazione.
Grafana non è in grado di eseguire calcoli tra le misurazioni + alerting, e noi abbiamo bisogno del tasso (requests-errors)/requests.
Gli errori sembrano gravi:

E meno gravi, se guardati insieme alle richieste riuscite:

D'accordo, possiamo calcolare preliminarmente il tasso nel servizio prima di grafana, e in alcuni casi andrà bene. Ma non nel nostro, poiché per ogni canale la sua proporzione è considerata «normale», e gli alert funzionano su valori statici (cerchiamo a occhio, cambiamo se avverte spesso).
Questi sono esempi di «normale» per diversi canali:


Tralasciamo il punto precedente e assumiamo che per tutti i fornitori la situazione «normale» sia simile. Ora va tutto bene, possiamo affidarci agli alert in grafana?
Possiamo, ma non ci piace molto, perché dobbiamo scegliere una delle opzioni:
a) creare molti grafici per ogni canale separatamente (e soffrire per mantenerli)
b) lasciare un grafico con tutti i canali (e perdersi nelle linee colorate e negli alert impostati)

Come abbiamo fatto?
Ancora una volta, nella documentazione c'è un buon esempio di partenza (), possiamo dare un'occhiata o prenderlo come base per compiti simili.
Cosa abbiamo fatto alla fine:
- join di due serie negli ultimi ore, raggruppamento per canali;
- riempiamo le serie per gruppi, se non ci sono dati;
- confrontiamo la mediana degli ultimi 10 minuti con i dati precedenti;
- ci facciamo sentire se scopriamo qualcosa;
- registriamo i tassi calcolati e gli alert avvenuti in influxdb;
- inviamo un messaggio utile in slack.
A mio avviso, siamo riusciti a ottenere il massimo dalla bellezza di ciò che volevamo in uscita (e persino un po' di più con i gestori personalizzati).
Su github.com è possibile vedere e del script ottenuto.
Ecco un esempio del codice risultante:
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')
// riempiamo i valori con zeri, se non ce ne sono
|default()
.field('err.value', 0.0)
.field('req.value', 0.0)
// se in lambda: calcoliamo il tasso, solo se ci sono stati errori
|eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
.as('rate')
// scriviamo i valori calcolati in Influx
rates
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('rates')
// selezioniamo i dati degli ultimi 10 minuti, calcoliamo la mediana
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()
// raccogliamo nel messaggio il link al grafico del dashboard di Grafana
.message(
'{{ .Level }}: {{ index .Tags "channel" }} rapporto err/req ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}Ora va bene{{ else }}
'+string(todayPeriod)+' mediana è {{ index .Fields "today.median" | printf "%0.2f" }}%, rispetto alla precedente '+string(period)+' {{ 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" duplicato come "value", scriviamo anche gli altri campi dell'alert in Influx (keep)
trigger
|eval(lambda: "today.median")
.as('value')
.keep()
|influxDBOut()
.quiet()
.create()
.database('kapacitor')
.retentionPolicy('autogen')
.measurement('alerts')
.tag('alertName', name)
E il risultato qual è?
Kapacitor è eccellente nel monitoraggio e allerta con numerose raggruppamenti, può effettuare calcoli aggiuntivi su metriche già registrate, eseguire azioni personalizzate e avviare script (udf).
La soglia di accesso non è molto alta – prova ad usarlo se Grafana o altri strumenti non soddisfano completamente le tue esigenze.
Fonte: habr.com
