<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: apsis-cc</title>
    <description>The latest articles on DEV Community by apsis-cc (apsis-cc).</description>
    <link>https://dev.to/apsis-cc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14122%2F536c6bfe-3d3a-4372-9fb1-7d8c6d80ccfd.webp</url>
      <title>DEV Community: apsis-cc</title>
      <link>https://dev.to/apsis-cc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apsis-cc"/>
    <language>en</language>
    <item>
      <title>AWS na prática - balanceamento de carga via DNS com o Route 53</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:30:29 +0000</pubDate>
      <link>https://dev.to/apsis-cc/aws-na-pratica-balanceamento-de-carga-via-dns-com-o-route-53-4hdn</link>
      <guid>https://dev.to/apsis-cc/aws-na-pratica-balanceamento-de-carga-via-dns-com-o-route-53-4hdn</guid>
      <description>&lt;h2&gt;
  
  
  1. Um outro tipo de balanceamento
&lt;/h2&gt;

&lt;p&gt;No artigo anterior desta série, subimos instâncias EC2 e as agrupamos em um target group, prontas para receber tráfego de um balanceador de carga tradicional. Mas existe uma camada de decisão que acontece &lt;strong&gt;antes&lt;/strong&gt; de qualquer pacote chegar a um Application Load Balancer: a resolução de nome. Quando um cliente pergunta "qual é o IP de &lt;code&gt;api.minhaempresa.com&lt;/code&gt;?", quem responde pode já estar escolhendo, com base em latência, saúde do destino ou uma política de peso, para qual região ou endpoint mandar esse cliente. É esse balanceamento — feito no &lt;strong&gt;Route 53&lt;/strong&gt;, o serviço de DNS da AWS — que vamos explorar aqui.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Por que balancear no DNS, se já existe um load balancer
&lt;/h2&gt;

&lt;p&gt;Um ALB/NLB distribui tráfego entre instâncias &lt;strong&gt;dentro de uma região&lt;/strong&gt;. O Route 53 resolve um problema diferente: distribuir tráfego &lt;strong&gt;entre regiões, entre contas ou entre serviços completamente distintos&lt;/strong&gt;, antes mesmo de uma conexão TCP ser aberta. Os dois não são concorrentes — na prática, é comum que registros no Route 53 apontem justamente para ALBs em regiões diferentes, formando duas camadas de balanceamento:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cliente → Route 53 (escolhe a região) → ALB da região → Target Group → EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A diferença central de mecanismo importa: um ALB decide por requisição, em tempo real, olhando para o estado atual de cada destino. O Route 53 decide por &lt;strong&gt;resposta de consulta DNS&lt;/strong&gt;, e o cliente (ou, mais frequentemente, o resolver DNS dele) fica com essa resposta em cache pelo tempo definido no TTL do registro — o que torna o balanceamento por DNS mais grosseiro, porém indispensável quando o requisito é geográfico ou cruza fronteiras que um load balancer sozinho não alcança.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Políticas de roteamento do Route 53
&lt;/h2&gt;

&lt;p&gt;O Route 53 suporta várias políticas de roteamento no mesmo nome, cada uma resolvendo um problema diferente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple&lt;/strong&gt;: um registro, uma resposta fixa (ou uma lista, escolhida aleatoriamente pelo cliente). Sem balanceamento real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weighted&lt;/strong&gt;: distribui consultas entre múltiplos endpoints por peso relativo — útil para canary release ou para migrar tráfego gradualmente de um ambiente para outro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency-based&lt;/strong&gt;: responde com o endpoint na região com menor latência medida a partir da localização do resolver que fez a pergunta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failover&lt;/strong&gt;: mantém um endpoint primário e um secundário, alternando automaticamente quando o health check do primário falha.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geolocation&lt;/strong&gt; / &lt;strong&gt;Geoproximity&lt;/strong&gt;: roteia com base na localização geográfica de quem pergunta, útil para requisitos de residência de dados ou conteúdo localizado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multivalue answer&lt;/strong&gt;: retorna várias respostas saudáveis por consulta, funcionando como um DNS round-robin com health check embutido.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Roteamento ponderado (weighted) na prática
&lt;/h2&gt;

&lt;p&gt;Para migrar tráfego gradualmente entre dois ALBs (por exemplo, uma versão antiga e uma nova da aplicação), dois registros com o mesmo nome e pesos diferentes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws route53 change-resource-record-sets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hosted-zone-id&lt;/span&gt; Z1234567890ABC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--change-batch&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "api.minhaempresa.com",
        "Type": "A",
        "SetIdentifier": "versao-atual",
        "Weight": 90,
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "alb-antigo-123456.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E o segundo registro, apontando para o novo ALB com 10% do tráfego:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws route53 change-resource-record-sets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hosted-zone-id&lt;/span&gt; Z1234567890ABC &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--change-batch&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "api.minhaempresa.com",
        "Type": "A",
        "SetIdentifier": "versao-nova",
        "Weight": 10,
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "alb-novo-654321.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aumentar gradualmente o peso do "versao-nova" (10 → 50 → 100) e reduzir o do "versao-atual" na mesma proporção é uma forma simples de canary release sem depender de lógica de roteamento na aplicação.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. O mesmo cenário em Terraform
&lt;/h2&gt;

&lt;p&gt;O provider da AWS modela cada registro ponderado como um &lt;code&gt;aws_route53_record&lt;/code&gt; com &lt;code&gt;set_identifier&lt;/code&gt; e bloco &lt;code&gt;weighted_routing_policy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"api_atual"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.minhaempresa.com"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;
  &lt;span class="nx"&gt;set_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"versao-atual"&lt;/span&gt;

  &lt;span class="nx"&gt;weighted_routing_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;antigo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;antigo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"api_nova"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.minhaempresa.com"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;
  &lt;span class="nx"&gt;set_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"versao-nova"&lt;/span&gt;

  &lt;span class="nx"&gt;weighted_routing_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;novo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;novo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Failover automático com health check
&lt;/h2&gt;

&lt;p&gt;Para um cenário de disaster recovery — um ambiente primário e um standby em outra região — o Route 53 monitora o endpoint primário e só resolve para o secundário quando ele falha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws route53 create-health-check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--caller-reference&lt;/span&gt; &lt;span class="s2"&gt;"hc-api-primario-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-config&lt;/span&gt; &lt;span class="s1"&gt;'{
    "IPAddress": "203.0.113.10",
    "Port": 443,
    "Type": "HTTPS",
    "ResourcePath": "/healthz",
    "RequestInterval": 30,
    "FailureThreshold": 3
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O &lt;code&gt;HealthCheckId&lt;/code&gt; retornado é referenciado nos dois registros de failover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"primario"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.minhaempresa.com"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;
  &lt;span class="nx"&gt;set_identifier&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"primario"&lt;/span&gt;
  &lt;span class="nx"&gt;health_check_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_health_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;failover_routing_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PRIMARY"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regiao_principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regiao_principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"secundario"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.minhaempresa.com"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;
  &lt;span class="nx"&gt;set_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secundario"&lt;/span&gt;

  &lt;span class="nx"&gt;failover_routing_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SECONDARY"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regiao_dr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;regiao_dr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enquanto o health check do primário estiver saudável, todas as consultas recebem o endpoint principal. Assim que três checagens consecutivas falham, o Route 53 passa a responder com o endpoint secundário — sem nenhuma intervenção manual, embora o efeito completo dependa do TTL do registro e do quanto os resolvers dos clientes respeitam esse cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Uma ressalva importante: TTL e propagação
&lt;/h2&gt;

&lt;p&gt;Diferente de um ALB, que reage a mudanças de saúde de um destino em segundos, uma mudança de resposta no Route 53 só se propaga para novos clientes conforme o &lt;strong&gt;TTL&lt;/strong&gt; do registro expira em cada resolver intermediário — e alguns resolvers ignoram TTLs muito baixos. Para cenários de failover, é comum usar um TTL de 30 a 60 segundos nos registros críticos, aceitando uma janela de indisponibilidade parcial maior do que a de um balanceador de camada 4/7, em troca da capacidade de rotear entre regiões inteiras.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusão
&lt;/h2&gt;

&lt;p&gt;Combinar as duas camadas — instâncias EC2 agrupadas em um target group, como vimos no artigo anterior, e políticas de roteamento no Route 53 por cima delas — cobre tanto o balanceamento fino dentro de uma região quanto decisões de mais alto nível: canary release gradual, redução de latência por geografia e failover entre regiões inteiras. A escolha de política (weighted, latency-based, failover) depende do problema: migração gradual de versão pede weighted, presença global pede latency-based, disaster recovery pede failover — e nada impede combinar mais de uma, já que o Route 53 permite políticas aninhadas apontando um conjunto de registros para outro.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo da AWS — &lt;a href="https://commons.wikimedia.org/wiki/File:Amazon_Web_Services_Logo.svg" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;, marca da Amazon.com, Inc. (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html" rel="noopener noreferrer"&gt;Amazon Route 53 — Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html" rel="noopener noreferrer"&gt;Route 53 — Choosing a Routing Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record" rel="noopener noreferrer"&gt;Terraform Provider: AWS — aws_route53_record&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>route53</category>
      <category>dns</category>
      <category>elb</category>
    </item>
    <item>
      <title>AWS na prática - subindo instâncias EC2 e um target group do zero</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Tue, 13 Oct 2026 09:30:20 +0000</pubDate>
      <link>https://dev.to/apsis-cc/aws-na-pratica-subindo-instancias-ec2-e-um-target-group-do-zero-b2p</link>
      <guid>https://dev.to/apsis-cc/aws-na-pratica-subindo-instancias-ec2-e-um-target-group-do-zero-b2p</guid>
      <description>&lt;h2&gt;
  
  
  1. Uma nova série: balanceamento de carga na AWS
&lt;/h2&gt;

&lt;p&gt;Colocar uma aplicação em produção quase nunca significa "uma máquina rodando o código". Significa várias instâncias idênticas, distribuição de tráfego entre elas, substituição automática de quem falhar e, cada vez mais, decisões de roteamento que acontecem antes mesmo do tráfego chegar a um balanceador — no próprio DNS. Nesta nova série de dois artigos, vamos construir esse cenário na AWS: primeiro, subindo instâncias EC2 e organizando-as em um &lt;strong&gt;target group&lt;/strong&gt;; depois, no próximo artigo, olhando para o &lt;strong&gt;Route 53&lt;/strong&gt; e como balanceamento de carga também acontece na camada de resolução de nomes.&lt;/p&gt;

&lt;p&gt;Começamos pela base: o que é uma instância EC2, como criar algumas delas de forma repetível, e como agrupá-las para que um balanceador (ou outro consumidor) saiba para onde mandar tráfego.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. O que é o EC2
&lt;/h2&gt;

&lt;p&gt;O &lt;strong&gt;Amazon EC2 (Elastic Compute Cloud)&lt;/strong&gt; é o serviço de máquinas virtuais da AWS. Cada instância roda em um &lt;strong&gt;tipo&lt;/strong&gt; (&lt;code&gt;t3.micro&lt;/code&gt;, &lt;code&gt;m5.large&lt;/code&gt;, etc.) que define CPU, memória e características de rede, a partir de uma &lt;strong&gt;AMI&lt;/strong&gt; (Amazon Machine Image) — uma imagem de disco com o sistema operacional e, opcionalmente, software já instalado. Os elementos que mais importam ao planejar uma frota de instâncias por trás de um balanceador são:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subnet&lt;/strong&gt;: em qual sub-rede (e portanto em qual zona de disponibilidade) a instância nasce. Distribuir instâncias em subnets de AZs diferentes é o que garante que a queda de um datacenter não tire a aplicação do ar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Group&lt;/strong&gt;: o firewall da instância, controlando portas de entrada e saída.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key pair&lt;/strong&gt;: o par de chaves usado para acesso SSH inicial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User data&lt;/strong&gt;: um script executado no primeiro boot, útil para instalar e subir a aplicação sem depender de uma AMI customizada.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Criando uma instância EC2 via CLI
&lt;/h2&gt;

&lt;p&gt;Para ilustrar de forma reprodutível, os exemplos abaixo usam a AWS CLI. Primeiro, um security group liberando HTTP e SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-security-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-name&lt;/span&gt; web-servers-sg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"SG para instancias web atras do target group"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-0123456789abcdef0

aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0

aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; sg-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 10.0.0.0/16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com o security group pronto, sobe-se a instância com um &lt;code&gt;user data&lt;/code&gt; que instala e inicia um servidor web mínimo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 run-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-id&lt;/span&gt; ami-0abcdef1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-type&lt;/span&gt; t3.micro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-name&lt;/span&gt; minha-chave &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; sg-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; subnet-0aaa111122223333 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=web-01}]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data&lt;/span&gt; &lt;span class="s1"&gt;'#!/bin/bash
yum install -y nginx
systemctl enable nginx
systemctl start nginx'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repetir o comando trocando &lt;code&gt;subnet-id&lt;/code&gt; (para uma subnet em outra AZ) e a tag &lt;code&gt;Name&lt;/code&gt; cria uma segunda instância idêntica em termos de aplicação, mas fisicamente isolada da primeira — o ponto de partida para alta disponibilidade.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. O mesmo cenário em Terraform
&lt;/h2&gt;

&lt;p&gt;Rodar &lt;code&gt;run-instances&lt;/code&gt; manualmente não escala nem é repetível. Em um projeto real, o mesmo cenário costuma ser descrito em Terraform, usando &lt;code&gt;count&lt;/code&gt; para gerar N instâncias distribuídas em subnets diferentes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"subnet_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"subnet-0aaa111122223333"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"subnet-0bbb444455556666"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-0abcdef1234567890"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"minha-chave"&lt;/span&gt;

  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
    #!/bin/bash
    yum install -y nginx
    systemctl enable nginx
    systemctl start nginx
&lt;/span&gt;&lt;span class="no"&gt;  EOF

&lt;/span&gt;  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"web-${count.index + 1}"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Criando o target group
&lt;/h2&gt;

&lt;p&gt;Um &lt;strong&gt;target group&lt;/strong&gt; é a lista de destinos (instâncias EC2, IPs ou funções Lambda) para onde um balanceador de carga da AWS (ALB ou NLB) encaminha tráfego, junto com a configuração de &lt;strong&gt;health check&lt;/strong&gt; que decide se cada destino está saudável o suficiente para receber requisições:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws elbv2 create-target-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; web-servers-tg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; HTTP &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-0123456789abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-protocol&lt;/span&gt; HTTP &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-path&lt;/span&gt; /healthz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-interval-seconds&lt;/span&gt; 15 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--healthy-threshold-count&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unhealthy-threshold-count&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O comando retorna um &lt;code&gt;TargetGroupArn&lt;/code&gt;, usado tanto para registrar instâncias quanto para, no próximo artigo, anexar o target group a um listener de balanceador.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Registrando as instâncias no target group
&lt;/h2&gt;

&lt;p&gt;Com as instâncias no ar e o target group criado, o passo final é registrar cada instância como um destino:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws elbv2 register-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web-servers-tg/abcdef1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="nv"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;i-0123456789abcdef0 &lt;span class="nv"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;i-0fedcba9876543210
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partir daí, o próprio target group passa a monitorar a saúde de cada instância via o health check configurado no passo anterior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws elbv2 describe-target-health &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web-servers-tg/abcdef1234567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A saída mostra o estado (&lt;code&gt;healthy&lt;/code&gt;, &lt;code&gt;unhealthy&lt;/code&gt;, &lt;code&gt;initial&lt;/code&gt;) de cada instância registrada — é esse estado que um Application Load Balancer usa para decidir se deve ou não rotear tráfego para um destino específico.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Por que isso importa antes de falar em balanceador
&lt;/h2&gt;

&lt;p&gt;Vale notar que, até aqui, nenhum balanceador de carga foi criado — só a infraestrutura que ele vai consumir. Essa separação é deliberada: um target group existe de forma independente do listener/load balancer que o usa, o que permite trocar de estratégia de roteamento (um ALB clássico, regras baseadas em path, ou o roteamento por DNS que veremos a seguir) sem recriar as instâncias nem o agrupamento delas.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusão
&lt;/h2&gt;

&lt;p&gt;Instâncias EC2 distribuídas em múltiplas zonas de disponibilidade e agrupadas em um target group com health check ativo são a base de qualquer arquitetura de alta disponibilidade na AWS — é esse agrupamento que qualquer balanceador, do ALB clássico a estratégias mais avançadas, vai consultar para saber quem está apto a receber tráfego. No próximo artigo desta série, vamos sair da camada de balanceador tradicional e olhar para o &lt;strong&gt;Route 53&lt;/strong&gt;: como o próprio DNS pode distribuir tráfego entre regiões e endpoints, com políticas de roteamento ponderado, por latência e failover automático.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo da AWS — &lt;a href="https://commons.wikimedia.org/wiki/File:Amazon_Web_Services_Logo.svg" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;, marca da Amazon.com, Inc. (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ec2/index.html" rel="noopener noreferrer"&gt;Amazon EC2 — User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html" rel="noopener noreferrer"&gt;Elastic Load Balancing — Target Groups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance" rel="noopener noreferrer"&gt;Terraform Provider: AWS — aws_instance&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>ec2</category>
      <category>elb</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Segurança e governança em infraestrutura - IAM com menor privilégio na prática</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:30:37 +0000</pubDate>
      <link>https://dev.to/apsis-cc/seguranca-e-governanca-em-infraestrutura-iam-com-menor-privilegio-na-pratica-1lid</link>
      <guid>https://dev.to/apsis-cc/seguranca-e-governanca-em-infraestrutura-iam-com-menor-privilegio-na-pratica-1lid</guid>
      <description>&lt;h2&gt;
  
  
  1. O problema: privilégio excessivo é a norma, não a exceção
&lt;/h2&gt;

&lt;p&gt;Nos dois artigos anteriores desta série, vimos como evitar que segredos vazem em texto puro e como bloquear recursos inseguros antes do apply com Policy as Code. Fechamos a série com um problema mais silencioso, porque não gera erro nem alerta: permissões de IAM concedidas com mais alcance do que o necessário. Uma role de aplicação com &lt;code&gt;s3:*&lt;/code&gt; quando só precisa de &lt;code&gt;s3:GetObject&lt;/code&gt; em um bucket específico. Um usuário de CI com &lt;code&gt;AdministratorAccess&lt;/code&gt; porque "assim resolve na hora e ninguém precisa pedir mais permissão depois". Nada disso quebra nada — até o dia em que uma credencial é comprometida, e o raio de ação do atacante é exatamente do tamanho da permissão que ninguém restringiu.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Erros comuns de permissão excessiva
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Actions com wildcard:&lt;/strong&gt; &lt;code&gt;"Action": "s3:*"&lt;/code&gt; ou &lt;code&gt;"Action": "*"&lt;/code&gt; em vez de listar exatamente as operações necessárias (&lt;code&gt;s3:GetObject&lt;/code&gt;, &lt;code&gt;s3:PutObject&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources com wildcard:&lt;/strong&gt; &lt;code&gt;"Resource": "*"&lt;/code&gt; quando a intenção real era restringir a um bucket ou prefixo específico.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles compartilhadas entre serviços diferentes:&lt;/strong&gt; uma única role "backend-role" usada por várias Lambdas/serviços com necessidades distintas, acumulando permissões de todos ao longo do tempo porque é mais fácil reaproveitar do que criar uma nova.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credenciais de longa duração:&lt;/strong&gt; access keys de usuário IAM sem rotação, em vez de roles assumidas temporariamente (via &lt;code&gt;sts:AssumeRole&lt;/code&gt;) ou identidades federadas (OIDC em pipelines de CI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissões concedidas e nunca revisadas:&lt;/strong&gt; um acesso liberado para resolver um incidente pontual ("dá acesso de admin no S3 pra debugar isso rápido") que nunca é removido depois.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Desenhando políticas mínimas: metodologia
&lt;/h2&gt;

&lt;p&gt;Least privilege na prática raramente é "acertar de primeira" — é um processo iterativo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Comece restritivo, não permissivo.&lt;/strong&gt; Ao criar uma role nova, liste apenas as actions que o código efetivamente chama, mesmo que isso signifique testar e ajustar algumas vezes até funcionar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use os logs de acesso reais para calibrar.&lt;/strong&gt; Ferramentas como o &lt;strong&gt;IAM Access Analyzer&lt;/strong&gt; (AWS) geram recomendações de política baseadas no uso real registrado em CloudTrail — em vez de adivinhar quais actions são necessárias, você vê quais foram efetivamente chamadas em um período.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrinja por &lt;code&gt;Resource&lt;/code&gt;, não só por &lt;code&gt;Action&lt;/code&gt;.&lt;/strong&gt; Permitir &lt;code&gt;s3:GetObject&lt;/code&gt; em &lt;code&gt;*&lt;/code&gt; ainda é privilégio excessivo se a aplicação só precisa de um bucket. Sempre que a API do serviço suportar, especifique o ARN exato.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adicione condições (&lt;code&gt;Condition&lt;/code&gt;) quando possível.&lt;/strong&gt; Restringir por IP de origem, exigir MFA, ou limitar por tag do recurso são camadas adicionais que reduzem o impacto de uma credencial vazada.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Exemplo prático: de permissivo a mínimo
&lt;/h2&gt;

&lt;p&gt;Política de partida, comum em código copiado de um tutorial genérico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Versão com least privilege, depois de identificar que a aplicação só lê objetos de um bucket específico, sob um prefixo específico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::app-uploads-prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::app-uploads-prod/incoming/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em Terraform, o mesmo desenho, evitando reescrever o JSON manualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"app_uploads_read"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"${aws_s3_bucket.app_uploads.arn}/incoming/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:PrincipalTag/environment"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"app_uploads_read"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-uploads-read-only"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_uploads_read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Automatizando a auditoria de acessos
&lt;/h2&gt;

&lt;p&gt;Desenhar a política mínima na criação não é suficiente — permissões tendem a acumular ao longo do tempo, e é preciso um processo contínuo para detectar isso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IAM Access Analyzer&lt;/strong&gt; identifica automaticamente recursos acessíveis por entidades externas à conta (buckets, roles, chaves KMS) e também gera relatórios de "permissões concedidas versus permissões usadas nos últimos 90 dias", apontando o que pode ser removido:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws accessanalyzer list-findings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--analyzer-arn&lt;/span&gt; arn:aws:access-analyzer:us-east-1:123456789012:analyzer/default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s1"&gt;'{"status": {"eq": ["ACTIVE"]}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cloudsplaining&lt;/strong&gt;, uma ferramenta open source, escaneia políticas IAM de uma conta AWS inteira e sinaliza padrões arriscados (wildcards, privilege escalation, ausência de resource constraints) em um relatório HTML:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;cloudsplaining

cloudsplaining download &lt;span class="nt"&gt;--profile&lt;/span&gt; production
cloudsplaining scan &lt;span class="nt"&gt;--input-file&lt;/span&gt; default.json &lt;span class="nt"&gt;--output&lt;/span&gt; results/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integrando com o pipeline de IaC:&lt;/strong&gt; o mesmo &lt;code&gt;conftest&lt;/code&gt;/OPA visto no artigo anterior desta série pode avaliar documentos de política IAM gerados pelo Terraform antes do apply, bloqueando qualquer &lt;code&gt;"Resource": "*"&lt;/code&gt; combinado com uma action de escrita, por exemplo:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iam&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt;

    &lt;span class="n"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Statement&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Effect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;

    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"política '%s' usa Resource=\"&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;restrinja&lt;/span&gt; &lt;span class="n"&gt;ao&lt;/span&gt; &lt;span class="n"&gt;ARN&lt;/span&gt; &lt;span class="n"&gt;específico&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Rotação e identidades temporárias
&lt;/h2&gt;

&lt;p&gt;Além de restringir o que uma credencial pode fazer, reduzir &lt;strong&gt;por quanto tempo&lt;/strong&gt; ela é válida limita ainda mais o impacto de um vazamento:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefira &lt;strong&gt;roles assumidas via &lt;code&gt;sts:AssumeRole&lt;/code&gt;&lt;/strong&gt; a usuários IAM com access key fixa sempre que possível.&lt;/li&gt;
&lt;li&gt;Em pipelines de CI, use &lt;strong&gt;OIDC federation&lt;/strong&gt; (GitHub Actions, GitLab CI já suportam nativamente) para que o pipeline assuma uma role temporária sem nenhuma credencial de longa duração armazenada como secret:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions assumindo uma role via OIDC, sem access key fixa&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/github-actions-deploy&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Conclusão da série
&lt;/h2&gt;

&lt;p&gt;Segredos guardados corretamente, recursos validados por política antes do apply e permissões desenhadas com o mínimo privilégio necessário são três frentes independentes, mas que se reforçam: um segredo vazado importa menos se a credencial associada tem alcance limitado; uma política de IAM bem desenhada é, ela mesma, um alvo natural de checagem via Policy as Code. Nenhuma automação de infraestrutura está completa sem tratar segurança e governança como parte do pipeline, não como uma revisão manual que acontece (ou não) antes de ir para produção.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial da HashiCorp, mantenedora do Vault — &lt;a href="https://commons.wikimedia.org/wiki/File:HashiCorp_Logo.png" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" rel="noopener noreferrer"&gt;AWS IAM — Grant Least Privilege&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html" rel="noopener noreferrer"&gt;AWS IAM Access Analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudsplaining.readthedocs.io/" rel="noopener noreferrer"&gt;cloudsplaining — Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>iam</category>
      <category>security</category>
      <category>cloud</category>
      <category>aws</category>
    </item>
    <item>
      <title>Segurança e governança em infraestrutura - policy as code com OPA</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:30:54 +0000</pubDate>
      <link>https://dev.to/apsis-cc/seguranca-e-governanca-em-infraestrutura-policy-as-code-com-opa-2lk9</link>
      <guid>https://dev.to/apsis-cc/seguranca-e-governanca-em-infraestrutura-policy-as-code-com-opa-2lk9</guid>
      <description>&lt;h2&gt;
  
  
  1. O problema: erros que só aparecem depois do apply
&lt;/h2&gt;

&lt;p&gt;No artigo anterior desta série, vimos como evitar que segredos vazem em texto puro. Mas existe uma categoria de erro igualmente comum e mais sutil: um &lt;code&gt;terraform apply&lt;/code&gt; tecnicamente correto — sem erro de sintaxe, sem segredo exposto — que ainda assim cria um recurso &lt;strong&gt;inseguro&lt;/strong&gt;: um bucket S3 público, um security group liberando &lt;code&gt;0.0.0.0/0&lt;/code&gt; na porta 22, um disco sem criptografia. Nada disso quebra o &lt;code&gt;plan&lt;/code&gt;. Só um revisor humano atento (ou um incidente) descobre depois.&lt;/p&gt;

&lt;p&gt;Revisão manual de código não escala e é falível — ninguém lê 40 arquivos &lt;code&gt;.tf&lt;/code&gt; de um PR grande linha a linha procurando um &lt;code&gt;cidr_blocks&lt;/code&gt; errado. É exatamente esse problema que o &lt;strong&gt;Policy as Code&lt;/strong&gt; resolve: transformar regras de segurança em código executável, que roda automaticamente antes do apply e bloqueia o que violar a política — sem depender de alguém lembrar de olhar.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. O que é policy as code
&lt;/h2&gt;

&lt;p&gt;Policy as Code é a prática de escrever regras de governança (segurança, compliance, convenções de nomenclatura, tags obrigatórias) como código versionado, testável e executado automaticamente em algum ponto do pipeline — geralmente entre o &lt;code&gt;plan&lt;/code&gt; e o &lt;code&gt;apply&lt;/code&gt;. As duas ferramentas mais usadas no ecossistema de IaC são:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OPA (Open Policy Agent)&lt;/strong&gt;, um motor de políticas open source e agnóstico de ferramenta, mantido pela CNCF, com sua própria linguagem de regras chamada &lt;strong&gt;Rego&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel&lt;/strong&gt;, a solução proprietária da HashiCorp, disponível no Terraform Cloud/Enterprise, com integração nativa ao plano do Terraform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos focar no OPA por ser open source, gratuito e utilizável em qualquer pipeline (GitHub Actions, GitLab CI, Jenkins), independente de estar ou não no Terraform Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Rego: a linguagem de políticas do OPA
&lt;/h2&gt;

&lt;p&gt;Rego é uma linguagem declarativa focada em fazer perguntas sobre dados estruturados (tipicamente JSON). Uma política Rego não "executa passos" — ela declara condições que, se verdadeiras, geram uma violação (&lt;code&gt;deny&lt;/code&gt;) ou uma decisão (&lt;code&gt;allow&lt;/code&gt;). O bloco básico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# condição que, se verdadeira, gera uma mensagem de violação&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s2"&gt;"exemplo de violação"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O padrão mais comum ao avaliar Terraform é gerar o plano em JSON e alimentar essa estrutura para o OPA avaliar contra as políticas.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Exemplo prático: bloquear bucket S3 público
&lt;/h2&gt;

&lt;p&gt;Considere este recurso, que qualquer pessoa pode escrever por engano ao copiar um exemplo de blog desatualizado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-logs-prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uma política Rego para bloquear esse padrão, avaliando o plano em JSON gerado por &lt;code&gt;terraform show -json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;

&lt;span class="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt;

    &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;

    &lt;span class="ow"&gt;some&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"block_public_acls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"block_public_policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="s2"&gt;"ignore_public_acls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"restrict_public_buckets"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"recurso '%s' permite acesso público (%s = false)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Rodando a política com Conftest
&lt;/h2&gt;

&lt;p&gt;O &lt;a href="https://www.conftest.dev/" rel="noopener noreferrer"&gt;Conftest&lt;/a&gt; é uma CLI que facilita testar arquivos estruturados (JSON, YAML, planos de Terraform) contra políticas Rego, sem precisar montar toda a infraestrutura de servidor do OPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Gera o plano em formato binário e depois em JSON&lt;/span&gt;
terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tfplan.binary
terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; tfplan.binary &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; tfplan.json

&lt;span class="c"&gt;# Avalia o plano contra as políticas no diretório policy/&lt;/span&gt;
conftest &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--policy&lt;/span&gt; policy/ tfplan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se a política do passo anterior for violada, a saída é algo como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAIL - tfplan.json - terraform.security - recurso 'aws_s3_bucket_public_access_block.logs' permite acesso público (block_public_acls = false)

1 test, 0 passed, 1 failed, 0 warnings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O código de saída não-zero do &lt;code&gt;conftest&lt;/code&gt; é o que permite usar esse comando como um &lt;strong&gt;gate&lt;/strong&gt; de CI: se falhar, o pipeline para antes de chegar no &lt;code&gt;apply&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Sentinel como alternativa
&lt;/h2&gt;

&lt;p&gt;Para quem já usa Terraform Cloud/Enterprise, o Sentinel oferece o mesmo conceito com integração nativa — as políticas rodam automaticamente entre &lt;code&gt;plan&lt;/code&gt; e &lt;code&gt;apply&lt;/code&gt;, sem precisar montar o pipeline de &lt;code&gt;conftest&lt;/code&gt; manualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tfplan/v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tfplan&lt;/span&gt;

&lt;span class="n"&gt;s3_buckets_publicos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;tfplan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_changes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_s3_bucket_public_access_block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_public_acls&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3_buckets_publicos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sintaxe é diferente do Rego, mas a ideia é idêntica: inspecionar o plano e recusar o apply se alguma condição de segurança não for satisfeita. A escolha entre OPA e Sentinel tende a ser menos sobre capacidade técnica e mais sobre onde o time já está: fora do Terraform Cloud, OPA/Conftest é a opção sem lock-in; dentro dele, o Sentinel remove a etapa de integração.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Onde rodar as políticas no pipeline
&lt;/h2&gt;

&lt;p&gt;O ganho de Policy as Code só se realiza se a checagem for &lt;strong&gt;obrigatória&lt;/strong&gt;, não opcional. Um pipeline típico de GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/terraform.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;plan-and-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -out=tfplan.binary&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform show -json tfplan.binary &amp;gt; tfplan.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Instalar conftest&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsSL https://github.com/open-policy-agent/conftest/releases/latest/download/conftest_linux_x86_64.tar.gz | tar xz&lt;/span&gt;
          &lt;span class="s"&gt;sudo mv conftest /usr/local/bin&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Avaliar políticas de segurança&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;conftest test --policy policy/ tfplan.json&lt;/span&gt;

      &lt;span class="c1"&gt;# O apply só roda se o passo anterior não falhar&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply -auto-approve tfplan.binary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Colocar a checagem &lt;strong&gt;antes&lt;/strong&gt; do apply, como um passo que pode falhar o job, é o que transforma a política de "documentação que ninguém lê" em "barreira que ninguém consegue pular sem querer".&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusão
&lt;/h2&gt;

&lt;p&gt;Policy as Code fecha uma lacuna que revisão manual de código não cobre bem: garantir, de forma automática e repetível, que todo recurso criado por IaC respeita as regras de segurança da organização — sem depender da atenção de quem está revisando o PR. OPA com Rego (via Conftest) e Sentinel resolvem o mesmo problema com integrações diferentes; o que importa é que a checagem rode como gate obrigatório no pipeline, não como sugestão. No próximo e último artigo desta série, entramos em outro pilar de governança: como desenhar permissões de IAM com o mínimo privilégio necessário — e como auditar, de forma automatizada, quando isso deixa de ser verdade.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial da HashiCorp, mantenedora do Vault — &lt;a href="https://commons.wikimedia.org/wiki/File:HashiCorp_Logo.png" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.openpolicyagent.org/docs/latest/" rel="noopener noreferrer"&gt;Open Policy Agent — Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.conftest.dev/" rel="noopener noreferrer"&gt;Conftest — Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/sentinel/docs" rel="noopener noreferrer"&gt;HashiCorp Sentinel — Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>opa</category>
      <category>security</category>
      <category>iac</category>
      <category>devops</category>
    </item>
    <item>
      <title>Segurança e governança em infraestrutura - secrets management na prática</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:30:52 +0000</pubDate>
      <link>https://dev.to/apsis-cc/seguranca-e-governanca-em-infraestrutura-secrets-management-na-pratica-31ab</link>
      <guid>https://dev.to/apsis-cc/seguranca-e-governanca-em-infraestrutura-secrets-management-na-pratica-31ab</guid>
      <description>&lt;h2&gt;
  
  
  1. Uma nova série: segurança e governança em infraestrutura
&lt;/h2&gt;

&lt;p&gt;Provisionar infraestrutura com código, versionar tudo em Git e automatizar o deploy resolve boa parte dos problemas operacionais de um time — mas também cria uma nova superfície de risco. Um &lt;code&gt;terraform apply&lt;/code&gt; errado pode expor um bucket ao mundo; um segredo comitado por engano fica para sempre no histórico do repositório; uma role com permissão de mais é só uma credencial roubada de distância de um incidente grave. Nesta nova série, vamos tratar de três pilares de segurança e governança que toda automação de infraestrutura precisa levar em conta: &lt;strong&gt;gestão de segredos&lt;/strong&gt;, &lt;strong&gt;policy as code&lt;/strong&gt; e &lt;strong&gt;least privilege em IAM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Começamos pelo problema mais básico e, ainda assim, um dos mais comuns em auditorias de segurança: como guardar e distribuir segredos sem deixá-los em texto puro em lugar nenhum.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. O problema de guardar segredos em texto puro
&lt;/h2&gt;

&lt;p&gt;É extremamente comum encontrar, em projetos reais, segredos vazando por canais aparentemente inofensivos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Um &lt;code&gt;terraform.tfvars&lt;/code&gt; com &lt;code&gt;db_password = "supersecreta123"&lt;/code&gt; comitado "só para testar localmente" — e que nunca foi removido do histórico do Git.&lt;/li&gt;
&lt;li&gt;Variáveis de ambiente com credenciais de nuvem definidas direto no arquivo de configuração do CI (visíveis em texto puro para qualquer pessoa com acesso de leitura ao pipeline).&lt;/li&gt;
&lt;li&gt;O próprio &lt;strong&gt;state file&lt;/strong&gt; do Terraform, que guarda em texto puro todos os atributos dos recursos que gerencia — incluindo senhas de banco de dados criadas via &lt;code&gt;random_password&lt;/code&gt; ou strings de conexão completas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;O problema não é só o vazamento inicial. Um segredo comitado no Git continua acessível no histórico mesmo depois de removido do arquivo atual — é preciso reescrever o histórico (&lt;code&gt;git filter-repo&lt;/code&gt;, BFG) e ainda assim tratar o segredo como comprometido, porque qualquer clone anterior já o tem. A prática correta é nunca deixar o segredo chegar ao repositório: ele deve viver em um sistema dedicado, com controle de acesso, auditoria e rotação.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Introdução ao Vault
&lt;/h2&gt;

&lt;p&gt;O &lt;a href="https://www.vaultproject.io/" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt; é o sistema de gestão de segredos mais usado no ecossistema de IaC. Os conceitos centrais são:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secrets engines:&lt;/strong&gt; módulos plugáveis que definem como os segredos são gerados e armazenados. O mais simples é o &lt;strong&gt;KV (key-value)&lt;/strong&gt;, que guarda pares chave-valor estáticos, criptografados em repouso. Outros, como o engine da AWS, geram &lt;strong&gt;segredos dinâmicos&lt;/strong&gt;: credenciais IAM criadas sob demanda, com tempo de vida limitado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leases:&lt;/strong&gt; todo segredo dinâmico tem um lease (contrato de validade). Quando o lease expira, o Vault revoga a credencial automaticamente — sem depender de ninguém lembrar de fazer rotação manual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Políticas de acesso:&lt;/strong&gt; definem quem pode ler/escrever em quais paths do Vault, integradas com métodos de autenticação (tokens, AppRole, Kubernetes service accounts, OIDC, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A alternativa mais comum fora do universo HashiCorp é o &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; (ou seu equivalente no Azure/GCP), que resolve o mesmo problema de forma mais simples quando o time já está 100% dentro de uma única nuvem, trocando flexibilidade multi-cloud por integração nativa e menos operação própria. Os exemplos abaixo usam Vault, mas o raciocínio de "nunca commitar segredo, sempre buscar em runtime" é o mesmo nos dois.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Subindo um Vault local para experimentar
&lt;/h2&gt;

&lt;p&gt;Para fins de estudo, o Vault tem um modo de desenvolvimento que roda tudo em memória, sem persistência real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault server &lt;span class="nt"&gt;-dev&lt;/span&gt; &lt;span class="nt"&gt;-dev-root-token-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"root-token-local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Em outro terminal, apontando o cliente para o servidor local:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:8200"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"root-token-local"&lt;/span&gt;

vault status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Guardando e lendo segredos no KV
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Habilita o secrets engine KV versão 2 no path "secret/"&lt;/span&gt;
vault secrets &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret kv-v2

&lt;span class="c"&gt;# Guarda um segredo&lt;/span&gt;
vault kv put secret/app/database &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app_user"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"uma-senha-bem-forte"&lt;/span&gt;

&lt;span class="c"&gt;# Lê o segredo de volta&lt;/span&gt;
vault kv get secret/app/database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O KV v2 guarda versões de cada segredo, o que permite auditar quando um valor mudou e fazer rollback se necessário — algo que uma variável de ambiente jogada num &lt;code&gt;.env&lt;/code&gt; nunca oferece.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Integrando Vault com Terraform
&lt;/h2&gt;

&lt;p&gt;O provider oficial do Vault para Terraform permite ler segredos em tempo de &lt;code&gt;plan&lt;/code&gt;/&lt;code&gt;apply&lt;/code&gt;, sem que eles apareçam em nenhum arquivo &lt;code&gt;.tf&lt;/code&gt; versionado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"vault"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:8200"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"vault_kv_secret_v2"&lt;/span&gt; &lt;span class="s2"&gt;"database"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secret"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app/database"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-db"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;

  &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vault_kv_secret_v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vault_kv_secret_v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um cuidado importante: o valor lido ainda vai parar no &lt;strong&gt;state file&lt;/strong&gt;, em texto puro, porque é assim que o Terraform rastreia atributos de recursos. Isso não elimina a necessidade de um backend de state criptografado (S3 com SSE, ou o encryption nativo do OpenTofu visto na série anterior) — o Vault resolve o problema de "de onde o segredo vem", não o de "onde o resultado final fica guardado depois".&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Segredos dinâmicos: indo além do KV estático
&lt;/h2&gt;

&lt;p&gt;O ganho real de segurança do Vault aparece nos secrets engines dinâmicos. Em vez de guardar uma credencial fixa da AWS, o Vault pode gerar uma nova a cada solicitação, com expiração automática:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Habilita o secrets engine da AWS&lt;/span&gt;
vault secrets &lt;span class="nb"&gt;enable &lt;/span&gt;aws

vault write aws/config/root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"AKIA..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;secret_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;

&lt;span class="c"&gt;# Define uma role que gera credenciais com uma policy IAM específica&lt;/span&gt;
vault write aws/roles/deploy-readonly &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;credential_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"iam_user"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;policy_document&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "*" }
  ]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Gera uma credencial temporária, válida por um tempo limitado&lt;/span&gt;
vault &lt;span class="nb"&gt;read &lt;/span&gt;aws/creds/deploy-readonly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada &lt;code&gt;vault read&lt;/code&gt; acima gera um par de chaves &lt;strong&gt;novo&lt;/strong&gt;, com lease próprio. Se uma pipeline de CI usa esse fluxo, uma credencial vazada em um log específico expira sozinha em minutos ou horas, em vez de ficar válida indefinidamente como uma chave de usuário IAM tradicional.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusão
&lt;/h2&gt;

&lt;p&gt;Segredos em texto puro no repositório — ou em variáveis de ambiente de CI sem controle — são um dos vetores de incidente mais comuns e mais fáceis de evitar em automação de infraestrutura. Um secrets manager dedicado (Vault, AWS Secrets Manager ou equivalente) resolve isso com armazenamento criptografado, controle de acesso granular e, no melhor caso, credenciais dinâmicas de curta duração. No próximo artigo desta série, vamos olhar para outro tipo de risco: recursos inseguros que passam pelo &lt;code&gt;apply&lt;/code&gt; porque nenhuma regra automatizada os bloqueou antes — e como o &lt;strong&gt;Policy as Code&lt;/strong&gt; resolve isso com OPA.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial da HashiCorp, mantenedora do Vault — &lt;a href="https://commons.wikimedia.org/wiki/File:HashiCorp_Logo.png" rel="noopener noreferrer"&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/vault/docs" rel="noopener noreferrer"&gt;HashiCorp Vault — Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/vault/latest/docs" rel="noopener noreferrer"&gt;Terraform Provider: Vault&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" rel="noopener noreferrer"&gt;AWS Secrets Manager — User Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>security</category>
      <category>vault</category>
      <category>secrets</category>
      <category>iac</category>
    </item>
    <item>
      <title>Debugando um incidente real - métricas, logs e traces juntos na prática</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Sun, 27 Sep 2026 09:30:45 +0000</pubDate>
      <link>https://dev.to/apsis-cc/debugando-um-incidente-real-metricas-logs-e-traces-juntos-na-pratica-4978</link>
      <guid>https://dev.to/apsis-cc/debugando-um-incidente-real-metricas-logs-e-traces-juntos-na-pratica-4978</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: chegou a hora de usar a stack de verdade
&lt;/h2&gt;

&lt;p&gt;Ao longo desta série, a stack cresceu peça por peça: métricas (artigos 2 e 3), logs (artigo 4), traces (artigo 5), tudo unificado em Docker Compose (artigo 6) e visualizado em dashboards (artigo 7). Este último artigo inverte o roteiro: em vez de adicionar mais uma peça, vamos &lt;strong&gt;quebrar&lt;/strong&gt; a mini app PHP de propósito e usar a stack inteira — dashboard, logs e traces — para investigar o problema do zero, como em um incidente real.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Simulando o problema
&lt;/h2&gt;

&lt;p&gt;No &lt;code&gt;app.php&lt;/code&gt; construído no artigo 3, uma mudança pequena e deliberada introduz um bug de performance: uma fração dos pedidos passa a demorar muito mais que o normal, e uma fração menor passa a falhar de verdade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// dentro do processamento do pedido, logo apos $state['queue_size']++&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// simula uma dependencia externa lenta (ex.: um servico de pagamento)&lt;/span&gt;
    &lt;span class="nb"&gt;usleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4_000_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 2-4 segundos, bem acima do normal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;logToLoki&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"falha ao processar pedido: timeout no servico de pagamento"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'queue_size'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;writeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stateFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"erro interno&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa mudança fica no ar, e a investigação a seguir segue exatamente o caminho que alguém sem acesso a este código-fonte teria: só olhando para o que a stack de observabilidade mostra.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Passo 1: o dashboard aponta o sintoma
&lt;/h2&gt;

&lt;p&gt;Gerando tráfego contínuo contra a mini app PHP e observando o dashboard construído no artigo 7 (com a variável &lt;code&gt;$app&lt;/code&gt; em &lt;code&gt;php-app&lt;/code&gt;), dois painéis mudam de forma visível:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O painel de &lt;strong&gt;latência p50/p95/p99&lt;/strong&gt; mostra o p50 relativamente estável, mas o p95 e p99 disparando bem acima — o padrão clássico de "uma minoria de requisições muito mais lentas que a maioria", coerente com os 15% de chance de lentidão introduzidos no passo anterior.&lt;/li&gt;
&lt;li&gt;O painel de &lt;strong&gt;volume de erros por minuto&lt;/strong&gt; (baseado em &lt;code&gt;count_over_time&lt;/code&gt; sobre logs com &lt;code&gt;level="error"&lt;/code&gt;) sai de zero e passa a mostrar uma taxa constante de erros.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neste ponto, o dashboard já respondeu a primeira pergunta — "algo está errado, e é especificamente na mini app PHP" — mas não o "por quê". Essa é a fronteira natural entre o que métricas conseguem responder sozinhas e onde é preciso descer um nível.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Passo 2: os logs explicam o erro
&lt;/h2&gt;

&lt;p&gt;Voltando ao painel de logs do dashboard (ou ao Explore, filtrando diretamente):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="php-app", level="error"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mensagem &lt;code&gt;falha ao processar pedido: timeout no servico de pagamento&lt;/code&gt; aparece repetida, associada aos horários em que o painel de erro subiu. Já é uma pista concreta — não é um erro genérico de PHP, é um erro específico de negócio, com uma causa nomeada no próprio texto do log (o "serviço de pagamento"). Mas o log sozinho não diz &lt;strong&gt;quanto tempo&lt;/strong&gt; foi gasto antes do erro acontecer, nem se as requisições lentas (vistas no painel de latência) são o mesmo fenômeno que as requisições que falham, ou dois problemas diferentes acontecendo ao mesmo tempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Passo 3: os traces confirmam onde o tempo foi gasto
&lt;/h2&gt;

&lt;p&gt;Com a instrumentação dos artigos 5 e 6, cada requisição gera um span &lt;code&gt;process_order&lt;/code&gt;. Consultando os logs do coletor OpenTelemetry (ou, em uma stack com backend de tracing dedicado, navegando visualmente pela árvore de spans) durante o mesmo intervalo de tempo do pico visto no dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs otel-collector &lt;span class="nt"&gt;--since&lt;/span&gt; 5m | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 5 process_order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Os spans capturados no período do incidente mostram uma duração (&lt;code&gt;end_time - start_time&lt;/code&gt;) concentrada em dois grupos bem separados: a maioria na faixa de dezenas de milissegundos (comportamento normal) e um grupo secundário na faixa de 2 a 4 segundos — exatamente a faixa introduzida artificialmente na simulação. O atributo &lt;code&gt;order.duration_ms&lt;/code&gt; (adicionado ao span no artigo 5) confirma numericamente essa distribuição bimodal em cada span individual, span a span, algo que nem o painel de latência agregada (que mostra percentis, não casos individuais) nem o log de erro (que só aparece nos 5% que efetivamente falham, não nos 15% que só ficam lentos) conseguiam mostrar isoladamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Juntando as três pontas
&lt;/h2&gt;

&lt;p&gt;A investigação completa, remontada:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Métricas&lt;/strong&gt; apontaram &lt;em&gt;que&lt;/em&gt; algo mudou e &lt;em&gt;quando&lt;/em&gt; — p95/p99 subindo, taxa de erro saindo de zero — sem precisar que ninguém soubesse previamente que aquele problema existia (exatamente a definição de observabilidade do artigo 1: responder a uma pergunta não antecipada).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; explicaram &lt;em&gt;o quê&lt;/em&gt; estava falhando, com uma mensagem específica de negócio ("timeout no serviço de pagamento"), restrita rapidamente pelos labels &lt;code&gt;app&lt;/code&gt; e &lt;code&gt;level&lt;/code&gt; graças ao modelo de indexação do Loki (artigo 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; confirmaram &lt;em&gt;onde&lt;/em&gt;, dentro da requisição, o tempo estava de fato sendo gasto, com granularidade por requisição individual — não apenas em agregado — e conectaram as requisições lentas (visíveis só na cauda do histograma) às requisições que geravam log de erro.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nenhum dos três pilares sozinho reconstrói essa história completa: métricas sem logs mostram &lt;em&gt;que&lt;/em&gt; algo mudou, mas não &lt;em&gt;por quê&lt;/em&gt;; logs sem métricas exigiriam vasculhar volume gigantesco de texto sem saber onde procurar primeiro; traces sem os outros dois não teriam sido acionados a tempo, já que ninguém estaria olhando spans individuais sem antes ver o dashboard disparar.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusão da série
&lt;/h2&gt;

&lt;p&gt;Ao longo desses oito artigos, esta série construiu uma stack de observabilidade completa a partir do zero: o que são métricas, logs e traces e por que nenhum substitui os outros dois; Prometheus coletando métricas via scraping de duas mini aplicações reais, em Python e PHP puros; Loki recebendo logs via API HTTP, com um modelo de indexação deliberadamente mais barato que ferramentas tradicionais; OpenTelemetry como padrão neutro para gerar traces, primeiro isolado, depois unificado via um coletor OTLP; dashboards no Grafana combinando PromQL e LogQL; e, neste último artigo, os três pilares trabalhando juntos para investigar um incidente simulado do sintoma até a causa raiz. A stack construída aqui é deliberadamente mínima — sem alta disponibilidade, sem retenção de longo prazo configurada, sem backend de tracing visual — mas os conceitos e o fluxo de investigação são exatamente os mesmos usados em stacks de observabilidade de produção, em qualquer escala.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/concepts/observability-primer/" rel="noopener noreferrer"&gt;OpenTelemetry — Observability Primer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/sre-book/monitoring-distributed-systems/" rel="noopener noreferrer"&gt;Google SRE Book — Monitoring Distributed Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/prometheus/latest/querying/basics/" rel="noopener noreferrer"&gt;Prometheus — Querying Basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/query/" rel="noopener noreferrer"&gt;Grafana Loki — LogQL&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
    <item>
      <title>Dashboards no Grafana - combinando PromQL e LogQL para acompanhar uma aplicação de ponta a ponta</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Sat, 26 Sep 2026 09:30:37 +0000</pubDate>
      <link>https://dev.to/apsis-cc/dashboards-no-grafana-combinando-promql-e-logql-para-acompanhar-uma-aplicacao-de-ponta-a-ponta-4k02</link>
      <guid>https://dev.to/apsis-cc/dashboards-no-grafana-combinando-promql-e-logql-para-acompanhar-uma-aplicacao-de-ponta-a-ponta-4k02</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: dados prontos, falta visualizar
&lt;/h2&gt;

&lt;p&gt;A stack unificada do artigo anterior já coleta métricas das duas mini apps, recebe seus logs e processa seus traces. Mas até aqui, toda consulta foi feita manualmente — uma expressão PromQL de cada vez no Prometheus, uma query LogQL de cada vez no Explore do Grafana. Este artigo constrói um dashboard real, com vários painéis lado a lado, para acompanhar as duas mini apps sem precisar reabrir consultas soltas toda vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Criando o dashboard e o primeiro painel
&lt;/h2&gt;

&lt;p&gt;No Grafana, em &lt;strong&gt;Dashboards → New → New Dashboard → Add visualization&lt;/strong&gt;, escolhendo o datasource &lt;strong&gt;Prometheus&lt;/strong&gt;, o primeiro painel é a taxa de requisições da mini app Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rate(app_requests_total{job="python-app"}[1m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configurações relevantes desse painel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visualization&lt;/strong&gt;: Time series (o padrão, adequado para qualquer métrica ao longo do tempo).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Title&lt;/strong&gt;: "Taxa de Requisições — Python App".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit&lt;/strong&gt; (na aba Standard options): "requests/sec", para o eixo Y ficar legível sem precisar interpretar o número bruto.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Painel de latência com histogram_quantile
&lt;/h2&gt;

&lt;p&gt;Um segundo painel, usando o histogram de duração criado no artigo 3 para a mini app PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;histogram_quantile(0.50, rate(orders_duration_seconds_bucket{job="php-app"}[5m]))
histogram_quantile(0.95, rate(orders_duration_seconds_bucket{job="php-app"}[5m]))
histogram_quantile(0.99, rate(orders_duration_seconds_bucket{job="php-app"}[5m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adicionando as três expressões como três queries (A, B, C) no mesmo painel de Time series, cada uma vira uma linha — p50, p95 e p99 sobrepostas no mesmo gráfico é o padrão mais comum para visualizar latência, porque a distância entre as três linhas já mostra visualmente o quão "cauda longa" (long tail) a latência está: linhas próximas indicam latência consistente, p99 muito acima do p50 indica que uma fração pequena de requisições está bem mais lenta que a maioria.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Painel de taxa de erro (gauge)
&lt;/h2&gt;

&lt;p&gt;Um painel do tipo &lt;strong&gt;Gauge&lt;/strong&gt; (ponteiro/velocímetro) para o tamanho atual da fila de pedidos da mini app PHP — útil por mostrar o valor instantâneo de forma proeminente, sem exigir interpretar um gráfico de linha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orders_queue_size{job="php-app"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Na aba &lt;strong&gt;Thresholds&lt;/strong&gt; desse painel, vale configurar faixas de cor — verde até 5, amarelo até 15, vermelho acima disso — para o painel comunicar "tudo bem" ou "atenção" de relance, sem precisar ler o número.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Painel de logs com LogQL
&lt;/h2&gt;

&lt;p&gt;Adicionando um novo painel, agora com o datasource &lt;strong&gt;Loki&lt;/strong&gt;, tipo &lt;strong&gt;Logs&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app=~"python-app|php-app"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A expressão &lt;code&gt;=~&lt;/code&gt; faz correspondência por regex no valor do label — nesse caso, trazendo logs das duas aplicações no mesmo painel, empilhados por tempo. Um segundo painel de logs, filtrado só para eventos de erro (útil para manter visível mesmo quando o volume geral de log é alto):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app=~"python-app|php-app", level="error"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Painel de volume de logs ao longo do tempo
&lt;/h2&gt;

&lt;p&gt;O LogQL também permite agregações numéricas sobre logs, não só listagem de linhas — útil para correlacionar visualmente um pico de volume de log de erro com um painel de métricas ao lado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum(count_over_time({app=~"python-app|php-app", level="error"}[1m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse painel, como &lt;strong&gt;Time series&lt;/strong&gt; (mesmo vindo de uma query LogQL), mostra quantas linhas de log de erro por minuto cada aplicação produziu — o equivalente, em logs, ao que &lt;code&gt;rate()&lt;/code&gt; faz para um counter do Prometheus.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Organizando o dashboard
&lt;/h2&gt;

&lt;p&gt;Com os cinco painéis criados — taxa de requisições, latência p50/p95/p99, tamanho da fila, logs recentes e volume de erro — a organização recomendada segue uma convenção comum em dashboards de observabilidade: do geral para o específico, de cima para baixo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────┬─────────────────────────┐
│ Taxa de Requisições      │ Latência p50/p95/p99     │
├─────────────────────────┼─────────────────────────┤
│ Tamanho da Fila (gauge)  │ Volume de Erros / min     │
├─────────────────────────┴─────────────────────────┤
│ Logs Recentes (filtrados por app e nível)           │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Painéis de métrica agregada (o "o quê") ficam no topo, e o painel de logs detalhado (o "por quê") fica embaixo — assim, o fluxo natural de investigação (ver o problema no topo, entender a causa embaixo) segue a ordem visual do próprio dashboard. Salvando o dashboard com um nome como "Mini Apps — Visão Geral" e uma tag &lt;code&gt;observabilidade&lt;/code&gt;, ele fica disponível na lista principal de dashboards do Grafana.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Variável de template para alternar entre apps
&lt;/h2&gt;

&lt;p&gt;Para não duplicar o dashboard inteiro entre Python e PHP, o Grafana permite criar uma &lt;strong&gt;variável&lt;/strong&gt; que filtra os painéis dinamicamente. Em &lt;strong&gt;Dashboard settings → Variables → Add variable&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: Query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data source&lt;/strong&gt;: Prometheus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query&lt;/strong&gt;: &lt;code&gt;label_values(app_requests_total, job)&lt;/code&gt; (ou, mais genérico, &lt;code&gt;label_values(job)&lt;/code&gt; para listar todos os jobs configurados no &lt;code&gt;prometheus.yml&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Com a variável criada, cada query dos painéis passa a usar &lt;code&gt;$app&lt;/code&gt; no lugar do valor fixo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rate(app_requests_total{job="$app"}[1m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um seletor aparece no topo do dashboard, permitindo trocar entre &lt;code&gt;python-app&lt;/code&gt; e &lt;code&gt;php-app&lt;/code&gt; sem editar nenhum painel manualmente — o mesmo dashboard serve para as duas mini apps, e para qualquer aplicação nova que a stack venha a ganhar no futuro.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Conclusão e próximos passos
&lt;/h2&gt;

&lt;p&gt;Construímos um dashboard real combinando métricas via PromQL (taxa de requisições, latência por percentil, tamanho de fila) e logs via LogQL (eventos recentes, volume de erro), organizados do geral para o específico e parametrizados com uma variável de template para reutilização entre aplicações. No último artigo desta série, esse dashboard vira ferramenta de investigação de verdade: vamos simular um incidente real nas mini apps e usar métricas, logs e traces juntos para encontrar a causa raiz.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/" rel="noopener noreferrer"&gt;Grafana — Build a Dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/grafana/latest/dashboards/variables/" rel="noopener noreferrer"&gt;Grafana — Add Template Variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/query/metric_queries/" rel="noopener noreferrer"&gt;Grafana Loki — Metric Queries (LogQL)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stack completa de observabilidade com Docker Compose - Python, PHP, Prometheus, Loki, OpenTelemetry e Grafana</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Fri, 25 Sep 2026 09:30:38 +0000</pubDate>
      <link>https://dev.to/apsis-cc/stack-completa-de-observabilidade-com-docker-compose-python-php-prometheus-loki-opentelemetry-4occ</link>
      <guid>https://dev.to/apsis-cc/stack-completa-de-observabilidade-com-docker-compose-python-php-prometheus-loki-opentelemetry-4occ</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: as peças soltas até aqui
&lt;/h2&gt;

&lt;p&gt;Cada artigo desta série subiu uma peça isolada: Prometheus + Loki + Grafana no artigo 1, a mini app Python no artigo 2, a mini app PHP no artigo 3, envio de logs para o Loki no artigo 4, e instrumentação de traces (por enquanto só impressos no console) no artigo 5. Este artigo une tudo em um único &lt;code&gt;docker-compose.yml&lt;/code&gt;, com uma peça nova: um &lt;strong&gt;coletor OpenTelemetry&lt;/strong&gt;, responsável por receber os traces das mini apps via OTLP e encaminhá-los adiante.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Por que um coletor no meio do caminho
&lt;/h2&gt;

&lt;p&gt;No artigo 5, as mini apps exportavam spans diretamente para o console (&lt;code&gt;ConsoleSpanExporter&lt;/code&gt;). Isso prova que a instrumentação funciona, mas não é assim que uma aplicação real se comunica com uma stack de observabilidade — cada aplicação teria que saber exportar diretamente para cada backend final, duplicando configuração de endpoint, autenticação e formato em todas elas.&lt;/p&gt;

&lt;p&gt;O &lt;strong&gt;OpenTelemetry Collector&lt;/strong&gt; resolve isso ficando no meio: as aplicações exportam sempre para o mesmo lugar (o coletor, via OTLP), e é o coletor — configurado uma vez, centralmente — quem decide para onde os dados seguem depois: um backend de traces, processamento adicional (amostragem, enriquecimento de atributos), ou múltiplos destinos ao mesmo tempo. Trocar de backend passa a ser uma mudança de configuração do coletor, não um redeploy de cada aplicação instrumentada.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Trocando o exportador de console por OTLP
&lt;/h2&gt;

&lt;p&gt;Nas duas mini apps, o único ponto que muda em relação ao artigo 5 é o exportador — a forma como os spans &lt;strong&gt;saem&lt;/strong&gt; do processo — não a instrumentação em si (os &lt;code&gt;tracer.start_as_current_span&lt;/code&gt; e &lt;code&gt;spanBuilder(...)-&amp;gt;startSpan()&lt;/code&gt; continuam idênticos).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tracing.py (Python) — troca do ConsoleSpanExporter por OTLP
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BatchSpanProcessor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.exporter.otlp.proto.http.trace_exporter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://otel-collector:4318/v1/traces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;opentelemetry-exporter-otlp-proto-http
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// tracing.php (PHP) — troca do ConsoleSpanExporter por OTLP&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry\Contrib\Otlp\SpanExporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry\SDK\Common\Transport\Http\PsrTransportFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PsrTransportFactory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'http://otel-collector:4318/v1/traces'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'application/x-protobuf'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$tracerProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exporter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$tracerProvider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'php-app'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O hostname &lt;code&gt;otel-collector&lt;/code&gt; usado nos dois casos é o nome do serviço do Compose que aparece na seção 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Configurando o coletor
&lt;/h2&gt;

&lt;p&gt;O coletor precisa de um arquivo de configuração declarando três coisas: &lt;strong&gt;receivers&lt;/strong&gt; (como os dados entram), &lt;strong&gt;processors&lt;/strong&gt; (transformações opcionais no meio do caminho) e &lt;strong&gt;exporters&lt;/strong&gt; (para onde os dados saem):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# otel-collector-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4318&lt;/span&gt;

&lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;verbosity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detailed&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;debug&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este exemplo usa o exportador &lt;code&gt;debug&lt;/code&gt; (o sucessor do antigo &lt;code&gt;logging&lt;/code&gt; exporter), que imprime os traces recebidos no log do próprio coletor — suficiente para confirmar que o pipeline completo funciona de ponta a ponta: mini app → coletor → saída. Visualizar traces de forma gráfica, com uma árvore de spans navegável, exigiria um backend de tracing dedicado (como Grafana Tempo ou Jaeger) recebendo esse mesmo exportador — fora do escopo desta stack mínima, mas um próximo passo natural para quem quiser aprofundar depois desta série.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. O docker-compose.yml unificado
&lt;/h2&gt;

&lt;p&gt;Juntando tudo o que a série construiu até aqui em um único arquivo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/prometheus:v2.55.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090:9090"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus.yml:/etc/prometheus/prometheus.yml:ro&lt;/span&gt;

  &lt;span class="na"&gt;loki&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/loki:3.2.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3100:3100"&lt;/span&gt;

  &lt;span class="na"&gt;otel-collector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel/opentelemetry-collector:0.112.0&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config=/etc/otel-collector-config.yaml"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4318:4318"&lt;/span&gt;

  &lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/grafana:11.3.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_PASSWORD=admin&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loki&lt;/span&gt;

  &lt;span class="na"&gt;python-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./python-app:/app:ro&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pip&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-cache-dir&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;opentelemetry-sdk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;opentelemetry-exporter-otlp-proto-http&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;python3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loki&lt;/span&gt;

  &lt;span class="na"&gt;php-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php:8.3-cli&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./php-app:/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-S"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0:8001"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.php"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8001:8001"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otel-collector&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loki&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E o &lt;code&gt;prometheus.yml&lt;/code&gt; reunindo os alvos dos artigos 2 e 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus.yml&lt;/span&gt;
&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prometheus"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:9090"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app:8000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php-app"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php-app:8001"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subindo tudo de uma vez:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seis serviços devem aparecer no ar: &lt;code&gt;prometheus&lt;/code&gt;, &lt;code&gt;loki&lt;/code&gt;, &lt;code&gt;otel-collector&lt;/code&gt;, &lt;code&gt;grafana&lt;/code&gt;, &lt;code&gt;python-app&lt;/code&gt; e &lt;code&gt;php-app&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Configurando os datasources no Grafana
&lt;/h2&gt;

&lt;p&gt;Com todos os serviços na mesma rede do Compose, em &lt;strong&gt;Connections → Data sources&lt;/strong&gt; no Grafana, dois datasources apontam para os serviços pelo nome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; — &lt;code&gt;http://prometheus:9090&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki&lt;/strong&gt; — &lt;code&gt;http://loki:3100&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esses são os dois datasources que o artigo 7 desta série vai efetivamente usar em dashboards. O coletor OpenTelemetry não aparece como datasource do Grafana neste setup — ele só encaminha traces para o exportador &lt;code&gt;debug&lt;/code&gt; configurado na seção 4; ligar um datasource de traces é o passo seguinte caso um backend de tracing seja adicionado à stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Confirmando que o pipeline completo funciona
&lt;/h2&gt;

&lt;p&gt;Gerando tráfego nas duas aplicações e conferindo cada ponta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/
curl http://localhost:8001/

&lt;span class="c"&gt;# metricas&lt;/span&gt;
curl http://localhost:8000/metrics
curl http://localhost:8001/metrics

&lt;span class="c"&gt;# logs do coletor, onde os traces devem aparecer impressos&lt;/span&gt;
docker compose logs otel-collector &lt;span class="nt"&gt;--tail&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se o &lt;code&gt;docker compose logs otel-collector&lt;/code&gt; mostrar blocos com &lt;code&gt;TraceID&lt;/code&gt;, &lt;code&gt;SpanID&lt;/code&gt; e &lt;code&gt;Name&lt;/code&gt; para as requisições feitas, o pipeline de ponta a ponta está funcionando: as mini apps geram spans, o coletor os recebe via OTLP e os processa.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusão e próximos passos
&lt;/h2&gt;

&lt;p&gt;Unimos as seis peças construídas ao longo da série em uma stack única e reproduzível: duas mini aplicações instrumentadas para métricas, logs e traces, Prometheus fazendo scraping, Loki recebendo logs, um coletor OpenTelemetry recebendo traces via OTLP, e Grafana com os datasources de Prometheus e Loki configurados. No próximo artigo, o foco volta para o Grafana: construir dashboards reais que combinam PromQL e LogQL para acompanhar as mini apps de ponta a ponta em um único painel.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/collector/" rel="noopener noreferrer"&gt;OpenTelemetry Collector — Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/collector/configuration/" rel="noopener noreferrer"&gt;OpenTelemetry Collector — Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/" rel="noopener noreferrer"&gt;OpenTelemetry — OTLP Exporter Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/how-tos/networking/" rel="noopener noreferrer"&gt;Docker Compose — Networking&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
    <item>
      <title>OpenTelemetry - o que é, por que virou padrão e como instrumentar traces</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Thu, 24 Sep 2026 09:30:37 +0000</pubDate>
      <link>https://dev.to/apsis-cc/opentelemetry-o-que-e-por-que-virou-padrao-e-como-instrumentar-traces-46d0</link>
      <guid>https://dev.to/apsis-cc/opentelemetry-o-que-e-por-que-virou-padrao-e-como-instrumentar-traces-46d0</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: métricas e logs prontos, falta conectar os pontos
&lt;/h2&gt;

&lt;p&gt;As mini apps desta série já expõem métricas (artigos 2 e 3) e enviam logs para o Loki (artigo 4). O que ainda falta é o terceiro pilar visto no artigo 1: traces, que mostram o caminho de uma requisição específica e onde exatamente o tempo foi gasto. Este artigo apresenta o &lt;strong&gt;OpenTelemetry&lt;/strong&gt;, o padrão atual para gerar esse tipo de dado, e instrumenta as duas mini apps para produzir traces reais.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. O que é o OpenTelemetry
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OpenTelemetry&lt;/strong&gt; (frequentemente abreviado "OTel") é um projeto da CNCF que define um conjunto de APIs, SDKs e um protocolo de transporte (o &lt;strong&gt;OTLP&lt;/strong&gt; — OpenTelemetry Protocol) para gerar, processar e exportar dados de telemetria — métricas, logs e, principalmente neste artigo, traces — de forma neutra em relação a qual backend vai efetivamente armazenar e visualizar esses dados.&lt;/p&gt;

&lt;p&gt;Na prática, isso significa que a aplicação se instrumenta &lt;strong&gt;uma vez&lt;/strong&gt;, contra a API do OpenTelemetry, e decide &lt;strong&gt;depois&lt;/strong&gt;, por configuração, para onde exportar — Prometheus, Loki, Jaeger, um vendor comercial, ou, como esta série vai fazer no próximo artigo, um coletor OpenTelemetry intermediário. Trocar de backend de observabilidade deixa de exigir reescrever a instrumentação.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Por que virou o padrão dominante
&lt;/h2&gt;

&lt;p&gt;Antes do OpenTelemetry, existiam dois projetos concorrentes e incompatíveis tentando resolver o mesmo problema: &lt;strong&gt;OpenTracing&lt;/strong&gt; (focado em traces) e &lt;strong&gt;OpenCensus&lt;/strong&gt; (traces e métricas, liderado pelo Google). Cada biblioteca de instrumentação precisava escolher um dos dois, fragmentando o ecossistema — uma biblioteca instrumentada para OpenTracing não conversava com um backend pensado para OpenCensus sem camadas de tradução.&lt;/p&gt;

&lt;p&gt;Em 2019, os dois projetos se fundiram no OpenTelemetry, sob a CNCF (a mesma fundação que abriga Kubernetes e Prometheus). Isso resolveu a fragmentação e, mais importante, deu ao projeto neutralidade de fornecedor: nenhum vendor de observabilidade controla o padrão, o que fez praticamente todos os grandes players (Datadog, New Relic, Grafana, AWS, Google Cloud, Azure) apoiarem OTLP como protocolo de entrada nativo. Hoje, instrumentar com OpenTelemetry é a opção que menos prende a aplicação a um fornecedor específico.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Conceitos de tracing: trace, span e contexto
&lt;/h2&gt;

&lt;p&gt;Retomando e aprofundando o que o artigo 1 introduziu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Span&lt;/strong&gt; — a unidade básica: representa uma operação com início, fim e duração (uma chamada HTTP, uma query, um cálculo). Carrega um nome, atributos (pares chave-valor arbitrários) e, opcionalmente, eventos pontuais dentro do seu intervalo de tempo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace&lt;/strong&gt; — o conjunto de todos os spans gerados para atender uma única requisição de ponta a ponta, conectados entre si por um &lt;code&gt;trace id&lt;/code&gt; compartilhado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Span pai/filho&lt;/strong&gt; — um span pode iniciar outros spans "dentro" dele (por exemplo, o span da requisição HTTP inicia um span filho para a query de banco que ela dispara). Essa relação de aninhamento é o que permite reconstruir a árvore de tempo vista no artigo 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propagação de contexto&lt;/strong&gt; — quando uma requisição atravessa um limite de processo (chamada HTTP entre dois serviços), o &lt;code&gt;trace id&lt;/code&gt; e o &lt;code&gt;span id&lt;/code&gt; atual viajam como cabeçalhos HTTP (o formato padrão é o &lt;code&gt;traceparent&lt;/code&gt;, definido pela especificação W3C Trace Context), permitindo que o serviço seguinte continue o mesmo trace em vez de iniciar um novo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Instrumentando a mini app Python
&lt;/h2&gt;

&lt;p&gt;O SDK oficial do Python precisa ser instalado (não é parte da biblioteca padrão, diferente do que as mini apps fizeram até aqui para métricas e logs — gerar spans manualmente sem SDK é possível, mas reimplementar amostragem, contexto e propagação à mão perde o sentido de usar um padrão justamente feito para isso):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;opentelemetry-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instrumentação manual, criando um span por requisição:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tracing.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.trace.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ConsoleSpanExporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConsoleSpanExporter&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrando no &lt;code&gt;app.py&lt;/code&gt; do artigo 2, envolvendo o processamento da requisição em um span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tracing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;

&lt;span class="c1"&gt;# dentro do do_GET, no branch que nao eh /metrics
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handle_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nf"&gt;log_to_loki&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requisicao recebida em &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http.status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Por enquanto, o exportador usado é o &lt;code&gt;ConsoleSpanExporter&lt;/code&gt;, que apenas imprime cada span no stdout — suficiente para confirmar que a instrumentação está gerando dados, sem depender de nenhuma infraestrutura adicional ainda. Rodando a aplicação e fazendo uma requisição, o terminal deve mostrar um bloco JSON com &lt;code&gt;trace_id&lt;/code&gt;, &lt;code&gt;span_id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; e &lt;code&gt;start_time&lt;/code&gt;/&lt;code&gt;end_time&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Instrumentando a mini app PHP
&lt;/h2&gt;

&lt;p&gt;O equivalente em PHP usa o SDK oficial, instalado via Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require open-telemetry/sdk open-telemetry/exporter-otlp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// tracing.php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry\SDK\Trace\TracerProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;OpenTelemetry\SDK\Trace\SpanExporter\ConsoleSpanExporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConsoleSpanExporter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$tracerProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$tracerProvider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'php-app'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrando no &lt;code&gt;app.php&lt;/code&gt; do artigo 3, ao redor do processamento de um pedido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/tracing.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$tracer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;spanBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'process_order'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$span&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... logica de processamento do pedido, ja existente ...&lt;/span&gt;
    &lt;span class="nv"&gt;$span&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order.duration_ms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'durations'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'durations'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$span&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O padrão &lt;code&gt;try/finally&lt;/code&gt; garante que o span é finalizado mesmo se o processamento lançar uma exceção no meio do caminho — um span que nunca termina (por um &lt;code&gt;end()&lt;/code&gt; que não é alcançado) é um erro comum de instrumentação manual, e polui o backend de traces com spans "presos".&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusão e próximos passos
&lt;/h2&gt;

&lt;p&gt;Vimos o que é o OpenTelemetry, por que a fusão de OpenTracing e OpenCensus em 2019 o tornou o padrão neutro de instrumentação, os conceitos de span, trace e propagação de contexto, e instrumentamos manualmente as duas mini apps para gerar spans — por enquanto, só visíveis no console de cada aplicação. No próximo artigo, a stack inteira é unificada em um único &lt;code&gt;docker-compose.yml&lt;/code&gt;: as mini apps trocam o &lt;code&gt;ConsoleSpanExporter&lt;/code&gt; por um exportador OTLP de verdade, apontando para um coletor OpenTelemetry, ao lado de Prometheus, Loki e Grafana já configurados.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/what-is-opentelemetry/" rel="noopener noreferrer"&gt;OpenTelemetry — What is OpenTelemetry?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/concepts/signals/traces/" rel="noopener noreferrer"&gt;OpenTelemetry — Traces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/trace-context/" rel="noopener noreferrer"&gt;W3C Trace Context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/languages/python/getting-started/" rel="noopener noreferrer"&gt;OpenTelemetry Python — Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/languages/php/getting-started/" rel="noopener noreferrer"&gt;OpenTelemetry PHP — Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
    <item>
      <title>Loki na prática - enviando logs via API HTTP a partir das mini apps Python e PHP</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Wed, 23 Sep 2026 09:30:35 +0000</pubDate>
      <link>https://dev.to/apsis-cc/loki-na-pratica-enviando-logs-via-api-http-a-partir-das-mini-apps-python-e-php-5en5</link>
      <guid>https://dev.to/apsis-cc/loki-na-pratica-enviando-logs-via-api-http-a-partir-das-mini-apps-python-e-php-5en5</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: métricas prontas, falta o segundo pilar
&lt;/h2&gt;

&lt;p&gt;As duas mini aplicações desta série — a de Python (artigo 2) e a de PHP (artigo 3) — já expõem métricas consumidas pelo Prometheus. Métricas respondem "quantos pedidos falharam nos últimos 5 minutos", mas não respondem "por que o pedido 4821 falhou especificamente". Essa é a pergunta que logs respondem, e este artigo cobre como enviá-los para o Loki diretamente das mini apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Por que a indexação do Loki é diferente
&lt;/h2&gt;

&lt;p&gt;Ferramentas de log tradicionais, como o Elasticsearch, indexam o conteúdo completo de cada linha — todo termo de texto vira pesquisável, o que permite buscas ricas, mas exige estruturas de índice caras em disco e CPU, que crescem proporcionalmente ao volume de log.&lt;/p&gt;

&lt;p&gt;O Loki toma uma decisão deliberadamente diferente, inspirada diretamente no modelo do Prometheus: &lt;strong&gt;indexa só um conjunto pequeno de labels&lt;/strong&gt; — como &lt;code&gt;app="python-app"&lt;/code&gt;, &lt;code&gt;env="production"&lt;/code&gt; — e trata o conteúdo da linha de log como texto opaco, comprimido e armazenado em blocos (chunks), sem índice próprio. Quando uma consulta pede um filtro de texto (&lt;code&gt;|= "erro"&lt;/code&gt;, por exemplo), o Loki varre os chunks relevantes (já filtrados pelos labels) no momento da consulta, em vez de consultar um índice pré-construído.&lt;/p&gt;

&lt;p&gt;O resultado prático dessa troca:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Armazenamento muito mais barato&lt;/strong&gt; — sem índice invertido de texto completo, o custo de disco cresce bem mais devagar com o volume de logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buscas por label são muito rápidas&lt;/strong&gt; — "todos os logs do serviço X, no ambiente Y" é uma operação barata, porque os labels são de fato indexados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buscas por texto livre são mais lentas conforme o volume cresce&lt;/strong&gt; — porque envolvem varredura, não um lookup de índice. Na prática, isso empurra o usuário a sempre filtrar primeiro por labels (que reduzem drasticamente o volume a varrer) antes de aplicar um filtro de texto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cardinalidade de labels importa tanto quanto no Prometheus&lt;/strong&gt; — usar um label com alta cardinalidade (como um &lt;code&gt;user_id&lt;/code&gt; único por requisição) quebra a mesma premissa de eficiência nos dois sistemas, e é um erro comum de quem vem de ferramentas que indexam tudo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. A API HTTP de push do Loki
&lt;/h2&gt;

&lt;p&gt;Diferente do Prometheus (que faz pull), o Loki recebe logs por &lt;strong&gt;push&lt;/strong&gt;: a aplicação envia ativamente cada lote de linhas para um endpoint HTTP, &lt;code&gt;POST /loki/api/v1/push&lt;/code&gt;. O corpo da requisição é um JSON com o seguinte formato:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"streams"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1735689600000000000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"requisicao recebida em /"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pontos importantes desse formato:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stream&lt;/code&gt; carrega os &lt;strong&gt;labels&lt;/strong&gt; — o que o Loki de fato indexa. Poucos e de baixa cardinalidade, como visto acima.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;values&lt;/code&gt; é uma lista de &lt;code&gt;[timestamp, linha]&lt;/code&gt;, onde o timestamp é uma string em &lt;strong&gt;nanosegundos desde epoch&lt;/strong&gt; (não segundos, nem milissegundos — um detalhe fácil de errar).&lt;/li&gt;
&lt;li&gt;É possível enviar múltiplas linhas do mesmo stream de uma vez, o que é mais eficiente do que uma requisição HTTP por linha de log.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Enviando logs a partir da mini app Python
&lt;/h2&gt;

&lt;p&gt;Usando só a biblioteca padrão (&lt;code&gt;urllib&lt;/code&gt;, sem instalar um client HTTP externo), uma função de log que envia diretamente para o Loki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# logger.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="n"&gt;LOKI_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://loki:3100/loki/api/v1/push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_to_loki&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;timestamp_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time_ns&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streams&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;timestamp_ns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;LOKI_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# nunca deixar uma falha de log derrubar a aplicacao
&lt;/span&gt;        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;falha ao enviar log para o Loki: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrando no &lt;code&gt;app.py&lt;/code&gt; do artigo 2, chamando &lt;code&gt;log_to_loki&lt;/code&gt; a cada requisição de negócio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dentro do do_GET, no branch que nao eh /metrics
&lt;/span&gt;&lt;span class="nf"&gt;log_to_loki&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requisicao recebida em &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Enviando logs a partir da mini app PHP
&lt;/h2&gt;

&lt;p&gt;O mesmo endpoint, chamado a partir do PHP puro com &lt;code&gt;file_get_contents&lt;/code&gt; e um &lt;code&gt;stream context&lt;/code&gt; (sem precisar de cURL nem bibliotecas externas):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// logger.php&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;logToLoki&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'info'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'php-app'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// microtime(true) da segundos com fracao decimal; convertendo para&lt;/span&gt;
    &lt;span class="c1"&gt;// nanossegundos desde epoch, o formato que o Loki espera&lt;/span&gt;
    &lt;span class="nv"&gt;$timestampNs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%.0f'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'streams'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;
            &lt;span class="s1"&gt;'stream'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'app'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'level'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$level&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'values'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;$timestampNs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;stream_context_create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'http'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'method'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'header'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'ignore_errors'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'http://loki:3100/loki/api/v1/push'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrando no &lt;code&gt;app.php&lt;/code&gt; do artigo 3, logo após processar um pedido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/logger.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;logToLoki&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pedido processado em &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'requests_total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ms de fila"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Consultando os logs no Grafana com LogQL
&lt;/h2&gt;

&lt;p&gt;Com o datasource do Loki já configurado no Grafana desde o artigo 1, em &lt;strong&gt;Explore&lt;/strong&gt;, selecionando o datasource Loki, uma consulta LogQL básica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="python-app"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isso retorna todas as linhas de log com o label &lt;code&gt;app="python-app"&lt;/code&gt; — o equivalente a um &lt;code&gt;grep&lt;/code&gt; pré-filtrado pelos labels indexados. Para refinar com um filtro de texto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="python-app"} |= "erro"

{app="php-app", level="info"} != "healthcheck"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;|=&lt;/code&gt; filtra linhas que &lt;strong&gt;contêm&lt;/strong&gt; o texto; &lt;code&gt;!=&lt;/code&gt; filtra linhas que &lt;strong&gt;não contêm&lt;/strong&gt;. Esses filtros de texto rodam sobre o conteúdo já restrito pelos labels — daí a importância de sempre começar a query pelos labels certos, como discutido na seção 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusão e próximos passos
&lt;/h2&gt;

&lt;p&gt;Vimos por que o Loki indexa só labels e trata o conteúdo da linha como texto não indexado — uma troca deliberada de riqueza de busca por custo —, como enviar logs diretamente via API HTTP sem bibliotecas externas em Python e PHP, e como consultar esses logs com LogQL no Grafana. Com métricas e logs das duas mini apps já fluindo para a stack, falta o terceiro pilar: o próximo artigo introduz o OpenTelemetry e instrumenta as mesmas aplicações para gerar traces.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs" rel="noopener noreferrer"&gt;Grafana Loki — HTTP API (push)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/query/" rel="noopener noreferrer"&gt;Grafana Loki — LogQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest/get-started/labels/bp-labels/" rel="noopener noreferrer"&gt;Grafana Loki — Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
    <item>
      <title>Métricas customizadas - counters, gauges e histograms com uma mini app PHP</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:30:28 +0000</pubDate>
      <link>https://dev.to/apsis-cc/metricas-customizadas-counters-gauges-e-histograms-com-uma-mini-app-php-2o8i</link>
      <guid>https://dev.to/apsis-cc/metricas-customizadas-counters-gauges-e-histograms-com-uma-mini-app-php-2o8i</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: de uma métrica a um conjunto completo
&lt;/h2&gt;

&lt;p&gt;No artigo anterior, a mini aplicação em Python expunha uma única métrica — uma contagem de requisições. Na prática, uma aplicação real quer responder perguntas de negócio mais específicas: quantos pedidos foram criados? Quanto tempo cada requisição levou? Quantas conexões estão abertas agora? Cada uma dessas perguntas pede um &lt;strong&gt;tipo diferente&lt;/strong&gt; de métrica. Este artigo cobre os três tipos que cobrem praticamente todo caso de uso — counter, gauge e histogram — com uma segunda mini aplicação, desta vez em PHP puro, expondo métricas de negócio.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Counter: só sobe (até o processo reiniciar)
&lt;/h2&gt;

&lt;p&gt;Um &lt;strong&gt;counter&lt;/strong&gt; é um valor que só aumenta (ou zera, se o processo reinicia). É o tipo certo para "quantas vezes algo aconteceu": número de requisições, número de erros, número de pedidos criados. A mini app do artigo anterior já usava um counter (&lt;code&gt;app_requests_total&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;O sufixo &lt;code&gt;_total&lt;/code&gt; no nome é uma convenção do Prometheus para deixar explícito que a métrica é um counter cumulativo, não um valor instantâneo — vale seguir essa convenção mesmo implementando o texto à mão.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="c"&gt;# HELP orders_created_total Total de pedidos criados&lt;/span&gt;
&lt;span class="c"&gt;# TYPE orders_created_total counter&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_created_total&lt;/span&gt; &lt;span class="mi"&gt;187&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Gauge: sobe e desce livremente
&lt;/h2&gt;

&lt;p&gt;Um &lt;strong&gt;gauge&lt;/strong&gt; representa um valor que pode aumentar ou diminuir a qualquer momento — é o tipo certo para "quanto existe agora": número de conexões abertas, itens numa fila, memória em uso, temperatura. Diferente do counter, não existe uma direção esperada.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="c"&gt;# HELP orders_queue_size Numero de pedidos aguardando processamento&lt;/span&gt;
&lt;span class="c"&gt;# TYPE orders_queue_size gauge&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_queue_size&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Histogram: distribuição de valores em buckets
&lt;/h2&gt;

&lt;p&gt;Um &lt;strong&gt;histogram&lt;/strong&gt; captura a distribuição de uma medida contínua — o caso mais comum é tempo de resposta. Em vez de guardar cada valor individual (inviável em alta escala) ou só uma média (que esconde outliers), um histogram conta quantas observações caíram dentro de faixas pré-definidas, chamadas &lt;strong&gt;buckets&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="c"&gt;# HELP orders_duration_seconds Tempo de processamento de um pedido&lt;/span&gt;
&lt;span class="c"&gt;# TYPE orders_duration_seconds histogram&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.5"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;195&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_duration_seconds_bucket&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"+Inf"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_duration_seconds_sum&lt;/span&gt; &lt;span class="mf"&gt;42.7&lt;/span&gt;
&lt;span class="o"&gt;or&lt;/span&gt;&lt;span class="n"&gt;ders_duration_seconds_count&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada linha &lt;code&gt;_bucket{le="X"}&lt;/code&gt; é cumulativa: "quantas observações foram menores ou iguais a X segundos". &lt;code&gt;_sum&lt;/code&gt; é a soma de todos os valores observados, e &lt;code&gt;_count&lt;/code&gt; é o total de observações (igual ao último bucket, &lt;code&gt;+Inf&lt;/code&gt;). Com essas peças, o Prometheus consegue calcular percentis aproximados (p50, p95, p99) em tempo de consulta, via &lt;code&gt;histogram_quantile&lt;/code&gt; — sem que a aplicação precise calcular percentil nenhum sozinha.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Mini app em PHP puro com métricas de negócio
&lt;/h2&gt;

&lt;p&gt;Sem nenhum framework, usando apenas o servidor embutido do PHP (&lt;code&gt;php -S&lt;/code&gt;), uma aplicação que simula processamento de pedidos e expõe as três métricas vistas acima:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app.php&lt;/span&gt;

&lt;span class="nv"&gt;$stateFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/metrics_state.json'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;readState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'requests_total'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'queue_size'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'durations'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[]];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;writeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stateFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_URI'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'/metrics'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$durations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'durations'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$durations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$durations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: text/plain; version=0.0.4'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# HELP orders_created_total Total de pedidos criados&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# TYPE orders_created_total counter&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"orders_created_total &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'requests_total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# HELP orders_queue_size Numero de pedidos aguardando processamento&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# TYPE orders_queue_size gauge&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"orders_queue_size &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'queue_size'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# HELP orders_duration_seconds Tempo de processamento de um pedido&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# TYPE orders_duration_seconds histogram&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$cumulative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$buckets&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$le&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cumulative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$durations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$le&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"orders_duration_seconds_bucket&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;le=\"{$le}\"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$cumulative&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"orders_duration_seconds_bucket&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;le=\"+Inf\"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"orders_duration_seconds_sum &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$sum&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"orders_duration_seconds_count &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Simula o processamento de um novo pedido&lt;/span&gt;
&lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'queue_size'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;usleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// simula trabalho real, 20-400ms&lt;/span&gt;
&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'queue_size'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'requests_total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'durations'&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;writeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stateFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: text/plain'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pedido processado&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subindo com o servidor embutido do PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-S&lt;/span&gt; 0.0.0.0:8001 app.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gerando alguns pedidos e conferindo as métricas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 10&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8001/ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;curl http://localhost:8001/metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O estado é persistido em um arquivo JSON local só para manter o exemplo simples e sem dependências — em uma aplicação real, esses contadores normalmente vivem em memória do próprio processo (e cada worker/processo exporia os seus, deixando a agregação entre réplicas por conta do Prometheus).&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Adicionando a app PHP à stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml (trecho adicionado)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;php-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php:8.3-cli&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./app.php:/app/app.php&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-S"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0:8001"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.php"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8001:8001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus.yml (job adicionado)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php-app"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php-app:8001"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Consultando um histogram com histogram_quantile
&lt;/h2&gt;

&lt;p&gt;Com dados fluindo, a consulta mais útil sobre um histogram no PromQL é o percentil de latência:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# p95 de tempo de processamento nos ultimos 5 minutos
histogram_quantile(0.95, rate(orders_duration_seconds_bucket[5m]))

# tamanho atual da fila
orders_queue_size

# taxa de pedidos criados por segundo
rate(orders_created_total[1m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;histogram_quantile&lt;/code&gt; reconstrói o percentil aproximado a partir da distribuição por buckets — quanto mais granulares os buckets ao redor do percentil de interesse, mais precisa a estimativa. É uma aproximação, não um cálculo exato sobre os valores individuais (que o Prometheus nunca armazena um a um), mas suficiente para a grande maioria dos casos de uso de observação de latência em produção.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusão e próximos passos
&lt;/h2&gt;

&lt;p&gt;Cobrimos os três tipos fundamentais de métrica — counter para contagens cumulativas, gauge para valores que sobem e descem, histogram para distribuições como latência — e construímos uma segunda mini aplicação, agora em PHP puro, expondo métricas de negócio reais. Com duas aplicações diferentes já enviando métricas para o Prometheus, o próximo artigo muda de pilar: como enviar &lt;strong&gt;logs&lt;/strong&gt; dessas mesmas aplicações para o Loki, e por que o modelo de indexação dele é tão diferente do Prometheus.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/concepts/metric_types/" rel="noopener noreferrer"&gt;Prometheus — Metric Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/practices/histograms/" rel="noopener noreferrer"&gt;Prometheus — Histograms and Summaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile" rel="noopener noreferrer"&gt;PromQL — histogram_quantile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/en/features.commandline.webserver.php" rel="noopener noreferrer"&gt;PHP Manual — Built-in web server&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
    <item>
      <title>Prometheus na prática - scraping, o endpoint /metrics e uma mini app Python expondo métricas</title>
      <dc:creator>Rafael Dutra</dc:creator>
      <pubDate>Mon, 21 Sep 2026 03:16:08 +0000</pubDate>
      <link>https://dev.to/apsis-cc/prometheus-na-pratica-scraping-o-endpoint-metrics-e-uma-mini-app-python-expondo-metricas-2j08</link>
      <guid>https://dev.to/apsis-cc/prometheus-na-pratica-scraping-o-endpoint-metrics-e-uma-mini-app-python-expondo-metricas-2j08</guid>
      <description>&lt;h2&gt;
  
  
  1. Retomando: da stack vazia à primeira fonte de dados
&lt;/h2&gt;

&lt;p&gt;No primeiro artigo desta série, subimos Prometheus, Loki e Grafana com Docker Compose, mas sem nenhuma aplicação real enviando dados — o Prometheus só monitorava a si mesmo. Este artigo resolve isso: vamos entender como o Prometheus efetivamente coleta métricas de uma aplicação (scraping) e construir uma mini aplicação em Python puro, sem nenhum framework, que expõe métricas nesse formato.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pull vs push: o modelo de scraping do Prometheus
&lt;/h2&gt;

&lt;p&gt;Existem duas formas de uma ferramenta de métricas obter dados de uma aplicação:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push&lt;/strong&gt; — a aplicação envia ativamente os valores para um servidor central sempre que quiser (ou em intervalos definidos por ela mesma). StatsD funciona assim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull (scraping)&lt;/strong&gt; — a aplicação apenas expõe seus valores atuais em um endpoint HTTP, e é o servidor de métricas que periodicamente visita esse endpoint e lê o estado atual. É o modelo do Prometheus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;O Prometheus escolheu pull deliberadamente, por algumas razões práticas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Descoberta centralizada de falhas&lt;/strong&gt; — se o Prometheus não consegue fazer scraping de um alvo, ele mesmo já sabe e registra isso (métrica &lt;code&gt;up == 0&lt;/code&gt;), sem depender da aplicação perceber que parou de enviar dados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilidade de testar manualmente&lt;/strong&gt; — como o endpoint é só HTTP, dá para abrir &lt;code&gt;/metrics&lt;/code&gt; num navegador ou com &lt;code&gt;curl&lt;/code&gt; e ver exatamente o que será coletado, sem precisar simular o lado do Prometheus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controle central do intervalo de coleta&lt;/strong&gt; — quem decide a frequência é quem consome os dados (o Prometheus), não cada aplicação individualmente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A exceção fica por conta de jobs de curta duração (um script batch que roda e termina em segundos, sem ficar no ar para ser "scrapeado") — para esses casos existe o &lt;strong&gt;Pushgateway&lt;/strong&gt;, um componente auxiliar que aceita push e fica exposto para o Prometheus fazer pull dele. Para aplicações de vida longa como as desta série (servidores HTTP), o modelo pull padrão é suficiente.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. O formato de exposição: o que vive em /metrics
&lt;/h2&gt;

&lt;p&gt;Um endpoint &lt;code&gt;/metrics&lt;/code&gt; do Prometheus é simplesmente texto plano, em um formato específico chamado &lt;strong&gt;exposition format&lt;/strong&gt;. Cada métrica segue esta estrutura:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="c"&gt;# HELP app_requests_total Total de requisições recebidas&lt;/span&gt;
&lt;span class="c"&gt;# TYPE app_requests_total counter&lt;/span&gt;
&lt;span class="n"&gt;app_requests_total&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;A linha &lt;code&gt;# HELP&lt;/code&gt; é uma descrição legível por humanos (opcional, mas boa prática).&lt;/li&gt;
&lt;li&gt;A linha &lt;code&gt;# TYPE&lt;/code&gt; declara o tipo da métrica — &lt;code&gt;counter&lt;/code&gt;, &lt;code&gt;gauge&lt;/code&gt;, &lt;code&gt;histogram&lt;/code&gt; ou &lt;code&gt;summary&lt;/code&gt; (o artigo 3 desta série detalha cada tipo).&lt;/li&gt;
&lt;li&gt;A linha de dados é &lt;code&gt;nome_da_metrica{labels_opcionais="valor"} valor&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nenhuma biblioteca é estritamente necessária para gerar esse texto — é só formatação de string. Isso torna o formato fácil de implementar em qualquer linguagem, mesmo sem um cliente oficial, o que é exatamente o que esta mini aplicação vai fazer.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Uma mini app em Python puro com http.server
&lt;/h2&gt;

&lt;p&gt;Usando apenas a biblioteca padrão do Python (módulo &lt;code&gt;http.server&lt;/code&gt;, sem Flask, Django ou qualquer outro framework), uma aplicação mínima que conta requisições e expõe essa contagem em &lt;code&gt;/metrics&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ThreadingHTTPServer&lt;/span&gt;

&lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;request_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;request_count&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;uptime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# HELP app_requests_total Total de requisicoes recebidas&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# TYPE app_requests_total counter&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app_requests_total &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# HELP app_uptime_seconds Tempo desde que o processo iniciou&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# TYPE app_uptime_seconds gauge&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app_uptime_seconds &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uptime&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/plain; version=0.0.4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;request_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ola do app em python&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# silencia o log padrao do http.server no stdout
&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadingHTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app rodando em http://0.0.0.0:8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rodando localmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada requisição a qualquer path diferente de &lt;code&gt;/metrics&lt;/code&gt; incrementa o contador &lt;code&gt;app_requests_total&lt;/code&gt; — importante notar que a própria requisição ao &lt;code&gt;/metrics&lt;/code&gt; &lt;strong&gt;não&lt;/strong&gt; conta como requisição de negócio, propositalmente, para não distorcer a métrica. Testando:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/
curl http://localhost:8000/
curl http://localhost:8000/metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A última chamada deve mostrar &lt;code&gt;app_requests_total 2&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Configurando o prometheus.yml para coletar a nova app
&lt;/h2&gt;

&lt;p&gt;Com o endpoint funcionando, o próximo passo é adicionar um &lt;code&gt;job&lt;/code&gt; ao &lt;code&gt;prometheus.yml&lt;/code&gt; do artigo anterior, apontando para essa aplicação:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus.yml&lt;/span&gt;
&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prometheus"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:9090"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python-app:8000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E incluir a aplicação no &lt;code&gt;docker-compose.yml&lt;/code&gt;, na mesma rede dos outros serviços (por isso o hostname &lt;code&gt;python-app&lt;/code&gt; funciona no target acima — é o nome do serviço no Compose):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml (trecho adicionado ao do artigo anterior)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;python-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./app.py:/app/app.py:ro&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depois de &lt;code&gt;docker compose up -d&lt;/code&gt;, o Prometheus passa a fazer scraping da aplicação a cada 15 segundos (o &lt;code&gt;scrape_interval&lt;/code&gt; global). Para confirmar que o alvo está saudável, em &lt;code&gt;http://localhost:9090/targets&lt;/code&gt; o job &lt;code&gt;python-app&lt;/code&gt; deve aparecer com estado &lt;code&gt;UP&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Consultando os dados no Prometheus
&lt;/h2&gt;

&lt;p&gt;Com alguns minutos de coleta (gerando tráfego manualmente com &lt;code&gt;curl&lt;/code&gt; em loop, se necessário), já é possível consultar no campo de expressão do Prometheus (&lt;code&gt;http://localhost:9090/graph&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# valor bruto do contador, acumulado desde que o processo subiu
app_requests_total

# taxa de requisições por segundo, calculada sobre a janela de 1 minuto
rate(app_requests_total[1m])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rate()&lt;/code&gt; é a função mais usada sobre contadores no PromQL: como um contador só cresce (e zera se o processo reiniciar), o valor bruto não diz muito sozinho — o que importa é a &lt;strong&gt;velocidade&lt;/strong&gt; de crescimento, que é exatamente o que &lt;code&gt;rate()&lt;/code&gt; calcula, já lidando corretamente com o caso de reinício do contador.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusão e próximos passos
&lt;/h2&gt;

&lt;p&gt;Vimos por que o Prometheus escolhe pull em vez de push, como o formato de exposição em &lt;code&gt;/metrics&lt;/code&gt; é apenas texto simples — implementável sem biblioteca alguma —, construímos uma mini aplicação Python que expõe uma métrica real, e configuramos o Prometheus para coletá-la periodicamente. No próximo artigo, a mini aplicação passa a ser em PHP puro, e o foco vai para os três tipos de métrica — counters, gauges e histograms — aplicados a métricas de negócio, não só contagem de requisições.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Imagem de capa: Logo oficial do Prometheus — repositório &lt;a href="https://github.com/prometheus/prometheus" rel="noopener noreferrer"&gt;prometheus/prometheus&lt;/a&gt;, licença Apache 2.0 (convertido para PNG via wsrv.nl)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Referências:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/instrumenting/exposition_formats/" rel="noopener noreferrer"&gt;Prometheus — Exposition Formats&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/" rel="noopener noreferrer"&gt;Prometheus — Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs/prometheus/latest/querying/basics/" rel="noopener noreferrer"&gt;Prometheus — Querying Basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/http.server.html" rel="noopener noreferrer"&gt;Python Docs — http.server&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>observability</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
