Logs Centralizados com Loki e Grafana Alloy

Published on 2026-09-26·v1.0
🔬 Open Lab

Objective

O kubectl logs lê arquivos de log que o kubelet mantém no nó, de containers que ainda existem, e só da execução atual e da anterior de cada um. Quando um Pod é apagado, reagendado ou reiniciado duas vezes, os logs dele somem. Logging centralizado coleta o stdout/stderr de todo container de todo nó num armazenamento que você consulta entre Pods e ao longo do tempo. No Kubernetes, o padrão leve é o Loki para armazenamento e um agente de nó para enviar os logs. Esse agente costumava ser o Promtail, que chegou ao fim de vida em 2 de março de 2026; o sucessor é o Grafana Alloy. Este conceito cobre como os logs dos containers são coletados, o que o add-on do MicroK8s oferece, a migração do agente para o Alloy, e consultas com LogQL.

Use Cases

  • Ler os logs de um Pod que caiu e foi substituído uma hora atrás.
  • Procurar um ID de requisição em todas as réplicas de um serviço.
  • Correlacionar erros entre namespaces durante um incidente.
  • Guardar eventos do Kubernetes ao lado dos logs das aplicações.

Deep Dive

Onde ficam os logs dos containers

Os containers escrevem no stdout e no stderr; o container runtime grava isso em arquivos no nó:

plaintext
/var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log

O kubelet faz a rotação deles (10 MiB, 5 arquivos por padrão). Um agente de nó (um DaemonSet) acompanha esses arquivos, anexa metadados do Kubernetes como labels, e os envia ao Loki. A aplicação só precisa logar no stdout/stderr, de preferência um evento por linha, idealmente estruturado (JSON).

O que o add-on do MicroK8s traz

microk8s enable observability instala, verificado na 1.35 no namespace observability: loki-0 (imagem grafana/loki:2.6.1) e um DaemonSet loki-promtail (imagem grafana/promtail:3.5.1), com o Loki configurado como data source do Grafana. Consultar a API do Loki mostrou os labels que o Promtail anexa:

plaintext
app, component, container, filename, instance, job, namespace, node_name, pod, stream

e linhas de log chegando do ingress controller:

plaintext
{"stream":{"namespace":"ingress","pod":"traefik-vkpfl","container":"traefik", "job":"ingress/traefik","stream":"stderr", ...}, "values":[["1790456488695750386","E0926 21:01:28 ... nodes is forbidden ..."]]}

Funciona, com duas ressalvas: o Loki 2.6 é um release de 2022, e o Promtail não recebe mais correções, incluindo de segurança. Como o resto do add-on, o Loki também não tem PVC, então os logs somem quando o Pod reinicia. Trate como um ambiente de laboratório.

A stack atual: Loki + Alloy

Para uma instalação mantida, instale a partir dos charts upstream:

  • Loki (Helm chart grafana/loki) em modo single-binary ou simple-scalable, com um PVC ou object storage (S3, MinIO) para os chunks, e um período de retenção.
  • Alloy (Helm chart grafana/alloy) como DaemonSet. Ele substitui o Promtail e também coleta métricas, traces e eventos do Kubernetes.

Um pipeline mínimo do Alloy para logs de Pods:

plaintext
discovery.kubernetes "pods" { role = "pod" } discovery.relabel "pods" { targets = discovery.kubernetes.pods.targets rule { source_labels = ["__meta_kubernetes_namespace"] target_label = "namespace" } rule { source_labels = ["__meta_kubernetes_pod_name"] target_label = "pod" } rule { source_labels = ["__meta_kubernetes_pod_container_name"] target_label = "container" } } loki.source.kubernetes "pods" { targets = discovery.relabel.pods.output forward_to = [loki.write.default.receiver] } loki.source.kubernetes_events "events" { forward_to = [loki.write.default.receiver] } loki.write "default" { endpoint { url = "http://loki.observability.svc.cluster.local:3100/loki/api/v1/push" } }

Uma configuração existente do Promtail é convertida automaticamente:

plaintext
alloy convert --source-format=promtail --output=config.alloy promtail.yaml

Labels: poucos e limitados

O Loki indexa labels, não o conteúdo dos logs. Cada combinação única de labels é um stream separado. Bons labels têm poucos valores: namespace, app, container, node. Nunca transforme IDs de requisição, de usuário ou de trace em labels; isso cria milhões de streams e deixa tudo lento. Procure por eles no conteúdo.

Consultando com LogQL

No Grafana, Explore, data source Loki:

plaintext
{namespace="team-a", app="web"} # todos os logs da aplicação {namespace="team-a"} |= "ERROR" # linha contém {namespace="team-a", app="web"} | json | status >= 500 # interpreta JSON, filtra um campo {namespace="team-a"} |= "req-7f3a9c" # uma requisição em todos os Pods sum by (pod) (count_over_time({namespace="team-a"} |= "ERROR" [5m])) # erros por Pod, como métrica

A última forma transforma logs em séries temporais, que o Grafana consegue plotar e o ruler do Loki consegue usar em alertas.

Dimensionamento e retenção

Logs crescem rápido. Decida de antemão:

  • Retenção (por exemplo 7 a 30 dias) aplicada pelo compactor do Loki.
  • Armazenamento: um PVC para um nó único, object storage além disso.
  • Ruído: descarte logs de debug ou logs de acesso de health checks no agente (loki.process com um stage.drop) em vez de pagar para guardá-los.

Trade-offs

  • Loki vs Elasticsearch/OpenSearch. O Loki indexa só labels, então é barato de rodar e de armazenar, e busca de texto completo é uma varredura; o Elasticsearch indexa o conteúdo para buscas arbitrárias rápidas, a um custo de recursos muito maior.
  • Add-on vs charts upstream. O add-on é um comando só, com componentes desatualizados e sem suporte; os charts upstream são atuais e precisam ser configurados e atualizados por você.
  • Agente em cada nó vs sidecars. Um agente em DaemonSet coleta tudo com um Pod por nó; sidecars só fazem sentido para aplicações que não conseguem logar no stdout.

Documentation Links