Oficina fundamental 1 — PromQL: uma métrica, um rótulo, uma função

Prática guiada15 min
Duração
20 min
Módulo
1/7
Pré-requisitos
o labo está rodando há pelo menos dois minutos (etat exibe 8/8 cibles up), a aba Query do Prometheus aberta em http://localhost:9090
Você vai construir
dez queries PromQL digitadas à mão, da mais curta (api_info) à primeira agregação de verdade (sum by (code) (rate(…[1m]))), mudando apenas uma coisa de cada vez
Entregável
a saída da etapa 10 tal como exibida na aba Table, duas linhas, com uma frase dizendo o que cada uma mede

Como ler esta página. Dez etapas, uma query de cada vez. Para cada uma: a query a digitar, a resposta exata do labo (aba Table), e o que observar nela. Digite você mesmo cada query (sem copiar e colar): é escrevendo as chaves, as aspas e os colchetes que a gramática entra na cabeça. Os números serão diferentes na sua máquina; os formatos (número de linhas, labels, ordem de grandeza) devem ser os mesmos. Os blocos "Para entender bem" são opcionais. Se o labo não estiver iniciado, volte à prática orientada: a seção En bref dá os comandos, incluindo o kit (https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr). Nada é criado nem modificado nesta oficina: o PromQL só faz leitura.

Objetivo

A prática orientada fez você digitar doze queries já escritas. Você viu os resultados, mas se tirarem a folha de você, você sabe escrever sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) sem errar um parêntese? Aqui, você recomeça pela query mais curta possível, um nome de métrica, e adiciona apenas uma peça a cada etapa: um label, um operador, uma função, uma janela de tempo, um agrupamento. Duas etapas são armadilhas propositais: você vai provocar as duas mensagens de erro que todo iniciante encontra, para reconhecê-las na próxima vez. No final, você sabe o que é uma métrica, um label e uma função porque você mesmo montou as três peças.

O vocabulário em uma imagem

O Prometheus é um caderno de medições. A cada 15 segundos, ele passa por cada target, lê sua página /metrics e anota cada valor com a hora. Uma métrica é o nome de uma coluna do caderno (up, http_requetes_total). Um label é uma etiqueta colada na linha para dizer do que se trata (job="api", code="200"); um mesmo nome de métrica com etiquetas diferentes são séries diferentes. Uma função é uma operação sobre o que foi lido: contar as linhas, calcular uma inclinação, somar.

PromQLBase SQL clássicaNesta oficina
métricatabelaup, api_info, http_requetes_total
sérielinha da tabelaup{instance="api:8000", job="api", service="api"}
labelcolunajob, instance, route, code
seletor {job="api"}WHERE job = 'api'etapa 3
=~WHERE job LIKE 'a%' (em expressão regular)etapa 5
count(…), sum(…)COUNT(*), SUM(…)etapas 6 e 10
by (code)GROUP BY codeetapa 10
janela [1m]"as linhas do último minuto"etapa 7
rate(…[1m])sem equivalente simples: uma inclinação por segundoetapa 8
vetor instantâneoum valor por série, agorao que up retorna
vetor de intervalovários valores com data por sérieo que up[1m] retorna

Onde digitar, e como ler uma resposta

Abra http://localhost:9090. Você está na página Query. O campo de entrada aceita uma query; Execute (ou Enter) a envia. O resultado aparece abaixo do campo, na aba Table. Fique em Table durante toda a oficina: é ali que você vê os labels escritos claramente. A aba Graph desenha a mesma coisa no tempo; a aba Explain decompõe a query.

Uma linha de resultado sempre tem a mesma forma: o nome da métrica, depois entre chaves os labels ordenados alfabeticamente, depois o valor à direita:

text
up{instance="api:8000", job="api", service="api"}    1

Quando a query fundiu o nome (uma função, uma agregação), as chaves permanecem, às vezes vazias: {} 8. Sob as abas, Result series: N diz quantas linhas você tem. Um resultado vazio se escreve Empty query result; uma query mal escrita exibe uma caixa vermelha Error executing query seguida da mensagem.

Etapa 1 — Ler uma métrica

promql
api_info

O que a query pede: o último valor da métrica api_info, para todas as suas séries.

text
api_info{instance="api:8000", job="api", service="api", version="1.0.0"}    1

A observar: uma única linha, Result series: 1. O valor é 1 e nunca vai mudar: api_info é uma métrica de informação, tudo o que ela tem a dizer está no seu label version="1.0.0". Três outros labels que a API não escreveu: instance, job e service foram adicionados pelo Prometheus no momento da leitura. Na página http://localhost:8000/metrics, a mesma linha se escreve api_info{version="1.0.0"} 1.0.

Para entender bem
  • Por que começar por api_info e não por up? Porque ela tem uma série. Você vê a forma completa de uma linha de resultado (nome, labels, valor) sem se distrair com sete outras linhas.
  • Um nome de métrica contém letras, números, _ e :. Sem hífen, sem espaço, sem ponto. api-info seria lido como api menos info.
  • O valor é sempre um número flutuante. 1 aqui; o Prometheus não armazena texto, é por isso que a versão está em um label.

Etapa 2 — Ler uma métrica com várias séries

promql
up

O que a query pede: o último valor de up, para todas as suas séries.

text
up{instance="localhost:9090", job="prometheus"}    1
up{instance="alloy:12345", job="alloy"}    1
up{instance="api:8000", job="api", service="api"}    1
up{instance="cadvisor:8080", job="cadvisor"}    1
up{instance="alertmanager:9093", job="alertmanager"}    1
up{instance="loki:3100", job="loki"}    1
up{instance="node-exporter:9100", job="node-exporter"}    1
up{instance="grafana:3000", job="grafana"}    1

A observar: Result series: 8, o mesmo nome em cada linha, e o que muda de uma linha para outra: os valores dos labels job e instance. Isso é uma série: um nome mais um conjunto de labels. Oito conjuntos diferentes, oito séries. Observe que apenas a terceira linha traz service="api": esse label foi adicionado manualmente em prometheus.yml, apenas para o job api. up não existe em nenhuma página /metrics: o Prometheus a fabrica ele mesmo, 1 se a leitura teve sucesso, 0 caso contrário.

Etapa 3 — Escolher uma série com um label

promql
up{job="api"}

O que a query pede: as séries de up cujo label job vale exatamente api.

text
up{instance="api:8000", job="api", service="api"}    1

A observar: uma única linha, a terceira da etapa 2. As chaves depois do nome são um filtro: chamamos isso de seletor. job é o nome do label, "api" seu valor, entre aspas duplas, = a igualdade exata. É o WHERE job = 'api' do SQL, palavra por palavra. Você pode colocar várias condições separadas por vírgula; todas devem ser verdadeiras.

Etapa 4 — A armadilha: sem aspas, depois com a caixa errada

Duas queries erradas, de propósito. Primeiro, esqueça as aspas:

promql
up{job=api}
text
Error executing query
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected string

A observar: parse error, o Prometheus nem tentou procurar: a query está mal escrita. 1:8 é a posição (linha 1, caractere 8, logo depois de up{job=). expected string: ele esperava uma string entre aspas. Um valor de label é sempre uma string, mesmo quando parece um número: {code=500} dá a mesma família de erro, {code="500"} é a forma correta.

Depois, coloque as aspas mas mude a caixa:

promql
up{job="API"}
text
Empty query result

A observar: nenhum erro, nenhuma linha. Essa é a armadilha mais traiçoeira: a query está correta, ela simplesmente pede uma série que não existe. Os valores dos labels são sensíveis à caixa e à ortografia ("api " com um espaço também não funciona). Quando você obtém Empty query result sem motivo, digite novamente a etapa 2 e releia os valores exatos.

Para entender bem: ler uma mensagem de erro PromQL

Uma mensagem de erro do Prometheus tem três partes: parse error (a query está mal formada) ou bad_data (a query está bem formada, mas impossível de executar), uma posição linha:coluna, e uma frase que diz o que ele esperava. Vá sempre à posição indicada: o erro está ali ou logo antes. As três mensagens desta oficina cobrem a grande maioria dos casos: expected string (aspas), expected "(" (parênteses em torno de by), expected type range vector (colchetes, etapa 9).

Etapa 5 — Escolher várias séries com um padrão

promql
up{job=~"a.*"}

O que a query pede: as séries de up cujo label job corresponde à expressão regular a.*: um a seguido de qualquer coisa.

text
up{instance="alloy:12345", job="alloy"}    1
up{instance="api:8000", job="api", service="api"}    1
up{instance="alertmanager:9093", job="alertmanager"}    1

A observar: três linhas, os três jobs que começam com a. Uma única novidade em relação à etapa 3: =~ no lugar de =. A expressão regular deve corresponder ao valor inteiro: "a" sozinho não retornaria nada, é preciso "a.*". Os quatro operadores de seleção: = (igual), != (diferente: up{job!="api"} retorna os outros sete), =~ (corresponde), !~ (não corresponde). É =~ que a prática orientada usava em {code=~"5.."} para capturar todos os 5xx.

Etapa 6 — Aplicar uma função

promql
count(up)

O que a query pede: o número de séries que up retorna.

text
{}    8

A observar: uma única linha, e o nome desapareceu: {} vazio, depois 8. Essa é a primeira função da oficina, e o resultado não é mais up, é um número calculado a partir de up. count é uma agregação: ela pega várias séries e faz uma só. Suas primas: sum (a soma dos valores: sum(up) também dá 8 enquanto tudo está em 1, e 7 assim que um target cai), min, max, avg. O painel de controle do kit e o comando etat contam os targets exatamente assim.

Etapa 7 — Pedir uma janela de tempo

promql
http_requetes_total{route="/cours", code="200"}[1m]

O que a query pede: todos os valores dessa série medidos durante o último minuto, não apenas o último.

text
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}
    2439 @1789505608.199
    2499 @1789505623.199
    2550 @1789505638.196
    2609 @1789505653.197

A observar: uma série, mas quatro valores, cada um seguido de @ e uma data em segundos. Quinze segundos de diferença entre dois: é o scrape_interval. O contador sobe de 2439 para 2609: 170 requisições 200 em /cours em 45 segundos. Uma única novidade: os colchetes [1m] depois do seletor. Eles transformam um vetor instantâneo (um valor por série) em vetor de intervalo (uma lista de valores com data por série). Clique na aba Graph: ela recusa essa query (Error executing query depois invalid expression type "range vector" for range query, must be Scalar or instant Vector). Não se desenha um intervalo bruto, se dá ele a uma função. É a etapa 8. Volte para Table.

Para entender bem: por que quatro valores e não cinco?

Um minuto contém quatro intervalos de 15 segundos, então quatro ou cinco medições dependendo do momento em que você executa a query em relação ao ciclo de scrape. Se você digitar a query várias vezes, às vezes verá cinco linhas. As datas @1789505608.199 são segundos desde 1º de janeiro de 1970 (o horário Unix); a aba Graph as converte em horas legíveis.

Etapa 8 — Aplicar uma função à janela

promql
rate(http_requetes_total{route="/cours", code="200"}[1m])

O que a query pede: a velocidade com que esse contador aumentou, em unidades por segundo, calculada na janela do último minuto.

text
{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}    3.7779456864749545

A observar: novamente um único valor, e o nome http_requetes_total desapareceu das chaves: não é mais um contador, é uma velocidade. 3,78 requisições por segundo. Verifique com a etapa 7: 170 requisições em 45 segundos dá 3,78. Uma única novidade: a função rate(), que pega um vetor de intervalo e retorna um vetor instantâneo. É a função mais importante do PromQL: um contador bruto nunca se lê, sua inclinação sim.

Etapa 9 — A armadilha: rate sem janela

promql
rate(http_requetes_total{route="/cours", code="200"})
text
Error executing query
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vector

A observar: expected type range vector … got instant vector. Você deu ao rate um vetor instantâneo (um valor), ele queria um intervalo (vários valores com data): sem dois pontos, sem inclinação a calcular. O gesto correto é a etapa 8, com [1m]. Você vai ler essa mensagem com frequência; ela sempre quer dizer "está faltando […]".

Uma variante que não dá erro mas não retorna nada:

promql
rate(http_requetes_total{route="/cours", code="200"}[10s])
text
Empty query result

A observar: uma janela de 10 segundos contém no máximo uma medição (elas estão espaçadas de 15 s), e rate precisa de pelo menos duas. Regra prática: a janela deve valer pelo menos duas vezes o scrape_interval, então [30s] no mínimo aqui; [1m] ou [5m] na vida real.

Etapa 10 — Agrupar

promql
sum by (code) (rate(http_requetes_total{route="/cours"}[1m]))

O que a query pede: a velocidade de todas as séries de /cours (todos os códigos), somada mantendo apenas o label code.

text
{code="200"}    3.7779456864749545
{code="500"}    0

A observar: duas linhas, e só sobra um label nas chaves: code. Todos os outros (instance, job, methode, route, service) se fundiram na soma. Uma única novidade: sum by (code) (…), a agregação da etapa 6 com uma cláusula by. Os parênteses em torno de code são obrigatórios (sum by code (…)parse error: … expected "("). A linha {code="500"} 0 merece atenção: ela vale zero porque nenhum 500 caiu em /cours durante o último minuto (a API produz cerca de um a cada doze segundos, todas as rotas juntas). Zero não é ausência: a série existe, ela só tem inclinação nula. Se você executar .\labo.ps1 casser erreurs (ou ./labo.sh casser erreurs) e digitar essa query novamente um minuto depois, a segunda linha sobe; reparer a faz descer novamente.

Esse resultado é seu entregável: as duas linhas, e uma frase para cada uma ("/cours serve 3,78 respostas 200 por segundo"; "nenhuma resposta 500 em /cours no último minuto").

Verificação final

Refaça as dez queries de memória, na ordem, e marque:

  • api_info retorna 1 série, valor 1, com version="1.0.0" nos labels.
  • up retorna 8 séries, todas em 1 (senão, um target caiu: etat vai dizer qual).
  • up{job="api"} retorna 1 série.
  • up{job=api} retorna parse error … expected string; up{job="API"} retorna Empty query result.
  • up{job=~"a.*"} retorna 3 séries: alloy, api, alertmanager.
  • count(up) retorna {} 8.
  • http_requetes_total{route="/cours", code="200"}[1m] retorna 1 série com 4 ou 5 valores com data @….
  • rate(…[1m]) retorna 1 valor, entre 3 e 4 requisições por segundo no labo do curso.
  • rate(…) sem colchetes retorna expected type range vector … got instant vector.
  • sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) retorna 2 séries, {code="200"} e {code="500"}.

Nada para limpar: você não criou nada. etat continua exibindo 8/8 cibles up e o mesmo número de séries em memória, com algumas dezenas de diferença (o Prometheus continua coletando).

Se travar

Afficher les cas où ça coince

Empty query result em api_info ou http_requetes_total. A API ainda não foi lida, ou está parada. etat: se labo-api estiver Exited, reparer; se tudo estiver healthy, espere 15 segundos (um scrape) e execute novamente.

up retorna 7 séries em vez de 8, todas em 1. Um job desapareceu da configuração, não um target que caiu (ele estaria em 0). Vá em Status → Target health e compare com os oito jobs da etapa 2. Se você modificou prometheus/prometheus.yml, restaure o arquivo original (git checkout prometheus/prometheus.yml) e docker compose restart prometheus.

A etapa 7 retorna Empty query result. Os 4 valores da janela precisam existir: logo depois de demarrer, é preciso esperar um minuto. Se a API acabou de reiniciar (reparer), a mesma coisa.

A etapa 8 retorna um valor negativo ou enorme. Impossível em princípio: rate lida com as reinicializações a zero do contador. Se você ver isso, verifique se não digitou rate em um gauge (requetes_en_cours): isso não gera erro, mas não faz sentido nenhum.

A aba Graph fica vazia. Para uma query de intervalo (etapa 7), isso é normal: Graph exibe invalid expression type "range vector". Para as outras, amplie o período (botão -/+ acima do gráfico): logo após a inicialização, só há alguns minutos de dados.

parse error que você não reconhece. Vá até a posição linha:coluna da mensagem. Conte seus parênteses: sum by (code) (rate(x[1m])) tem três pares. Verifique cada aspas: elas vêm em pares, retas ("), nunca tipográficas (" "); um copiar e colar de um processador de texto às vezes as substitui.