Guía: centralización de logs con Fluentd y Elasticsearch para aplicaciones PHP en VPS

Por · Publicado el · Actualizado el · 10 min de lectura

Cables de red verdes conectados a un panel de rack en un centro de datos
Detalle de cables en rack, representando infraestructura para recolección y procesamiento de logs. Foto: Brett Sayles / Pexels

Esta guía muestra, de forma práctica, cómo implementar la centralización de logs con Fluentd y Elasticsearch para aplicaciones PHP en VPS. Vas a estandarizar logs en JSON, levantar la stack con Docker Compose, configurar el enrutamiento en Fluentd y definir retención segura en Elasticsearch.

Arquitectura: qué vamos a montar

La solución recoge logs de aplicaciones PHP y servicios (Nginx/Apache), los envía a Fluentd y los indexa en Elasticsearch. Opcionalmente, puedes usar Kibana para consultas y dashboards. Todo corre en una VPS, con aislamiento por Docker y red interna.

El foco es fiabilidad y simplicidad: logs estructurados en JSON en origen, buffer en Fluentd, índices con retención automática en Elasticsearch y puertos expuestos solo en la interfaz local de la VPS.

  • Las aplicaciones PHP escriben logs en JSON (Monolog/PSR-3).
  • Fluentd hace tail de los archivos y enriquece metadatos (host, app, env).
  • Elasticsearch almacena e indexa; Kibana visualiza.
  • Red Docker interna para los servicios; puertos externos ligados a 127.0.0.1.
Fuente de logs Cómo colectar en Fluentd Observaciones
App PHP (archivo) Tail en /var/log/php/app.log (JSON por línea) Asegura rotación de archivo y timestamp consistente.
Nginx/Apache Tail access/error.log + parser (regex/grok) Unifica zona horaria e incluye request_id si es posible.
Contenedores Docker Tail de archivos del contenedor o driver fluentd El driver facilita pero aumenta acoplamiento; evalúa.

Precondiciones y planificación de capacidad

Necesitas una VPS con Docker/Compose instalados, puertos 9200 y 5601 accesibles solo localmente y disco suficiente para el volumen de logs. Elasticsearch consume RAM y I/O; ajusta el heap según la máquina.

Checklist rápido:

- Docker y Compose instalados; si es necesario, sigue la guía cómo instalar Docker y Docker Compose en Linux.

- Firewall configurado; consulta el checklist de seguridad para VPS Linux.

- Directorios de datos dedicados para Elasticsearch y Fluentd.

- Plan de retención de índices para evitar llenar el disco.

Importante: nunca expongas la puerta 9200 a Internet sin autenticación y TLS. Haz bind a 127.0.0.1 y usa túnel SSH o un proxy autenticado para acceso.

# 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

Estandariza logs PHP en JSON con contexto (PSR-3/Monolog)

La centralización funciona mejor con logs estructurados. Usa Monolog con JSONFormatter e incluye campos como level, message, datetime, request_id, user_id y app. Esto simplifica búsquedas y correlación. Consulta también la guía de logs estructurados (JSON) y correlación: guía para prácticas de modelado.

Ejemplo básico de configuración con Monolog en un proyecto PHP independiente. En frameworks como Laravel, ajusta en config/logging.php (revisa la documentación en 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]);

Levanta Elasticsearch y Kibana con Docker Compose

Arrancaremos un Elasticsearch de nodo único y Kibana para visualización. Haz bind de puertos solo en 127.0.0.1. Define la variable ELASTIC_PASSWORD en el entorno del shell antes de ejecutar. Ajusta ES_JAVA_OPTS (-Xms/-Xmx) a tu RAM.

Para acceso remoto, usa túnel SSH o un proxy con autenticación. No expongas 9200/5601 públicamente.

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

Configura Fluentd: inputs, enriquecimiento y salida a Elasticsearch

Fluentd hará tail de los archivos de log, enriquecerá con host/app/env y enviará a Elasticsearch con buffer en disco. Ajusta rutas y nombres de tag según tu entorno.

En el ejemplo usamos app.log del PHP y access.log del Nginx. El prefijo del índice será php-logs, generando índices diarios.

# 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>

# Enriquecimiento: 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>

Orquestación de Fluentd con Docker (plugin Elasticsearch)

La imagen oficial de Fluentd necesita el plugin elasticsearch. Crea un Dockerfile sencillo para instalar el plugin y un servicio en Compose para ejecutarlo con volúmenes y red compartida con Elasticsearch.

Monta el archivo de logs de la app en el contenedor de Fluentd como solo lectura. Expón el puerto del forwarder (24224) solo si vas a recibir logs de otros 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 y retención: template + ILM en Elasticsearch

Sin retención, el disco se llenará. Usa una policy ILM para eliminar índices antiguos y un template para aplicar la política a nuevos índices con prefijo php-logs-*. Esto funciona bien con el formato diario generado por Fluentd.

Ajusta min_age según tu necesidad y considera mover a caliente->templado antes de eliminar en entornos mayores.

# 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
}'

Cómo confirmar que funcionó y próximos pasos

Genera un evento de prueba en el archivo de log de la app y verifica los logs de Fluentd. Luego busca el evento en Elasticsearch. Para dashboards y análisis, usa Kibana. Para métricas, completa con la guía de configurar Prometheus y Grafana con Docker Compose en VPS.

Para trazado distribuido en PHP, consulta Trazado distribuido con OpenTelemetry en 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'

Solución de problemas comunes

Algunos problemas recurrentes y correcciones rápidas para mantener la ingestión estable. Revisa permisos, formato y buffers. Ajusta el heap de Elasticsearch y el chunk_limit_size de Fluentd según el volumen.

  • Fluentd no lee el archivo: verifica ruta, permisos y si read_from_head está correcto.
  • Timestamp incorrecto: estandariza la zona horaria en la app y en el parser; prefiere ISO 8601 con Z.
  • Índice mapeó tipo inválido: corrige el campo en la app y recrea el índice/template.
  • Disco lleno: reduce la retención ILM, compacta logs y aumenta la rotación en la app.
  • Lentitud de Elasticsearch: ajusta ES_JAVA_OPTS, shards=1, replicas=0 en nodo único.
  • El parser de Nginx falla: adapta la expresión regex a tu formato real de log.

Preguntas frecuentes

¿Puedo usar Fluent Bit en lugar de Fluentd?

Sí. Fluent Bit es más ligero y recomendado como agente en hosts o contenedores. En esta guía usamos Fluentd por la flexibilidad de plugins y buffers. La arquitectura general y el envío a Elasticsearch permanecen iguales.

¿Cuánta RAM necesita Elasticsearch en nodo único?

Depende del volumen y de la complejidad de las búsquedas. Para entornos pequeños, ajusta la heap (ES_JAVA_OPTS) para usar parte de la RAM disponible y monitoriza. Empieza modesto y aumenta según crezcan la ingestión y las consultas.

¿Cómo proteger el acceso a Elasticsearch y Kibana?

Usa la autenticación integrada de Elasticsearch, haz bind en 127.0.0.1 y utiliza túnel SSH o un proxy con autenticación y TLS. Para firewall y hardening de la VPS, sigue el checklist de seguridad para VPS Linux.

¿Es obligatorio usar JSON en los logs del PHP?

No es obligatorio, pero sí recomendable. JSON reduce ambigüedades, mejora el parsing y acelera las búsquedas. Si mantienes texto libre, deberás crear parsers fiables y aceptar pérdida de detalles estructurados en consultas.

¿Cómo manejar múltiples VPS o microservicios?

Ejecuta un Fluentd en cada VPS y envía a un Elasticsearch central. Usa tags por servicio y entorno, y unifica índices por prefijo. Asegura buffers locales y políticas de retención para evitar pérdida durante fallos de red.

¿Kibana es obligatorio? ¿Puedo usar otra herramienta?

Kibana no es obligatorio, pero facilita mucho la exploración y la creación de dashboards. Puedes consultar vía la API de Elasticsearch o integrar otras herramientas compatibles. Lo importante es mantener los índices accesibles y bien mapeados.

¿Quieres acelerar tu implementación de logs?

Si necesitas adaptar la guía a tu stack PHP y a tu VPS, hablemos sobre los próximos pasos y riesgos comunes.

Hablar sobre tu escenario

Ver también