Tutorial completo para subir uma stack de observabilidade de logs do zero até dashboards e alertas funcionando, integrando aplicações Spring Boot, frontends Vue, nginx-proxy, Keycloak e Postgres.
Versão alvo da Elastic Stack: 8.16.x (linha 8.x estável atual em 2026). Sempre que ver ${STACK_VERSION} nos exemplos, substitua pela tag exata que você quer fixar.
1. Visão geral
A Elastic Stack (antiga “ELK”) é um conjunto de componentes para coletar, processar, armazenar, buscar e visualizar dados — principalmente logs, métricas e traces.
| Componente | Função | Onde roda |
|---|---|---|
| Elasticsearch | Banco de dados de busca distribuído (storage + query engine). Indexa documentos JSON. | Servidor central |
| Kibana | Frontend web para Elasticsearch. Discover, dashboards, ILM UI, Stack Management, Alerts. | Mesmo host do ES |
| Logstash | Pipeline pesada de ETL. Lê de N inputs (Beats, Kafka, syslog, JDBC), aplica filters (grok, mutate, ruby), envia pra N outputs. | Opcional, host central |
| Beats | Família de shippers leves (Go). Filebeat para logs de arquivo, Metricbeat para métricas de host, Packetbeat, Auditbeat, Heartbeat. | Em cada host produtor de log |
| Elastic Agent | Substituto unificado dos Beats. Um único binário que faz log + metrics + APM + endpoint security. Gerenciado centralmente via Fleet. | Em cada host produtor |
| Fleet Server | Plano de controle dos Elastic Agents. Roda como subprocesso dentro de um Elastic Agent dedicado. Distribui policies e coleta status. | Host central, ao lado do ES |
Logstash vs Ingest Pipeline
Você não precisa de Logstash. A maioria dos casos práticos é resolvida com Ingest Pipelines — pipelines executadas dentro do próprio Elasticsearch (nó coordinador), configuradas via API ou Kibana. Use Logstash quando:
- Precisar de input não-Elastic (Kafka, JDBC, syslog UDP, IMAP).
- Tiver fan-out complexo (mesmo evento vai pra ES + S3 + webhook).
- Quiser buffer persistente em disco (Logstash persistent queue) entre Beats e ES.
- Usar plugins ruby/aggregate que ingest pipeline não tem.
Para logs JSON estruturados do Spring Boot + nginx, ingest pipeline cobre 100%.
Basic license vs Gold/Platinum
A partir da 8.x quase tudo importante está na basic license (gratuita, código fonte aberto sob Elastic License v2):
- Security (TLS, RBAC, API keys, file/PKI realms)
- Alerting básico (rule type “Elasticsearch query”, “Index threshold”)
- ILM, SLM, snapshots
- Canvas, Lens, Maps
- APM Server + agentes
- Fleet + Elastic Agent
Fica para Gold/Platinum/Enterprise: machine learning anomaly detection, LDAP/SAML/OIDC realms (no Kibana), cross-cluster replication, searchable snapshots, alerting webhook “connector” pre-built avançado, suporte. Para a maioria dos ambientes self-hosted a basic é mais que suficiente.
Alternativas
| Stack | Quando faz sentido |
|---|---|
| OpenSearch (fork AWS) | Quer licença Apache 2.0 pura, sem Elastic License v2. APIs compatíveis até ES 7.10. |
| Grafana Loki | Já tem Grafana e quer só logs (sem busca full-text indexada — Loki indexa só labels). Muito mais barato em disco. |
| VictoriaLogs | Single binary, performático, simples. Sem ML/APM. |
| Graylog | UI mais simples para audit/SIEM. Usa Elasticsearch/OpenSearch por baixo. |
Para esse tutorial seguimos com Elastic Stack 8.x porque integra logs + métricas + traces (APM) num único produto, e Kibana > Grafana para investigação de logs (Discover, Lens, runtime fields).
2. Arquitetura
flowchart LR
subgraph Apps[Aplicações]
SF[api-backend<br/>Spring Boot + logback JSON]
FE[frontends Vue<br/>nginx access/error]
NP[nginx-proxy<br/>access/error logs]
KC[keycloak<br/>stdout JSON]
PG[postgres<br/>csvlog]
end
subgraph Coletor[Coletor por host]
FB[Filebeat<br/>autodiscover docker]
EA[Elastic Agent<br/>gerenciado por Fleet]
end
subgraph Pipeline[Processamento opcional]
LS[Logstash<br/>se precisar]
end
subgraph Storage[Storage + Query]
ES[(Elasticsearch<br/>single-node)]
KB[Kibana 5601]
end
subgraph Control[Plano de controle]
FS[Fleet Server]
end
User[Usuário via VPN] --> NX[nginx-proxy<br/>kibana.example.com]
NX --> KB
SF -- docker logs --> FB
FE -- docker logs --> FB
NP -- docker logs --> FB
KC -- docker logs --> FB
PG -- docker logs --> FB
FB -- bulk API HTTPS --> ES
FB -. opcional .-> LS
LS --> ES
EA -- HTTPS --> FS
FS -- policies --> EA
EA -- bulk API --> ES
KB <--> ES
FS --> ESO caminho recomendado para começar é: docker logs (stdout) → Filebeat com autodiscover → Elasticsearch (com ingest pipeline) → Kibana
Quando crescer e quiser gerenciamento central de agentes (atualizar policies sem mexer em arquivo em cada host), migra para Elastic Agent + Fleet.
3. Sizing
RAM
- Elasticsearch heap: mínimo 2 GB, ideal 4 GB. Regra: heap = 50% da RAM disponível do container, nunca passar de 32 GB (compressed oops).
- Resto da RAM (não-heap) é usado para o file system cache do Lucene — crítico para performance de busca. Reserve pelo menos heap × 1 para cache.
- Kibana: 1 GB suficiente para uso individual.
- Filebeat: 100-200 MB por host.
Mínimo prático: 6 GB RAM dedicados à stack (4 ES + 1 Kibana + 1 buffer). Confortável: 8 GB.
Disco
- Logs JSON do Spring Boot comprimem na ordem de 8:1 a 12:1 depois de indexados (depende muito da cardinalidade dos campos).
- Regra grossa: cada GB de log “bruto” vira ~150-250 MB indexado.
- Aplicações de baixo tráfego geram tipicamente 100-500 MB de log/dia → 20-50 GB para 90 dias de retenção é folgado.
- Use SSD. ES é I/O-bound. Em HDD trava.
CPU
- 2 vCPU é o mínimo. 4 vCPU dão folga para ingest pipelines com grok pesado e queries de dashboard simultâneas.
4. Pré-requisitos
Kernel: vm.max_map_count
Elasticsearch usa mmap intensivamente. O default do Linux (65530) quebra na inicialização com erro max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144].
# Aplicar agora
sudo sysctl -w vm.max_map_count=262144
# Persistir
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-elasticsearch.conf
sudo sysctl --system
# Verificar
sysctl vm.max_map_countulimits
O container já vem com nofile=65535 e memlock=unlimited configurados via ulimits no compose (mostrado adiante). Confirmar no host que /etc/security/limits.conf não limita o usuário do docker.
Swap
ES recomenda desabilitar swap ou usar bootstrap.memory_lock=true + memlock ulimit. Como rodamos em docker, basta ulimits.memlock=-1:-1 no compose.
# Opcional: desabilitar swap do host
sudo swapoff -a
# Comentar a linha de swap em /etc/fstabDocker e Docker Compose
- Docker Engine >= 24
- Docker Compose v2 (
docker compose, nãodocker-compose) - Network externa
proxyjá criada (compartilhada com nginx-proxy):
docker network ls | grep proxy || docker network create proxyDiretório base
sudo mkdir -p /opt/docker/elastic/{config,data/es,data/kibana,backups,certs}
cd /opt/docker/elastic5. Instalação docker compose (single-node)
Estrutura de arquivos:
/opt/docker/elastic/
├── docker-compose.yml
├── .env
├── config/
│ ├── filebeat.docker.yml
│ ├── ingest-nginx.json
│ └── ilm-logs-policy.json
├── data/
│ ├── es/ (volume Elasticsearch)
│ └── kibana/
├── backups/ (snapshots)
└── certs/ (TLS autogerado).env
STACK_VERSION=8.16.1
CLUSTER_NAME=logs-cluster
ES_PORT=9200
KIBANA_PORT=5601
# Preencher depois do bootstrap (passo 6)
ELASTIC_PASSWORD=
KIBANA_PASSWORD=
ENCRYPTION_KEY= # 32+ chars aleatóriosGerar a ENCRYPTION_KEY antes:
openssl rand -hex 32docker-compose.yml
name: elastic
networks:
elastic_internal:
driver: bridge
proxy:
external: true
volumes:
es_data:
driver: local
driver_opts:
type: none
o: bind
device: /opt/docker/elastic/data/es
kibana_data:
driver: local
driver_opts:
type: none
o: bind
device: /opt/docker/elastic/data/kibana
es_backups:
driver: local
driver_opts:
type: none
o: bind
device: /opt/docker/elastic/backups
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
container_name: elasticsearch
restart: unless-stopped
environment:
- node.name=es01
- cluster.name=${CLUSTER_NAME}
- discovery.type=single-node
- bootstrap.memory_lock=true
- ES_JAVA_OPTS=-Xms2g -Xmx2g
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.transport.ssl.enabled=true
- xpack.license.self_generated.type=basic
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
- path.repo=/usr/share/elasticsearch/backups
- cluster.routing.allocation.disk.watermark.low=85%
- cluster.routing.allocation.disk.watermark.high=90%
- cluster.routing.allocation.disk.watermark.flood_stage=95%
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
volumes:
- es_data:/usr/share/elasticsearch/data
- es_backups:/usr/share/elasticsearch/backups
ports:
- "127.0.0.1:${ES_PORT}:9200"
healthcheck:
test:
- CMD-SHELL
- >
curl -s -k -u elastic:${ELASTIC_PASSWORD}
https://localhost:9200/_cluster/health
| grep -E '"status":"(green|yellow)"' || exit 1
interval: 20s
timeout: 10s
retries: 10
start_period: 60s
networks:
- elastic_internal
kibana:
image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
container_name: kibana
restart: unless-stopped
depends_on:
elasticsearch:
condition: service_healthy
environment:
- SERVER_NAME=kibana.example.com
- SERVER_PUBLICBASEURL=https://kibana.example.com
- ELASTICSEARCH_HOSTS=https://elasticsearch:9200
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
- ELASTICSEARCH_SSL_VERIFICATIONMODE=certificate
- ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=/usr/share/kibana/config/certs/ca/ca.crt
- XPACK_SECURITY_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- XPACK_REPORTING_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- TELEMETRY_OPTIN=false
volumes:
- kibana_data:/usr/share/kibana/data
- /opt/docker/elastic/certs:/usr/share/kibana/config/certs:ro
ports:
- "127.0.0.1:${KIBANA_PORT}:5601"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:5601/api/status | grep -q '\"level\":\"available\"' || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
networks:
- elastic_internal
- proxy
filebeat:
image: docker.elastic.co/beats/filebeat:${STACK_VERSION}
container_name: filebeat
restart: unless-stopped
user: root
depends_on:
elasticsearch:
condition: service_healthy
command: ["filebeat", "-e", "--strict.perms=false"]
environment:
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
volumes:
- /opt/docker/elastic/config/filebeat.docker.yml:/usr/share/filebeat/filebeat.yml:ro
- /opt/docker/elastic/certs/ca/ca.crt:/usr/share/filebeat/ca.crt:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- elastic_internalRepare:
- ES e Kibana bindam só em 127.0.0.1. Exposição externa é via nginx-proxy.
proxyestá só no Kibana (nginx-proxy precisa alcançar Kibana, não ES).mem_limitnão é setado — ajuste se quiser teto duro de RSS.
6. Bootstrap de segurança
Na primeira subida o ES auto-configura CA + certificado de nó interno. Você só precisa extrair a CA e resetar as senhas dos usuários built-in.
6.1 Primeira subida com senha inicial
Defina um ELASTIC_PASSWORD provisório no .env (ele é o admin):
# .env
ELASTIC_PASSWORD=mude_isto_no_primeiro_bootSuba só o Elasticsearch:
docker compose up -d elasticsearch
docker compose logs -f elasticsearchAguarde started e o healthcheck virar healthy:
docker compose ps elasticsearch6.2 Extrair a CA autogerada
mkdir -p /opt/docker/elastic/certs/ca
docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt \
/opt/docker/elastic/certs/ca/ca.crt
chmod 644 /opt/docker/elastic/certs/ca/ca.crtEsse ca.crt será montado no Kibana, Filebeat e qualquer outro cliente.
6.3 Resetar/gerar senha do kibana_system
O usuário kibana_system é built-in mas vem sem senha conhecida. Gere uma:
docker exec -it elasticsearch \
bin/elasticsearch-reset-password -u kibana_system -bA flag -b (batch) imprime a senha no stdout. Copie e cole no .env:
# .env
KIBANA_PASSWORD=<senha-gerada>Você também pode resetar o usuário elastic (admin) para gerar uma senha forte:
docker exec -it elasticsearch \
bin/elasticsearch-reset-password -u elastic -b
# atualizar ELASTIC_PASSWORD no .env6.4 (Alternativa) Enrollment token
Se em vez de configurar ELASTICSEARCH_USERNAME/PASSWORD no Kibana você preferir o fluxo de enrollment automático:
docker exec -it elasticsearch \
bin/elasticsearch-create-enrollment-token --scope kibanaToken válido por 30 minutos. Cole no setup wizard do Kibana — ele auto-popula kibana.yml com a senha do kibana_system e a CA. Para deploy “infra-as-code” o caminho do ELASTICSEARCH_USERNAME/PASSWORD é mais reproduzível.
6.5 Subir o Kibana
docker compose up -d kibana
docker compose logs -f kibanaEspere Kibana is now available. Teste local:
curl -sf http://127.0.0.1:5601/api/status | jq '.status.overall'6.6 TLS
O ES já está com TLS no HTTP layer (porta 9200) e transport layer. A CA é autogerada e vale para o nome elasticsearch (DNS interno do compose). Clientes externos via 127.0.0.1:9200 precisam usar -k no curl ou montar a CA.
Para uma instalação single-node isso é suficiente. Se um dia quiser adicionar nós ou usar um cert público (Let’s Encrypt) para a API HTTP, regenere com bin/elasticsearch-certutil.
7. Reverse proxy nginx para Kibana
Adicione um vhost em /opt/docker/nginx/conf.d/kibana.conf (assumindo seu layout de nginx-proxy + Let’s Encrypt já existente):
server {
listen 443 ssl http2;
server_name kibana.example.com;
ssl_certificate /etc/letsencrypt/live/kibana.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/kibana.example.com/privkey.pem;
include /etc/nginx/conf.d/ssl_common.conf;
# Allowlist da rede confiável — restringir ao range da VPN
allow 10.10.0.0/24;
allow 100.64.0.0/10; # Tailscale, se for o caso
deny all;
client_max_body_size 50m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_connect_timeout 30s;
location / {
proxy_pass http://kibana:5601;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
}
server {
listen 80;
server_name kibana.example.com;
return 301 https://$host$request_uri;
}Reload:
docker exec nginx-proxy nginx -t && docker exec nginx-proxy nginx -s reloadAponte o DNS interno (ou hosts file via VPN) kibana.example.com → 203.0.113.10, acesse via VPN e logue com elastic / <ELASTIC_PASSWORD>.
8. Index lifecycle (ILM)
A ideia: indexar logs em índices rotativos (rollover), manter os recentes em “hot”, mover os antigos para “warm” (force-merge + read-only), deletar passada a retenção.
8.1 Policy logs-policy
config/ilm-logs-policy.json:
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_age": "7d",
"max_primary_shard_size": "20gb"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 },
"readonly": {}
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}Aplicar:
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_ilm/policy/logs-policy \
-H 'Content-Type: application/json' \
-d @config/ilm-logs-policy.json8.2 Index template para logs-*
Single-node não tem replicas (não tem outro nó pra replicar), então 0 replicas.
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_index_template/logs-template \
-H 'Content-Type: application/json' \
-d '{
"index_patterns": ["logs-*"],
"priority": 200,
"data_stream": {},
"template": {
"settings": {
"index.lifecycle.name": "logs-policy",
"index.number_of_shards": 1,
"index.number_of_replicas": 0,
"index.refresh_interval": "5s",
"index.codec": "best_compression"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"service.name": { "type": "keyword" },
"service.env": { "type": "keyword" },
"log.level": { "type": "keyword" },
"message": { "type": "text" },
"trace.id": { "type": "keyword" },
"span.id": { "type": "keyword" },
"container.name":{ "type": "keyword" },
"host.name": { "type": "keyword" }
}
}
}
}'Confira:
curl -k -u elastic:$ELASTIC_PASSWORD \
https://127.0.0.1:9200/_index_template/logs-template?pretty9. Ingerindo logs do Docker
Opção A — Filebeat container modular (recomendada para começar)
config/filebeat.docker.yml:
filebeat.autodiscover:
providers:
- type: docker
hints.enabled: true
hints.default_config:
type: container
paths:
- /var/lib/docker/containers/${data.docker.container.id}/*.log
processors:
- add_docker_metadata: ~
- add_host_metadata: ~
processors:
- drop_event:
when:
equals:
container.name: "filebeat"
output.elasticsearch:
hosts: ["https://elasticsearch:9200"]
username: "elastic"
password: "${ELASTIC_PASSWORD}"
ssl.certificate_authorities: ["/usr/share/filebeat/ca.crt"]
# Escreve em data stream logs-<service>-<namespace>
index: "logs-%{[container.labels.co_elastic_logs/service_name]:generic}-default"
setup.template.enabled: false
setup.ilm.enabled: false # Estamos usando o template+ILM que criamos no passo 8
logging.level: info
logging.to_files: falseSuba:
docker compose up -d filebeat
docker compose logs -f filebeatHints nos containers das aplicações
No compose das suas aplicações, adicione labels para o Filebeat reconhecer:
services:
api-backend:
image: registry.example.com/api-backend:1.4.2
labels:
co.elastic.logs/enabled: "true"
co.elastic.logs/json.keys_under_root: "true"
co.elastic.logs/json.overwrite_keys: "true"
co.elastic.logs/json.add_error_key: "true"
co.elastic.logs/json.expand_keys: "true"
co.elastic.logs/service_name: "api-backend"
web-frontend:
image: registry.example.com/web-frontend:2.1.0
labels:
co.elastic.logs/enabled: "true"
co.elastic.logs/module: "nginx"
co.elastic.logs/fileset.stdout: "access"
co.elastic.logs/fileset.stderr: "error"
co.elastic.logs/service_name: "web-frontend"Opção B — docker logging driver
Alternativa sem coletor: cada container envia direto via driver.
services:
app:
logging:
driver: gelf
options:
gelf-address: "udp://127.0.0.1:12201"
tag: "app"Requer Logstash com input gelf (porta 12201). Não recomendado: perde se Logstash reiniciar (UDP), e qualquer pull novo do container reseta config. Fica de plano B.
Opção C — Elastic Agent + Fleet (caminho moderno)
Adicione um Fleet Server e um Elastic Agent (mesmo container faz ambos):
fleet-server:
image: docker.elastic.co/elastic-agent/elastic-agent:${STACK_VERSION}
container_name: fleet-server
restart: unless-stopped
depends_on:
kibana:
condition: service_healthy
environment:
- FLEET_SERVER_ENABLE=true
- FLEET_SERVER_ELASTICSEARCH_HOST=https://elasticsearch:9200
- FLEET_SERVER_ELASTICSEARCH_CA=/certs/ca.crt
- FLEET_SERVER_SERVICE_TOKEN=${FLEET_SERVER_TOKEN}
- FLEET_SERVER_POLICY_ID=fleet-server-policy
- KIBANA_FLEET_SETUP=true
- KIBANA_HOST=http://kibana:5601
volumes:
- /opt/docker/elastic/certs/ca/ca.crt:/certs/ca.crt:ro
ports:
- "127.0.0.1:8220:8220"
networks:
- elastic_internalGerar FLEET_SERVER_TOKEN:
docker exec -it elasticsearch bin/elasticsearch-service-tokens create elastic/fleet-server token1Depois, em Kibana → Management → Fleet → Add Agent, gere o enrollment token e instale um Elastic Agent em cada host (curl -L https://artifacts.elastic.co/.../elastic-agent-linux.tar.gz). Toda integração (nginx, kafka, postgres, system) é então adicionada via UI sem mexer em arquivo YAML.
Recomendação: comece com Opção A (Filebeat), migre para Opção C quando tiver 3+ hosts produzindo log.
10. Ingerindo logs de uma aplicação Spring Boot
Objetivo: o Spring loga JSON estruturado em stdout; o docker captura; Filebeat parseia o JSON como objeto e indexa cada campo. Sem grok, sem regex.
10.1 Dependência Maven/Gradle
build.gradle.kts:
dependencies {
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
}10.2 logback-spring.xml
src/main/resources/logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty name="appName" source="spring.application.name" defaultValue="api-backend"/>
<springProperty name="appEnv" source="spring.profiles.active" defaultValue="default"/>
<!-- Console plain em dev -->
<springProfile name="default | dev | test">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</springProfile>
<!-- JSON em prod -->
<springProfile name="prod | docker">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>false</includeContext>
<includeMdc>true</includeMdc>
<customFields>{"service.name":"${appName}","service.env":"${appEnv}"}</customFields>
<fieldNames>
<timestamp>@timestamp</timestamp>
<message>message</message>
<thread>thread.name</thread>
<logger>logger.name</logger>
<level>log.level</level>
<levelValue>[ignore]</levelValue>
<version>[ignore]</version>
</fieldNames>
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<maxLength>3000</maxLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</encoder>
</appender>
</springProfile>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.springframework.security" level="INFO"/>
<logger name="org.hibernate.SQL" level="WARN"/>
</configuration>10.3 MDC com trace ID
Spring Boot 3 + Micrometer Tracing já preenche traceId e spanId no MDC automaticamente. Para refletirem como trace.id / span.id (ECS), adicione no application.yml:
logging:
pattern:
correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]"E configure o LogstashEncoder com:
<fieldNames>
<mdc>[ignore]</mdc> <!-- não dumpar todo MDC, mapear individual -->
</fieldNames>
<provider class="net.logstash.logback.composite.loggingevent.MdcJsonProvider">
<fieldName>mdc</fieldName>
</provider>Ou, mais simples, deixa o MDC inteiro virar campos top-level e renomeia via ingest pipeline (passo 11).
10.4 Sample de evento ingerido
Em Discover você verá:
{
"@timestamp": "2026-05-11T15:32:11.421Z",
"service.name": "api-backend",
"service.env": "prod",
"log.level": "ERROR",
"logger.name": "com.example.api.LancamentoService",
"thread.name": "http-nio-8080-exec-3",
"message": "Falha ao buscar lançamentos do mês",
"traceId": "8f3c2e...",
"spanId": "a1b2c3...",
"stack_trace": "java.lang.NullPointerException\n at ...",
"container.name": "api-backend",
"container.image.name": "registry.example.com/api-backend:1.4.2"
}11. Parsing com Ingest Pipeline
Use para logs não-JSON (ex: nginx access log padrão).
11.1 Pipeline para nginx access
config/ingest-nginx.json:
{
"description": "Parse nginx access log",
"processors": [
{
"grok": {
"field": "message",
"patterns": [
"%{IPORHOST:source.address} - %{DATA:user.name} \\[%{HTTPDATE:nginx.access.time}\\] \"%{WORD:http.request.method} %{DATA:url.original} HTTP/%{NUMBER:http.version}\" %{NUMBER:http.response.status_code:int} %{NUMBER:http.response.body.bytes:long} \"%{DATA:http.request.referrer}\" \"%{DATA:user_agent.original}\""
],
"ignore_missing": true
}
},
{
"date": {
"field": "nginx.access.time",
"formats": ["dd/MMM/yyyy:HH:mm:ss Z"],
"target_field": "@timestamp"
}
},
{ "remove": { "field": "nginx.access.time", "ignore_missing": true } },
{ "geoip": { "field": "source.address", "target_field": "source.geo", "ignore_missing": true } },
{ "user_agent":{ "field": "user_agent.original", "target_field": "user_agent", "ignore_missing": true } },
{
"set": {
"field": "event.kind",
"value": "event"
}
}
],
"on_failure": [
{
"set": {
"field": "error.message",
"value": "{{ _ingest.on_failure_message }}"
}
}
]
}Criar:
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_ingest/pipeline/nginx-access \
-H 'Content-Type: application/json' \
-d @config/ingest-nginx.json11.2 Aplicar via template
Edite o template logs-template para apontar index.default_pipeline quando o índice casar com logs-nginx-*, ou crie um template específico:
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_index_template/logs-nginx \
-H 'Content-Type: application/json' \
-d '{
"index_patterns": ["logs-nginx-*"],
"priority": 300,
"data_stream": {},
"template": {
"settings": {
"index.default_pipeline": "nginx-access",
"index.lifecycle.name": "logs-policy",
"index.number_of_shards": 1,
"index.number_of_replicas": 0
}
}
}'11.3 Testar a pipeline
Use _simulate antes de produção:
curl -k -u elastic:$ELASTIC_PASSWORD -X POST \
https://127.0.0.1:9200/_ingest/pipeline/nginx-access/_simulate \
-H 'Content-Type: application/json' \
-d '{
"docs": [{
"_source": {
"message": "192.168.1.10 - - [11/May/2026:15:32:11 +0000] \"GET /api/v1/lancamentos HTTP/1.1\" 200 1532 \"-\" \"Mozilla/5.0\""
}
}]
}'12. Visualização no Kibana
12.1 Data view
Stack Management → Data Views → Create:
- Name:
logs - Index pattern:
logs-* - Timestamp field:
@timestamp
12.2 Discover
Discover → seleciona o data view logs. Filtros úteis:
log.level: ERRORservice.name: api-backend AND log.level: (ERROR OR WARN)http.response.status_code >= 500
Salve searches frequentes (botão Save).
12.3 Dashboards básicos
Crie via Lens (drag & drop) — não precisa decorar Vega.
Dashboard “Plataforma — visão geral”:
Taxa de erro por serviço — Bar vertical
- Eixo X:
service.name(Top values, 10) - Eixo Y:
Count of recordscom filtrolog.level: ERROR - Break down: nenhum
- Eixo X:
Logs por nível ao longo do tempo — Area stacked
- Eixo X:
@timestamp(auto bucket) - Eixo Y:
Count - Break down:
log.level
- Eixo X:
Top loggers em erro — Tabela
- Linhas:
logger.name(Top 20) - Métricas:
Count(filtrolog.level: ERROR)
- Linhas:
Status code distribution (nginx) — Donut
- Slice by:
http.response.status_code - Métrica:
Count
- Slice by:
p95 de latência — se você logar
http.response.time_ms- Linha: percentil 95 de
http.response.time_ms - Eixo X:
@timestamp - Break down:
service.name
- Linha: percentil 95 de
Salve o dashboard como “Plataforma — visão geral” e marque como home (Kibana → Settings).
12.4 Saved object JSON (exemplo)
Você pode exportar/importar dashboards via NDJSON:
{
"attributes": {
"title": "Logs por nível",
"visualizationType": "lnsXY",
"state": {
"datasourceStates": { "formBased": { "layers": {} } },
"visualization": { "preferredSeriesType": "area_stacked" }
}
},
"type": "lens"
}Stack Management → Saved Objects → Import.
13. Alertas
Use Rules (Stack Management → Alerts and Insights → Rules).
13.1 Conector Slack/Discord webhook
Stack Management → Connectors → Create:
- Type: Webhook
- Name:
discord-ops - Method:
POST - URL:
https://discord.com/api/webhooks/<id>/<token> - Headers:
Content-Type: application/json - Body template (Mustache):
{
"content": "**[{{context.rule.name}}]** {{context.message}}\nHits: {{context.hits}} | Período: {{params.timeWindowSize}}{{params.timeWindowUnit}}"
}13.2 Regra “5xx > 50 em 5 min”
Rules → Create → tipo Elasticsearch query:
- Name:
nginx-5xx-spike - Index:
logs-nginx-* - Time field:
@timestamp - Query (DSL):
{
"query": {
"bool": {
"filter": [
{ "range": { "http.response.status_code": { "gte": 500 } } }
]
}
}
}- Threshold:
is above 50 - Time window:
5 minutes - Check every:
1 minute - Action: conector
discord-ops, run whenQuery matched
13.3 Regra “erro fatal no backend”
- Index:
logs-api-backend-* - Query:
log.level:FATAL OR log.level:ERROR - Threshold:
is above 10em5 minutes - Action: webhook
13.4 Health do cluster
Stack Monitoring (Kibana) → Cluster alerts. Habilite as default rules: license expira, heap > 85%, disk watermark, version mismatch.
14. APM (opcional)
Stack Management → Integrations → instale “APM” para subir um APM Server (recente: ele é integrado ao Elastic Agent, não precisa container separado).
Spring Boot — agente Java
Baixe elastic-apm-agent-<ver>.jar e adicione ao Dockerfile:
ADD https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/1.50.0/elastic-apm-agent-1.50.0.jar /opt/apm-agent.jar
ENV JAVA_TOOL_OPTIONS="-javaagent:/opt/apm-agent.jar"
ENV ELASTIC_APM_SERVICE_NAME=api-backend
ENV ELASTIC_APM_SERVER_URL=http://apm-server:8200
ENV ELASTIC_APM_ENVIRONMENT=prod
ENV ELASTIC_APM_APPLICATION_PACKAGES=com.example.apiVue (frontend) — RUM
npm i @elastic/apm-rumimport { init as initApm } from "@elastic/apm-rum";
initApm({
serviceName: "web-frontend",
serverUrl: "https://apm.example.com",
serviceVersion: import.meta.env.VITE_APP_VERSION,
environment: "prod",
distributedTracingOrigins: ["https://api.example.com"],
});Em Kibana → Observability → APM você vê services, transactions, dependencies, e o trace.id correlaciona com os logs (clique “View related logs”).
15. Backup (snapshot)
15.1 Registrar repositório fs
Já está mapeado em path.repo=/usr/share/elasticsearch/backups no compose, montado para /opt/docker/elastic/backups.
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_snapshot/local-fs \
-H 'Content-Type: application/json' \
-d '{
"type": "fs",
"settings": {
"location": "/usr/share/elasticsearch/backups",
"compress": true,
"max_snapshot_bytes_per_sec": "50mb",
"max_restore_bytes_per_sec": "50mb"
}
}'
# Verificar
curl -k -u elastic:$ELASTIC_PASSWORD \
-X POST https://127.0.0.1:9200/_snapshot/local-fs/_verify15.2 SLM policy diária
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_slm/policy/daily-snapshots \
-H 'Content-Type: application/json' \
-d '{
"schedule": "0 30 3 * * ?",
"name": "<snap-{now/d}>",
"repository": "local-fs",
"config": {
"indices": ["logs-*", ".kibana*", ".security*"],
"include_global_state": true
},
"retention": {
"expire_after": "30d",
"min_count": 7,
"max_count": 30
}
}'Rodar manualmente:
curl -k -u elastic:$ELASTIC_PASSWORD -X POST \
https://127.0.0.1:9200/_slm/policy/daily-snapshots/_execute15.3 Cópia offsite
Faça rsync do diretório /opt/docker/elastic/backups/ para outro host (NAS, borg, restic):
restic -r sftp:backup@nas:/backups/elastic backup /opt/docker/elastic/backups15.4 Restore
curl -k -u elastic:$ELASTIC_PASSWORD -X POST \
https://127.0.0.1:9200/_snapshot/local-fs/<snapshot-name>/_restore \
-H 'Content-Type: application/json' \
-d '{
"indices": "logs-api-backend-*",
"include_global_state": false,
"rename_pattern": "logs-(.+)",
"rename_replacement": "restored-logs-$1"
}'16. Upgrade
Elastic Stack tem regra: upgrade Elasticsearch antes do Kibana, e Kibana nunca pode ser de versão maior que ES.
16.1 Checklist pré-upgrade
# 1. Cluster verde?
curl -k -u elastic:$ELASTIC_PASSWORD \
https://127.0.0.1:9200/_cluster/health?pretty
# 2. Deprecations
curl -k -u elastic:$ELASTIC_PASSWORD \
https://127.0.0.1:9200/_migration/deprecations?pretty
# 3. Snapshot fresco
curl -k -u elastic:$ELASTIC_PASSWORD -X POST \
https://127.0.0.1:9200/_slm/policy/daily-snapshots/_executeVerifique também o Upgrade Assistant em Kibana → Stack Management → Upgrade Assistant.
16.2 Procedimento
- Bump
STACK_VERSIONno.env(ex:8.16.1→8.17.0) docker compose pull elasticsearchdocker compose up -d elasticsearch— espera healthydocker compose pull kibana && docker compose up -d kibanadocker compose pull filebeat && docker compose up -d filebeat- Confere
_cluster/healthe Discover
Upgrades entre majors (8 → 9) exigem reindex de índices criados em majors antigas. Upgrade Assistant marca quais.
17. Troubleshooting
max virtual memory areas vm.max_map_count [65530] is too low
ES não sobe. Resolução: passo 4.
sudo sysctl -w vm.max_map_count=262144Persistir em /etc/sysctl.d/99-elasticsearch.conf.
circuit_breaking_exception ou OOM no heap
ES está estourando heap. Sinais: logs [parent] Data too large, restart loop, OOM Killer matando o container.
- Aumente
ES_JAVA_OPTS=-Xms4g -Xmx4g(50% da RAM do container, máx 31g). - Reduza tamanho de queries (
from + size, agregações semsize). - Verifique mappings explodindo (mapping explosion) —
_cat/indices?v&h=index,docs.count,store.size,pri,rep.
Cluster vira read_only_allow_delete — disco cheio
Os flood_stage watermark default (95%) trava escrita. Limpe disco, ou:
# Tira o read-only
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_all/_settings \
-H 'Content-Type: application/json' \
-d '{ "index.blocks.read_only_allow_delete": null }'
# Ajusta watermarks (cuidado em prod)
curl -k -u elastic:$ELASTIC_PASSWORD -X PUT \
https://127.0.0.1:9200/_cluster/settings \
-H 'Content-Type: application/json' \
-d '{
"transient": {
"cluster.routing.allocation.disk.watermark.low": "85%",
"cluster.routing.allocation.disk.watermark.high": "90%",
"cluster.routing.allocation.disk.watermark.flood_stage": "95%"
}
}'Filebeat duplicando linhas
Causa comum: registry corrompido depois de mover container. O registry guarda offset por arquivo de log.
docker compose stop filebeat
docker exec filebeat rm -rf /usr/share/filebeat/data/registry
docker compose start filebeatVocê pode perder posição (re-indexa do começo). Para evitar duplicação no índice, mantenha campo _id determinístico via processor fingerprint na ingest pipeline:
{ "fingerprint": { "fields": ["@timestamp", "message", "container.id"], "target_field": "_id" } }Kibana: “Kibana server is not ready yet”
Olhe docker compose logs kibana. Causas frequentes:
xpack.encryptedSavedObjects.encryptionKeyfaltando ou inconsistente entre restarts (alertas e connectors viram inutilizáveis). UseXPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEYdo passo 5.kibana_systemcom senha errada → resetar (passo 6.3).- ES ainda não está pronto — o
depends_on: service_healthycobre. - Versão Kibana ≠ versão ES.
MasterNotDiscoveredException em single-node
Faltou discovery.type=single-node. Confira no compose.
Filebeat não vê containers (autodiscover vazio)
- Socket docker montado?
/var/run/docker.sockprecisa estar no compose do Filebeat com permissão de leitura. user: rootno Filebeat (defaultfilebeatnão tem grupo docker).- Containers das apps têm a label
co.elastic.logs/enabled=true?
Logs JSON aparecendo como string em message
O Filebeat não decodificou. Cheque labels no container alvo:
labels:
co.elastic.logs/json.keys_under_root: "true"
co.elastic.logs/json.overwrite_keys: "true"
co.elastic.logs/json.add_error_key: "true"Ou no filebeat.docker.yml dentro de hints.default_config:
json.keys_under_root: true
json.overwrite_keys: true
json.add_error_key: trueCertificado autogerado expira
Os certs autogerados pelo ES têm validade default de 2 anos. Antes de expirar, regenere:
docker exec -it elasticsearch bin/elasticsearch-certutil httpOu rode com xpack.security.http.ssl.enabled=false atrás de proxy nginx terminando TLS (não recomendado se Filebeat fala direto com ES de outros hosts).
18. Referências
- Elastic Stack docs (raiz): https://www.elastic.co/guide/index.html
- Install Elasticsearch with Docker: https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
- elasticsearch-reset-password: https://www.elastic.co/guide/en/elasticsearch/reference/current/reset-password.html
- elasticsearch-create-enrollment-token: https://www.elastic.co/docs/reference/elasticsearch/command-line-tools/create-enrollment-token
- Configuring stack security: https://www.elastic.co/guide/en/elasticsearch/reference/current/configuring-stack-security.html
- ILM tutorial: https://www.elastic.co/guide/en/elasticsearch/reference/current/example-using-index-lifecycle-policy.html
- ILM phases & actions: https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management/index-lifecycle
- Snapshot & restore: https://www.elastic.co/docs/deploy-manage/tools/snapshot-and-restore
- SLM: https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-lifecycle-management.html
- Fleet Server: https://www.elastic.co/guide/en/fleet/current/fleet-server.html
- Filebeat autodiscover: https://www.elastic.co/guide/en/beats/filebeat/current/configuration-autodiscover.html
- logstash-logback-encoder: https://github.com/logfellow/logstash-logback-encoder
- Elastic APM agents: https://www.elastic.co/guide/en/apm/agent/index.html
- ECS (Elastic Common Schema): https://www.elastic.co/guide/en/ecs/current/index.html
Repos de referência:
- Getting started compose: https://www.elastic.co/blog/getting-started-with-the-elastic-stack-and-docker-compose
- docker-elk (deviantony): https://github.com/deviantony/docker-elk
- elastdocker (sherifabdlnaby): https://github.com/sherifabdlnaby/elastdocker
- log aggregation Spring Boot (cassiomolin): https://github.com/cassiomolin/log-aggregation-spring-boot-elastic-stack
Alternativas:
- OpenSearch: https://opensearch.org/docs/latest/
- Grafana Loki: https://grafana.com/docs/loki/latest/
- VictoriaLogs: https://docs.victoriametrics.com/victorialogs/