TL;DR: A stack LGTM (Loki, Grafana, Tempo, Mimir) mais o Pyroscope cobre os quatro sinais da observabilidade — métricas, logs, traces e profiles — usando os charts oficiais da Grafana, portáveis e sem lock-in. A chave da alta disponibilidade é object storage S3-compatível: ele desacopla estado do compute, deixando cada serviço stateless e descartável, com custo e retenção sob controle. O Grafana roda efêmero, provisionado por sidecar via ConfigMap, e o Alloy alimenta tudo com um agente só.
Por que observabilidade self-hosted, e por que agora
Observabilidade deixou de ser luxo de time grande e virou pré-requisito de governança. Não se audita o que não se enxerga — e isso vale tanto para uma fila de pagamentos que travou às 3 da manhã quanto para o comportamento de um agente de IA em produção. O problema é que a resposta fácil — contratar um SaaS de observabilidade e mandar tudo para lá — cobra o preço em dois lugares: lock-in (seu dado de telemetria e seus dashboards viram reféns do fornecedor) e conta imprevisível (a fatura cresce com a cardinalidade, exatamente quando você mais precisa observar).
A alternativa madura é self-hosted sob padrões abertos: a mesma stack roda em qualquer cluster, em qualquer nuvem ou on-premises, e o custo é uma decisão de arquitetura sua — não uma surpresa no fim do mês. Este guia mostra como montar essa stack de ponta a ponta com os charts oficiais da Grafana, colocando object storage S3-compatível como a fundação da alta disponibilidade, e explicando o papel de cada peça. É o mesmo desenho que rodamos em produção aqui na Nuvem Online — e as evidências ao longo do texto vêm do nosso próprio Grafana.
O que é a stack LGTM (+ Pyroscope), e por que ela é anti-lock-in?
LGTM é o acrônimo dos quatro projetos open source da Grafana Labs que, juntos, cobrem os sinais da observabilidade moderna:
| Componente | Sinal | O que faz |
|---|---|---|
| Mimir | Métricas | Backend Prometheus de longo prazo, horizontalmente escalável |
| Loki | Logs | Agregação de logs indexada por labels, barata em cardinalidade |
| Tempo | Traces | Backend de tracing distribuído, ingestão via OTLP |
| Grafana | Visualização | A camada de consulta, dashboards e alertas |
| Pyroscope | Profiles | Continuous profiling — o "quarto sinal", CPU e memória por função |
O que torna esse conjunto anti-lock-in não é ideologia — é engenharia. Todos falam protocolos abertos (a API do Prometheus para métricas, OTLP para traces), todos são mantidos por uma comunidade sob a Grafana Labs, e todos rodam com os charts Helm oficiais (grafana/mimir-distributed, grafana/loki, grafana/tempo-distributed, grafana/pyroscope, grafana/grafana). Você não depende de um registry proprietário, de um operator fechado nem de um formato de dado que só um fornecedor lê. A stack inteira é portável por construção.

O fluxo é o do diagrama: as aplicações e o cluster produzem telemetria, o Alloy coleta tudo, os quatro backends processam e persistem no S3, e o Grafana lê dos quatro. Guarde essa imagem — cada seção a seguir detalha uma parte dela.
Por que object storage S3 é a fundação da alta disponibilidade?
Esta é a decisão de arquitetura que separa "subi o Grafana com um Prometheus do lado" de "tenho uma plataforma de observabilidade de verdade". A ideia central é separar compute de storage.
Num desenho ingênuo, cada instância de Prometheus/Loki guarda os dados no próprio disco. Isso amarra o dado à réplica: se o pod morre, o dado vai junto; se você quer HA, precisa duplicar o disco; se quer retenção longa, precisa de discos gigantes e caros. Na stack LGTM, o modelo é outro: os componentes escrevem blocks (métricas), chunks (logs), traces e profiles num object storage S3-compatível, e ficam eles próprios stateless — guardam localmente, no máximo, um WAL e caches efêmeros.

As consequências práticas são diretas:
- Alta disponibilidade real. Réplicas stateless são descartáveis: um pod cai, o scheduler sobe outro, e ele lê os mesmos dados do bucket. Rollout de versão não perde nada.
- Durabilidade delegada. Um object storage entrega tipicamente 11 noves de durabilidade e replicação interna — você não reinventa isso em PV.
- Custo sob controle. Object storage é barato e elástico. Guardar 30, 90, 365 dias de métricas vira uma linha de retenção, não uma corrida de disco.
- Portabilidade. "S3-compatível" é um contrato, não um fornecedor. Roda igual em MinIO (in-cluster), Ceph RGW (on-premises), Backblaze B2, OCI Object Storage ou AWS S3. Trocar o backend é trocar o endpoint.
Um detalhe que costuma passar batido: credencial de object storage é segredo, e segredo não mora no values.yaml. A forma correta é injetar via Secret do Kubernetes, expandindo variáveis de ambiente na config:
# grafana/mimir-distributed — S3 com credenciais via Secret (NUNCA inline)
mimir:
structuredConfig:
common:
storage:
backend: s3
s3:
endpoint: <S3_ENDPOINT> # MinIO, Ceph, Backblaze, OCI, AWS...
region: <REGION>
access_key_id: ${AWS_ACCESS_KEY_ID} # expandido do ambiente
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
global:
extraEnvFrom:
- secretRef:
name: mimir-s3-credentials # chaves: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
extraArgs:
config.expand-env: "true"
O mesmo princípio — endpoint no values, credencial no Secret — se repete em Loki, Tempo e Pyroscope.
Como cada serviço funciona — e como implantá-lo
Mimir: métricas de longo prazo
O Mimir é o backend de métricas: recebe o remote_write do Prometheus/Alloy e serve a API Prometheus para consulta. No modo distributed (microsserviços), cada função é um workload que escala sozinho: distributor recebe as amostras, ingester as agrupa em blocks, store-gateway e querier servem as consultas, compactor compacta os blocks no S3, e o ruler avalia recording e alerting rules. Um desenho típico de produção roda 3 distributors, 3 ingesters, 2 queriers, 3 store-gateways e 2 rulers — e todos os blocks TSDB, o estado do ruler e do alertmanager vão para o S3, em buckets (ou prefixos) separados por finalidade.
Para a alta disponibilidade das séries, há duas estratégias — e vale conhecer as duas:
- Ring clássico com
replication_factor: 3e zone-aware replication. Cada série é escrita em 3 ingesters, distribuídos por zonas lógicas com anti-affinity. Você pode reiniciar uma zona inteira sem perder quórum de escrita. É o caminho consolidado e o default para a maioria. - Ingest storage via Kafka. No modelo mais novo, os distributors publicam num log de eventos (Kafka) e os ingesters consomem de partições. A durabilidade da ingestão passa a ser do Kafka, e o desacoplamento write/read fica ainda mais nítido. É mais peça para operar — adote quando a escala justificar.
# grafana/mimir-distributed — HA clássica sobre S3
mimir:
structuredConfig:
ingester:
ring:
replication_factor: 3
blocks_storage:
s3: { bucket_name: <bucket-metrics-blocks> }
ruler_storage:
s3: { bucket_name: <bucket-metrics-ruler> }
ingester:
replicas: 3
zoneAwareReplication:
enabled: true
distributor: { replicas: 3 }
store_gateway: { replicas: 3 }
helm repo add grafana https://grafana.github.io/helm-charts
helm upgrade --install mimir grafana/mimir-distributed -n mimir --create-namespace -f mimir-values.yaml
Depois de coletar, o resultado aparece no Grafana como um datasource Prometheus comum. Métricas de cluster com 24 horas de retenção, servidas pelo Mimir:

Loki: logs indexados por labels
O Loki trata logs como o Prometheus trata métricas: indexa por labels, não pelo conteúdo. Isso o torna barato — você não paga por full-text index de tudo. O chart oferece três modos de deployment, e escolher o certo é metade do trabalho:
SingleBinary— todos os componentes num processo. Com réplicas e S3 já entrega HA para volumes pequenos e médios.SimpleScalable— separa write (distributor + ingester), read (querier + query-frontend) e backend (compactor + ruler + index-gateway); cada caminho escala independente.Distributed— cada microsserviço exposto individualmente, para volumes muito altos.
Comece no menor que atende. Em ambos os casos, replication_factor: 3 mais o S3 compartilhado é o que dá HA: as três réplicas de write apontam para o mesmo bucket.
# grafana/loki — SingleBinary x3 sobre S3
deploymentMode: SingleBinary
singleBinary:
replicas: 3
loki:
storage:
type: s3
bucketNames: { chunks: <bucket-logs>, ruler: <bucket-logs>, admin: <bucket-logs> }
s3:
endpoint: <S3_ENDPOINT>
region: <REGION>
s3ForcePathStyle: true
schemaConfig:
configs:
- from: "2024-01-01"
store: tsdb
object_store: s3
schema: v13
index: { prefix: loki_index_, period: 24h }
commonConfig:
replication_factor: 3
limits_config:
retention_period: 672h # 28 dias
No Explore do Grafana, o volume de logs por nível mostra a agregação funcionando — sem precisar abrir uma única linha para ver a saúde do fluxo:

Tempo: tracing distribuído
O Tempo é o backend de traces. No modo distributed, ele recebe spans via OTLP (gRPC e HTTP), monta os traces e os persiste no S3 — sem precisar de um índice caro, porque a busca é por trace ID e por metadados. O ganho operacional é enorme: um trace amarra a jornada de uma request atravessando vários serviços, e correlaciona direto com logs (Loki) e métricas (Mimir) dentro do Grafana.
# grafana/tempo-distributed — traces sobre S3, ingestão OTLP
storage:
trace:
backend: s3
s3:
endpoint: <S3_ENDPOINT>
bucket: <bucket-traces>
distributor:
receivers:
otlp:
protocols:
grpc: { }
http: { }
querier: { replicas: 3 }
Como o Tempo só ingere por OTLP, o Alloy (ou o OpenTelemetry Collector) é quem entrega os spans a ele — voltamos a isso na seção do Alloy.
Pyroscope: continuous profiling
O Pyroscope é o quarto sinal, o menos difundido e o que mais surpreende quando entra: continuous profiling. Em vez de perfilar sob demanda numa crise, ele coleta profiles de CPU e memória continuamente, por serviço e por função, e guarda tudo no S3. Quando um serviço começa a queimar CPU, você não adivinha: abre o flame graph e vê exatamente qual função é responsável.
# grafana/pyroscope — profiles sobre S3, multi-tenant
pyroscope:
structuredConfig:
storage:
backend: s3
s3:
endpoint: <S3_ENDPOINT>
bucket_name: <bucket-profiles>
region: <REGION>
limits:
max_profile_size_bytes: 4194304
O flame graph abaixo é de um profile de CPU real, com bilhões de amostras acumuladas — cada bloco é um frame de pilha, e a largura é o tempo de CPU gasto ali:

Por que rodar o Grafana sem persistência?
Aqui vai a decisão que mais gera dúvida — e que, quando você entende, nunca mais volta atrás: o Grafana roda efêmero, sem PVC.
A intuição diz "mas e meus dashboards?". A resposta é que o estado que importa não vive no Grafana — vive em ConfigMaps versionados em Git, carregados na inicialização por um sidecar. O sidecar observa os ConfigMaps do cluster que tenham um label específico (grafana_dashboard para painéis, grafana_datasource para fontes de dados) e injeta o conteúdo no Grafana automaticamente. Com persistence.enabled: false, o pod vira descartável e idêntico a cada restart.
# grafana/grafana — efêmero + provisionamento por sidecar
persistence:
enabled: false
sidecar:
dashboards:
enabled: true
label: grafana_dashboard
searchNamespace: ALL
datasources:
enabled: true
label: grafana_datasource
searchNamespace: ALL
grafana.ini:
auth.generic_oauth:
enabled: true # SSO via OIDC (Keycloak, Authentik, etc.) — IdP substituível
Um datasource, então, é só um ConfigMap com o label certo:
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasources
labels:
grafana_datasource: "1"
data:
datasources.yaml: |
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://mimir-query-frontend.<ns>:8080/prometheus
isDefault: true
- name: Loki
type: loki
url: http://loki.<ns>:3100
- name: Tempo
type: tempo
url: http://tempo-query-frontend.<ns>:3200
- name: Pyroscope
type: grafana-pyroscope-datasource
url: http://pyroscope-query-frontend.<ns>:4040
Os ganhos são concretos:
- Sem PVC RWO travando upgrade. Um dos motivos clássicos de um pod de Grafana ficar preso em
Pendingnum upgrade é o volumeReadWriteOnceque não desprende do nó antigo. Sem persistência, o problema deixa de existir. - Réplicas idênticas. Você pode subir N Grafanas atrás de um load balancer sem estado divergente entre eles.
- GitOps de verdade. Todo painel e todo datasource é um objeto declarativo, revisável em merge request, reproduzível em qualquer cluster.
É esse mecanismo que provisiona as fontes de dados que você vê aqui — Mimir (como Prometheus), Loki, Tempo, Pyroscope e o Alertmanager, todos apontando para os Services internos do cluster:

Nenhum desses datasources foi criado na mão: todos nasceram de ConfigMaps carregados pelo sidecar. O SSO fica na frente via OAuth/OIDC — o que mantém o IdP substituível, mais um ponto de portabilidade.
Como o Alloy coleta tudo com um agente só?
Repare que até aqui falamos dos backends, mas não de quem os alimenta. Esse é o papel do Grafana Alloy — um coletor único que substitui, na prática, o Prometheus de scraping, o Promtail e o OpenTelemetry Collector, todos de uma vez.
A forma mais direta de implantá-lo em Kubernetes é pelo meta-chart grafana/k8s-monitoring, que orquestra várias instâncias do Alloy por função: alloy-metrics (raspa métricas), alloy-logs (DaemonSet lendo logs de pod), alloy-profiles (profiling via eBPF), alloy-receiver (recebe OTLP de aplicações) e alloy-singleton (sincroniza as regras). Você declara os destinos e liga as features; o chart cuida do resto.
# grafana/k8s-monitoring — um agente, quatro sinais
cluster:
name: <cluster-name>
destinations:
- name: mimir
type: prometheus
url: http://mimir-distributor.<ns>:8080/api/v1/push
- name: loki
type: loki
url: http://loki.<ns>:3100/loki/api/v1/push
- name: tempo
type: otlp
url: http://tempo-distributor.<ns>:4318
metrics: { enabled: false }
logs: { enabled: false }
traces: { enabled: true }
- name: pyroscope
type: pyroscope
url: http://pyroscope-distributor.<ns>:4040
clusterMetrics: { enabled: true } # cAdvisor, kube-state-metrics, node-exporter
clusterEvents: { enabled: true }
podLogs: { enabled: true }
applicationObservability: # recebe traces OTLP das apps
enabled: true
receivers: { otlp: { grpc: { enabled: true }, http: { enabled: true } } }
profiling: { enabled: true } # eBPF + pprof
prometheusOperatorObjects:
enabled: true # descobre ServiceMonitor/PodMonitor existentes
Dois pontos que valem ouro na operação:
- Descoberta nativa do ecossistema Prometheus. Ligando
prometheusOperatorObjects, o Alloy passa a honrar osServiceMonitorePodMonitorque outros charts já criam — você reaproveita o que a comunidade já configurou em vez de reescrever scrape configs. - Regras no lugar certo. O
alloy-singletonsincroniza osPrometheusRuledo cluster para o ruler do Mimir. Recording rules pré-agregam o que você consulta sempre; alerting rules disparam. Mas cuidado: uma regra colocada no componente errado (no Alloy em vez de no ruler central, ou vice-versa) é silenciosamente ignorada — vale entender onde cada uma mora.
Para traces de aplicação, o par OpenTelemetry Operator + Alloy fecha o ciclo: o operator injeta a auto-instrumentação nos pods por annotation, e a app passa a exportar OTLP para o alloy-receiver, que repassa ao Tempo. Zero código de instrumentação na aplicação.
Cardinalidade, retenção e custo: como a observabilidade "se paga"?
A stack só "se paga" quando cardinalidade e retenção são decisões de projeto, não acaso. Três alavancas concentram o custo:
- Cardinalidade de labels. Cada combinação única de labels é uma série (ou stream). Um label de alta cardinalidade —
user_id,request_id,podcru — multiplica o custo de armazenamento e de query. Honre os labels da fonte, não invente dimensão que ninguém consulta, e use recording rules para pré-agregar as consultas quentes: em vez de recalcular umsum(rate(...))pesado a cada abertura de dashboard, você o materializa uma vez. - Retenção por tier. Nem todo dado merece 1 ano. Métricas agregadas podem viver muito; logs de debug podem viver dias. O object storage torna a retenção uma linha de configuração — e o custo, previsível.
- Multi-tenancy. Um único backend serve vários tenants (por cluster, por app, por time) via header
X-Scope-OrgID, sem duplicar a stack. Um Mimir, um Loki, um Tempo — vários inquilinos isolados.
O resultado é uma governança de custo tratada como observabilidade, não como planilha: você mede o que a telemetria custa e ajusta cardinalidade e retenção com a mesma disciplina com que ajusta um HPA.
Quando escolher — e o que considerar antes
Self-hosted não é para todo mundo, e ser honesto sobre isso é parte da recomendação:
- Vá de self-hosted LGTM quando você já opera Kubernetes, quer portabilidade e previsibilidade de custo, tem volume que torna o SaaS caro, ou tem requisito de soberania do dado de telemetria. É o caso da maioria de quem leva infra a sério no Brasil.
- Fique no SaaS se o time é pequeno, o volume é baixo e você prefere não ter a stack de observabilidade como mais um sistema para operar de plantão.
- Distributed vs. SingleBinary/monolithic: não comece grande.
SingleBinaryno Loki e um Mimir enxuto atendem muito mais casos do que o hype sugere. Suba para distributed quando a cardinalidade e o volume realmente pedirem — não antes. - As armadilhas reais de operar você mesmo são conhecidas: PVC
ReadWriteOncetravando upgrade (por isso o Grafana efêmero), dependências stateful mal isoladas, e regras no lugar errado sendo ignoradas em silêncio. Nenhuma é impeditiva — todas são gerenciáveis quando você as conhece de antemão.
Portabilidade combina com portabilidade: a mesma lógica anti-lock-in que guia essa stack vale para o cluster embaixo dela — o que discutimos em Kubernetes on-premises: infraestrutura estatal sem lock-in. E a razão de fundo para investir em observabilidade — enxergar para governar — é a mesma que trata de avaliar, medir e governar agentes de IA em produção: não se governa o que não se mede.
Conclusão
A stack LGTM + Pyroscope entrega os quatro sinais da observabilidade com charts oficiais, protocolo aberto e portabilidade real. O fio que costura tudo é o object storage S3-compatível: é ele que torna cada serviço stateless e, portanto, altamente disponível, escalável e barato de reter. Some a isso um Grafana efêmero governado por Git via sidecar, e um Alloy que coleta os quatro sinais com um agente só, e você tem uma plataforma de observabilidade que não amarra a fornecedor, roda em qualquer nuvem e cujo custo é uma decisão sua. Comece pequeno, use padrões abertos, e deixe o object storage carregar o peso do estado.
Perguntas Frequentes
Preciso mesmo de object storage S3 para rodar a stack LGTM?
Para produção com alta disponibilidade, sim. É o S3 que torna os serviços stateless: os dados vivem no bucket (durável e replicado pelo provedor), não no disco do pod. Sem ele, cada componente vira stateful com PV, e você perde a réplica descartável e a retenção barata. Vale qualquer object storage S3-compatível — MinIO, Ceph, Backblaze, OCI, AWS S3.
Qual a diferença entre os modos SingleBinary, SimpleScalable e Distributed do Loki?
SingleBinary roda todos os componentes num processo (com réplicas + S3 já dá HA para volumes pequenos/médios). SimpleScalable separa os caminhos de write, read e backend, cada um escalando sozinho. Distributed expõe cada microsserviço individualmente, para volumes muito altos. Comece no menor que atende e suba conforme a cardinalidade cresce.
Por que rodar o Grafana sem persistência (sem PVC)?
Porque o estado que importa — dashboards e datasources — vive em ConfigMaps versionados em Git, carregados por um sidecar. O pod do Grafana vira descartável e idêntico a cada restart, sem PVC RWO travando upgrade nem estado divergente entre réplicas. É GitOps aplicado à camada de visualização.
O Alloy substitui o Prometheus, o Promtail e o OpenTelemetry Collector?
Na prática, sim. O Alloy é um coletor único que descobre alvos no cluster e envia métricas (remote_write ao Mimir), logs (ao Loki), traces (OTLP ao Tempo) e profiles (ao Pyroscope). Um chart, um agente, os quatro sinais — em vez de três ou quatro coletores separados.
Dá para começar sem todos os cinco componentes?
Dá, e é o recomendado. Comece por Mimir (métricas) e Grafana, adicione Loki (logs) em seguida, e traga Tempo e Pyroscope quando tracing e profiling entrarem na sua necessidade. A arquitetura é a mesma; você só liga mais destinos no Alloy.
Fontes: