怎样在PHP项目中实现日志检索?

wen java案例 1

本文目录导读:

怎样在PHP项目中实现日志检索?

  1. 基础方案:文件搜索
  2. 结构化日志方案(推荐)
  3. 高级方案:日志管理系统
  4. 性能优化建议
  5. 最佳实践建议

在PHP项目中实现日志检索,通常有以下几种主流方案,按复杂度从低到高排列:

基础方案:文件搜索

使用PHP原生函数

<?php
class SimpleLogSearcher {
    private $logDir;
    public function __construct($logDir) {
        $this->logDir = rtrim($logDir, '/');
    }
    /**
     * 搜索日志文件
     */
    public function search($keyword, $options = []) {
        $results = [];
        $dateFrom = $options['date_from'] ?? null;
        $dateTo = $options['date_to'] ?? null;
        $logLevel = $options['level'] ?? null;
        $limit = $options['limit'] ?? 100;
        // 获取日志文件列表
        $files = glob($this->logDir . '/*.log');
        foreach ($files as $file) {
            // 日期过滤
            if ($dateFrom || $dateTo) {
                $fileDate = basename($file, '.log');
                if ($dateFrom && $fileDate < $dateFrom) continue;
                if ($dateTo && $fileDate > $dateTo) continue;
            }
            $handle = fopen($file, 'r');
            if ($handle) {
                while (($line = fgets($handle)) !== false) {
                    // 关键词匹配
                    if (strpos($line, $keyword) !== false) {
                        // 日志级别过滤
                        if ($logLevel && !preg_match("/\[$logLevel\]/i", $line)) {
                            continue;
                        }
                        $results[] = [
                            'file' => basename($file),
                            'line' => $line,
                            'time' => $this->extractTime($line)
                        ];
                        if (count($results) >= $limit) {
                            break 2;
                        }
                    }
                }
                fclose($handle);
            }
        }
        return $results;
    }
    private function extractTime($line) {
        preg_match('/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/', $line, $matches);
        return $matches[1] ?? '';
    }
}
// 使用示例
$searcher = new SimpleLogSearcher('/var/log/app');
$results = $searcher->search('ERROR', [
    'date_from' => '2024-01-01',
    'level' => 'ERROR',
    'limit' => 50
]);

使用grep命令(推荐大文件)

<?php
function grepSearch($keyword, $logFile, $options = []) {
    $cmd = "grep -i '{$keyword}' {$logFile}";
    if (!empty($options['before'])) {
        $cmd .= " -B {$options['before']}";
    }
    if (!empty($options['after'])) {
        $cmd .= " -A {$options['after']}";
    }
    if (!empty($options['max_count'])) {
        $cmd .= " -m {$options['max_count']}";
    }
    $output = [];
    exec($cmd, $output);
    return $output;
}

结构化日志方案(推荐)

Monolog + Elasticsearch

<?php
// composer require elasticsearch/elasticsearch
// composer require monolog/monolog
use Monolog\Logger;
use Monolog\Handler\ElasticSearchHandler;
// 日志写入
$logger = new Logger('app');
$handler = new ElasticSearchHandler([
    'hosts' => ['localhost:9200'],
    'index' => 'app_logs_' . date('Y_m_d'),
]);
$logger->pushHandler($handler);
$logger->error('User login failed', [
    'user_id' => 123,
    'ip' => '192.168.1.1',
    'attempt_time' => date('Y-m-d H:i:s')
]);
// 日志检索
class ElasticsearchLogSearcher {
    private $client;
    public function __construct() {
        $this->client = Elasticsearch\ClientBuilder::create()
            ->setHosts(['localhost:9200'])
            ->build();
    }
    public function search($query, $page = 1, $perPage = 20) {
        $params = [
            'index' => 'app_logs_*',
            'body' => [
                'query' => [
                    'bool' => [
                        'must' => [
                            ['match' => ['message' => $query]],
                            // 可以添加更多过滤条件
                            // ['term' => ['level' => 'ERROR']]
                        ]
                    ]
                ],
                'sort' => ['@timestamp' => 'desc'],
                'from' => ($page - 1) * $perPage,
                'size' => $perPage
            ]
        ];
        $response = $this->client->search($params);
        return [
            'total' => $response['hits']['total']['value'],
            'hits' => array_map(function($hit) {
                return $hit['_source'];
            }, $response['hits']['hits'])
        ];
    }
}

使用日志数据库表

<?php
// 数据库表结构
// CREATE TABLE `logs` (
//   `id` bigint(20) NOT NULL AUTO_INCREMENT,
//   `level` varchar(20) NOT NULL,
//   `message` text NOT NULL,
//   `context` json DEFAULT NULL,
//   `created_at` datetime NOT NULL,
//   PRIMARY KEY (`id`),
//   KEY `idx_level` (`level`),
//   KEY `idx_created_at` (`created_at`),
//   FULLTEXT KEY `ft_message` (`message`)
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
class DatabaseLogSearcher {
    private $db;
    public function __construct(PDO $db) {
        $this->db = $db;
    }
    public function search($keyword, $filters = []) {
        $sql = "SELECT * FROM logs WHERE 1=1";
        $params = [];
        // 关键词搜索(使用LIKE或FULLTEXT)
        if (!empty($keyword)) {
            $sql .= " AND message LIKE :keyword";
            $params[':keyword'] = "%{$keyword}%";
        }
        // 级别过滤
        if (!empty($filters['level'])) {
            $sql .= " AND level = :level";
            $params[':level'] = $filters['level'];
        }
        // 时间范围
        if (!empty($filters['date_from'])) {
            $sql .= " AND created_at >= :date_from";
            $params[':date_from'] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $sql .= " AND created_at <= :date_to";
            $params[':date_to'] = $filters['date_to'];
        }
        // 分页
        $page = $filters['page'] ?? 1;
        $perPage = $filters['per_page'] ?? 20;
        $offset = ($page - 1) * $perPage;
        $sql .= " ORDER BY created_at DESC LIMIT :limit OFFSET :offset";
        $params[':limit'] = $perPage;
        $params[':offset'] = $offset;
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

高级方案:日志管理系统

使用成熟工具

<?php
// 集成ELK Stack
class ElkLogViewer {
    private $elasticsearch;
    private $kibanaUrl;
    public function __construct($elasticsearchHost, $kibanaUrl) {
        $this->elasticsearch = Elasticsearch\ClientBuilder::create()
            ->setHosts([$elasticsearchHost])
            ->build();
        $this->kibanaUrl = $kibanaUrl;
    }
    // 构建Kibana查询URL
    public function getKibanaQueryUrl($query) {
        $baseUrl = $this->kibanaUrl . '/app/discover#/?_a=(query:(language:kuery,query:\'' . urlencode($query) . '\'))';
        return $baseUrl;
    }
    // 通过API查询日志
    public function searchLogs($query, $from = 'now-24h', $to = 'now') {
        $params = [
            'index' => 'app-logs-*',
            'body' => [
                'size' => 100,
                'query' => [
                    'bool' => [
                        'must' => [
                            ['query_string' => ['query' => $query]],
                            ['range' => ['@timestamp' => ['gte' => $from, 'lte' => $to]]]
                        ]
                    ]
                ]
            ]
        ];
        return $this->elasticsearch->search($params);
    }
}

自定义日志管理系统

<?php
class LogManagementSystem {
    private $config;
    public function __construct($config) {
        $this->config = $config;
    }
    /**
     * 多维度日志检索
     */
    public function advancedSearch($criteria) {
        $logs = [];
        // 1. 解析查询语法
        $parsedQuery = $this->parseQuery($criteria['query']);
        // 2. 选择数据源
        $dataSource = $this->selectDataSource($criteria);
        // 3. 执行搜索
        switch ($dataSource) {
            case 'elasticsearch':
                $logs = $this->searchElasticsearch($parsedQuery);
                break;
            case 'database':
                $logs = $this->searchDatabase($parsedQuery);
                break;
            case 'file':
                $logs = $this->searchFiles($parsedQuery);
                break;
        }
        // 4. 后处理
        $logs = $this->postProcess($logs, $criteria);
        return $logs;
    }
    private function parseQuery($query) {
        // 支持语法: ERROR, level:ERROR, user.id:123, time>2024-01-01
        $parsed = [
            'keywords' => [],
            'filters' => []
        ];
        $parts = preg_split('/\s+/', $query);
        foreach ($parts as $part) {
            if (strpos($part, ':') !== false) {
                list($field, $value) = explode(':', $part, 2);
                $parsed['filters'][$field] = $value;
            } else {
                $parsed['keywords'][] = $part;
            }
        }
        return $parsed;
    }
}

性能优化建议

索引优化

<?php
// 为日志文件创建索引
class LogIndexer {
    public function createIndex($logDir) {
        $indexDir = $logDir . '/index';
        if (!is_dir($indexDir)) {
            mkdir($indexDir, 0755, true);
        }
        // 使用SplFileObject逐行读取
        $files = new DirectoryIterator($logDir);
        foreach ($files as $file) {
            if ($file->isFile() && $file->getExtension() === 'log') {
                $this->indexFile($file->getPathname(), $indexDir);
            }
        }
    }
    private function indexFile($filePath, $indexDir) {
        // 创建倒排索引
        $index = [];
        $file = new SplFileObject($filePath);
        foreach ($file as $lineNumber => $line) {
            $words = str_word_count($line, 1);
            foreach ($words as $word) {
                $word = strtolower($word);
                if (!isset($index[$word])) {
                    $index[$word] = [];
                }
                $index[$word][] = [
                    'file' => basename($filePath),
                    'line' => $lineNumber + 1
                ];
            }
        }
        // 保存索引
        file_put_contents(
            $indexDir . '/' . basename($filePath) . '.idx',
            serialize($index)
        );
    }
}

缓存策略

<?php
class CachedLogSearcher {
    private $cache;
    private $searcher;
    public function __construct($cache, $searcher) {
        $this->cache = $cache;
        $this->searcher = $searcher;
    }
    public function search($query, $filters = []) {
        $cacheKey = $this->buildCacheKey($query, $filters);
        // 尝试从缓存获取
        if ($cached = $this->cache->get($cacheKey)) {
            return $cached;
        }
        // 执行搜索
        $results = $this->searcher->search($query, $filters);
        // 缓存结果(1分钟)
        $this->cache->set($cacheKey, $results, 60);
        return $results;
    }
    private function buildCacheKey($query, $filters) {
        return 'log_search_' . md5($query . serialize($filters));
    }
}

最佳实践建议

  1. 日志格式标准化:使用JSON格式,便于解析和搜索
  2. 日志轮转:按日期/大小自动轮转,避免单个文件过大
  3. 异步写入:使用消息队列(如RabbitMQ)异步处理日志
  4. 分级存储:热日志(最近7天)存SSD,冷日志存廉价存储
  5. 监控告警:设置搜索规则,异常时自动告警

根据项目规模和需求,选择适合的方案,小项目可以使用简单的文件搜索,中大型项目推荐使用ELK或自定义数据库方案。

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