PHP项目日志聚合与ELK

wen PHP项目 2

PHP项目日志聚合与ELK实践指南

PHP应用 → 日志文件/标准输出 → Filebeat → Logstash → Elasticsearch → Kibana
                                   ↓
                               Redis/Kafka (缓冲层,可选)

PHP端日志配置

基础日志配置 (Monolog)

// composer.json
{
    "require": {
        "monolog/monolog": "^3.0"
    }
}
// logger.php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
$logger = new Logger('app');
$handler = new StreamHandler('/var/log/php-app/app.log', Logger::INFO);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);

结构化日志格式

<?php
// 使用结构化日志
class StructuredLogger
{
    private $logger;
    public function info($message, array $context = [])
    {
        $context['@timestamp'] = date('c');
        $context['app_name'] = 'my_app';
        $context['environment'] = $_ENV['APP_ENV'] ?? 'production';
        $context['request_id'] = $this->getRequestId();
        $this->logger->info($message, $context);
    }
    private function getRequestId()
    {
        // 从请求头获取或生成
        return $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid('req_', true);
    }
}

Filebeat配置

基本配置 (filebeat.yml)

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/php-app/*.log
  fields:
    app_type: php
    environment: production
  fields_under_root: true
  # 多行日志处理
  multiline.pattern: '^\d{4}-\d{2}-\d{2}'
  multiline.negate: true
  multiline.match: after
output.logstash:
  hosts: ["logstash:5044"]
# 输出到Elasticsearch (可选)
#output.elasticsearch:
#  hosts: ["elasticsearch:9200"]
#  index: "php-app-%{+yyyy.MM.dd}"

高级配置

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/php-app/*.log
  # 文件轮转处理
  close_inactive: 5m
  clean_inactive: 72h
  ignore_older: 48h
  # 包含/排除规则
  include_lines: ['^(INFO|ERROR|WARNING|DEBUG)']
  exclude_lines: ['^DEBUG.*health_check']
  # 字段处理
  processors:
    - add_cloud_metadata: ~
    - add_host_metadata: ~
    - add_docker_metadata: ~
    - decode_json_fields:
        fields: ["message"]
        target: "json"
        overwrite_keys: true

Logstash配置

Pipeline配置

# logstash/pipeline/php-app.conf
input {
  beats {
    port => 5044
    client_inactivity_timeout => 60
    ssl => false
  }
  # 备用:直接读取日志文件
  file {
    path => "/var/log/php-app/*.log"
    start_position => "beginning"
    sincedb_path => "/dev/null"
  }
}
filter {
  # 解析JSON日志
  json {
    source => "message"
    target => "parsed_json"
    skip_on_invalid_json => true
  }
  # 如果JSON解析成功,提取字段
  if [parsed_json] {
    mutate {
      replace => { "[message]" => "%{[parsed_json][message]}" }
      add_field => {
        "log_level" => "%{[parsed_json][level]}"
        "php_channel" => "%{[parsed_json][channel]}"
        "php_extra" => "%{[parsed_json][context]}"
      }
      remove_field => ["parsed_json"]
    }
  }
  # 日期解析
  date {
    match => ["timestamp", "ISO8601", "yyyy-MM-dd HH:mm:ss"]
    target => "@timestamp"
  }
  # 解析PHP错误格式
  grok {
    match => { "message" => [
      # PHP Fatal error
      '\[%{TIMESTAMP_ISO8601:timestamp}\] PHP %{WORD:php_error_type}: %{GREEDYDATA:php_error_msg} in %{PATH:php_error_file} on line %{NUMBER:php_error_line}',
      # Stack trace
      '\#%{NUMBER:stack_level} %{PATH:stack_file}\(%{NUMBER:stack_line}\): %{GREEDYDATA:stack_call}',
      # General PHP error
      '\[%{TIMESTAMP_ISO8601:timestamp}\] %{LOGLEVEL:log_level} %{GREEDYDATA:error_message}'
    ]}
  }
  # 地理IP解析
  geoip {
    source => "[client_ip]"
    target => "geoip"
  }
  # 用户代理解析
  useragent {
    source => "[user_agent]"
    target => "user_agent"
  }
}
output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "php-app-logs-%{+YYYY.MM.dd}"
    document_type => "log"
    template => "/etc/logstash/templates/php-app.json"
    template_name => "php-app"
    template_overwrite => true
  }
  # 调试输出
  stdout {
    codec => rubydebug
  }
}

Elasticsearch配置

索引模板

// elasticsearch/templates/php-app.json
{
  "index_patterns": ["php-app-logs-*"],
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 1,
    "refresh_interval": "5s",
    "analysis": {
      "analyzer": {
        "php_error_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "stop"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "@timestamp": { "type": "date" },
      "message": { 
        "type": "text",
        "analyzer": "php_error_analyzer",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "log_level": { "type": "keyword" },
      "php_error_type": { "type": "keyword" },
      "php_error_msg": { "type": "text" },
      "php_error_file": { 
        "type": "text",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "php_error_line": { "type": "integer" },
      "request_id": { "type": "keyword" },
      "client_ip": { "type": "ip" },
      "geoip": {
        "properties": {
          "location": { "type": "geo_point" }
        }
      },
      "response_time": { "type": "float" },
      "memory_usage": { "type": "long" }
    }
  }
}

Docker部署方案

docker-compose.yml

version: '3.7'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
    environment:
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
      - xpack.security.enabled=false
      - xpack.monitoring.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data
      - ./elasticsearch/templates:/etc/elasticsearch/templates
  logstash:
    image: docker.elastic.co/logstash/logstash:7.17.0
    ports:
      - "5044:5044"
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline
      - ./logstash/templates:/etc/logstash/templates
    depends_on:
      - elasticsearch
  kibana:
    image: docker.elastic.co/kibana/kibana:7.17.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
  filebeat:
    image: docker.elastic.co/beats/filebeat:7.17.0
    volumes:
      - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/log/php-app/:/var/log/php-app/:ro
    depends_on:
      - logstash
      - elasticsearch
  php-app:
    build: ./php-app
    volumes:
      - ./php-app/logs:/var/log/php-app
    depends_on:
      - filebeat
volumes:
  es_data:

PHP应用集成示例

Symfony框架集成

// config/packages/monolog.yaml
monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            formatter: 'monolog.formatter.json'
        error:
            type: stream
            path: "%kernel.logs_dir%/error.log"
            level: error
            formatter: 'monolog.formatter.json'
        # ELK专用处理器
        elk:
            type: stream
            path: "%kernel.logs_dir%/elk.log"
            level: info
            formatter: 'App\Logging\ElkFormatter'
            channels: ["!event"]
// src/Logging/ElkFormatter.php
namespace App\Logging;
use Monolog\Formatter\JsonFormatter;
class ElkFormatter extends JsonFormatter
{
    public function format(array $record): string
    {
        $extra = [
            'app_name' => $_ENV['APP_NAME'] ?? 'my_app',
            'environment' => $_ENV['APP_ENV'] ?? 'dev',
            'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid(),
            'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
            'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
            'response_time' => $this->getExecutionTime(),
            'memory_usage' => memory_get_usage(true),
        ];
        $record['extra'] = array_merge($record['extra'] ?? [], $extra);
        return parent::format($record);
    }
    private function getExecutionTime(): float
    {
        if (defined('LARAVEL_START')) {
            return microtime(true) - LARAVEL_START;
        }
        return 0;
    }
}

Laravel框架集成

// config/logging.php
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'elk'],
    ],
    'elk' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\StreamHandler::class,
        'handler_with' => [
            'stream' => storage_path('logs/elk.log'),
        ],
        'formatter' => Monolog\Formatter\JsonFormatter::class,
        'level' => 'debug',
    ],
    // 自定义ELK处理器
    'elk_json' => [
        'driver' => 'monolog',
        'handler' => Monolog\Handler\StreamHandler::class,
        'handler_with' => [
            'stream' => storage_path('logs/elk-json.log'),
        ],
        'formatter' => App\Logging\ElkFormatter::class,
        'level' => 'debug',
    ],
],

Kibana仪表板配置

索引模式创建

# 通过API创建索引模式
curl -X POST "localhost:5601/api/saved_objects/index-pattern" \
  -H "kbn-xsrf: true" \
  -H "Content-Type: application/json" \
  -d '{
    "attributes": {
      "title": "php-app-logs-*",
      "timeFieldName": "@timestamp"
    }
  }'

可视化配置

// 错误类型饼图
{
  "attributes": {: "PHP Error Types Distribution",
    "visState": "{\"type\":\"pie\",\"params\":{\"addTooltip\":true,\"legendPosition\":\"right\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"php_error_type.keyword\",\"size\":5,\"order\":\"desc\"}}]}"
  }
}
// 日错误趋势图
{
  "attributes": {: "PHP Errors Over Time",
    "visState": "{\"type\":\"line\",\"params\":{},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"log_level.keyword\",\"size\":5}}]}"
  }
}

性能优化策略

日志缓冲

// 使用Redis作为缓冲层
class BufferedLogger
{
    private $redis;
    private $buffer = [];
    private $bufferSize = 100;
    public function log($level, $message, array $context = [])
    {
        $this->buffer[] = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'timestamp' => microtime(true)
        ];
        if (count($this->buffer) >= $this->bufferSize) {
            $this->flush();
        }
    }
    public function flush()
    {
        if (empty($this->buffer)) return;
        $pipeline = $this->redis->pipeline();
        foreach ($this->buffer as $log) {
            $pipeline->rPush('log_queue', json_encode($log));
        }
        $pipeline->execute();
        $this->buffer = [];
    }
}

Filebeat优化配置

# filebeat.yml
filebeat.inputs:
- type: log
  harvester_buffer_size: 16384  # 读取缓冲区
  max_bytes: 10485760  # 最大单行长度 10MB
  backoff: 1s  # 重试间隔
  max_backoff: 10s  # 最大重试间隔
  backoff_factor: 2  # 重试因子
# 使用多输出负载均衡
output.logstash:
  hosts: ["logstash1:5044", "logstash2:5044", "logstash3:5044"]
  loadbalance: true
  bulk_max_size: 2048   # 批量发送大小
  worker: 4             # 工作者线程数

监控与告警

Watcher 配置示例

// 错误率告警
{
  "trigger": {
    "schedule": { "interval": "1m" }
  },
  "input": {
    "search": {
      "request": {
        "indices": ["php-app-logs-*"],
        "body": {
          "query": {
            "bool": {
              "filter": [
                { "range": { "@timestamp": { "gte": "now-5m" } } },
                { "term": { "log_level.keyword": "ERROR" } }
              ]
            }
          }
        }
      }
    }
  },
  "condition": {
    "compare": {
      "ctx.payload.hits.total": { "gte": 100 }
    }
  },
  "actions": {
    "email_admin": {
      "email": {
        "to": "ops@example.com",
        "subject": "High PHP Error Rate Alert",
        "body": "Detected {{ctx.payload.hits.total}} errors in last 5 minutes"
      }
    },
    "webhook": {
      "webhook": {
        "url": "http://alert-manager/webhook",
        "method": "POST"
      }
    }
  }
}

十一、常见问题排查

日志不显示排查流程

# 1. 检查Filebeat状态
systemctl status filebeat
journalctl -u filebeat -f
# 2. 测试日志路径
filebeat test output
filebeat test config
# 3. 检查Elasticsearch索引
curl -X GET "localhost:9200/_cat/indices?v"
# 4. 测试Logstash
bin/logstash -f test.conf --debug
# 5. 验证PHP JSON格式
echo '{"message":"test","level":"INFO"}' | filebeat -c filebeat.yml

性能问题诊断

# 检查Elasticsearch性能
curl -X GET "localhost:9200/_nodes/stats?pretty"
# 查看Logstash pipeline状态
curl -X GET "localhost:9600/_node/stats?pretty"
# 监控Filebeat指标
curl -X GET "localhost:5066/stats"

十二、最佳实践总结

日志规范

  • 始终使用结构化日志(JSON格式)
  • 包含唯一请求ID 便于追踪
  • 标准化日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
  • 限制单条日志大小 (建议<10KB)

性能考量

  • PHP端使用异步日志写入
  • Filebeat使用压缩传输 (gzip)
  • Logstash启用批量处理
  • 设置合理的索引生命周期 (ILM)

安全建议

  • 日志中不要记录敏感信息 (密码、Token等)
  • 启用HTTPS传输
  • 配置访问控制 (RBAC)
  • 定期审计日志内容

监控指标

  • 日志产生速率 (logs/s)
  • Elasticsearch索引速率
  • 各组件CPU/内存使用率
  • 数据流延迟 (端到端<10s)

通过以上配置,您可以构建一个完整的PHP日志聚合系统,实现日志的集中管理、实时分析、故障排查和告警通知。

PHP项目日志聚合与ELK

抱歉,评论功能暂时关闭!