Trucos para procesar métricas en Kapacitor

O máis probable é que hoxe ninguén se pregunte por que é necesario recoller as métricas do servizo. O seguinte paso lóxico é configurar unha alerta para as métricas recollidas, que notificará calquera desvío nos datos nas canles que che convén (correo, Slack, Telegram). No servizo de reserva de hoteis en liña Ostrovok.ru todas as métricas dos nosos servizos vertense en InfluxDB e móstranse en Grafana, e alí tamén se configuran alertas básicas. Para tarefas como "cómpre calcular algo e comparar con el", usamos Kapacitor.

Trucos para procesar métricas en Kapacitor
Kapacitor forma parte da pila TICK que pode procesar métricas de InfluxDB. Pode conectar varias medicións xuntas (unir), calcular algo útil a partir dos datos recibidos, escribir o resultado de novo en InfluxDB, enviar unha alerta a Slack/Telegram/mail.

Toda a pila é xenial e detallada documentación, pero sempre haberá cousas útiles que non estean expresamente indicadas nos manuais. Neste artigo, decidín recoller unha serie de consellos tan útiles e non obvios (descríbese a sintaxe básica de TICKscipt aquí) e mostrar como se poden aplicar utilizando un exemplo de resolución dun dos nosos problemas.

Imos alí!

float e int, erros de cálculo

Un problema absolutamente estándar, resolto a través de castas:

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

Usando default()

Se non se enche unha etiqueta/campo, produciranse erros de cálculo:

|default()
        .tag('status', 'empty')
        .field('value', 0)

completar unión (interior vs exterior)

De forma predeterminada, join descartará os puntos onde non hai datos (interiores).
Con fill('null'), realizarase unha unión externa, despois de que cómpre facer un default() e encher os valores baleiros:

var data = res1
    |join(res2)
        .as('res1', 'res2)
        .fill('null')
    |default()
        .field('res1.value', 0.0)
        .field('res2.value', 100.0)

Aquí aínda hai un matiz. No exemplo anterior, se unha das series (res1 ou res2) está baleira, a serie resultante (datos) tamén estará baleira. Hai varias entradas sobre este tema en Github (1633, 1871, 6967) – estamos esperando arranxos e sufrindo un pouco.

Usando condicións nos cálculos (se está en lambda)

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

Últimos cinco minutos do gasoduto para o período

Por exemplo, cómpre comparar os valores dos últimos cinco minutos coa semana anterior. Podes tomar dous lotes de datos en dous lotes separados ou extraer parte dos datos dun período maior:

 |where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)

Unha alternativa durante os últimos cinco minutos sería usar un BarrierNode, que corta os datos antes do tempo especificado:

|barrier()
        .period(5m)

Exemplos de uso de modelos de Go na mensaxe

Os modelos corresponden ao formato do paquete texto.modeloAbaixo amósanse algúns crebacabezas que se atopan con frecuencia.

se-senón

Ordenamos as cousas e non activamos a xente con texto unha vez máis:

|alert()
    ...
    .message(
        '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}'
    )

Dous díxitos despois do punto decimal na mensaxe

Mellorar a lexibilidade da mensaxe:

|alert()
    ...
    .message(
        'now value is {{ index .Fields "value" | printf "%0.2f" }}'
    )

Ampliación de variables na mensaxe

Amosamos máis información na mensaxe para responder á pregunta "Por que está berrando"?

var warnAlert = 10
  |alert()
    ...
    .message(
       'Today value less then '+string(warnAlert)+'%'
    )

Identificador único de alerta

Isto é necesario cando hai máis dun grupo nos datos, se non, só se xerará unha alerta:

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

Manexadores personalizados

A gran lista de controladores inclúe exec, que che permite executar o teu script cos parámetros pasados ​​(stdin): creatividade e nada máis!

Un dos nosos costumes é un pequeno script de Python para enviar notificacións a Slack.
Nun primeiro momento, queriamos enviar unha imaxe de grafana protexida por autorización nunha mensaxe. Despois, escribe OK no fío da alerta anterior do mesmo grupo, e non como unha mensaxe separada. Un pouco máis tarde - engade á mensaxe o erro máis común dos últimos X minutos.

Un tema separado é a comunicación con outros servizos e calquera acción iniciada por unha alerta (só se o teu seguimento funciona o suficientemente ben).
Un exemplo de descrición dun controlador, onde slack_handler.py é o noso script autoescrito:

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"]

Como depurar?

Opción con saída de rexistro

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

Watch (cli): kapacitor -url host-ou-ip:9092 rexistros lvl=erro

Opción con httpOut

Mostra os datos da canalización actual:

|httpOut('something')

Ver (obter): host-ou-ip:9092/kapacitor/v1/tasks/task_name/algo

Esquema de execución

  • Cada tarefa devolve unha árbore de execución con números útiles no formato gráficamente.
  • Toma un bloque punto.
  • Pegalo no visor, gozar.

Onde máis podes conseguir un anciño?

marca de tempo en influxdb durante a escritura

Por exemplo, configuramos unha alerta para a suma de solicitudes por hora (groupBy(1h)) e queremos rexistrar a alerta que se produciu en influxdb (para mostrar ben o feito do problema no gráfico en grafana).

influxDBOut() escribirá o valor de tempo desde a alerta ata a marca de tempo; polo tanto, o punto do gráfico escribirase antes/despois do que chegou a alerta.

Cando se require precisión: solucionamos este problema chamando a un controlador personalizado, que escribirá datos en influxdb coa marca de tempo actual.

docker, compilación e implantación

No inicio, kapacitor pode cargar tarefas, modelos e controladores desde o directorio especificado na configuración no bloque [load].

Para crear correctamente unha tarefa, necesitas as seguintes cousas:

  1. Nome do ficheiro: expandido a ID/nome de script
  2. Tipo: fluxo/lote
  3. dbrp: palabra clave para indicar en que base de datos + política se executa o script (dbrp "proveedor". "autoxen")

Se algunha tarefa por lotes non contén unha liña con dbrp, todo o servizo rexeitará iniciarse e escribirá honestamente sobre iso no rexistro.

En chronograf, pola contra, esta liña non debería existir, non se acepta a través da interface e xera un erro.

Corte ao construír un contedor: Dockerfile sae con -1 se hai liñas con //.+dbrp, o que lle permitirá comprender inmediatamente o motivo do fallo ao montar a compilación.

únete un a moitos

Tarefa de exemplo: cómpre tomar o percentil 95 do tempo de funcionamento do servizo durante unha semana, comparar cada minuto dos últimos 10 con este valor.

Non podes facer unha unión un a varios, a última/media/media sobre un grupo de puntos converte o nodo nun fluxo, devolverase o erro "non se poden engadir bordos non coincidentes fillos: lote -> fluxo".

O resultado dun lote, como variable nunha expresión lambda, tampouco se substitúe.

Hai unha opción para gardar os números necesarios do primeiro lote nun ficheiro mediante udf e cargar este ficheiro mediante sideload.

Que solucionamos con isto?

Temos uns 100 provedores hostaleiros, cada un deles pode ter varias conexións, chamémoslle canle. Hai aproximadamente 300 destas canles, cada unha delas pode caer. De todas as métricas rexistradas, supervisaremos a taxa de erro (solicitudes e erros).

Por que non grafana?

As alertas de erro configuradas en Grafana teñen varias desvantaxes. Algunhas son críticas, outras ás que podes pechar os ollos, dependendo da situación.

Grafana non sabe calcular entre medicións + alerta, pero necesitamos unha taxa (solicitudes-erros)/solicitudes.

Os erros parecen desagradables:

Trucos para procesar métricas en Kapacitor

E menos malo cando se ve con solicitudes exitosas:

Trucos para procesar métricas en Kapacitor

Está ben, podemos precalcular a tarifa no servizo antes da grafana, e nalgúns casos isto funcionará. Pero no noso non, porque... para cada canle a súa propia proporción considérase "normal" e as alertas funcionan segundo valores estáticos (buscámolas cos ollos, cambiámolas se hai alertas frecuentes).

Estes son exemplos de "normal" para diferentes canles:

Trucos para procesar métricas en Kapacitor

Trucos para procesar métricas en Kapacitor

Ignoramos o punto anterior e asumimos que a imaxe "normal" é similar para todos os provedores. Agora todo está ben, e podemos saír con alertas en grafana?
Podemos, pero realmente non queremos, porque temos que escoller unha das opcións:
a) fai moitos gráficos para cada canle por separado (e acompáñaos dolorosamente)
b) deixar un gráfico con todas as canles (e perderse nas liñas coloridas e nas alertas personalizadas)

Trucos para procesar métricas en Kapacitor

Como o fixeches?

De novo, hai un bo exemplo de partida na documentación (Cálculo de taxas en series unidas), pódese ver ou tomar como base en problemas similares.

O que fixemos ao final:

  • xunta dúas series en poucas horas, agrupando por canles;
  • cubrir as series por grupo se non houbera datos;
  • comparar a mediana dos últimos 10 minutos cos datos anteriores;
  • berramos se atopamos algo;
  • escribimos as tarifas calculadas e as alertas que se produciron en influxdb;
  • envía unha mensaxe útil a Slack.

Na miña opinión, conseguimos conseguir todo o que queriamos conseguir ao final (e incluso un pouco máis con manipuladores personalizados) o máis bonito posible.

Podes mirar en github.com exemplo de código и circuíto mínimo (graphviz) o guión resultante.

Un exemplo do código resultante:

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')
    // заполняем значения нулями, если их не было
    |default()
        .field('err.value', 0.0)
        .field('req.value', 0.0)
    // if в lambda: считаем рейт, только если ошибки были
    |eval(lambda: if("err.value" > 0, 100.0 * (float("req.value") - float("err.value")) / float("req.value"), 100.0))
        .as('rate')

// записываем посчитанные значения в инфлюкс
rates
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('rates')

// выбираем данные за последние 10 минут, считаем медиану
var todayRate = rates
    |where(lambda: duration((unixNano(now()) - unixNano("time")) / 1000, 1u) < todayPeriod)
    |median('rate')
        .as('median')

var prevRate = rates
    |median('rate')
        .as('median')

var joined = todayRate
    |join(prevRate)
        .as('today', 'prev')
    |httpOut('join')

var trigger = joined
    |alert()
        .warn(lambda: ("prev.median" - "today.median") > warnAlert)
        .warnReset(lambda: ("prev.median" - "today.median") < warnReset)
        .flapping(0.25, 0.5)
        .stateChangesOnly()
        // собираем в message ссылку на график дашборда графаны
        .message(
            '{{ .Level }}: {{ index .Tags "channel" }} err/req ratio ({{ index .Tags "supplier" }})
{{ if eq .Level "OK" }}It is ok now{{ else }}
'+string(todayPeriod)+' median is {{ index .Fields "today.median" | printf "%0.2f" }}%, by previous '+string(period)+' is {{ 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=UTC%2B03%3A00'
        )
        .id('{{ index .Tags "name" }}/{{ index .Tags "channel" }}')
        .levelTag('level')
        .messageField('message')
        .durationField('duration')
        .topic('slack_graph')

// "today.median" дублируем как "value", также пишем в инфлюкс остальные филды алерта (keep)
trigger
    |eval(lambda: "today.median")
        .as('value')
        .keep()
    |influxDBOut()
        .quiet()
        .create()
        .database('kapacitor')
        .retentionPolicy('autogen')
        .measurement('alerts')
        .tag('alertName', name)

Cal é a conclusión?

Kapacitor é excelente para realizar alertas de seguimento cunha serie de agrupacións, realizar cálculos adicionais baseados en métricas xa rexistradas, realizar accións personalizadas e executar scripts (udf).

A barreira de entrada non é moi alta: próbao se a grafana ou outras ferramentas non satisfacen totalmente os teus desexos.

Fonte: www.habr.com

Engadir un comentario