Guia: centralização de logs com Fluentd e Elasticsearch para aplicações PHP em VPS

Por · Publicado em · Atualizado em · 9 min de leitura

Cabos de rede verdes conectados a um painel de rack em data center
Detalhe de cabos em rack, representando infraestrutura para coleta e processamento de logs. Foto: Brett Sayles / Pexels

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ário

Veja também