Guia: centralização de logs com Fluentd e Elasticsearch para aplicações PHP em VPS
Por Kevin Marcondes · Publicado em · Atualizado em · 9 min de leitura
Este guia mostra, na prática, como implementar centralização de logs com Fluentd e Elasticsearch para aplicações PHP em VPS. Você vai padronizar logs em JSON, subir a stack via Docker Compose, configurar roteamento no Fluentd e definir retenção segura no Elasticsearch.
Arquitetura: o que vamos montar
A solução coleta logs de apps PHP e serviços (Nginx/Apache), envia ao Fluentd e indexa no Elasticsearch. Opcionalmente, você usa o Kibana para consulta e dashboards. Tudo roda em uma VPS, com isolamento via Docker e rede interna.
O foco é confiabilidade e simplicidade: logs estruturados JSON na origem, buffer no Fluentd, índices com retenção automática no Elasticsearch, e portas expostas apenas na interface local da VPS.
- Aplicações PHP escrevem logs em JSON (Monolog/PSR-3).
- Fluentd faz tail dos arquivos e enriquece metadados (host, app, env).
- Elasticsearch armazena e indexa; Kibana visualiza.
- Rede Docker interna para serviços; portas externas bindadas em 127.0.0.1.
| Fonte de logs | Como coletar no Fluentd | Observações |
|---|---|---|
| App PHP (arquivo) | Tail em /var/log/php/app.log (JSON por linha) | Garanta rotação de arquivo e timestamp consistente. |
| Nginx/Apache | Tail access/error.log + parser (regex/grok) | Unifique timezone e inclua request_id se possível. |
| Containers Docker | Tail de arquivos do container ou driver fluentd | Driver facilita mas aumenta acoplamento; avalie. |
Pré-requisitos e planejamento de capacidade
Você precisa de uma VPS com Docker/Compose instalados, porta 9200 e 5601 acessíveis apenas localmente e disco suficiente para o volume de logs. Elasticsearch consome RAM e I/O; ajuste heap conforme a máquina.
Checklist rápido:
- Docker e Compose instalados; se necessário, siga o guia como instalar o Docker e o Docker Compose no Linux.
- Firewall configurado; veja o checklist de segurança para VPS Linux.
- Diretórios de dados dedicados para Elasticsearch e Fluentd.
- Plano de retenção de índices para evitar disco cheio.
Importante: nunca exponha a porta 9200 na internet sem autenticação e TLS. Bind em 127.0.0.1 e use túnel/SSH ou um proxy autenticado para acesso.
# Requisito do Elasticsearch
sudo sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-elastic.conf
# Estrutura de diretórios (ajuste caminho conforme sua preferência)
mkdir -p ~/observability/elasticsearch/{data,logs}
mkdir -p ~/observability/fluentd/{state,buffer}
mkdir -p ~/observability/logs
Padronize logs PHP em JSON com contexto (PSR-3/Monolog)
Centralização funciona melhor com logs estruturados. Use Monolog com JSONFormatter e inclua campos como level, message, datetime, request_id, user_id e app. Isso simplifica buscas e correlação. Veja também o guia de logs estruturados (JSON) e correlação: guia para práticas de modelagem.
Exemplo básico de configuração com Monolog em um projeto PHP independente. Em frameworks como Laravel, ajuste em config/logging.php (veja docs em laravel.com).
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
$logDir = __DIR__ . '/logs';
if (!is_dir($logDir)) mkdir($logDir, 0775, true);
$logger = new Logger('my-php-app');
$handler = new StreamHandler($logDir . '/app.log', Logger::INFO);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
// Contexto útil para correlação
$context = [
'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8)),
'user_id' => $currentUserId ?? null,
'env' => getenv('APP_ENV') ?: 'prod',
];
$logger->info('User logged in', $context);
$logger->error('Payment failed', $context + ['order_id' => 123]);
Suba Elasticsearch e Kibana com Docker Compose
Vamos iniciar um Elasticsearch de nó único e Kibana para visualização. Bind de portas apenas em 127.0.0.1. Defina a variável ELASTIC_PASSWORD no ambiente do shell antes de rodar. Ajuste ES_JAVA_OPTS (-Xms/-Xmx) à sua RAM.
Para acesso remoto, use túnel SSH ou um proxy com autenticação. Não exponha 9200/5601 publicamente.
version: "3.8"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
container_name: elasticsearch
environment:
- discovery.type=single-node
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-changeme}
- xpack.security.enabled=true
- xpack.security.transport.ssl.enabled=true
- ES_JAVA_OPTS=-Xms1g -Xmx1g
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./elasticsearch/data:/usr/share/elasticsearch/data
- ./elasticsearch/logs:/usr/share/elasticsearch/logs
ports:
- "127.0.0.1:9200:9200"
networks:
- observability
kibana:
image: docker.elastic.co/kibana/kibana:8.13.4
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=elastic
- ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD:-changeme}
ports:
- "127.0.0.1:5601:5601"
depends_on:
- elasticsearch
networks:
- observability
networks:
observability:
name: observability
Configure o Fluentd: inputs, enriquecimento e saída para Elasticsearch
O Fluentd fará tail nos arquivos de log, enriquece com host/app/env e envia para o Elasticsearch com buffer em disco. Ajuste caminhos e nomes de tag conforme seu ambiente.
No exemplo, usamos app.log do PHP e access.log do Nginx. O prefixo de índice será php-logs, gerando índices diários.
# fluent.conf
# Input: app PHP em JSON (uma linha por evento)
<source>
@type tail
path /var/log/php/app.log
pos_file /fluentd/state/app.log.pos
tag php.app
read_from_head true
<parse>
@type json
time_key datetime
time_type string
</parse>
</source>
# Input: Nginx access log (texto); adapte regex conforme seu formato
<source>
@type tail
path /var/log/nginx/access.log
pos_file /fluentd/state/nginx.access.pos
tag nginx.access
read_from_head true
<parse>
@type regexp
expression /^(?<remote>\S+) - (?<user>\S+) \[(?<time>[^\]]+)\] "(?<method>\S+) (?<path>\S+) (?<proto>[^"]+)" (?<status>\d{3}) (?<bytes>\d+)/
time_key time
time_format %d/%b/%Y:%H:%M:%S %z
</parse>
</source>
# Enriquecimento: host/env/app
<filter **>
@type record_transformer
enable_ruby true
<record>
host ${Socket.gethostname}
env ${ENV['env'] || 'prod'}
app my-php-app
</record>
</filter>
# Saída: Elasticsearch (índices diários com prefixo)
<match **>
@type elasticsearch
host elasticsearch
port 9200
scheme http
user elastic
password ${ENV['ELASTIC_PASSWORD']}
logstash_format true
logstash_prefix php-logs
include_tag_key true
reconnect_on_error true
reload_connections false
request_timeout 30s
<buffer>
@type file
path /fluentd/buffer
flush_mode interval
flush_interval 5s
chunk_limit_size 8m
queued_chunks_limit_size 256
retry_forever true
retry_max_interval 30
</buffer>
</match>
Orquestração do Fluentd com Docker (plugin Elasticsearch)
A imagem oficial do Fluentd precisa do plugin elasticsearch. Crie um Dockerfile simples para instalar o plugin e um serviço no Compose para executar com volumes e rede compartilhada com Elasticsearch.
Monte o arquivo de logs do app no container do Fluentd como somente leitura. Exponha a porta do Forwarder (24224) apenas se for receber logs de outros hosts.
# fluentd/Dockerfile
FROM fluent/fluentd:v1.16-1
USER root
RUN gem install fluent-plugin-elasticsearch --no-document \
&& mkdir -p /fluentd/state /fluentd/buffer \
&& chown -R fluent:fluent /fluentd
COPY fluent.conf /fluentd/etc/fluent.conf
USER fluent
---
# docker-compose.fluentd.yml
version: "3.8"
services:
fluentd:
build: ./fluentd
container_name: fluentd
environment:
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-changeme}
- env=prod
volumes:
- ./fluentd/fluent.conf:/fluentd/etc/fluent.conf:ro
- ./logs/app.log:/var/log/php/app.log:ro
- ./fluentd/state:/fluentd/state
- ./fluentd/buffer:/fluentd/buffer
depends_on:
- elasticsearch
networks:
- observability
# Descomente se for receber de outros hosts/containers via forward
# ports:
# - "127.0.0.1:24224:24224"
networks:
observability:
external: true
Índices e retenção: template + ILM no Elasticsearch
Sem retenção, seu disco vai encher. Use um Policy ILM para deletar índices antigos e um template para aplicar a política em novos índices com prefixo php-logs-*. Isso funciona bem com o formato diário gerado pelo Fluentd.
Ajuste o min_age à sua necessidade e considere mover para quente->morno antes da deleção em ambientes maiores.
# Crie uma policy de deleção após 14 dias
curl -u elastic:${ELASTIC_PASSWORD} -X PUT \
'http://localhost:9200/_ilm/policy/php-logs-14d' \
-H 'Content-Type: application/json' -d '{
"policy": {"phases": {"hot": {"actions": {}}, "delete": {"min_age": "14d", "actions": {"delete": {}}}}}
}'
# Template para índices que começam com php-logs-
curl -u elastic:${ELASTIC_PASSWORD} -X PUT \
'http://localhost:9200/_index_template/php-logs' \
-H 'Content-Type: application/json' -d '{
"index_patterns": ["php-logs-*-*", "php-logs-*"],
"template": {
"settings": {
"index.lifecycle.name": "php-logs-14d",
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"dynamic": true,
"properties": {
"datetime": {"type": "date", "format": "strict_date_optional_time||epoch_millis"},
"level": {"type": "keyword"},
"app": {"type": "keyword"},
"host": {"type": "keyword"},
"message": {"type": "text"}
}
}
},
"priority": 500
}'
Como confirmar que funcionou e próximos passos
Gere um evento de teste no arquivo de log do app e verifique os logs do Fluentd. Em seguida, busque o evento no Elasticsearch. Para dashboards e análises, use o Kibana. Para métricas, complemente com o guia de configurar Prometheus e Grafana com Docker Compose em VPS.
Para rastreamento distribuído em PHP, veja Tracing distribuído com OpenTelemetry no PHP.
# 1) Gere um log de teste (ajuste caminho)
echo '{"datetime":"2024-05-01T12:00:00Z","level":"INFO","message":"hello from php","request_id":"abc123"}' \
>> ~/observability/logs/app.log
# 2) Veja o Fluentd entregando
docker compose -f docker-compose.fluentd.yml logs -f fluentd
# 3) Busque no Elasticsearch
curl -u elastic:${ELASTIC_PASSWORD} \
'http://localhost:9200/php-logs-*/_search?q=message:hello&pretty'
Solução de problemas comuns
Alguns problemas recorrentes e correções rápidas para manter a ingestão estável. Revise permissões, formato e buffers. Ajuste heap do Elasticsearch e o chunk_limit_size do Fluentd conforme o volume.
- Fluentd não lê arquivo: verifique caminho, permissões e se read_from_head está correto.
- Timestamp errado: padronize timezone no app e no parser; prefira ISO 8601 com Z.
- Índice mapeou tipo inválido: conserte o campo no app e recrie o índice/template.
- Disco enchendo: reduza retenção ILM, compacte logs e aumente rotação no app.
- Lentidão do Elasticsearch: ajuste ES_JAVA_OPTS, shards=1, replicas=0 em nó único.
- Nginx parser falhando: adapte a expressão regex ao seu formato real de log.
Perguntas frequentes
Posso usar Fluent Bit em vez de Fluentd?
Sim. Fluent Bit é mais leve e indicado como agente em hosts ou containers. Neste guia usamos Fluentd pela flexibilidade de plugins e buffers. A arquitetura geral e o envio ao Elasticsearch permanecem os mesmos.
Quanta RAM o Elasticsearch precisa em nó único?
Depende do volume e da complexidade das buscas. Para ambientes pequenos, ajuste a heap (ES_JAVA_OPTS) para usar parte da RAM disponível e monitore. Comece modesto e aumente conforme ingestão e consultas crescerem.
Como proteger o acesso ao Elasticsearch e ao Kibana?
Use autenticação embutida do Elasticsearch, bind em 127.0.0.1 e túnel SSH ou um proxy com autenticação e TLS. Para firewall e hardening da VPS, siga o checklist de segurança para VPS Linux.
É obrigatório usar JSON nos logs do PHP?
Não é obrigatório, mas recomendado. JSON reduz ambiguidades, melhora o parsing e acelera buscas. Se você mantiver texto livre, terá de criar parsers confiáveis e aceitar perda de detalhes estruturados em consultas.
Como lidar com múltiplas VPS ou microserviços?
Rode um Fluentd em cada VPS e envie para um Elasticsearch central. Use tags por serviço e ambiente, e unifique índices por prefixo. Garanta buffers locais e política de retenção para evitar perda durante falhas de rede.
Kibana é obrigatório? Posso usar outra ferramenta?
Kibana não é obrigatório, mas facilita muito a exploração e os dashboards. Você pode consultar via API do Elasticsearch ou integrar outras ferramentas compatíveis. O importante é manter os índices acessíveis e bem mapeados.
Quer acelerar sua implementação de logs?
Se você precisa adaptar o guia ao seu stack PHP e à sua VPS, vamos conversar sobre os próximos passos e riscos comuns.
Falar sobre seu cenárioVeja também
- Guia: checklist de pré‑deploy e plano de rollback rápido para aplicações PHP em produção
- Checklist de segurança para pipelines CI/CD com runners self‑hosted e secrets management
- Guia: organizar migrations e rollbacks em PostgreSQL para equipes pequenas
- Checklist de segurança para VPS Linux antes de colocar serviços em produção: hardening, SSH, backups e monitoramento