Trucchi per elaborare le metriche in Kapacitor

Probabilmente, oggi non si pone più la questione del perché sia necessario raccogliere le metriche dei servizi. Il passo successivo logico è impostare l'alerting sulle metriche raccolte, che avviserà di qualsiasi deviazione nei dati nei canali che preferite (email, Slack, Telegram). Nel servizio di prenotazione online degli hotel Ostrovok.ru tutte le metriche dei nostri servizi confluiscono in InfluxDB e vengono visualizzate in Grafana, dove è anche impostato un alerting di base. Per compiti del tipo "è necessario calcolare qualcosa e confrontarlo con questo", utilizziamo Kapacitor.

Trucchi per elaborare le metriche in Kapacitor
Kapacitor fa parte del stack TICK, che è in grado di elaborare le metriche da InfluxDB. Può unire più misurazioni (join), calcolare qualcosa di utile dai dati ottenuti, registrare il risultato nuovamente in InfluxDB e inviare un avviso in Slack/Telegram/email.

L'intero stack ha un'interfaccia elegante e dettagliata la documentazione, ma ci saranno sempre cose utili che non sono esplicitamente indicate nei manuali. In questo articolo ho deciso di raccogliere una serie di consigli utili e non ovvi (la sintassi principale di TICKscript è descritta qui) e mostrare come possano essere applicati, usando un esempio per risolvere uno dei nostri problemi.

Andiamo!

float & int, errori di calcolo

Un problema assolutamente standard, che si risolve con un cast:

var alert_float = 5.0
var alert_int = 10
data|eval(lambda: float("value") > alert_float OR float("value") < float("alert_int"))

Utilizzo 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)

Per default il join scarterà i punti dove non ci sono dati (inner).
Con 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 una sfumatura. Se in uno degli esempi sopra una delle serie (res1 o res2) è vuota, la serie finale (data) sarà anch'essa vuota. Ci sono diversi ticket su GitHub riguardo a questo tema (1633, 1871, 6967) – stiamo aspettando le correzioni e ne soffriamo un po'.

Utilizzo di condizioni nei calcoli (if in lambda)

|eval(lambda: if("value" > 0, true, false)

Ultimi cinque minuti dal pipeline per il periodo

Ad esempio, è necessario confrontare i valori degli ultimi cinque minuti con quelli della settimana precedente. Si possono prendere due set di dati con due batch separati oppure 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 può essere l'uso del nodo BarrierNode, che interrompe i dati prima del tempo specificato:

|barrier()
        .period(5m)

Esempi di utilizzo dei template in Go nel messaggio

I template seguono il formato del pacchetto text.template, di seguito alcune comuni piccole attività.

if-else

Mettiamo ordine, senza allertare le persone con testi superflui:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}Va tutto bene ora{{ else }}Capo, è tutto rotto{{end}}'
    )

Due cifre dopo la virgola nel messaggio

Miglioriamo la leggibilità del messaggio:

|alert()
    ...
    .message(
        'il valore attuale è {{ index .Fields "value" | printf "%0.2f" }}'
    )

Espansione delle variabili nel messaggio

Inviamo più informazioni nel messaggio per rispondere alla domanda «Perché sta urlando?»

var warnAlert = 10
  |alert()
    ...
    .message(
       'Oggi il valore è inferiore a '+string(warnAlert)+'%'
    )

Identificatore univoco dell'allerta

Una cosa utile quando ci sono più di un gruppo di dati, altrimenti verrà generato solo un avviso:

|alert()
      ...
      .id('{{ index .Tags "myname" }}\/{{ index .Tags "myfield" }}')

Handler personalizzati

Nell'ampia lista di handler c'è exec, che consente di eseguire il proprio script con i parametri forniti (stdin) – pura creatività!

Uno dei nostri script personalizzati è un piccolo script Python per inviare notifiche su Slack.
All'inizio volevamo inviare un'immagine da Grafana, protetta da autenticazione, nel messaggio. Poi – scrivere OK nel thread del precedente allerta dello stesso gruppo, e non come messaggio separato. Ancora un po' più tardi – aggiungere nel messaggio l'errore più comune negli ultimi X minuti.

Un argomento a parte è il collegamento con altri servizi e eventuali azioni avviate dall'allerta (solo se il tuo monitoraggio funziona abbastanza bene).
Esempio di descrizione dell'handler, dove slack_handler.py è il nostro script fatto in casa:

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 il debug?

Opzione con output nel log

|log()
      .level("error")
      .prefix("qualcosa")

Guarda (cli): kapacitor -url host-o-ip:9092 logs lvl=error

Opzione con httpOut

Mostra i dati nell'attuale pipeline:

|httpOut('qualcosa')

Guarda (get): host-o-ip:9092\/kapacitor\/v1\/tasks\/task_name\/qualcosa

Schema di esecuzione

  • Ogni attività restituisce un albero di esecuzione con numeri utili nel formato graphviz.
  • Prendiamo un blocco dot.
  • Inseriamolo nel visualizzatore, ci godiamo il risultato.

Dove possiamo ancora ottenere un timestamp in influxdb durante la registrazione inversa

timestamp in influxdb alla registrazione inversa

Ad esempio, stiamo impostando un avviso sulla somma delle richieste all'ora (groupBy(1h)) e vogliamo registrare l'avviso avvenuto in influxdb (per mostrare in modo chiaro la presenza di un problema nel grafico di grafana).

influxDBOut() registrerà nel timestamp il valore di time dall'avviso, quindi il punto nel grafico sarà registrato prima/dopo che è arrivato l'avviso.

Quando è necessaria precisione: affrontiamo questo problema chiamando un handler personalizzato che registrerà i dati in influxdb con il timestamp attuale.

docker, build e deploy

All'avvio, kapacitor può caricare task, template e handler dalla directory specificata nella configurazione, nel blocco [load].

Per creare correttamente un task sono necessari i seguenti elementi:

  1. Nome del file – si espande in id/nome dello script
  2. Tipo – stream/batch
  3. dbrp – keyword per indicare in quale database + policy funziona lo script (dbrp «supplier».«autogen»)

Se in un batch-task non c'è una riga con dbrp, l'intero servizio si rifiuterà di avviarsi e lo scriverà onestamente nel log.

In chronograf, al contrario, questa riga non dovrebbe esserci, attraverso l'interfaccia non viene accettata e restituisce un errore.

Hack durante la build del contenitore: il Dockerfile esce con -1 se ci sono righe con \/\/.+dbrp, il che permetterà di capire subito la causa del fallimento durante la compilazione della build.

join uno a molti

Esempio di task: dobbiamo prendere il 95° percentile del tempo di funzionamento del servizio durante la settimana, confrontando ogni minuto degli ultimi 10 con questo valore.

Non è possibile fare join uno a molti, last/mean/median su un insieme di punti trasforma il nodo in stream, restituisce un errore «cannot add child mismatched edges: batch -> stream».

Il risultato del batch, come variabile in un'espressione lambda, non viene nemmeno passato.

C'è un'opzione 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 più connessioni, chiamiamole canali. Questi canali sono circa 300, ognuno di essi può cadere. Monitoreremo il tasso di errori (requests e errors) su tutte le metriche registrate.

Perché non grafana?

Gli avvisi sugli errori impostati in grafana hanno alcuni svantaggi. Alcuni sono 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 cattivi:

Trucchi per elaborare le metriche in Kapacitor

E meno cattivi quando si guarda con richieste riuscite:

Trucchi per elaborare le metriche in Kapacitor

Va bene, possiamo calcolare preliminarmente il tasso nel servizio prima di Grafana, e in alcuni casi questo andrà bene. Ma non nel nostro, poiché per ogni canale si considera un rapporto "normale", e gli alert funzionano su valori statici (cerchiamo a occhio, cambiamo se alerta spesso).

Questi sono esempi di "normale" per diversi canali:

Trucchi per elaborare le metriche in Kapacitor

Trucchi per elaborare le metriche in Kapacitor

Trascuriamo il punto precedente e supponiamo che per tutti i fornitori il quadro "normale" sia simile. Ora va tutto bene, possiamo gestire gli alert in Grafana?
Possiamo, ma non lo vogliamo molto, perché dobbiamo scegliere una delle opzioni:
a) creare numerosi grafici per ogni canale separatamente (e accompagnarli con difficoltà)
b) mantenere un solo grafico con tutti i canali (e perdersi tra linee colorate e alert impostati)

Trucchi per elaborare le metriche in Kapacitor

Cosa abbiamo fatto?

Ancora una volta, nella documentazione c'è un buon esempio di partenza (Calcolo dei tassi attraverso serie unite), puoi dare un'occhiata o usarlo come base per compiti simili.

Cosa abbiamo fatto alla fine:

  • unione di due serie in pochi ore, raggruppamento per canali;
  • compiliamo le serie per gruppi, se non ci sono dati;
  • confrontiamo la mediana degli ultimi 10 minuti con i dati precedenti;
  • grida se troviamo qualcosa;
  • registriamo i tassi calcolati e gli alert verificatisi in InfluxDB;
  • inviamo un messaggio utile su Slack.

A mio avviso, siamo riusciti a ottenere il massimo di quanto speravamo in uscita (e anche qualcosa di più con gestori personalizzati).

Puoi vedere su github.com un esempio di codice e uno schema minimo (graphviz) dello 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 nulli con zeri se non ci sono stati
 |default()
 .field('err.value', 0.0)
 .field('req.value', 0.0)
 // if 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')

// registriamo i valori calcolati in influx
rates
 |influxDBOut()
 .quiet()
 .create()
 .database('kapacitor')
 .retentionPolicy('autogen')
 .measurement('rates')

// scegliamo 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" }}Adesso 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" viene duplicato come "value", scriviamo anche negli altri campi dell'allerta in influx (keep)
trigger
 |eval(lambda: "today.median")
 .as('value')
 .keep()
 |influxDBOut()
 .quiet()
 .create()
 .database('kapacitor')
 .retentionPolicy('autogen')
 .measurement('alerts')
 .tag('alertName', name)

E qual è il risultato?

Kapacitor è ottimo per eseguire monitoraggio e alerting con molte raggruppamenti, eseguire calcoli aggiuntivi su metriche già registrate, eseguire azioni personalizzate e attivare script (udf).

La barriera d'ingresso non è molto alta: provalo, se Grafana o altri strumenti non soddisfano completamente le tue esigenze.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster