Trucos para procesar métricas en Kapacitor

Lo más probable es que hoy nadie se pregunte por qué es necesario recopilar métricas de servicio. El siguiente paso lógico es configurar una alerta para las métricas recopiladas, que notificará sobre cualquier desviación en los datos en los canales que le resulten convenientes (correo, Slack, Telegram). En el servicio de reserva de hotel online ostrovok.ru Todas las métricas de nuestros servicios se vierten en InfluxDB y se muestran en Grafana, y las alertas básicas también se configuran allí. Para tareas como "necesitas calcular algo y compararlo", utilizamos Kapacitor.

Trucos para procesar métricas en Kapacitor
Kapacitor es parte de la pila TICK que puede procesar métricas de InfluxDB. Puede conectar varias mediciones (unirse), calcular algo útil a partir de los datos recibidos, escribir el resultado en InfluxDB, enviar una alerta a Slack/Telegram/mail.

Toda la pila es genial y detallada. documentación, pero siempre habrá cosas útiles que no estén explícitamente indicadas en los manuales. En este artículo, decidí recopilar una serie de consejos útiles y no obvios (la sintaxis básica de TICKscipt se describe aquí) y muestre cómo se pueden aplicar usando un ejemplo de resolución de uno de nuestros problemas.

¡Vamos!

float & int, errores de cálculo

Un problema absolutamente estándar, resuelto mediante castas:

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

Usando predeterminado()

Si una etiqueta/campo no se completa, se producirán errores de cálculo:

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

completar unión (interna versus externa)

De forma predeterminada, la unión descartará los puntos donde no hay datos (internos).
Con fill('null'), se realizará una unión externa, después de lo cual deberá realizar un default() y completar los valores vacíos:

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

Todavía hay un matiz aquí. En el ejemplo anterior, si una de las series (res1 o res2) está vacía, la serie resultante (datos) también estará vacía. Hay varios tickets sobre este tema en Github (1633, 1871, 6967) – estamos esperando soluciones y sufriendo un poco.

Usar condiciones en los cálculos (si está en lambda)

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

Últimos cinco minutos del oleoducto del período.

Por ejemplo, es necesario comparar los valores de los últimos cinco minutos con los de la semana anterior. Puede tomar dos lotes de datos en dos lotes separados o extraer parte de los datos de un período mayor:

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

Una alternativa para los últimos cinco minutos sería utilizar un BarrierNode, que corta los datos antes del tiempo especificado:

|barrier()
        .period(5m)

Ejemplos de uso de plantillas Go en mensajes

Las plantillas corresponden al formato del paquete. plantilla.textoA continuación se muestran algunos acertijos que se encuentran con frecuencia.

si-si no

Ponemos las cosas en orden y no volvemos a provocar a las personas con mensajes de texto:

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

Dos dígitos después del punto decimal en el mensaje

Mejorar la legibilidad del mensaje:

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

Expandir variables en el mensaje

Mostramos más información en el mensaje para responder a la pregunta “¿Por qué grita”?

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

Identificador de alerta único

Esto es necesario cuando hay más de un grupo en los datos; de lo contrario, solo se generará una alerta:

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

Controlador personalizado

La gran lista de controladores incluye exec, que le permite ejecutar su script con los parámetros pasados ​​(stdin): ¡creatividad y nada más!

Una de nuestras costumbres es un pequeño script en Python para enviar notificaciones a Slack.
Al principio, queríamos enviar una imagen de grafana protegida por autorización en un mensaje. Luego, escribe OK en el hilo de la alerta anterior del mismo grupo, y no como un mensaje separado. Un poco más tarde, agregue al mensaje el error más común en los últimos X minutos.

Un tema aparte es la comunicación con otros servicios y cualquier acción iniciada por una alerta (solo si su monitoreo funciona lo suficientemente bien).
Un ejemplo de descripción de controlador, donde slack_handler.py es nuestro script escrito por nosotros mismos:

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

¿Cómo depurar?

Opción con salida de registro

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

Ver (cli): kapacitor -url host o ip:9092 niveles de registros = error

Opción con httpOut

Muestra datos en la canalización actual:

|httpOut('something')

Ver (obtener): host o ip:9092/kapacitor/v1/tareas/nombre_tarea/algo

esquema de ejecución

  • Cada tarea devuelve un árbol de ejecución con números útiles en el formato Graphviz.
  • tomar un bloque punto.
  • Pégalo en el visor disfrutar.

¿Dónde más puedes conseguir un rastrillo?

marca de tiempo en influxdb en reescritura

Por ejemplo, configuramos una alerta para la suma de solicitudes por hora (groupBy(1h)) y queremos registrar la alerta que ocurrió en influxdb (para mostrar bellamente el hecho del problema en el gráfico en grafana).

influxDBOut() escribirá el valor de tiempo desde la alerta en la marca de tiempo; en consecuencia, el punto en el gráfico se escribirá antes o después de la llegada de la alerta.

Cuando se requiere precisión: solucionamos este problema llamando a un controlador personalizado, que escribirá datos en influxdb con la marca de tiempo actual.

ventana acoplable, construcción e implementación

Al inicio, kapacitor puede cargar tareas, plantillas y controladores desde el directorio especificado en la configuración en el bloque [cargar].

Para crear correctamente una tarea, necesita lo siguiente:

  1. Nombre de archivo: ampliado a ID/nombre del script
  2. Tipo: flujo/lote
  3. dbrp: palabra clave para indicar en qué base de datos + política se ejecuta el script (dbrp “proveedor”. “autogen”)

Si alguna tarea por lotes no contiene una línea con dbrp, todo el servicio se negará a iniciarse y honestamente lo escribirá en el registro.

En cronograf por el contrario esta línea no debería existir, no es aceptada a través de la interfaz y genera un error.

Hack al construir un contenedor: Dockerfile sale con -1 si hay líneas con //.+dbrp, lo que le permitirá comprender de inmediato el motivo del error al ensamblar la compilación.

unir uno a muchos

Tarea de ejemplo: debe tomar el percentil 95 del tiempo de funcionamiento del servicio durante una semana, comparar cada minuto de los últimos 10 con este valor.

No puede realizar una unión de uno a muchos, la última/media/mediana sobre un grupo de puntos convierte el nodo en una secuencia; se devolverá el error "no se pueden agregar bordes secundarios no coincidentes: lote -> secuencia".

El resultado de un lote, como variable en una expresión lambda, tampoco se sustituye.

Existe una opción para guardar los números necesarios del primer lote en un archivo mediante udf y cargar este archivo mediante carga lateral.

¿Qué solucionamos con esto?

Tenemos alrededor de 100 proveedores hoteleros, cada uno de ellos puede tener varias conexiones, llamémoslo canal. Hay aproximadamente 300 de estos canales y cada uno de los canales puede caerse. De todas las métricas registradas, monitorearemos la tasa de error (solicitudes y errores).

¿Por qué no grafaña?

Las alertas de error configuradas en Grafana tienen varias desventajas. Algunas son críticas, otras puedes cerrar los ojos, dependiendo de la situación.

Grafana no sabe calcular entre mediciones + alertas, pero necesitamos una tasa (solicitudes-errores)/solicitudes.

Los errores parecen desagradables:

Trucos para procesar métricas en Kapacitor

Y menos malvado cuando se ve con solicitudes exitosas:

Trucos para procesar métricas en Kapacitor

Bien, podemos precalcular la tarifa en el servicio antes de grafana y, en algunos casos, esto funcionará. Pero no en el nuestro, porque... para cada canal su propia proporción se considera "normal" y las alertas funcionan según valores estáticos (los buscamos con los ojos, los cambiamos si hay alertas frecuentes).

Estos son ejemplos de “normal” para diferentes canales:

Trucos para procesar métricas en Kapacitor

Trucos para procesar métricas en Kapacitor

Ignoramos el punto anterior y asumimos que el panorama “normal” es similar para todos los proveedores. ¿Ahora todo está bien y podemos arreglárnoslas con alertas en grafana?
Podemos, pero realmente no queremos, porque tenemos que elegir una de las opciones:
a) hacer muchos gráficos para cada canal por separado (y acompañarlos dolorosamente)
b) dejar un gráfico con todos los canales (y perderse entre las líneas coloridas y las alertas personalizadas)

Trucos para procesar métricas en Kapacitor

¿Cómo lo hiciste?

Nuevamente, hay un buen ejemplo inicial en la documentación (Calcular tasas en series unidas), se puede echar un vistazo o tomar como base en problemas similares.

Lo que hicimos al final:

  • unir dos series en pocas horas, agrupando por canales;
  • completar la serie por grupo si no hubiera datos;
  • comparar la mediana de los últimos 10 minutos con datos anteriores;
  • gritamos si encontramos algo;
  • escribimos las tarifas calculadas y las alertas que ocurrieron en influxdb;
  • Envía un mensaje útil a Slack.

En mi opinión, logramos lograr todo lo que queríamos al final (e incluso un poco más con controladores personalizados) de la manera más hermosa posible.

Puedes mirar en github.com. ejemplo de código и circuito mínimo (graphviz) el guión resultante.

Un ejemplo del 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)

¿Y cuál es la conclusión?

Kapacitor es excelente para realizar alertas de monitoreo con un montón de agrupaciones, realizar cálculos adicionales basados ​​en métricas ya registradas, realizar acciones personalizadas y ejecutar scripts (udf).

La barrera de entrada no es muy alta: pruébela si grafana u otras herramientas no satisfacen plenamente sus deseos.

Fuente: habr.com

Añadir un comentario