本文目录导读:

在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));
}
}
最佳实践建议
- 日志格式标准化:使用JSON格式,便于解析和搜索
- 日志轮转:按日期/大小自动轮转,避免单个文件过大
- 异步写入:使用消息队列(如RabbitMQ)异步处理日志
- 分级存储:热日志(最近7天)存SSD,冷日志存廉价存储
- 监控告警:设置搜索规则,异常时自动告警
根据项目规模和需求,选择适合的方案,小项目可以使用简单的文件搜索,中大型项目推荐使用ELK或自定义数据库方案。