Study guide

Observability, see what your system is doing.

Metrics, KPIs and percentiles (p95, p99), Prometheus and Grafana from scratch, traces and logs when metrics are not enough, hands-on labs with Docker, and how to run it securely in production.

O que é observabilidade e por que importaWhat observability is and why it matters

Observabilidade é a capacidade de entender o que acontece dentro de um sistema olhando só para o que ele emite para fora: a telemetria. Pense no painel de um carro: o velocímetro e a luz do óleo (monitoramento) avisam de problemas que você já previu; já o scanner do mecânico, que lê o motor e responde perguntas novas, é a observabilidade. Em software, você vai precisar das duas coisas.Observability is the ability to understand what happens inside a system by looking only at what it emits to the outside: its telemetry. Think of a car dashboard: the speedometer and the oil light (monitoring) warn about problems you foresaw; the mechanic's scanner, which reads the engine and answers new questions, is observability. In software you need both.

Sem isso, o usuário reclama (“está lento”) e você descobre o problema depois, sem saber onde olhar. Com isso, você responde em minutos: está lento? para quem? desde quando? em qual serviço? por quê?Without it, the user complains (“it is slow”) and you find out afterwards, with no idea where to look. With it, you answer in minutes: is it slow? for whom? since when? in which service? why?

Os três sinaisThe three signals

MétricasMetrics Quanto? Com que frequência?How much? How often? números ao longo do temponumbers over time barato, ótimo para alertascheap, great for alerts Prometheus Traces Onde o tempo foi gasto?Where did the time go? o caminho de UMA requisiçãothe path of ONE request atravessa vários serviçoscrosses several services Jaeger · Tempo Logs O que aconteceu, e por quê?What happened, and why? eventos em texto ou JSONevents as text or JSON detalhe alto, custo altohigh detail, high cost Loki · Elasticsearch Juntos: a métrica avisa, o trace localiza, o log explica.Together: the metric alerts, the trace locates, the log explains.
Cada sinal responde uma pergunta diferente; nenhum substitui os outros.Each signal answers a different question; none replaces the others.
SinalSignalO que éWhat it isBoa paraGood forLimiteLimit
MétricasMetricsNúmeros agregados ao longo do tempo (requisições/s, % de erro, p99).Aggregated numbers over time (requests/s, % errors, p99).painéis, alertas, tendências, SLOsdashboards, alerts, trends, SLOsnão dizem qual requisição nem por quêdo not say which request or why
TracesTracesO caminho de uma requisição, dividido em spans (cada etapa, com início e duração).The path of one request, split into spans (each step, with start and duration).achar onde o tempo foi gasto, sobretudo entre serviçosfinding where the time went, especially across servicescustam armazenamento; normalmente amostradosstorage cost; usually sampled
LogsLogsRegistros de eventos, de preferência em JSON estruturado.Event records, preferably structured JSON.explicar o motivo: mensagem de erro, entrada, estadoexplaining the reason: error message, input, statevolume e custo altos; difíceis de agregarhigh volume and cost; hard to aggregate
O padrão aberto para produzir esses três sinais é o OpenTelemetry (OTel), da CNCF: uma API/SDK por linguagem, um formato de transporte (OTLP) e um Collector. Instrumentando com OTel, você troca o destino (Jaeger, Tempo, Datadog, X-Ray…) sem reescrever o app.The open standard for producing these three signals is OpenTelemetry (OTel), from the CNCF: one API/SDK per language, a transport format (OTLP) and a Collector. If you instrument with OTel, you change the destination (Jaeger, Tempo, Datadog, X-Ray…) without rewriting the app.

Métricas e KPIs: o que medirMetrics and KPIs: what to measure

Medir tudo é o mesmo que não medir nada. Três “receitas” curtas dizem o que olhar primeiro:Measuring everything is the same as measuring nothing. Three short “recipes” say what to look at first:

ReceitaRecipePara quêUsed forO que medeWhat it measures
Quatro sinais de ouro (Google SRE)Four golden signals (Google SRE)qualquer serviçoany servicelatência, tráfego, erros e saturação (quão cheio está o recurso mais limitado)latency, traffic, errors and saturation (how full the most constrained resource is)
REDREDserviços que respondem requisições (APIs, sites)request-serving services (APIs, sites)Rate (req/s), Errors (% de falhas), Duration (latência por percentil)Rate (req/s), Errors (% failing), Duration (latency by percentile)
USEUSErecursos: CPU, disco, rede, filas, poolsresources: CPU, disk, network, queues, poolsUtilization (% ocupado), Saturation (fila de espera), ErrorsUtilization (% busy), Saturation (waiting queue), Errors

Na prática: RED para cada serviço, USE para cada recurso compartilhado (banco, fila, pool de conexões). É o que o painel do laboratório mostra.In practice: RED for every service, USE for every shared resource (database, queue, connection pool). That is what the lab's dashboard shows.

KPIs: do técnico ao negócioKPIs: from technical to business

CamadaLayerExemplos de KPIKPI examplesPergunta que respondeQuestion it answers
ServiçoServicep95/p99 de latência por rota · taxa de erro 5xx · requisições/s · disponibilidade (% de requisições bem-sucedidas)p95/p99 latency per route · 5xx error rate · requests/s · availability (% of successful requests)o serviço está saudável para o usuário?is the service healthy for the user?
InfraestruturaInfrastructureCPU, memória, atraso do event loop, conexões do banco em uso, profundidade e idade da filaCPU, memory, event loop lag, DB connections in use, queue depth and ageonde vamos ficar sem fôlego?where will we run out of room?
Entrega (DORA)Delivery (DORA)frequência de deploy · lead time · taxa de falha de mudança · tempo para restaurardeploy frequency · lead time · change failure rate · time to restoreentregamos rápido e com segurança?do we ship fast and safely?
NegócioBusinesspedidos/min · taxa de sucesso do pagamento · cadastros · conversão do checkout · receita/minorders/min · payment success rate · sign-ups · checkout conversion · revenue/mino produto está funcionando para o negócio?is the product working for the business?
A métrica de negócio costuma ser o primeiro alarme confiável: se “pedidos por minuto” cai, algo está errado mesmo que CPU, memória e erros pareçam normais.The business metric is often the first reliable alarm: if “orders per minute” drops, something is wrong even when CPU, memory and errors look normal.

SLI, SLO e orçamento de erroSLI, SLO and error budget

Metas sem número viram discussão. O vocabulário do SRE resolve isso:Goals without numbers turn into arguments. SRE vocabulary fixes that:

  • SLI (indicador): o que você mede. Ex.: “% de requisições /checkout respondidas em menos de 500 ms e sem erro”.SLI (indicator): what you measure. E.g. “% of /checkout requests answered in under 500 ms with no error”.
  • SLO (objetivo): a meta para o SLI. Ex.: “99,9% em 30 dias”.SLO (objective): the target for the SLI. E.g. “99.9% over 30 days”.
  • Orçamento de erro: o que sobra para falhar. 99,9% em 30 dias = 0,1% de 43.200 min ≈ 43 minutos (99% = 7,2 h; 99,95% = 21,6 min). Gastou o orçamento, pare de lançar novidades e conserte a confiabilidade.Error budget: what is left to fail. 99.9% over 30 days = 0.1% of 43,200 min ≈ 43 minutes (99% = 7.2 h; 99.95% = 21.6 min). Budget spent, stop shipping features and fix reliability.
  • SLA: o contrato com o cliente (com multa). Costuma ser mais folgado que o SLO interno.SLA: the contract with the customer (with penalties). Usually looser than the internal SLO.

Percentis (p50, p95, p99): por que a média enganaPercentiles (p50, p95, p99): why the average lies

O percentil responde “qual o tempo que X% das requisições não ultrapassam?”. Se o p95 = 300 ms, 95% das requisições terminaram em até 300 ms e 5% foram mais lentas. O p50 é a mediana (a experiência típica); p95 e p99 mostram a cauda: os piores casos que ainda são frequentes o bastante para incomodar usuários reais.A percentile answers “what time do X% of requests not exceed?”. If p95 = 300 ms, 95% of requests finished within 300 ms and 5% were slower. p50 is the median (the typical experience); p95 and p99 show the tail: the worst cases that are still frequent enough to bother real users.

0 ms 500 ms 1000 ms 1500 ms 2000 ms p50 ≈ 95 ms média ≈ 151 msmean ≈ 151 ms p95 ≈ 296 ms p99 ≈ 1602 ms tempo da requisição (altura da barra = nº de requisições)request time (bar height = number of requests)
A maioria das requisições é rápida; poucas, muito lentas. A média cai no meio do nada e o p99 mostra a cauda.Most requests are fast; a few are very slow. The mean lands in the middle of nowhere and p99 shows the tail.

No gráfico (1.000 requisições de exemplo), a média dá ≈ 151 ms: um número que nenhum usuário real viveu (a maioria levou ~100 ms; alguns levaram ~1,6 s). Um painel só com a média diria que está tudo bem, enquanto 1 em cada 100 usuários espera mais de 1,6 s.In the chart (1,000 sample requests), the mean is ≈ 151 ms: a number that no real user experienced (most took ~100 ms; a few took ~1.6 s). A mean-only dashboard would say all is well while 1 in every 100 users waits over 1.6 s.

Por que a cauda importa mais do que pareceWhy the tail matters more than it looks

Uma tela quase nunca faz uma chamada: faz dezenas (API, banco, cache, serviços internos). Se cada chamada tem 1% de chance de cair na cauda, a chance de a página pegar ao menos uma chamada lenta é 1 − 0,99n: com 100 chamadas, 63%. O p99 de cada peça vira a experiência típica do todo (o artigo “The Tail at Scale”, de Dean e Barroso, trata disso).A screen almost never makes one call: it makes dozens (API, database, cache, internal services). If each call has a 1% chance of landing in the tail, the chance that the page hits at least one slow call is 1 − 0.99n: with 100 calls, 63%. Each piece's p99 becomes the typical experience of the whole (the paper “The Tail at Scale” by Dean and Barroso covers this).

0 50 100 150 200 0% 25% 50% 75% 100% p99 por chamadap99 per call p99.9 por chamadap99.9 per call chamadas feitas por uma página/açãocalls made by one page/action chance de pegar pelo menos 1 chamada lentachance of hitting at least 1 slow call
Com 100 chamadas e 1% lentas, 63% das páginas pegam ao menos uma: o p99 de cada serviço vira a experiência típica do usuário.With 100 calls and 1% slow, 63% of pages hit at least one: each service's p99 becomes the user's typical experience.

Regras para trabalhar com percentisRules for working with percentiles

  1. Nunca faça a média de percentis. A média dos p99 de 10 servidores não é o p99 do conjunto. Some os buckets dos histogramas de todos os servidores e só depois calcule o percentil (é o que o histogram_quantile faz sobre sum by (le)).Never average percentiles. The average of 10 servers' p99s is not the fleet's p99. Sum the histograms' buckets across all servers and only then compute the percentile (which is what histogram_quantile does over sum by (le)).
  2. Meça por rota (ou por operação). Um endpoint rápido e muito chamado esconde o lento e crítico, como o /checkout.Measure per route (or per operation). A fast, heavily-called endpoint hides the slow, critical one, like /checkout.
  3. Escolha o percentil pelo impacto: p95 para a experiência geral, p99 para fluxos críticos e dependências chamadas em cascata, p99.9 para sistemas com milhões de chamadas.Pick the percentile by impact: p95 for the general experience, p99 for critical flows and cascaded dependencies, p99.9 for systems with millions of calls.
  4. Percentil sobre poucas amostras é ruído. Com 20 requisições por janela, o “p99” é basicamente o máximo. Use janelas maiores ou ignore rotas com pouco tráfego.A percentile over few samples is noise. With 20 requests per window, the “p99” is basically the maximum. Use longer windows or ignore low-traffic routes.
  5. Histogramas são estimativas. A precisão depende dos buckets (veja o laboratório): ponha limites perto do seu SLO (ex.: 0,5 s e 1 s).Histograms are estimates. Precision depends on the buckets (see the lab): put boundaries near your SLO (e.g. 0.5 s and 1 s).

Prometheus e Grafana: como funcionamPrometheus and Grafana: how they work

O Prometheus é um banco de séries temporais que busca (pull) as métricas: a cada intervalo (ex.: 15 s) ele faz um GET /metrics em cada alvo, lê o texto e guarda os números com a hora. O Grafana não guarda nada: só consulta o Prometheus (e outros) e desenha painéis. O Alertmanager recebe os alertas que o Prometheus dispara e cuida de agrupar, silenciar e enviar (Slack, e-mail, PagerDuty).Prometheus is a time-series database that pulls metrics: every interval (e.g. 15 s) it does a GET /metrics on each target, reads the text and stores the numbers with the time. Grafana stores nothing: it only queries Prometheus (and others) and draws dashboards. Alertmanager receives the alerts Prometheus fires and handles grouping, silencing and sending (Slack, email, PagerDuty).

seu appyour app /metrics (texto)/metrics (text) node_exporter CPU/disco da máquinamachine CPU/disk banco, fila…database, queue… exporters prontosready-made exporters Prometheus scrape a cada 15 sscrape every 15 s TSDB séries no discoseries on disk regrasrules gravação e alertarecording & alerts PromQL puxa (pull)pulls Alertmanager agrupa e silenciagroups and silences Slack · e-mail · PagerDutySlack · email · PagerDuty Grafana painéis via PromQLdashboards via PromQL
O Prometheus busca (pull) as métricas, guarda como séries temporais e avalia regras; o Grafana só consulta.Prometheus pulls the metrics, stores them as time series and evaluates rules; Grafana only queries.

Os quatro tipos de métricaThe four metric types

TipoTypeComportamentoBehaviourExemploExampleComo consultarHow to query
CounterCountersó cresce (zera ao reiniciar)only goes up (resets on restart)http_requests_totalhttp_requests_totalrate(x[5m]) = por segundorate(x[5m]) = per second
GaugeGaugesobe e descegoes up and downmemória usada, itens na filamemory in use, items in queueo valor direto, ou avg_over_timethe value itself, or avg_over_time
HistogramHistogramconta observações em buckets acumulados (le = “menor ou igual”), mais _sum e _countcounts observations into cumulative buckets (le = “less than or equal”), plus _sum and _counthttp_request_duration_secondshttp_request_duration_secondshistogram_quantile(0.99, …)histogram_quantile(0.99, …)
SummarySummarycalcula percentis no próprio appcomputes percentiles inside the app(evite para latência)(avoid for latency)não dá para agregar entre instânciascannot be aggregated across instances
Para latência use Histogram: é o único que permite calcular o p95/p99 do conjunto de servidores. O histogram_quantile estima o valor interpolando dentro do bucket onde o percentil cai.For latency use a Histogram: it is the only one that lets you compute the p95/p99 of the whole fleet. histogram_quantile estimates the value by interpolating inside the bucket where the percentile falls.

Rótulos e cardinalidade: o erro mais caroLabels and cardinality: the costliest mistake

Cada combinação única de rótulos vira uma série separada na memória e no disco. route="/orders/:id" é uma série; usar a URL crua (/orders/8841, /orders/8842…) cria uma série por pedido e derruba o Prometheus. Regra: rótulos têm poucos valores possíveis (rota-modelo, método, status, região). Nunca use user_id, e-mail, ID de requisição ou URL crua como rótulo; esses dados vão em logs e traces.Every unique label combination becomes a separate series in memory and on disk. route="/orders/:id" is one series; using the raw URL (/orders/8841, /orders/8842…) creates a series per order and brings Prometheus down. Rule: labels have few possible values (route template, method, status, region). Never use user_id, email, request ID or raw URL as a label; that data belongs in logs and traces.

PromQL: o essencialPromQL: the essentials

# Throughput: requests per second, per route (average over the last 5 minutes)
sum by (route) (rate(http_request_duration_seconds_count[5m]))

# Errors: percentage of requests that returned 5xx
100 * sum(rate(http_request_duration_seconds_count{status_code=~"5.."}[5m]))
    / sum(rate(http_request_duration_seconds_count[5m]))

# Duration: p95 and p99 per route
histogram_quantile(0.95, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

# Average (for comparison with the percentiles)
sum(rate(http_request_duration_seconds_sum[5m])) / sum(rate(http_request_duration_seconds_count[5m]))

# Is everything being scraped? (1 = up, 0 = down)
up

Leia assim: rate(...[5m]) transforma o contador em “por segundo” numa janela de 5 minutos (use pelo menos 4× o intervalo de coleta); sum by (le) soma os servidores mantendo os buckets; histogram_quantile estima o percentil. Documentação: Querying basics no site do Prometheus.Read it like this: rate(...[5m]) turns the counter into “per second” over a 5-minute window (use at least 4× the scrape interval); sum by (le) adds up the servers keeping the buckets; histogram_quantile estimates the percentile. Docs: Querying basics on the Prometheus site.

Alertas que valem a penaAlerts worth having

  • Alerte em sintomas, não em causas. “p99 do checkout > 1 s” e “erros > 1%” importam para o usuário; “CPU > 80%” pode ser normal. Causas ficam em painéis.Alert on symptoms, not causes. “checkout p99 > 1 s” and “errors > 1%” matter to the user; “CPU > 80%” may be normal. Causes belong on dashboards.
  • Use for: (ex.: 5 min) para não acordar ninguém por um pico de 10 segundos.Use for: (e.g. 5 min) so nobody is woken by a 10-second spike.
  • Alerte pela velocidade de queima do orçamento (burn rate), em duas janelas (ex.: 1 h e 5 min). Reduz ruído e avisa cedo (veja Alerting on SLOs, no SRE Workbook).Alert on error-budget burn rate, over two windows (e.g. 1 h and 5 min). It cuts noise and warns early (see Alerting on SLOs in the SRE Workbook).
  • Todo alerta que acorda alguém precisa de ação humana e de um link para o runbook (passo a passo). Alerta que ninguém age vira ruído e é apagado.Every alert that wakes someone needs a human action and a link to a runbook (step by step). An alert nobody acts on becomes noise and gets deleted.

Prometheus + Grafana bastam? Traces, logs e outras ferramentasAre Prometheus + Grafana enough? Traces, logs and other tools

Para latência e percentis, sim: com histogramas você tem p50/p95/p99 por rota, alertas e SLOs. Para depurar, não sozinhos. A métrica diz que o p99 do /checkout está em 2 s, mas não diz qual requisição, qual etapa demorou nem por quê. Para isso entram traces e logs:For latency and percentiles, yes: with histograms you get p50/p95/p99 per route, alerts and SLOs. For debugging, not on their own. The metric says the /checkout p99 is 2 s, but not which request, which step was slow or why. That is where traces and logs come in:

1. Alerta1. Alert p99 de /checkout > 1 sp99 of /checkout > 1 s Prometheus 2. Trace qual span demorou?which span was slow? Jaeger · Tempo 3. Log por quê? (trace_id)why? (trace_id) Loki · Elastic 4. Correção4. Fix e teste/alerta novoand a new test/alert k6 · CI do sintoma agregado à causa em uma requisição concretafrom the aggregate symptom to the cause in one concrete request
O caminho típico de um incidente: alerta de métrica, trace da requisição lenta, log da causa.The typical incident path: metric alert, trace of the slow request, log of the cause.
PerguntaQuestionQuem respondeWho answers
O p99 subiu? Desde quando? Em qual rota?Did p99 go up? Since when? On which route?Prometheus + Grafana (métricas)Prometheus + Grafana (metrics)
Qual etapa dessa requisição lenta demorou (banco, API de pagamento, cache)?Which step of that slow request took the time (database, payment API, cache)?Traces: Jaeger, Grafana Tempo, Elastic APM, AWS X-RayTraces: Jaeger, Grafana Tempo, Elastic APM, AWS X-Ray
Qual foi o erro e com que dados? Quem foi afetado?What was the error and with what data? Who was affected?Logs: Loki, Elasticsearch/OpenSearch, CloudWatch LogsLogs: Loki, Elasticsearch/OpenSearch, CloudWatch Logs
Quais clientes, versões ou regiões estão piores? (dimensões de alta cardinalidade)Which customers, versions or regions are worse? (high-cardinality dimensions)Traces/logs consultados por atributo (ou plataformas de eventos como Honeycomb)Traces/logs queried by attribute (or event platforms such as Honeycomb)

O elo entre os três é o trace_id: o app o coloca em cada log e o OpenTelemetry o propaga entre serviços (cabeçalho traceparent). Assim, do log de erro você abre o trace completo, e do trace lento você lê os logs daquela requisição. No laboratório você fará exatamente isso.The link between the three is the trace_id: the app puts it in every log and OpenTelemetry propagates it between services (the traceparent header). So from an error log you open the full trace, and from a slow trace you read the logs of that very request. In the lab you will do exactly that.

Que pilha escolherWhich stack to choose

OpçãoOptionComposiçãoMakeupPontos fortesStrengthsCusto de operar / atençãoCost to run / watch out
Grafana stack (o do laboratório)Grafana stack (the lab's)Prometheus (métricas) + Loki (logs) + Tempo ou Jaeger (traces) + Grafana; coleta com OTel Collector ou Grafana AlloyPrometheus (metrics) + Loki (logs) + Tempo or Jaeger (traces) + Grafana; collection with OTel Collector or Grafana Alloyopen source, uma tela só, armazenamento barato (o Loki indexa só os rótulos, não o texto)open source, single pane, cheap storage (Loki indexes only labels, not the text)você opera tudo; busca de texto livre nos logs é mais lenta que no Elasticsearchyou run everything; free-text log search is slower than in Elasticsearch
Elastic Stack (Elasticsearch + Kibana + APM)Elastic Stack (Elasticsearch + Kibana + APM)tudo em um motor de busca; aceita OpenTelemetryeverything in one search engine; accepts OpenTelemetrybusca de texto e análise de logs excelentes, agregações poderosas (percentis nos documentos), um só lugar para logs, traces e métricasexcellent text search and log analytics, powerful aggregations (percentiles over documents), one place for logs, traces and metricspesado (JVM, memória, shards, retenção); é o mais caro de manter. OpenSearch é o fork Apache 2.0 (Amazon OpenSearch Service)heavy (JVM, memory, shards, retention); the costliest to maintain. OpenSearch is the Apache 2.0 fork (Amazon OpenSearch Service)
Gerenciado na AWSManaged on AWSCloudWatch (métricas, Logs Insights, Application Signals) + X-Ray ou OTel; ou Amazon Managed Prometheus + Managed GrafanaCloudWatch (metrics, Logs Insights, Application Signals) + X-Ray or OTel; or Amazon Managed Prometheus + Managed Grafanaquase nada para operar; já integrado a ALB, ECS, EKS, Lambda; percentis do ALB sem instrumentaralmost nothing to run; already integrated with ALB, ECS, EKS, Lambda; ALB percentiles with no instrumentationpreço por métrica, log ingerido e consulta: monitore a conta; menos flexível que o PromQLpriced per metric, ingested log and query: watch the bill; less flexible than PromQL
SaaS (Datadog, New Relic, Honeycomb, Grafana Cloud)SaaS (Datadog, New Relic, Honeycomb, Grafana Cloud)plataforma pronta, com agentesready platform, with agentscomeça em minutos; ótimas buscas por alta cardinalidadestarts in minutes; great high-cardinality searchcusto cresce rápido com volume; dependência do fornecedor (OTel reduz isso)cost grows fast with volume; vendor lock-in (OTel reduces it)
Recomendação para começar: métricas com Prometheus + Grafana, instrumentação com OpenTelemetry, e adicione traces (Jaeger/Tempo) e logs (Loki) quando o primeiro incidente pedir. Se sua equipe é pequena e já está na AWS, comece pelo CloudWatch e pelo Managed Prometheus/Grafana para não operar a pilha. Escolha Elasticsearch/OpenSearch quando a busca e a análise de logs forem o centro do trabalho.Recommendation to start: metrics with Prometheus + Grafana, instrumentation with OpenTelemetry, and add traces (Jaeger/Tempo) and logs (Loki) when the first incident asks for them. If your team is small and already on AWS, start with CloudWatch and Managed Prometheus/Grafana so you do not run the stack. Choose Elasticsearch/OpenSearch when log search and analytics are the centre of the work.
Percentil de histograma (Prometheus) é uma estimativa barata e agregável; percentil de eventos brutos (Elasticsearch, traces) é exato, mas custa armazenar cada requisição. Os dois se complementam: o histograma alerta, os eventos investigam. Os exemplars do Prometheus ligam um ponto do gráfico a um trace específico.A histogram percentile (Prometheus) is a cheap, aggregatable estimate; a percentile over raw events (Elasticsearch, traces) is exact but costs storing every request. They complement each other: the histogram alerts, the events investigate. Prometheus exemplars link a point on the graph to a specific trace.

Como instrumentar um projetoHow to instrument a project

Instrumentar é fazer o app emitir os sinais. Três lugares cobrem quase tudo:Instrumenting is making the app emit the signals. Three places cover almost everything:

  1. Middleware HTTP (entrada): cronometra cada requisição e gera o RED por rota. É o que o laboratório faz.HTTP middleware (the entry): times every request and produces RED per route. That is what the lab does.
  2. Chamadas de saída (banco, HTTP, fila, cache): viram spans nos traces. A auto-instrumentação do OpenTelemetry cobre as bibliotecas comuns sem mexer no código.Outbound calls (database, HTTP, queue, cache): become spans in the traces. OpenTelemetry auto-instrumentation covers common libraries without touching the code.
  3. Eventos de negócio (pedido criado, pagamento aprovado): contadores e atributos de span escritos por você.Business events (order created, payment approved): counters and span attributes written by you.
StackStackMétricas PrometheusPrometheus metricsTraces OpenTelemetryOpenTelemetry traces
Node.jsNode.jsprom-clientprom-client@opentelemetry/auto-instrumentations-node@opentelemetry/auto-instrumentations-node
PythonPythonprometheus-client (ou prometheus-fastapi-instrumentator)prometheus-client (or prometheus-fastapi-instrumentator)opentelemetry-instrument (agente)opentelemetry-instrument (agent)
Java / Spring BootJava / Spring BootActuator + Micrometer (/actuator/prometheus)Actuator + Micrometer (/actuator/prometheus)OTel Java agent (-javaagent)OTel Java agent (-javaagent)
GoGoclient_golangclient_golangOTel Go SDK + otelhttpOTel Go SDK + otelhttp
Nginx, Postgres, Redis, hostNginx, Postgres, Redis, hostexporters prontos (nginx-prometheus-exporter, postgres_exporter, redis_exporter, node_exporter)ready-made exporters (nginx-prometheus-exporter, postgres_exporter, redis_exporter, node_exporter)——
Cuidado com dados sensíveis. Telemetria vaza fácil: senha, token, CPF, e-mail e corpo de requisição não entram em rótulo, log nem atributo de span. Registre IDs internos, não dados pessoais, e mascare o que for inevitável. Veja Secure web apps para o raciocínio de superfície de ataque.Watch out for sensitive data. Telemetry leaks easily: passwords, tokens, personal IDs, emails and request bodies must not go into labels, logs or span attributes. Record internal IDs, not personal data, and mask what is unavoidable. See Secure web apps for the attack-surface reasoning.

Praticar na sua máquina com DockerPractise on your machine with Docker

Você vai montar, com o Docker Compose, um mini-ambiente de produção: uma API Node.js de “loja” com um endpoint lento de propósito, e ao redor dela Prometheus, Grafana, Jaeger, Loki e um gerador de carga (k6). Cada atividade é pequena e usa a anterior. Todos os arquivos e comandos abaixo foram executados de ponta a ponta, e os resultados esperados vêm dessas execuções (os números variam um pouco a cada rodada, porque a lentidão é aleatória).With Docker Compose you will build a mini production environment: a “shop” Node.js API with a deliberately slow endpoint, and around it Prometheus, Grafana, Jaeger, Loki and a load generator (k6). Each activity is small and builds on the previous one. Every file and command below was run end to end, and the expected results come from those runs (numbers vary a bit between runs, because the slowness is random).

k6 gera cargagenerates load app (Node) /products /checkout Prometheus :9090 scrape /metrics Jaeger :16686 OTLP Alloy lê logs do Dockerreads Docker logs stdout JSONJSON stdout Loki guarda os logsstores the logs Grafana :3001 você: http://localhost:3001you: http://localhost:3001 e :9090 · :16686 · :8081and :9090 · :16686 · :8081
Seis contêineres no Docker Compose: a app emite métricas, traces e logs; o Grafana consulta os três.Six containers in Docker Compose: the app emits metrics, traces and logs; Grafana queries all three.
##AtividadeActivityVocê aprendeYou learn
00Criar os arquivos e subirCreate the files and starta estrutura do projetothe project layout
11Ver o /metrics cruSee the raw /metricso formato de counters e histogramasthe counter and histogram format
22Prometheus coletandoPrometheus scrapingtargets, up e PromQLtargets, up and PromQL
33Painel no GrafanaGrafana dashboardRED por rotaRED per route
44Carga com k6: média × p95 × p99Load with k6: average × p95 × p99por que a média enganawhy the average lies
55AlertasAlertsregras e estado firingrules and the firing state
66Traces no JaegerTraces in Jaegerachar a etapa lentafinding the slow step
77Logs no Loki e o trace_idLogs in Loki and the trace_iddo log ao tracefrom log to trace
88Opção ElasticsearchElasticsearch optionpercentis sobre eventos brutospercentiles over raw events
99LimparClean upapagar tudodelete everything

Atividade 0: criar os arquivos e subirActivity 0: create the files and start

Pré-requisitos: Docker com Compose v2 (docker compose version). Crie esta estrutura:Prerequisites: Docker with Compose v2 (docker compose version). Create this layout:

obs-lab/
├── docker-compose.yml
├── app/
│   ├── Dockerfile
│   ├── package.json
│   └── server.js
├── prometheus/
│   ├── prometheus.yml
│   └── rules.yml
├── alloy/config.alloy
├── grafana/
│   ├── provisioning/datasources/datasources.yml
│   ├── provisioning/dashboards/dashboards.yml
│   └── dashboards/shop-red.json
└── k6/load.js

A aplicação. Uma API Express com três rotas: /products (rápida), /checkout (lenta em 5% das chamadas e com 4% de erros) e /metrics. O trecho que importa: o histograma da duração, o middleware que o alimenta (note que o rótulo é o modelo da rota, não a URL), os logs em JSON com o trace_id e um span manual em volta do pagamento.The application. An Express API with three routes: /products (fast), /checkout (slow on 5% of calls and failing 4%) and /metrics. The part that matters: the duration histogram, the middleware feeding it (note the label is the route template, not the URL), JSON logs with the trace_id and a manual span around the payment.

app/server.js

const express = require('express');
const client = require('prom-client');
const { trace, SpanStatusCode } = require('@opentelemetry/api');

const tracer = trace.getTracer('shop-api');

const app = express();
client.collectDefaultMetrics();                       // CPU, memory, event loop lag, GC...

// Duration of every HTTP request. Buckets decide how precise p95/p99 will be.
const httpDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = (level, msg, extra = {}) => {
  const span = trace.getActiveSpan();
  console.log(JSON.stringify({
    time: new Date().toISOString(), level, msg,
    trace_id: span ? span.spanContext().traceId : undefined,   // lets you jump from a log to its trace
    ...extra,
  }));
};

// Middleware: time every request. Use the route TEMPLATE (/orders/:id), never the raw URL.
app.use((req, res, next) => {
  const end = httpDuration.startTimer();
  res.on('finish', () => {
    const route = req.route ? req.route.path : 'unmatched';
    end({ method: req.method, route, status_code: res.statusCode });
  });
  next();
});

app.get('/', (req, res) => res.json({ ok: true }));

app.get('/products', async (req, res) => {          // fast: 10-40 ms
  await sleep(10 + Math.random() * 30);
  res.json([{ id: 1 }, { id: 2 }]);
});

app.get('/checkout', async (req, res) => {          // mostly 50-150 ms, 5% take ~1.5-2 s (the "tail")
  const slow = Math.random() < 0.05;
  const failed = Math.random() < 0.04;               // 4% fail

  // A manual span: the slow part now shows up by name inside the trace.
  await tracer.startActiveSpan('payment-provider.charge', async (span) => {
    span.setAttribute('payment.slow', slow);
    await sleep(slow ? 1500 + Math.random() * 500 : 50 + Math.random() * 100);
    if (failed) span.setStatus({ code: SpanStatusCode.ERROR, message: 'provider timeout' });
    span.end();
  });

  if (failed) {
    log('error', 'payment provider timeout', { route: '/checkout' });
    return res.status(500).json({ error: 'payment failed' });
  }
  log('info', 'checkout ok', { route: '/checkout', slow });
  res.json({ ok: true });
});

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
});

app.listen(3000, () => log('info', 'listening on :3000'));

app/package.json

{
  "name": "shop-api",
  "version": "1.0.0",
  "private": true,
  "main": "server.js",
  "dependencies": {
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/auto-instrumentations-node": "^0.56.0",
    "express": "^4.21.0",
    "prom-client": "^15.1.3"
  }
}

app/Dockerfile

FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
USER node
EXPOSE 3000
CMD ["node", "--require", "@opentelemetry/auto-instrumentations-node/register", "server.js"]

O CMD carrega @opentelemetry/auto-instrumentations-node/register: sem mudar o código, o OpenTelemetry cria spans para cada requisição HTTP e rota Express e os envia por OTLP ao Jaeger.The CMD loads @opentelemetry/auto-instrumentations-node/register: without changing the code, OpenTelemetry creates spans for each HTTP request and Express route and sends them over OTLP to Jaeger.

O Compose. Todas as portas publicadas ficam em 127.0.0.1 (só a sua máquina). O Grafana usa a porta 3001 para não colidir com apps que já usem a 3000.Compose. All published ports bind to 127.0.0.1 (your machine only). Grafana uses port 3001 so it does not clash with apps already using 3000.

docker-compose.yml

services:
  app:
    build: ./app
    environment:
      OTEL_SERVICE_NAME: shop-api
      OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318      # traces go to Jaeger over OTLP/HTTP
      OTEL_TRACES_EXPORTER: otlp
      OTEL_METRICS_EXPORTER: none                          # metrics come from prom-client instead
      OTEL_LOGS_EXPORTER: none
      OTEL_NODE_ENABLED_INSTRUMENTATIONS: http,express
    ports: ["127.0.0.1:8081:3000"]

  prometheus:
    image: prom/prometheus:v3.5.0
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules.yml:/etc/prometheus/rules.yml:ro
    ports: ["127.0.0.1:9090:9090"]

  grafana:
    image: grafana/grafana:12.1.0
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: admin          # lab only
      GF_AUTH_ANONYMOUS_ENABLED: "false"
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    ports: ["127.0.0.1:3001:3000"]
    depends_on: [prometheus]

  jaeger:
    image: jaegertracing/jaeger:2.9.0
    ports: ["127.0.0.1:16686:16686"]             # UI; OTLP (4318) stays inside the network

  loki:
    image: grafana/loki:3.5.0
    command: ["-config.file=/etc/loki/local-config.yaml"]

  alloy:                                         # reads container logs and ships them to Loki
    image: grafana/alloy:v1.10.0
    command: ["run", "/etc/alloy/config.alloy"]
    volumes:
      - ./alloy/config.alloy:/etc/alloy/config.alloy:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro   # LAB ONLY: this socket is root on the host
    depends_on: [loki]

  k6:
    image: grafana/k6:1.2.0
    profiles: ["load"]                           # only runs when you ask for it
    volumes: ["./k6:/scripts:ro"]
    command: ["run", "/scripts/load.js"]
    depends_on: [app]
O Alloy monta o /var/run/docker.sock para ler os logs dos contêineres. Quem acessa esse socket controla o Docker (na prática, é root na máquina). Aceitável num laboratório local; em produção não faça isso: use agentes por nó com permissão mínima (veja a seção de produção).Alloy mounts /var/run/docker.sock to read container logs. Whoever reaches that socket controls Docker (effectively root on the machine). Acceptable in a local lab; do not do this in production: use per-node agents with minimal permissions (see the production section).

Prometheus: o arquivo de coleta e as regras (gravação do p99 e dois alertas).Prometheus: the scrape file and the rules (p99 recording rule and two alerts).

prometheus/prometheus.yml

global:
  scrape_interval: 5s          # lab value; production usually 15s-60s
  evaluation_interval: 5s

rule_files:
  - /etc/prometheus/rules.yml

scrape_configs:
  - job_name: shop-api
    static_configs:
      - targets: ["app:3000"]  # Prometheus PULLS http://app:3000/metrics
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]

prometheus/rules.yml

groups:
  - name: shop-api
    rules:
      # Recording rule: compute the expensive p99 once and store it as a new series
      - record: route:http_request_duration_seconds:p99_5m
        expr: histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

      - alert: HighLatencyP99
        expr: route:http_request_duration_seconds:p99_5m{route="/checkout"} > 1
        for: 1m
        labels: { severity: page }
        annotations:
          summary: "p99 of {{ $labels.route }} is above 1s"

      - alert: HighErrorRate
        expr: |
          sum(rate(http_request_duration_seconds_count{status_code=~"5.."}[5m]))
            / sum(rate(http_request_duration_seconds_count[5m])) > 0.01
        for: 1m
        labels: { severity: page }
        annotations:
          summary: "more than 1% of requests are failing"

Grafana e Alloy: as fontes de dados (Prometheus, Loki, Jaeger) e o painel “RED” são carregados sozinhos (provisioning); o campo derivado do Loki transforma o trace_id de cada log em link para o Jaeger. O Alloy descobre os contêineres e envia os logs ao Loki.Grafana and Alloy: the data sources (Prometheus, Loki, Jaeger) and the “RED” dashboard load themselves (provisioning); Loki's derived field turns each log's trace_id into a link to Jaeger. Alloy discovers the containers and ships the logs to Loki.

grafana/provisioning/datasources/datasources.yml

apiVersion: 1
datasources:
  - name: Prometheus
    uid: prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true
  - name: Loki
    uid: loki
    type: loki
    url: http://loki:3100
    jsonData:
      derivedFields:                      # turns trace_id in a log line into a link to the trace
        - name: trace_id
          matcherRegex: '"trace_id":"(\w+)"'
          url: '$${__value.raw}'
          datasourceUid: jaeger
  - name: Jaeger
    uid: jaeger
    type: jaeger
    url: http://jaeger:16686

grafana/provisioning/dashboards/dashboards.yml

apiVersion: 1
providers:
  - name: lab
    type: file
    options:
      path: /var/lib/grafana/dashboards

grafana/dashboards/shop-red.json

{
  "title": "Shop API (RED)",
  "uid": "shop-red",
  "refresh": "5s",
  "time": { "from": "now-15m", "to": "now" },
  "schemaVersion": 39,
  "panels": [
    { "id": 1, "type": "timeseries", "title": "Rate: requests per second",
      "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] },
      "targets": [ { "refId": "A", "legendFormat": "{{route}}",
        "expr": "sum by (route) (rate(http_request_duration_seconds_count[1m]))" } ] },

    { "id": 2, "type": "timeseries", "title": "Errors: % of requests with 5xx",
      "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "percent" }, "overrides": [] },
      "targets": [ { "refId": "A", "legendFormat": "5xx %",
        "expr": "100 * sum(rate(http_request_duration_seconds_count{status_code=~\"5..\"}[1m])) / sum(rate(http_request_duration_seconds_count[1m]))" } ] },

    { "id": 3, "type": "timeseries", "title": "Duration: /checkout p50, p95, p99 and average",
      "gridPos": { "x": 0, "y": 8, "w": 16, "h": 9 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "p50", "expr": "histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket{route=\"/checkout\"}[1m])))" },
        { "refId": "B", "legendFormat": "p95", "expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{route=\"/checkout\"}[1m])))" },
        { "refId": "C", "legendFormat": "p99", "expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{route=\"/checkout\"}[1m])))" },
        { "refId": "D", "legendFormat": "average", "expr": "sum(rate(http_request_duration_seconds_sum{route=\"/checkout\"}[1m])) / sum(rate(http_request_duration_seconds_count{route=\"/checkout\"}[1m]))" } ] },

    { "id": 4, "type": "timeseries", "title": "Saturation: event loop lag p99",
      "gridPos": { "x": 16, "y": 8, "w": 8, "h": 9 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] },
      "targets": [ { "refId": "A", "legendFormat": "lag p99", "expr": "nodejs_eventloop_lag_p99_seconds" } ] }
  ]
}

alloy/config.alloy

discovery.docker "containers" {
  host = "unix:///var/run/docker.sock"
}

discovery.relabel "containers" {
  targets = discovery.docker.containers.targets
  rule {
    source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
    target_label  = "service"
  }
}

loki.source.docker "containers" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.relabel.containers.output
  forward_to = [loki.write.default.receiver]
}

loki.write "default" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

E o teste de carga: 10 usuários virtuais por 2 minutos, com thresholds que fazem o teste falhar se o SLO for violado (p95 < 500 ms, p99 < 1 s, erros < 1%).And the load test: 10 virtual users for 2 minutes, with thresholds that make the test fail if the SLO is broken (p95 < 500 ms, p99 < 1 s, errors < 1%).

k6/load.js

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  vus: 10,
  duration: '2m',
  summaryTrendStats: ['avg', 'med', 'p(95)', 'p(99)', 'max'],   // print p99 in the summary
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],   // the test FAILS if the SLO is broken
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  http.get('http://app:3000/products');
  http.get('http://app:3000/checkout');
  sleep(0.5);
}
cd obs-lab
docker compose up -d --build
docker compose ps

Resultado esperado: seis serviços Up (o k6 não aparece: só roda quando você pede).Expected result: six services Up (k6 does not appear: it only runs when you ask).

NAME                 SERVICE      STATUS
obs-lab-alloy-1      alloy        Up 20 seconds
obs-lab-app-1        app          Up 22 seconds
obs-lab-grafana-1    grafana      Up 21 seconds
obs-lab-jaeger-1     jaeger       Up 21 seconds
obs-lab-loki-1       loki         Up 20 seconds
obs-lab-prometheus-1 prometheus   Up 22 seconds
Se alguma porta (8081, 9090, 3001, 16686) já estiver em uso, o Compose falha com “port is already allocated”. Troque só o número da esquerda em ports.If any port (8081, 9090, 3001, 16686) is already in use, Compose fails with “port is already allocated”. Change only the left-hand number in ports.

Atividade 1: ver o /metrics cruActivity 1: see the raw /metrics

curl -s localhost:8081/products
curl -s localhost:8081/metrics | grep -E 'http_request_duration_seconds_(bucket|sum|count)\{.*route="/products"'

Resultado esperado: um histograma em texto: cada linha _bucket{le="X"} conta quantas requisições levaram até X segundos (os números são acumulados), mais a soma e a contagem.Expected result: a histogram in plain text: each _bucket{le="X"} line counts how many requests took up to X seconds (the numbers are cumulative), plus the sum and the count.

http_request_duration_seconds_bucket{le="0.025",method="GET",route="/products",status_code="200"} 0
http_request_duration_seconds_bucket{le="0.05",method="GET",route="/products",status_code="200"} 1
http_request_duration_seconds_bucket{le="0.1",method="GET",route="/products",status_code="200"} 1
...
http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/products",status_code="200"} 1
http_request_duration_seconds_sum{method="GET",route="/products",status_code="200"} 0.0373
http_request_duration_seconds_count{method="GET",route="/products",status_code="200"} 1

Aqui está o segredo do percentil: o Prometheus não guarda cada requisição, só estes contadores por faixa. Dá para estimar p95/p99 a partir deles, de graça e para qualquer número de requisições.Here is the percentile secret: Prometheus does not store each request, only these per-range counters. You can estimate p95/p99 from them, cheaply and for any number of requests.

Atividade 2: o Prometheus coletandoActivity 2: Prometheus scraping

Abra http://localhost:9090/targets: os dois alvos devem estar UP. Depois, em http://localhost:9090/query, rode up. Ou pela linha de comando:Open http://localhost:9090/targets: both targets should be UP. Then, at http://localhost:9090/query, run up. Or from the command line:

curl -s localhost:9090/api/v1/targets | grep -o '"health":"[a-z]*"'
curl -s -G localhost:9090/api/v1/query --data-urlencode 'query=up'

Resultado esperado: os dois alvos saudáveis e up igual a 1 para cada job.Expected result: both targets healthy and up equal to 1 for each job.

"health":"up"
"health":"up"
{"metric":{"__name__":"up","instance":"app:3000","job":"shop-api"},"value":[...,"1"]}
{"metric":{"__name__":"up","instance":"localhost:9090","job":"prometheus"},"value":[...,"1"]}

Atividade 3: o painel no GrafanaActivity 3: the Grafana dashboard

Abra http://localhost:3001 (usuário admin, senha admin; só no laboratório) e vá em Dashboards → Shop API (RED). Há quatro painéis: Rate (req/s por rota), Errors (% de 5xx), Duration (p50, p95, p99 e média do /checkout) e Saturation (atraso do event loop). Sem tráfego, os gráficos ficam vazios. Gere um pouco de tráfego:Open http://localhost:3001 (user admin, password admin; lab only) and go to Dashboards → Shop API (RED). There are four panels: Rate (req/s per route), Errors (% of 5xx), Duration (p50, p95, p99 and average of /checkout) and Saturation (event loop lag). With no traffic the charts are empty. Generate some traffic:

for i in $(seq 1 100); do curl -s -o /dev/null localhost:8081/checkout; done

Clique no título de um painel e escolha Explore para ver (e editar) a consulta PromQL: é a mesma da seção anterior. Aprender a ler o PromQL por trás de cada painel é metade do ofício.Click a panel's title and choose Explore to see (and edit) the PromQL query: it is the same as in the previous section. Learning to read the PromQL behind each panel is half the craft.

Atividade 4: carga com k6, média × p95 × p99Activity 4: load with k6, average × p95 × p99

docker compose --profile load run --rm k6 run --quiet /scripts/load.js

Resultado esperado: em 2 minutos o k6 imprime o resumo. Repare: a média é ≈ 108 ms, o p95 passa no limite, e só o p99 revela o problema. O k6 sai com código 99, porque dois thresholds falharam.Expected result: after 2 minutes k6 prints the summary. Note: the average is ≈ 108 ms, p95 passes its limit, and only p99 reveals the problem. k6 exits with code 99, because two thresholds failed.

  █ THRESHOLDS

    http_req_duration
    ✓ 'p(95)<500' p(95)=148.3ms
    ✗ 'p(99)<1000' p(99)=1.79s

    http_req_failed
    ✗ 'rate<0.01' rate=2.05%

  █ TOTAL RESULTS

    http_req_duration..............: avg=108.26ms med=53.5ms   p(95)=148.3ms  p(99)=1.79s max=1.99s
    http_req_failed................: 2.05%  69 out of 3356
    http_reqs......................: 3356   27.710167/s

Agora volte ao Grafana (painel Duration, últimos 5 minutos) ou consulte o Prometheus. Os valores só do /checkout, com a carga rodando:Now go back to Grafana (the Duration panel, last 5 minutes) or query Prometheus. The values for /checkout alone, with the load running:

curl -s -G localhost:9090/api/v1/query --data-urlencode \
 'query=histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{route="/checkout"}[1m])))'
Métrica do /checkout/checkout metricValor típicoTypical valueLeituraReading
p50p50≈ 0,10 s≈ 0.10 sa experiência típica é ótimathe typical experience is great
médiaaverage≈ 0,18 s≈ 0.18 sparece “ok”, mas esconde a caudalooks “fine” but hides the tail
p95p95≈ 0,25 s≈ 0.25 sainda dentro do SLO de 500 msstill within the 500 ms SLO
p99p99≈ 2 s≈ 2 s1 em cada 100 usuários espera ~2 s: SLO violado1 in 100 users waits ~2 s: SLO broken
% de erros% errors≈ 2%≈ 2%acima do limite de 1%above the 1% limit
Uma lição escondida no número: o p99 do Prometheus deu ≈ 2,2 s, mas o k6 mediu ≈ 1,8 s. A lentidão real fica entre 1,5 e 2 s; o histograma só conhece o bucket de 1 s a 2,5 s e interpola dentro dele. Quanto mais largos os buckets perto do valor que importa, mais impreciso o percentil. Desafio: troque buckets em server.js para incluir 1.5 e 2, rode docker compose up -d --build app, repita a carga e compare.A lesson hidden in the number: Prometheus's p99 came out ≈ 2.2 s, but k6 measured ≈ 1.8 s. The real slowness sits between 1.5 and 2 s; the histogram only knows the 1 s to 2.5 s bucket and interpolates inside it. The wider the buckets near the value that matters, the less precise the percentile. Challenge: change buckets in server.js to include 1.5 and 2, run docker compose up -d --build app, repeat the load and compare.

Atividade 5: alertasActivity 5: alerts

Com a carga rodando (ou logo depois), abra http://localhost:9090/alerts. A regra HighLatencyP99 fica pending por 1 minuto (o for:) e então firing; HighErrorRate também. Pela API:With the load running (or right after), open http://localhost:9090/alerts. The HighLatencyP99 rule stays pending for 1 minute (the for:) and then goes firing; so does HighErrorRate. Through the API:

curl -s localhost:9090/api/v1/alerts | grep -o '"alertname":"[A-Za-z0-9]*"\|"state":"[a-z]*"'

Resultado esperado: os alertas disparados.Expected result: the fired alerts.

"alertname":"HighLatencyP99"
"state":"firing"
"alertname":"HighErrorRate"
"state":"firing"

No laboratório não há Alertmanager, então nada é enviado a ninguém; em produção, o Prometheus aponta para um Alertmanager que manda a mensagem ao Slack ou ao PagerDuty. Depois que a carga termina e a janela de 5 minutos esvazia, os alertas voltam ao normal sozinhos. Se editar o rules.yml, aplique com docker compose restart prometheus.The lab has no Alertmanager, so nothing is sent to anyone; in production, Prometheus points to an Alertmanager that posts to Slack or PagerDuty. After the load ends and the 5-minute window drains, the alerts resolve themselves. If you edit rules.yml, apply it with docker compose restart prometheus.

Atividade 6: traces no JaegerActivity 6: traces in Jaeger

O alerta disse que o /checkout está lento. Agora descubra onde. Abra http://localhost:16686, escolha o serviço shop-api, a operação GET /checkout, ponha Min Duration = 1s e clique em Find Traces. Abra um trace. Ou pela API:The alert said that /checkout is slow. Now find where. Open http://localhost:16686, choose the shop-api service, the GET /checkout operation, set Min Duration = 1s and click Find Traces. Open a trace. Or through the API:

curl -s "localhost:16686/api/traces?service=shop-api&operation=GET%20%2Fcheckout&minDuration=1s&limit=2" \
 | python3 -c "
import sys, json
for t in json.load(sys.stdin)['data']:
    spans = sorted(t['spans'], key=lambda s: -s['duration'])[:2]
    print(t['traceID'], [(s['operationName'], s['duration'] // 1000) for s in spans])"

Resultado esperado: quase todo o tempo está no span manual payment-provider.charge (em ms), com o atributo payment.slow = true. A causa não está no Express nem no seu código de rota, mas na chamada ao provedor de pagamento.Expected result: almost all the time is in the manual span payment-provider.charge (in ms), with the attribute payment.slow = true. The cause is not Express nor your route code, but the call to the payment provider.

a0b4fcd7e981c58dfc6fef6ab8c7455a [('GET /checkout', 1677), ('payment-provider.charge', 1675)]
42c31eab59b6ca2e89853d257e72435c [('GET /checkout', 1729), ('payment-provider.charge', 1728)]

Na interface, o trace aparece como barras horizontais (um flame/Gantt): a barra do payment-provider.charge ocupa quase toda a largura do trace. Num sistema real, essas barras seriam chamadas ao banco, ao cache e a outros serviços, e é assim que você acha o culpado em uma arquitetura distribuída.In the UI the trace shows as horizontal bars (a flame/Gantt): the payment-provider.charge bar takes almost the whole width of the trace. In a real system, those bars would be calls to the database, cache and other services, and that is how you find the culprit in a distributed architecture.

Atividade 7: logs no Loki e o trace_idActivity 7: logs in Loki and the trace_id

No Grafana, abra Explore (ícone de bússola), escolha a fonte Loki e consulte os erros do app com LogQL:In Grafana, open Explore (the compass icon), choose the Loki data source and query the app's errors with LogQL:

{service="app"} |= "error"

Resultado esperado: linhas JSON de erro, cada uma com um trace_id. Ao lado do campo aparece um link Jaeger (o campo derivado do datasources.yml): clique e você cai no trace daquela requisição que falhou.Expected result: JSON error lines, each with a trace_id. Next to the field a Jaeger link appears (the derived field from datasources.yml): click it and you land on the trace of that failed request.

{"time":"2026-10-21T12:15:04.678Z","level":"error","msg":"payment provider timeout","trace_id":"41c4b47ae4d055290cf8035332d751b5","route":"/checkout"}
{"time":"2026-10-21T12:15:00.433Z","level":"error","msg":"payment provider timeout","trace_id":"d11ce37091a1f000fd889b7ac959154a","route":"/checkout"}

Outras consultas úteis: {service="app"} | json | slow="true" (só as lentas) e sum(rate({service="app"} |= "error" [1m])) (erros por segundo, a partir dos logs).Other useful queries: {service="app"} | json | slow="true" (slow ones only) and sum(rate({service="app"} |= "error" [1m])) (errors per second, from the logs).

Você acabou de percorrer o fluxo completo de um incidente: alerta (métrica) → trace → log. Esse é o valor de ter os três sinais ligados pelo trace_id.You just walked the full incident flow: alert (metric) → trace → log. That is the value of having the three signals tied together by the trace_id.

Atividade 8: a opção Elasticsearch (percentis sobre eventos brutos)Activity 8: the Elasticsearch option (percentiles over raw events)

Para ver como o outro mundo faz, suba um Elasticsearch de um nó (sem segurança, só no laboratório e só em 127.0.0.1), grave 1.000 eventos de requisição sintéticos e peça os percentis. Aqui o cálculo é sobre cada evento, não sobre buckets:To see how the other world does it, start a single-node Elasticsearch (no security, lab only and on 127.0.0.1 only), store 1,000 synthetic request events and ask for the percentiles. Here the calculation runs over each event, not over buckets:

docker run -d --name es-lab -p 127.0.0.1:9200:9200 \
  -e discovery.type=single-node -e xpack.security.enabled=false \
  -e ES_JAVA_OPTS="-Xms512m -Xmx512m" \
  docker.elastic.co/elasticsearch/elasticsearch:9.1.0

# wait until it answers, then generate 1000 events (3% slow) and bulk-load them
until curl -s localhost:9200 >/dev/null; do sleep 3; done
awk 'BEGIN{srand(42); for(i=0;i<1000;i++){slow=(rand()<0.03); d=slow?1500+rand()*500:50+rand()*100;
  printf "{\"index\":{}}\n{\"@timestamp\":\"2026-01-01T00:00:00Z\",\"route\":\"/checkout\",\"duration_ms\":%d}\n", d}}' > bulk.ndjson
curl -s -H 'Content-Type: application/x-ndjson' -X POST 'localhost:9200/requests/_bulk?refresh=true' --data-binary @bulk.ndjson > /dev/null

curl -s -H 'Content-Type: application/json' 'localhost:9200/requests/_search?size=0&pretty' -d '{
  "aggs": {
    "latency": { "percentiles": { "field": "duration_ms", "percents": [50, 95, 99] } },
    "avg_ms":  { "avg": { "field": "duration_ms" } } } }'

Resultado esperado: de novo, p50 e p95 baixos, p99 enorme e média “enganosa” (os números exatos dependem do seu awk).Expected result: again, low p50 and p95, a huge p99 and a “misleading” average (exact numbers depend on your awk).

  "aggregations" : {
    "latency" : { "values" : { "50.0" : 100.5, "95.0" : 147.0, "99.0" : 1818.0 } },
    "avg_ms"  : { "value" : 147.6 }
  }

Na prática você não carrega dados à mão: o Elastic Agent, o Filebeat ou um OpenTelemetry Collector enviam logs e traces, e o Kibana (a interface) faz os gráficos. O que este exercício mostra é a diferença de modelo: Prometheus guarda contagens agregadas (barato, estimado); Elasticsearch guarda cada evento (caro, exato e pesquisável por qualquer campo).In practice you do not load data by hand: Elastic Agent, Filebeat or an OpenTelemetry Collector ship logs and traces, and Kibana (the UI) draws the charts. What this exercise shows is the model difference: Prometheus stores aggregated counts (cheap, estimated); Elasticsearch stores every event (costly, exact and searchable by any field).

Atividade 9: limparActivity 9: clean up

docker compose down -v          # stops the stack and removes its volumes
docker rm -f es-lab             # only if you ran activity 8
rm -f bulk.ndjson

Desafios para continuarChallenges to continue

  1. Cardinalidade na prática: adicione um rótulo user_id (valor aleatório) ao histograma, gere carga e compare curl -s localhost:8081/metrics | wc -l antes e depois. Veja o crescimento e entenda por que isso derruba o Prometheus.Cardinality in practice: add a user_id label (random value) to the histogram, generate load and compare curl -s localhost:8081/metrics | wc -l before and after. See the growth and understand why it brings Prometheus down.
  2. Seu próprio SLO: escreva uma regra de gravação para a proporção de requisições /checkout abaixo de 500 ms e sem erro, e um painel “orçamento de erro restante”.Your own SLO: write a recording rule for the ratio of /checkout requests under 500 ms and error-free, and an “error budget remaining” panel.
  3. Mais um serviço: faça o /checkout chamar outro serviço (um segundo contêiner) e veja o trace atravessar os dois, com o mesmo trace_id.One more service: make /checkout call another service (a second container) and watch the trace cross both, with the same trace_id.
  4. Alertmanager: adicione o contêiner prom/alertmanager com um receptor de webhook e veja o alerta chegar.Alertmanager: add the prom/alertmanager container with a webhook receiver and watch the alert arrive.
  5. Seus próprios apps: ponha postgres_exporter e node_exporter no Compose e monte um painel USE para o banco (use o Postgres da outra nota).Your own apps: put postgres_exporter and node_exporter in Compose and build a USE panel for the database (use the Postgres from the other note).

Colocar em produção com segurançaRunning it in production, securely

O laboratório tem tudo num só Compose, sem alta disponibilidade e com senha admin. Em produção, o desenho muda em três eixos: onde roda, como os dados são guardados e quem pode ver ou mexer. A arquitetura de referência:The lab has everything in one Compose, with no high availability and an admin password. In production, the design changes along three axes: where it runs, how data is stored and who may see or touch it. The reference architecture:

cluster / VMs app 1 SDK OpenTelemetryOpenTelemetry SDK app 2 SDK OpenTelemetryOpenTelemetry SDK app 3 SDK OpenTelemetryOpenTelemetry SDK /metrics · OTLP · stdout Collector OTel / Alloy Alertmanager → Slack · PagerDuty MétricasMetrics Prometheus / AMP Traces Tempo / X-Ray Logs Loki / CloudWatch Grafana tudo atrás de login/SSO e rede privada; o Grafana é a única janelaeverything behind login/SSO and a private network; Grafana is the only window
Os apps falam só com um coletor; o armazenamento de cada sinal pode ser trocado sem mexer nos apps.Apps talk only to a collector; each signal's storage can be swapped without touching the apps.

Os apps falam só com um coletor (OpenTelemetry Collector ou Grafana Alloy, rodando como agente em cada nó). É o coletor que decide para onde cada sinal vai, faz amostragem e remove dados sensíveis: trocar de fornecedor vira configuração, não reescrita de código.Apps talk only to a collector (OpenTelemetry Collector or Grafana Alloy, running as an agent on each node). The collector decides where each signal goes, samples and strips sensitive data: changing vendor becomes configuration, not a rewrite.

Caminho A: Kubernetes (kube-prometheus-stack)Path A: Kubernetes (kube-prometheus-stack)

Se você já roda Kubernetes, o jeito padrão é o Helm chart kube-prometheus-stack: instala Prometheus (via Operator), Alertmanager, Grafana, node-exporter e kube-state-metrics já configurados, com painéis prontos para o cluster. Os alvos são declarados com um objeto ServiceMonitor, em vez de editar o prometheus.yml:If you already run Kubernetes, the standard way is the kube-prometheus-stack Helm chart: it installs Prometheus (via the Operator), Alertmanager, Grafana, node-exporter and kube-state-metrics already wired, with cluster dashboards included. Targets are declared with a ServiceMonitor object instead of editing prometheus.yml:

kubectl create namespace monitoring
kubectl -n monitoring create secret generic grafana-admin \
  --from-literal=admin-user=admin --from-literal=admin-password="$(openssl rand -base64 24)"

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kps prometheus-community/kube-prometheus-stack -n monitoring \
  --set grafana.admin.existingSecret=grafana-admin \
  --set grafana.admin.userKey=admin-user --set grafana.admin.passwordKey=admin-password
# read the password later with:
# kubectl -n monitoring get secret grafana-admin -o jsonpath='{.data.admin-password}' | base64 -d
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: shop-api
  namespace: monitoring
  labels:
    release: kps                  # must match the chart's serviceMonitorSelector
spec:
  namespaceSelector:
    matchNames: [prod]
  selector:
    matchLabels:
      app: shop-api               # the Service's labels
  endpoints:
    - port: http                  # the Service's port NAME
      path: /metrics
      interval: 30s

Para logs e traces, instale Loki e Tempo (charts da Grafana) e um Alloy como DaemonSet que lê os logs de cada nó. O app envia OTLP para o Collector do cluster (OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.monitoring:4318).For logs and traces, install Loki and Tempo (Grafana charts) and an Alloy as a DaemonSet reading each node's logs. The app sends OTLP to the cluster's Collector (OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.monitoring:4318).

Caminho B: AWS gerenciadaPath B: managed AWS

Para não operar armazenamento, retenção e alta disponibilidade:To avoid operating storage, retention and high availability:

  • Amazon Managed Service for Prometheus (AMP): seu Prometheus (ou o Collector) faz remote_write para um workspace gerenciado e durável; você mantém PromQL e regras.Amazon Managed Service for Prometheus (AMP): your Prometheus (or the Collector) does remote_write to a managed, durable workspace; you keep PromQL and rules.
  • Amazon Managed Grafana (AMG): Grafana gerenciado com login por SSO (IAM Identity Center) e fontes AMP, CloudWatch e X-Ray.Amazon Managed Grafana (AMG): managed Grafana with SSO login (IAM Identity Center) and AMP, CloudWatch and X-Ray sources.
  • CloudWatch: o ALB já publica TargetResponseTime com percentis, sem instrumentar nada; Logs Insights consulta logs; Application Signals dá latência, erros e SLOs por serviço no EKS/ECS.CloudWatch: the ALB already publishes TargetResponseTime with percentiles, with no instrumentation; Logs Insights queries logs; Application Signals gives latency, errors and SLOs per service on EKS/ECS.
  • AWS Distro for OpenTelemetry (ADOT) / X-Ray: coletor suportado pela AWS que envia traces ao X-Ray e métricas ao AMP.AWS Distro for OpenTelemetry (ADOT) / X-Ray: an AWS-supported collector that ships traces to X-Ray and metrics to AMP.
# prometheus.yml: keep scraping locally, also copy everything to AMP (SigV4 = IAM-signed requests, no passwords)
remote_write:
  - url: https://aps-workspaces.us-east-1.amazonaws.com/workspaces/ws-REPLACE-ME/api/v1/remote_write
    sigv4:
      region: us-east-1           # credentials come from the pod/instance IAM role
# p95 and p99 of an ALB with zero instrumentation (CloudWatch extended statistics)
aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB --metric-name TargetResponseTime \
  --dimensions Name=LoadBalancer,Value=app/my-alb/0123456789abcdef \
  --start-time "$(date -u -d '-1 hour' +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
  --period 300 --extended-statistics p95 p99
A conta IAM da escrita no AMP é mínima: aps:RemoteWrite no workspace, atrelada à role do Pod (Pod Identity/IRSA) ou da instância. Sem chaves de acesso no arquivo.The IAM permission to write to AMP is minimal: aps:RemoteWrite on the workspace, attached to the Pod's role (Pod Identity/IRSA) or the instance's. No access keys in the file.

Caminho C: uma VM com Docker ComposePath C: a single VM with Docker Compose

Para um projeto pequeno, o próprio Compose do laboratório serve em uma VM, depois dos ajustes de segurança abaixo: tire o docker.sock, ponha as portas atrás de um proxy reverso com HTTPS e login (ou de uma VPN/Tailscale), troque as senhas, defina retenção e faça backup dos volumes.For a small project, the lab's own Compose works on a VM, after the security changes below: remove docker.sock, put the ports behind a reverse proxy with HTTPS and login (or a VPN/Tailscale), change the passwords, set retention and back up the volumes.

Lista de verificação de produçãoProduction checklist

ÁreaAreaO que fazerWhat to do
AcessoAccessGrafana atrás de SSO/OIDC (nada de admin/admin), sem cadastro aberto, papéis de leitura para a equipe; Prometheus, Alertmanager, Loki e Jaeger nunca expostos à internet (sem autenticação própria forte, entregam seus dados e permitem consultas pesadas).Grafana behind SSO/OIDC (no admin/admin), no open sign-up, read roles for the team; Prometheus, Alertmanager, Loki and Jaeger never exposed to the internet (without strong built-in auth they hand over your data and allow heavy queries).
RedeNetwork/metrics só na rede interna (NetworkPolicy/Security Group); o app responde /metrics em uma porta separada da pública quando possível; TLS entre componentes que cruzam redes./metrics only on the internal network (NetworkPolicy/Security Group); the app serves /metrics on a port separate from the public one when possible; TLS between components crossing networks.
SegredosSecretsSenhas e tokens (webhook do Slack, chave do PagerDuty) em Secrets Manager/Secret do Kubernetes, nunca no docker-compose.yml nem no Git.Passwords and tokens (Slack webhook, PagerDuty key) in Secrets Manager/Kubernetes Secrets, never in docker-compose.yml or Git.
Privilégio do coletorCollector privilegeSem docker.sock: agente por nó (DaemonSet) com RBAC mínimo e leitura dos logs do nó. Nunca privileged.No docker.sock: a per-node agent (DaemonSet) with minimal RBAC and read access to the node's logs. Never privileged.
Dados sensíveisSensitive dataSem PII, senha ou token em logs, rótulos e atributos; redija no app e no Collector (processor de redação); cuide da LGPD/GDPR: retenção curta de logs com dados pessoais.No PII, password or token in logs, labels or attributes; redact in the app and in the Collector (a redaction processor); mind GDPR/LGPD: short retention for logs with personal data.
Cardinalidade e custoCardinality and costRevise séries ativas (prometheus_tsdb_head_series); use sample_limit e label_limit no scrape_config para um app com bug não derrubar o Prometheus; rótulos sem ID.Review active series (prometheus_tsdb_head_series); use sample_limit and label_limit in scrape_config so a buggy app cannot take Prometheus down; no IDs in labels.
Retenção e discoRetention and diskDefina retenção (o padrão do Prometheus é 15 dias; --storage.tsdb.retention.time); volume dedicado, com alerta de espaço; dados de longo prazo via remote_write (AMP, Mimir, Thanos).Set retention (Prometheus defaults to 15 days; --storage.tsdb.retention.time); dedicated volume with a disk-space alert; long-term data via remote_write (AMP, Mimir, Thanos).
Alta disponibilidadeHigh availabilityDois Prometheus idênticos ou remote_write para um armazenamento gerenciado; Alertmanager em cluster (3 réplicas). Monitore o monitor: um alerta “sempre disparando” (dead man's switch) que avisa quando o alerta deixa de chegar.Two identical Prometheus instances or remote_write to managed storage; clustered Alertmanager (3 replicas). Monitor the monitor: an “always firing” alert (a dead man's switch) that tells you when alerts stop arriving.
TracesTracesAmostragem: guarde de 1% a 10% por head sampling, ou 100% dos erros e dos lentos com tail sampling no Collector. Propague o traceparent entre serviços.Sampling: keep 1% to 10% with head sampling, or 100% of errors and slow ones with tail sampling in the Collector. Propagate traceparent between services.
LogsLogsJSON estruturado com trace_id, nível e serviço; níveis debug desligados em produção; amostre logs repetitivos.Structured JSON with trace_id, level and service; debug level off in production; sample repetitive logs.
AlertasAlertsSintoma, não causa; burn rate de SLO em duas janelas; severidade (página × ticket); runbook em cada alerta; revisão mensal dos que mais dispararam.Symptom, not cause; SLO burn rate over two windows; severity (page × ticket); a runbook on every alert; monthly review of the noisiest.
Painéis como códigoDashboards as codeVersione os JSON (provisioning, como no laboratório) ou use o provider Grafana do Terraform. Painel editado só na interface se perde.Version the JSON (provisioning, as in the lab) or use the Grafana provider of Terraform. A dashboard edited only in the UI gets lost.
AtualizaçõesUpgradesFixe versões de imagem (nada de :latest) e atualize de propósito; leia as notas de versão (ex.: mudanças entre Prometheus 2 e 3).Pin image versions (no :latest) and upgrade on purpose; read release notes (e.g. changes between Prometheus 2 and 3).
CulturaCultureDefina SLOs com o produto; revise o orçamento de erro toda semana; faça post-mortems sem culpa e transforme cada incidente em um alerta, painel ou teste novo.Define SLOs with product; review the error budget weekly; run blameless post-mortems and turn every incident into a new alert, dashboard or test.
O erro mais comum não é técnico: é ter painéis lindos que ninguém olha e alertas demais que todos silenciam. Comece com poucos alertas que acordam por sintomas reais, um painel RED por serviço e um SLO combinado com o negócio. Dá para refinar depois; o contrário (apagar 200 alertas ruidosos) é muito mais difícil.The most common failure is not technical: it is beautiful dashboards nobody looks at and too many alerts everyone mutes. Start with a few alerts that wake people for real symptoms, one RED dashboard per service and an SLO agreed with the business. You can refine later; the reverse (deleting 200 noisy alerts) is far harder.

Cola e revisãoCheat sheet and review

ColaCheat sheet

Quero…I want…UsoI use
ver o p95 por rotasee p95 per routehistogram_quantile(0.95, sum by (le, route) (rate(x_bucket[5m])))histogram_quantile(0.95, sum by (le, route) (rate(x_bucket[5m])))
requisições por segundorequests per secondsum(rate(x_count[5m]))sum(rate(x_count[5m]))
% de erros% errorssum(rate(x_count{status_code=~"5.."}[5m])) / sum(rate(x_count[5m]))sum(rate(x_count{status_code=~"5.."}[5m])) / sum(rate(x_count[5m]))
média (para comparar)average (to compare)rate(x_sum[5m]) / rate(x_count[5m])rate(x_sum[5m]) / rate(x_count[5m])
todo mundo está sendo coletado?is everything being scraped?up == 0 (lista o que caiu)up == 0 (lists what is down)
orçamento de erro de 99,9%/30 diaserror budget of 99.9%/30 days0,1% × 43.200 min ≈ 43 min0.1% × 43,200 min ≈ 43 min
achar o trace lentofind the slow traceJaeger/Tempo: serviço + operação + duração mínimaJaeger/Tempo: service + operation + min duration
achar o errofind the errorLoki: {service="app"} |= "error"; clique no trace_idLoki: {service="app"} |= "error"; click the trace_id
RED / USE / golden signalsRED / USE / golden signalsRED por serviço; USE por recurso; latência, tráfego, erros, saturaçãoRED per service; USE per resource; latency, traffic, errors, saturation
evitar derrubar o Prometheusavoid taking Prometheus downrótulos com poucos valores; nunca ID de usuário/requisição/URL crualabels with few values; never user/request IDs or raw URLs

Perguntas de revisãoReview questions

  • Qual a diferença entre monitoramento e observabilidade? O que cada um dos três sinais responde?What is the difference between monitoring and observability? What does each of the three signals answer?
  • O que significa p99 = 2 s? Por que a média não mostra isso?What does p99 = 2 s mean? Why does the average not show it?
  • Por que um p99 de 1% de lentidão por chamada vira 63% das páginas lentas com 100 chamadas?Why does a 1% per-call tail become 63% slow pages with 100 calls?
  • Por que não se faz a média de percentis de vários servidores? Como se calcula o certo?Why do you not average percentiles across servers? How do you compute it correctly?
  • Por que usar Histogram e não Summary para latência? Como a escolha dos buckets afeta o p99?Why use a Histogram rather than a Summary for latency? How does bucket choice affect p99?
  • O que é cardinalidade, e por que user_id como rótulo é um erro?What is cardinality, and why is user_id as a label a mistake?
  • Que alertas valem a pena? Por que alertar em sintomas e não em causas?Which alerts are worth having? Why alert on symptoms rather than causes?
  • Dado um alerta de p99 alto, descreva o caminho até a causa (métrica → trace → log).Given a high-p99 alert, describe the path to the cause (metric → trace → log).
  • Quando escolher Elasticsearch/OpenSearch no lugar de (ou junto com) Loki? E o CloudWatch?When would you choose Elasticsearch/OpenSearch instead of (or alongside) Loki? And CloudWatch?
  • Cite cinco cuidados de segurança ao colocar a observabilidade em produção.Name five security precautions when taking observability to production.