Provavelmente, hoje ninguém pergunta por que eles precisam coletar métricas de serviço. O próximo passo lógico é configurar o alerta para as métricas coletadas, que o notificarão de quaisquer desvios nos dados nos canais convenientes para você (correio, Slack, Telegram). No serviço de reserva on-line dos hotéis
Ostrovok.ru , todas as métricas de nossos serviços são despejadas no InfluxDB e exibidas no Grafana. Também é definido um alerta básico. Para tarefas como "você precisa calcular algo e comparar com ele", usamos o Kapacitor.
O Kapacitor faz parte da pilha TICK que pode lidar com métricas do InfluxDB. Ele pode conectar várias dimensões entre si (ingressar), calcular algo útil a partir dos dados recebidos, escrever o resultado de volta ao InfluxDB, enviar um alerta ao Slack / Telegram / mail.
Toda a pilha possui
documentação detalhada e legal, mas sempre há coisas úteis que não são explicitamente especificadas nos manuais. Neste artigo, decidi coletar várias dicas úteis não óbvias (a sintaxe básica do TICKscipt é descrita
aqui ) e mostrar como elas podem ser aplicadas, usando um exemplo de solução de um dos nossos problemas.
Vamos lá!
float & int, erros de cálculo
Problema absolutamente padrão, é resolvido atravé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 a tag / campo não for preenchida, ocorrerão erros nos cálculos:
|default() .tag('status', 'empty') .field('value', 0)
junção de preenchimento (interno x externo)
Por padrão, a junção eliminará pontos onde não há dados (internos).
Com fill ('null'), a junção externa será executada, após a qual você precisará fazer default () e preencher os valores vazios:
var data = res1 |join(res2) .as('res1', 'res2) .fill('null') |default() .field('res1.value', 0.0) .field('res2.value', 100.0)
Ainda há uma nuance. Se no exemplo acima uma das séries (res1 ou res2) estiver vazia, a série final (dados) também estará vazia. Existem vários tickets sobre esse tópico no github (
1633 ,
1871 ,
6967 ) - estamos aguardando correções e sofrendo um pouco.
Usando condições nos cálculos (se estiver em lambda)
|eval(lambda: if("value" > 0, true, false)
Os últimos cinco minutos do pipeline no período
Por exemplo, você precisa comparar os valores dos últimos cinco minutos com a semana anterior. Você pode pegar dois pacotes de dados com dois lotes separados ou extrair parte dos dados de um período maior:
|where(lambda: duration((unixNano(now()) - unixNano("time"))/1000, 1u) < 5m)
Uma alternativa para os últimos cinco minutos pode ser usar o nó BarrierNode, que corta os dados antes do tempo especificado:
|barrier() .period(5m)
Exemplos de uso de padrões Go'sh na mensagem
Os modelos correspondem ao formato do pacote
text.template , abaixo estão algumas tarefas comuns.
se-mais
Colocamos as coisas em ordem, não acionamos as pessoas com o texto mais uma vez:
|alert() ... .message( '{{ if eq .Level "OK" }}It is ok now{{ else }}Chief, everything is broken{{end}}' )
Duas casas decimais na mensagem
Melhorando a legibilidade da mensagem:
|alert() ... .message( 'now value is {{ index .Fields "value" | printf "%0.2f" }}' )
Expandindo variáveis na mensagem
Exibimos mais informações na mensagem para responder à pergunta "Por que está gritando?"
var warnAlert = 10 |alert() ... .message( 'Today value less then '+string(warnAlert)+'%' )
Identificador de alerta exclusivo
A coisa certa quando há mais de um grupo nos dados; caso contrário, apenas um alerta será gerado:
|alert() ... .id('{{ index .Tags "myname" }}/{{ index .Tags "myfield" }}')
Manipulador personalizado
A grande lista de manipuladores possui exec, que permite executar seu script com os parâmetros passados (stdin) - criatividade e muito mais!
Uma de nossas ferramentas personalizadas é um pequeno script python para enviar notificações para folga.
Primeiro, queríamos enviar uma foto de uma grafana protegida por autorização na mensagem. Depois - escreva OK no encadeamento para o alerta anterior do mesmo grupo, e não como uma mensagem separada. Um pouco mais tarde - para anexar o erro mais comum nos últimos X minutos à mensagem.
Um tópico separado é a comunicação com outros serviços e quaisquer ações iniciadas por um alerta (somente se o seu monitoramento funcionar bem o suficiente).
Um exemplo de descrição de um manipulador, em que slack_handler.py é nosso script auto-escrito:
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 estrear?
Opção de saída de log
|log() .level("error") .prefix("something")
Assista (cli): kapacitor -url
host-or-ip : 9092 logs lvl = error
Variante com httpOut
Mostra dados no pipeline atual:
|httpOut('something')
Assista (obtenha):
host-ou-ip : 9092 / kapacitor / v1 / tasks / task_name / something
Esquema de execução
- Cada tarefa retorna uma árvore de execução com números úteis no formato graphviz .
- Pegue o bloco de pontos .
- Nós inserimos no visualizador, aproveite .
Onde mais posso obter um ancinho
carimbo de data / hora do influxdb na gravação
Por exemplo, configuramos um alerta para a soma de solicitações por hora (groupBy (1h)) e queremos registrar o alerta ocorrido no influxdb (para mostrar lindamente o fato de um problema no gráfico em grafana).
influxDBOut () gravará o valor de tempo do alerta no registro de data e hora, respectivamente, o ponto no gráfico será registrado mais cedo / mais tarde que o alerta veio.
Quando a precisão é necessária: contornamos esse problema chamando um manipulador personalizado, que gravará dados no influxdb com o carimbo de data / hora atual.
janela de encaixe, criar e implantar
Na inicialização, o kapacitor pode carregar tarefas, modelos e manipuladores a partir do diretório especificado na configuração no bloco [load].
Para criar corretamente uma tarefa, são necessários os seguintes itens:
- Nome do arquivo - expande para id / script name
- Tipo - fluxo / lote
- dbrp - palavra-chave para indicar em qual banco de dados + política o script funciona (dbrp "supplier". "autogen")
Se não houver linha com o dbrp em alguma tarefa em lote, todo o serviço se recusará a iniciar e escrever honestamente sobre isso no log.
No cronógrafo, pelo contrário, essa linha não deve ser, não é aceita pela interface e gera um erro.
Hack ao criar o contêiner: o Dockerfile sai com -1 se houver linhas com //.+dbrp, que entenderão imediatamente o motivo do arquivo ao criar a compilação.
junte um a muitos
Exemplo de tarefa: você precisa ter o percentil 95 do tempo de operação do serviço por semana, comparar cada minuto dos últimos 10 com esse valor.
Você não pode associar um a muitos, a última / média / mediana por grupo de pontos transforma o nó em fluxo; o erro "não é possível adicionar bordas incompatíveis filho: lote -> fluxo" retornará.
O resultado do lote, como variável em uma expressão lambda, também não é substituído.
Existe uma opção para salvar os números necessários do primeiro lote em um arquivo via udf e carregá-lo através do carregamento lateral.
O que nós decidimos?
Temos cerca de 100 fornecedores de hotéis, cada um deles pode ter várias conexões, vamos chamá-lo de canal. Existem aproximadamente 300 desses canais; cada um deles pode cair. De todas as métricas registradas, monitoraremos a taxa de erros (solicitações e erros).
Por que não grafana?
Os alertas de erro configurados no grafan têm várias desvantagens. Alguns críticos, outros podem fechar os olhos, dependendo da situação.
O Grafana não sabe calcular entre dimensões + alerta, mas precisamos de uma taxa (pedidos-erros) / pedidos.
Os erros parecem cruéis:

E menos cruel quando visualizado com solicitações bem-sucedidas:

Ok, podemos pré-calcular a taxa no serviço antes da grafana e, em alguns casos, ele fará. Mas não a nossa, porque para cada canal, sua proporção é considerada "normal" e os alertas funcionam de acordo com valores estáticos (olhamos com os olhos, mudamos, se frequentemente alertas).
Estes são exemplos de "normal" para diferentes canais:


Negligenciamos o parágrafo anterior e assumimos que todos os fornecedores têm uma imagem "normal". Agora está tudo bem, e podemos conviver com alertas na grafana?
Podemos, mas realmente não queremos, porque devemos escolher uma das opções:
a) fazer muitos gráficos para cada canal separadamente (e acompanhá-los dolorosamente)
b) deixe um gráfico com todos os canais (e se perca em linhas coloridas e alertas ajustados)

Como você fez isso?
Novamente, a documentação tem um bom exemplo inicial (
Calculando taxas entre séries unidas ), você pode espiar ou tomar como base tarefas similares.
O que você fez como resultado:
- junte dois episódios em poucas horas, agrupando por canal;
- preencher a série por grupos, se não houver dados;
- comparar a mediana dos últimos 10 minutos com os dados anteriores;
- gritamos se encontrarmos algo;
- escreva taxas calculadas e ocorreu alertas no influxdb;
- envie uma mensagem útil para folga.
Na minha opinião, gerenciamos da melhor maneira possível tudo o que gostaríamos de obter na saída (e até um pouco mais com manipuladores personalizados).
No github.com, você pode ver o
código de amostra e o
diagrama mínimo (graphviz) do script resultante.
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)
Qual é a conclusão?
O Kapacitor é muito bom em monitorar alertas com vários grupos, executando cálculos adicionais em métricas já gravadas, executando ações personalizadas e executando scripts (udf).
O limite de entrada não é muito alto - tente se o grafana ou outras ferramentas não satisfizerem totalmente a sua lista de desejos.