PHP大数据处理与人工智能集成实战:构建高并发智能系统-5

内容分享2天前发布
1 0 0

第5章:综合项目实战:智能实时日志分析系统

在数据驱动的时代,海量日志的实时处理与智能洞察已成为系统运维和业务分析的刚需。面对每秒产生的数以万计的日志条目,传统的手工检查或简单脚本早已力不从心。本章将引领您将前序章节所学的PHP大数据处理与人工智能技术融会贯通,亲手打造一个能够实时摄入、分析日志,并自动发现异常、预测趋势的“智能实时日志分析系统”。

本章的学习目标明确而具体:第一,掌握使用PHP进行流式数据处理的工程方法,构建高吞吐量的日志摄取管道。第二,学会将训练好的AI模型(如异常检测、文本分类模型)集成到PHP应用中,实现对日志内容的实时智能分析。第三,精通利用异步编程与队列技术来解耦处理环节,提升系统整体的并发与容错能力。第四,完成一个从数据输入、实时处理、智能分析到结果可视化输出的完整项目,形成解决复杂问题的架构思维。

本综合项目实战章节,在整个教程中扮演着“总装车间”与“试金石”的角色。它不再孤立地讲解某个函数或库,而是要求您有机地组合运用前四章的知识——例如,利用第一章的高效IO与数据结构管理原始日志,运用第二章的并行处理技术加速计算,集成第三章所学的机器学习PHP库进行智能判断,并借助第四章的性能优化与缓存策略确保系统稳定高效。它将分散的知识点串联成一个能够解决实际生产问题的有机整体。

本章的主要内容将带您经历一个微缩版的项目开发周期:从需求分析与系统设计开始,规划系统模块与数据流;接着搭建基于ReactPHP或Swoole的实时日志摄取与流处理框架;然后,核心环节是集成AI分析引擎,您将学习如何在PHP中加载TensorFlow或PyTorch模型,对日志进行实时分类与异常评分;之后,通过消息队列(如RabbitMQ、Kafka的PHP客户端)实现处理过程的异步化与解耦,并构建实时仪表盘,将分析结果通过WebSocket或Server-Sent Events动态推送到前端;最后,我们还会探讨系统的监控、部署与优化策略。

通过本章的学习,您将获得的远不止几段代码。您将收获一个可直接扩展应用于生产环境的项目原型,深刻理解PHP在实时数据处理与AI集成领域的可行性与最佳实践。更重要的是,您将建立起面对大数据量、高实时性需求的复杂系统时的完整架构设计与实现能力,这无疑会显著提升您作为后端或全栈PHP工程师的核心竞争力。让我们即刻开始,用代码构建这个智能系统的“大脑”与“中枢神经”。

二、核心概念与技术要点

1. 实时日志流处理与消息队列

实时日志流处理是系统的中枢神经,它负责接收、缓冲和分发海量的日志事件。直接处理可能导致系统在高峰时段过载或丢失数据。因此,我们引入消息队列(如Redis的Pub/Sub或List结构)作为缓冲区,实现生产者和消费者模型的解耦。生产者(日志收集客户端)将日志事件推入队列,而消费者(日志处理Worker)以可控的速度从队列中取出并处理,从而保证系统的弹性和数据可靠性。

技术要点:

异步处理: 日志的写入与分析不再阻塞主应用程序。流量削峰: 平滑突发性日志流量,防止处理服务被冲垮。应用解耦: 日志源与处理服务独立部署和伸缩。

与其它概念的关系: 这是数据管道的第一步,处理后的结构化日志将被送入数据库存储,并为后续分析提供原料。

应用场景: 网站访问高峰期的流量监控、应用错误告警的实时触发。

PHP代码示例:

示例1:使用Redis List作为简易消息队列的生产者(日志收集端)


<?php
/**
 * 日志生产者示例
 * 模拟将应用日志发送到Redis消息队列
 */

class LogProducer {
    private $redis;
    
    public function __construct($host = '127.0.0.1', $port = 6379) {
        // 连接到Redis服务器
        $this->redis = new Redis();
        try {
            $connected = $this->redis->connect($host, $port);
            if (!$connected) {
                throw new Exception("无法连接到Redis服务器。");
            }
        } catch (Exception $e) {
            die("生产者初始化失败: " . $e->getMessage());
        }
    }
    
    /**
     * 发送一条日志到指定队列
     * @param string $queueName 队列名称
     * @param array $logData 日志数据数组
     * @return bool|int 成功返回队列长度,失败返回false
     */
    public function sendLog($queueName, array $logData) {
        // 将日志数据编码为JSON字符串,便于传输和存储
        $logMessage = json_encode($logData, JSON_UNESCAPED_UNICODE);
        
        // 使用RPUSH命令将消息添加到列表尾部(生产者)
        $result = $this->redis->rPush($queueName, $logMessage);
        
        if ($result) {
            echo "日志已发送到队列 '{$queueName}'。当前队列长度: {$result}
";
        }
        return $result;
    }
    
    /**
     * 模拟生成一条应用访问日志
     * @return array 构造的日志数据
     */
    public function generateAccessLog() {
        $logLevels = ['INFO', 'WARNING', 'ERROR'];
        $userAgents = ['Mozilla/5.0', 'curl/7.68.0', 'PostmanRuntime/7.26'];
        $paths = ['/home', '/api/data', '/login', '/product/123'];
        
        return [
            'timestamp' => date('Y-m-d H:i:s'),
            'level' => $logLevels[array_rand($logLevels)],
            'client_ip' => '192.168.1.' . rand(1, 255),
            'method' => (rand(0,1) ? 'GET' : 'POST'),
            'path' => $paths[array_rand($paths)],
            'status_code' => rand(200, 500),
            'response_time_ms' => rand(50, 2000),
            'user_agent' => $userAgents[array_rand($userAgents)],
            'message' => '处理用户请求'
        ];
    }
}

// 使用示例
$producer = new LogProducer();
// 模拟发送10条日志
for ($i = 0; $i < 10; $i++) {
    $logData = $producer->generateAccessLog();
    $producer->sendLog('log_queue', $logData);
    usleep(100000); // 暂停0.1秒,模拟实时产生
}
?>

示例2:使用Redis List的消费者(日志处理Worker)


<?php
/**
 * 日志消费者示例
 * 从Redis队列中取出并处理日志
 */

class LogConsumer {
    private $redis;
    private $isRunning = false;
    
    public function __construct($host = '127.0.0.1', $port = 6379) {
        $this->redis = new Redis();
        $this->redis->connect($host, $port);
        // 设置阻塞模式下的超时时间(秒)
        $this->redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
    }
    
    /**
     * 启动消费者,持续监听队列
     * @param string $queueName 要监听的队列名称
     */
    public function startConsuming($queueName) {
        $this->isRunning = true;
        echo "消费者启动,正在监听队列: {$queueName}
";
        
        while ($this->isRunning) {
            // 使用BLPOP进行阻塞式弹出,避免空轮询消耗CPU
            // 参数:队列名, 超时时间(秒)。0表示无限等待。
            $result = $this->redis->blPop([$queueName], 0);
            
            if ($result && isset($result[1])) {
                $logMessage = $result[1]; // $result[0]是键名,$result[1]是值
                $this->processLogMessage($logMessage);
            }
            
            // 添加一个小的延迟,防止在极端情况下CPU占用过高
            usleep(10000); // 0.01秒
        }
    }
    
    /**
     * 处理单条日志消息
     * @param string $logMessage JSON格式的日志字符串
     */
    private function processLogMessage($logMessage) {
        $logData = json_decode($logMessage, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            echo "错误:无法解析JSON日志消息。
";
            return;
        }
        
        // 在此处进行实际处理,例如:存入数据库、分析、告警等
        echo "处理日志 [{$logData['timestamp']}] {$logData['level']} {$logData['method']} {$logData['path']} ({$logData['status_code']})
";
        
        // 模拟处理耗时
        usleep(rand(50000, 200000)); // 0.05到0.2秒
    }
    
    /**
     * 停止消费者
     */
    public function stop() {
        $this->isRunning = false;
        echo "消费者正在停止...
";
    }
}

// 使用示例(通常在命令行中长期运行)
// $consumer = new LogConsumer();
// $consumer->startConsuming('log_queue');
?>

2. 结构化日志存储与查询

原始日志文本不利于高效分析。通过将日志解析并存储到结构化的数据库(如MySQL)中,我们可以利用SQL的强大查询能力。通常设计一个
logs
表,包含时间戳、日志级别、来源IP、请求路径、状态码、响应时间等字段,并添加合适的索引(如在
timestamp

level
上)以加速查询。

技术要点:

数据规范化: 将非结构化的文本转换为结构化的记录。索引优化: 对常用查询字段建立索引,提升检索速度。数据分区: 对于超大量数据,可按时间进行分区管理,提高维护和查询效率。

与其它概念的关系: 消息队列处理后的数据将持久化存储在此,为数据分析模块提供稳定、可靠的数据源。

应用场景: 按时间范围查询错误日志、统计某个API端点的历史性能。

PHP代码示例:

示例1:将日志数据存入MySQL数据库


<?php
/**
 * 日志存储服务示例
 * 负责将结构化的日志数据持久化到MySQL数据库
 */

class LogStorageService {
    private $pdo;
    
    public function __construct($dbHost, $dbName, $dbUser, $dbPass) {
        $dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 抛出异常
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false, // 使用真正的预处理语句
        ];
        
        try {
            $this->pdo = new PDO($dsn, $dbUser, $dbPass, $options);
        } catch (PDOException $e) {
            die("数据库连接失败: " . $e->getMessage());
        }
        
        // 确保表存在(在实际项目中,这通常由迁移脚本完成)
        $this->createTableIfNotExists();
    }
    
    /**
     * 创建日志表(如果不存在)
     */
    private function createTableIfNotExists() {
        $sql = "CREATE TABLE IF NOT EXISTS `app_logs` (
                `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                `timestamp` DATETIME NOT NULL,
                `log_level` VARCHAR(20) NOT NULL,
                `client_ip` VARCHAR(45) NOT NULL,
                `request_method` VARCHAR(10) NOT NULL,
                `request_path` VARCHAR(500) NOT NULL,
                `status_code` SMALLINT UNSIGNED NOT NULL,
                `response_time_ms` INT UNSIGNED NOT NULL,
                `user_agent` TEXT,
                `log_message` TEXT,
                `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_timestamp (`timestamp`),
                INDEX idx_log_level (`log_level`),
                INDEX idx_path (`request_path`(255)),
                INDEX idx_status_code (`status_code`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
        
        $this->pdo->exec($sql);
    }
    
    /**
     * 插入一条日志记录
     * @param array $logData 日志数据数组
     * @return string 最后插入的ID
     */
    public function insertLog(array $logData) {
        $sql = "INSERT INTO `app_logs` 
                (`timestamp`, `log_level`, `client_ip`, `request_method`, `request_path`, `status_code`, `response_time_ms`, `user_agent`, `log_message`)
                VALUES (:timestamp, :level, :client_ip, :method, :path, :status_code, :response_time_ms, :user_agent, :message)";
        
        $stmt = $this->pdo->prepare($sql);
        
        try {
            $stmt->execute([
                ':timestamp' => $logData['timestamp'],
                ':level' => $logData['level'],
                ':client_ip' => $logData['client_ip'],
                ':method' => $logData['method'],
                ':path' => $logData['path'],
                ':status_code' => $logData['status_code'],
                ':response_time_ms' => $logData['response_time_ms'],
                ':user_agent' => $logData['user_agent'],
                ':message' => $logData['message']
            ]);
            return $this->pdo->lastInsertId();
        } catch (PDOException $e) {
            error_log("插入日志失败: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * 批量插入日志记录,提高效率
     * @param array $logsArray 日志数据数组的数组
     * @return int 成功插入的行数
     */
    public function batchInsertLogs(array $logsArray) {
        if (empty($logsArray)) {
            return 0;
        }
        
        $placeholders = [];
        $values = [];
        $columns = ['timestamp', 'log_level', 'client_ip', 'request_method', 'request_path', 'status_code', 'response_time_ms', 'user_agent', 'log_message'];
        
        foreach ($logsArray as $index => $log) {
            $paramPrefix = ":i{$index}_";
            $placeholders[] = '(' . implode(', ', array_map(function($col) use ($paramPrefix) {
                return $paramPrefix . $col;
            }, $columns)) . ')';
            
            foreach ($columns as $col) {
                $values[$paramPrefix . $col] = $log[$col] ?? null;
            }
        }
        
        $sql = "INSERT INTO `app_logs` (`" . implode('`, `', $columns) . "`) VALUES " . implode(', ', $placeholders);
        $stmt = $this->pdo->prepare($sql);
        
        try {
            $stmt->execute($values);
            return $stmt->rowCount();
        } catch (PDOException $e) {
            error_log("批量插入日志失败: " . $e->getMessage());
            return 0;
        }
    }
}

// 使用示例
$storageService = new LogStorageService('localhost', 'log_analysis_db', 'root', 'password');

// 插入单条日志
$sampleLog = [
    'timestamp' => date('Y-m-d H:i:s'),
    'level' => 'ERROR',
    'client_ip' => '10.0.0.123',
    'method' => 'POST',
    'path' => '/api/submit',
    'status_code' => 500,
    'response_time_ms' => 1250,
    'user_agent' => 'Mozilla/5.0',
    'message' => '数据库连接超时'
];
$insertId = $storageService->insertLog($sampleLog);
echo "插入单条日志,ID: {$insertId}
";

// 模拟批量插入(例如从消息队列消费一批后存入)
$batchLogs = [];
for ($i = 0; $i < 5; $i++) {
    $batchLogs[] = [
        'timestamp' => date('Y-m-d H:i:s', time() - rand(0, 300)),
        'level' => rand(0,1) ? 'INFO' : 'ERROR',
        'client_ip' => '192.168.1.' . rand(1, 100),
        'method' => 'GET',
        'path' => '/api/data/' . $i,
        'status_code' => rand(200, 404),
        'response_time_ms' => rand(30, 800),
        'user_agent' => 'TestClient',
        'message' => '批量插入测试'
    ];
}
$rowCount = $storageService->batchInsertLogs($batchLogs);
echo "批量插入 {$rowCount} 条日志完成。
";
?>

示例2:从数据库查询和分析日志


<?php
/**
 * 日志查询与分析服务示例
 */

class LogQueryService {
    private $pdo;
    
    public function __construct($pdoConnection) {
        $this->pdo = $pdoConnection;
    }
    
    /**
     * 查询指定时间范围内的日志
     * @param string $startTime 开始时间 (Y-m-d H:i:s)
     * @param string $endTime 结束时间 (Y-m-d H:i:s)
     * @param int $limit 限制条数
     * @param int $offset 偏移量
     * @return array 日志记录数组
     */
    public function getLogsByTimeRange($startTime, $endTime, $limit = 100, $offset = 0) {
        $sql = "SELECT * FROM `app_logs` 
                WHERE `timestamp` BETWEEN :start_time AND :end_time
                ORDER BY `timestamp` DESC 
                LIMIT :limit OFFSET :offset";
        
        $stmt = $this->pdo->prepare($sql);
        // PDO参数绑定需要明确指定类型
        $stmt->bindValue(':start_time', $startTime, PDO::PARAM_STR);
        $stmt->bindValue(':end_time', $endTime, PDO::PARAM_STR);
        $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', (int)$offset, PDO::PARAM_INT);
        
        $stmt->execute();
        return $stmt->fetchAll();
    }
    
    /**
     * 统计错误日志(级别为ERROR)的数量
     * @param string $timeRange 时间范围,例如 '1 HOUR', '1 DAY'
     * @return int 错误数量
     */
    public function countErrorLogs($timeRange = '1 HOUR') {
        $sql = "SELECT COUNT(*) as error_count FROM `app_logs` 
                WHERE `log_level` = 'ERROR' 
                AND `timestamp` >= DATE_SUB(NOW(), INTERVAL {$timeRange})";
        
        $stmt = $this->pdo->query($sql);
        $result = $stmt->fetch();
        return (int) $result['error_count'];
    }
    
    /**
     * 获取最常见的请求路径(Top N)
     * @param int $topN 返回前N名
     * @param string $sinceTime 从何时开始统计
     * @return array 路径和请求次数的数组
     */
    public function getTopRequestPaths($topN = 10, $sinceTime = '2024-01-01 00:00:00') {
        $sql = "SELECT `request_path`, COUNT(*) as request_count
                FROM `app_logs`
                WHERE `timestamp` > :since_time
                GROUP BY `request_path`
                ORDER BY request_count DESC
                LIMIT :top_n";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->bindValue(':since_time', $sinceTime, PDO::PARAM_STR);
        $stmt->bindValue(':top_n', (int)$topN, PDO::PARAM_INT);
        $stmt->execute();
        
        return $stmt->fetchAll();
    }
    
    /**
     * 计算API的平均响应时间
     * @param string $pathPattern 路径匹配模式,例如 '/api/%'
     * @return float 平均响应时间(毫秒)
     */
    public function getAverageResponseTime($pathPattern = '/api/%') {
        $sql = "SELECT AVG(`response_time_ms`) as avg_response_time
                FROM `app_logs`
                WHERE `request_path` LIKE :path_pattern
                AND `timestamp` >= DATE_SUB(NOW(), INTERVAL 1 DAY)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':path_pattern' => $pathPattern]);
        $result = $stmt->fetch();
        
        return round($result['avg_response_time'] ?? 0, 2);
    }
}

// 使用示例
// 假设已有一个PDO连接 $pdo
$queryService = new LogQueryService($pdo);

// 1. 查询最近一小时的日志
$recentLogs = $queryService->getLogsByTimeRange(
    date('Y-m-d H:i:s', strtotime('-1 hour')),
    date('Y-m-d H:i:s')
);
echo "最近一小时日志条数: " . count($recentLogs) . "
";

// 2. 统计过去24小时的错误数
$errorCount = $queryService->countErrorLogs('24 HOUR');
echo "过去24小时错误数: {$errorCount}
";

// 3. 获取Top 5请求路径
$topPaths = $queryService->getTopRequestPaths(5, date('Y-m-d', strtotime('-7 days')) . ' 00:00:00');
echo "过去7天最常访问的路径:
";
foreach ($topPaths as $path) {
    echo "  {$path['request_path']}: {$path['request_count']} 次
";
}

// 4. 计算API平均响应时间
$avgResponseTime = $queryService->getAverageResponseTime();
echo "API平均响应时间: {$avgResponseTime} ms
";
?>

3. 日志聚合分析与统计

单纯存储和查询日志不足以发挥其价值。我们需要对数据进行聚合分析,以生成有意义的指标和洞察。这包括计算请求量(QPS)、错误率、平均响应时间、按状态码或用户代理分布等。这些聚合结果可以实时计算并缓存,用于仪表盘展示或触发告警。

技术要点:

聚合函数: 使用SQL的
COUNT
,
AVG
,
SUM
,
GROUP BY
进行数据汇总。时间窗口统计: 按分钟、小时、天等时间维度聚合数据,观察趋势。实时与离线分析结合: 简单的统计可实时计算,复杂的关联分析可安排离线任务执行。

与其它概念的关系: 该模块直接依赖于结构化存储的数据,生成的分析结果是整个系统价值输出的最终体现,可用于监控和业务决策。

应用场景: 生成系统健康度仪表盘、每周错误报告、识别最耗时的API端点。

PHP代码示例:

示例1:基础聚合统计(总请求数、错误率、平均延迟)


<?php
/**
 * 基础日志聚合分析器
 */

class BasicLogAnalyzer {
    private $pdo;
    
    public function __construct($pdoConnection) {
        $this->pdo = $pdoConnection;
    }
    
    /**
     * 生成指定时间范围内的基础统计报告
     * @param string $startTime 开始时间
     * @param string $endTime 结束时间
     * @return array 包含多种统计指标的关联数组
     */
    public function generateSummaryReport($startTime, $endTime) {
        $report = [];
        
        // 1. 总请求数
        $report['total_requests'] = $this->countTotalRequests($startTime, $endTime);
        
        // 2. 按日志级别统计
        $report['requests_by_level'] = $this->countRequestsByLevel($startTime, $endTime);
        
        // 3. 错误率 (ERROR级别占比)
        if ($report['total_requests'] > 0) {
            $errorCount = $report['requests_by_level']['ERROR'] ?? 0;
            $report['error_rate_percent'] = round(($errorCount / $report['total_requests']) * 100, 2);
        } else {
            $report['error_rate_percent'] = 0;
        }
        
        // 4. 平均、最小、最大响应时间
        $responseTimeStats = $this->getResponseTimeStats($startTime, $endTime);
        $report['avg_response_time_ms'] = $responseTimeStats['avg'] ?? 0;
        $report['min_response_time_ms'] = $responseTimeStats['min'] ?? 0;
        $report['max_response_time_ms'] = $responseTimeStats['max'] ?? 0;
        
        // 5. HTTP状态码分布 (Top 10)
        $report['status_code_distribution'] = $this->getStatusCodeDistribution($startTime, $endTime, 10);
        
        return $report;
    }
    
    private function countTotalRequests($startTime, $endTime) {
        $sql = "SELECT COUNT(*) as total FROM `app_logs` WHERE `timestamp` BETWEEN ? AND ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$startTime, $endTime]);
        $result = $stmt->fetch();
        return (int) $result['total'];
    }
    
    private function countRequestsByLevel($startTime, $endTime) {
        $sql = "SELECT `log_level`, COUNT(*) as count FROM `app_logs` 
                WHERE `timestamp` BETWEEN ? AND ?
                GROUP BY `log_level`";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$startTime, $endTime]);
        $results = $stmt->fetchAll();
        
        $distribution = [];
        foreach ($results as $row) {
            $distribution[$row['log_level']] = (int)$row['count'];
        }
        return $distribution;
    }
    
    private function getResponseTimeStats($startTime, $endTime) {
        $sql = "SELECT 
                    AVG(`response_time_ms`) as avg_time,
                    MIN(`response_time_ms`) as min_time,
                    MAX(`response_time_ms`) as max_time
                FROM `app_logs` 
                WHERE `timestamp` BETWEEN ? AND ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$startTime, $endTime]);
        $result = $stmt->fetch();
        
        return [
            'avg' => round($result['avg_time'] ?? 0, 2),
            'min' => (int) ($result['min_time'] ?? 0),
            'max' => (int) ($result['max_time'] ?? 0)
        ];
    }
    
    private function getStatusCodeDistribution($startTime, $endTime, $limit) {
        $sql = "SELECT `status_code`, COUNT(*) as count FROM `app_logs` 
                WHERE `timestamp` BETWEEN ? AND ?
                GROUP BY `status_code`
                ORDER BY count DESC
                LIMIT ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$startTime, $endTime, $limit]);
        $results = $stmt->fetchAll();
        
        $distribution = [];
        foreach ($results as $row) {
            $distribution[$row['status_code']] = (int)$row['count'];
        }
        return $distribution;
    }
    
    /**
     * 格式化并打印报告
     */
    public function printReport($report) {
        echo "========== 日志分析报告 ==========
";
        echo "总请求数: {$report['total_requests']}
";
        echo "错误率: {$report['error_rate_percent']}%
";
        echo "平均响应时间: {$report['avg_response_time_ms']} ms
";
        echo "最小响应时间: {$report['min_response_time_ms']} ms
";
        echo "最大响应时间: {$report['max_response_time_ms']} ms

";
        
        echo "按级别分布:
";
        foreach ($report['requests_by_level'] as $level => $count) {
            echo "  {$level}: {$count}
";
        }
        echo "
";
        
        echo "HTTP状态码分布 (Top 10):
";
        foreach ($report['status_code_distribution'] as $code => $count) {
            echo "  {$code}: {$count}
";
        }
        echo "==================================
";
    }
}

// 使用示例
// $analyzer = new BasicLogAnalyzer($pdo);
// $report = $analyzer->generateSummaryReport('2024-01-10 00:00:00', '2024-01-10 23:59:59');
// $analyzer->printReport($report);
?>

示例2:时间序列聚合(按小时统计请求量)


<?php
/**
 * 时间序列分析器 - 用于生成趋势图表数据
 */

class TimeSeriesAnalyzer {
    private $pdo;
    
    public function __construct($pdoConnection) {
        $this->pdo = $pdoConnection;
    }
    
    /**
     * 按小时聚合请求数量
     * @param string $date 日期 (Y-m-d)
     * @return array 每小时请求数的数组,键为小时(0-23),值为数量
     */
    public function getHourlyRequestCount($date) {
        $startTime = $date . ' 00:00:00';
        $endTime = $date . ' 23:59:59';
        
        // 使用DATE_FORMAT函数提取小时
        $sql = "SELECT 
                    HOUR(`timestamp`) as hour_of_day,
                    COUNT(*) as request_count
                FROM `app_logs`
                WHERE `timestamp` BETWEEN :start_time AND :end_time
                GROUP BY HOUR(`timestamp`)
                ORDER BY hour_of_day";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':start_time' => $startTime, ':end_time' => $endTime]);
        $results = $stmt->fetchAll();
        
        // 初始化24小时的数组
        $hourlyData = array_fill(0, 24, 0);
        foreach ($results as $row) {
            $hour = (int)$row['hour_of_day'];
            $hourlyData[$hour] = (int)$row['request_count'];
        }
        
        return $hourlyData;
    }
    
    /**
     * 按天聚合错误数和总请求数(过去N天)
     * @param int $lastDays 过去多少天
     * @return array 每天的数据数组
     */
    public function getDailySummary($lastDays = 7) {
        $endDate = date('Y-m-d');
        $startDate = date('Y-m-d', strtotime("-$lastDays days"));
        
        $sql = "SELECT 
                    DATE(`timestamp`) as log_date,
                    COUNT(*) as total_requests,
                    SUM(CASE WHEN `log_level` = 'ERROR' THEN 1 ELSE 0 END) as error_count,
                    AVG(`response_time_ms`) as avg_response_time
                FROM `app_logs`
                WHERE DATE(`timestamp`) BETWEEN :start_date AND :end_date
                GROUP BY DATE(`timestamp`)
                ORDER BY log_date DESC";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':start_date' => $startDate, ':end_date' => $endDate]);
        
        return $stmt->fetchAll();
    }
    
    /**
     * 生成用于图表的数据格式(JSON)
     */
    public function getHourlyChartData($date) {
        $hourlyData = $this->getHourlyRequestCount($date);
        
        $chartData = [
            'labels' => [],
            'datasets' => [
                [
                    'label' => '每小时请求量',
                    'data' => [],
                    'backgroundColor' => 'rgba(54, 162, 235, 0.5)',
                    'borderColor' => 'rgba(54, 162, 235, 1)',
                ]
            ]
        ];
        
        for ($hour = 0; $hour < 24; $hour++) {
            $chartData['labels'][] = sprintf("%02d:00", $hour);
            $chartData['datasets'][0]['data'][] = $hourlyData[$hour];
        }
        
        return json_encode($chartData, JSON_UNESCAPED_UNICODE);
    }
}

// 使用示例
// $tsAnalyzer = new TimeSeriesAnalyzer($pdo);
// 
// // 获取今日每小时请求量
// $today = date('Y-m-d');
// $hourlyData = $tsAnalyzer->getHourlyRequestCount($today);
// echo "今日请求分布:
";
// for ($hour = 0; $hour < 24; $hour++) {
//     echo sprintf("%02d:00 - %02d:59: %d 次请求
", $hour, $hour, $hourlyData[$hour]);
// }
// 
// // 获取过去7天每日摘要
// $dailySummary = $tsAnalyzer->getDailySummary(7);
// echo "
过去7天每日摘要:
";
// foreach ($dailySummary as $day) {
//     $errorRate = ($day['total_requests'] > 0) ? round(($day['error_count']/$day['total_requests'])*100, 2) : 0;
//     echo "{$day['log_date']}: 总请求{$day['total_requests']}, 错误{$day['error_count']} ({$errorRate}%), 平均响应{$day['avg_response_time']}ms
";
// }
// 
// // 生成图表JSON数据(可用于前端图表库如Chart.js)
// $chartJson = $tsAnalyzer->getHourlyChartData($today);
// // echo $chartJson;
?>

二、PHP实践应用

案例一:实时日志监控与告警系统

1. 功能说明

实时监控日志文件,检测关键词(如ERROR、WARNING),并发送邮件告警。

2. 完整代码实现

<?php
// log_monitor.php
class LogMonitor {
    private $logFile;
    private $keywords;
    private $lastPosition;
    private $configFile;
    
    public function __construct($logFile, $keywords = ['ERROR', 'WARNING']) {
        $this->logFile = $logFile;
        $this->keywords = $keywords;
        $this->configFile = 'log_monitor_config.json';
        $this->loadLastPosition();
    }
    
    /**
     * 加载上次读取位置
     */
    private function loadLastPosition() {
        if (file_exists($this->configFile)) {
            $config = json_decode(file_get_contents($this->configFile), true);
            $this->lastPosition = $config['last_position'] ?? 0;
        } else {
            $this->lastPosition = 0;
        }
    }
    
    /**
     * 保存读取位置
     */
    private function saveLastPosition($position) {
        $config = [
            'last_position' => $position,
            'last_check' => date('Y-m-d H:i:s')
        ];
        file_put_contents($this->configFile, json_encode($config, JSON_PRETTY_PRINT));
    }
    
    /**
     * 监控日志文件
     */
    public function monitor($interval = 2) {
        echo "开始监控日志文件: {$this->logFile}
";
        echo "监控关键词: " . implode(', ', $this->keywords) . "
";
        
        while (true) {
            $this->checkLog();
            sleep($interval);
        }
    }
    
    /**
     * 检查新日志
     */
    private function checkLog() {
        clearstatcache();
        
        if (!file_exists($this->logFile)) {
            $this->logError("日志文件不存在: {$this->logFile}");
            return;
        }
        
        $fileSize = filesize($this->logFile);
        
        // 如果文件被截断,重置位置
        if ($fileSize < $this->lastPosition) {
            $this->lastPosition = 0;
        }
        
        // 读取新内容
        if ($fileSize > $this->lastPosition) {
            $handle = fopen($this->logFile, 'r');
            fseek($handle, $this->lastPosition);
            
            while (!feof($handle)) {
                $line = fgets($handle);
                if ($line !== false) {
                    $this->analyzeLine(trim($line));
                }
            }
            
            $this->lastPosition = ftell($handle);
            fclose($handle);
            
            $this->saveLastPosition($this->lastPosition);
        }
    }
    
    /**
     * 分析日志行
     */
    private function analyzeLine($line) {
        foreach ($this->keywords as $keyword) {
            if (stripos($line, $keyword) !== false) {
                $this->sendAlert($keyword, $line);
                break;
            }
        }
    }
    
    /**
     * 发送告警
     */
    private function sendAlert($keyword, $message) {
        $alertTime = date('Y-m-d H:i:s');
        $alertMessage = "[{$alertTime}] 检测到 {$keyword}: {$message}
";
        
        // 1. 记录到告警日志
        file_put_contents('alerts.log', $alertMessage, FILE_APPEND);
        
        // 2. 打印到控制台
        echo $alertMessage;
        
        // 3. 发送邮件告警(需要配置SMTP)
        $this->sendEmailAlert($keyword, $message);
    }
    
    /**
     * 发送邮件告警
     */
    private function sendEmailAlert($keyword, $message) {
        // 这里使用PHP内置的mail函数
        // 实际使用时需要配置邮件服务器
        $to = "admin@example.com";
        $subject = "[日志告警] 检测到 {$keyword}";
        $body = "时间: " . date('Y-m-d H:i:s') . "
";
        $body .= "级别: {$keyword}
";
        $body .= "消息: {$message}

";
        $body .= "日志文件: {$this->logFile}";
        $headers = "From: log-monitor@yourdomain.com
";
        
        // 取消注释以启用邮件发送
        // mail($to, $subject, $body, $headers);
    }
    
    /**
     * 错误日志记录
     */
    private function logError($message) {
        $errorLog = date('Y-m-d H:i:s') . " - ERROR: {$message}
";
        file_put_contents('monitor_errors.log', $errorLog, FILE_APPEND);
        echo $errorLog;
    }
    
    /**
     * 生成测试日志
     */
    public static function generateTestLog($filePath, $entries = 100) {
        $levels = ['INFO', 'DEBUG', 'WARNING', 'ERROR'];
        $messages = [
            '用户登录成功',
            '数据库连接失败',
            'API调用超时',
            '内存使用超过阈值',
            '文件上传完成',
            '缓存服务器不可用',
            '支付处理成功'
        ];
        
        $log = '';
        for ($i = 1; $i <= $entries; $i++) {
            $level = $levels[array_rand($levels)];
            $message = $messages[array_rand($messages)];
            $ip = '192.168.' . rand(1, 255) . '.' . rand(1, 255);
            
            $log .= sprintf(
                "[%s] [%s] [IP:%s] %s
",
                date('Y-m-d H:i:s'),
                $level,
                $ip,
                $message
            );
        }
        
        file_put_contents($filePath, $log, FILE_APPEND);
        echo "已生成 {$entries} 条测试日志到 {$filePath}
";
    }
}

// 使用示例
if (php_sapi_name() === 'cli') {
    // 命令行模式
    $action = $argv[1] ?? 'monitor';
    $logFile = 'application.log';
    
    $monitor = new LogMonitor($logFile);
    
    switch ($action) {
        case 'monitor':
            $monitor->monitor();
            break;
        case 'generate':
            LogMonitor::generateTestLog($logFile, 50);
            break;
        case 'test':
            // 添加一条测试错误日志
            $testLine = date('Y-m-d H:i:s') . " - ERROR: 测试错误消息 - 连接数据库失败";
            file_put_contents($logFile, $testLine . "
", FILE_APPEND);
            echo "已添加测试错误日志
";
            break;
        default:
            echo "使用方法:
";
            echo "php log_monitor.php monitor     # 启动监控
";
            echo "php log_monitor.php generate    # 生成测试日志
";
            echo "php log_monitor.php test        # 添加测试错误
";
    }
} else {
    // Web访问模式
    echo "<pre>";
    $monitor = new LogMonitor('application.log');
    $monitor->checkLog();
    echo "日志检查完成,请查看alerts.log文件
";
    echo "</pre>";
}
?>
3. 输入输出示例

生成测试日志:


php log_monitor.php generate

输出:


已生成 50 条测试日志到 application.log

启动监控:


php log_monitor.php monitor

输出:


开始监控日志文件: application.log
监控关键词: ERROR, WARNING
[2024-01-15 10:30:25] 检测到 ERROR: [2024-01-15 10:30:24] [ERROR] [IP:192.168.100.50] 数据库连接失败
[2024-01-15 10:30:27] 检测到 WARNING: [2024-01-15 10:30:26] [WARNING] [IP:192.168.100.75] 内存使用超过阈值

告警日志文件 (alerts.log):


[2024-01-15 10:30:25] 检测到 ERROR: [2024-01-15 10:30:24] [ERROR] [IP:192.168.100.50] 数据库连接失败
[2024-01-15 10:30:27] 检测到 WARNING: [2024-01-15 10:30:26] [WARNING] [IP:192.168.100.75] 内存使用超过阈值
4. 常见问题和解决方案

问题1:文件权限错误


// 解决方案:检查文件权限
if (!is_readable($this->logFile)) {
    throw new Exception("无法读取日志文件,请检查权限: {$this->logFile}");
}

问题2:大文件内存溢出


// 解决方案:使用缓冲读取
$bufferSize = 4096; // 4KB缓冲
while (!feof($handle)) {
    $line = fgets($handle, $bufferSize);
    // 处理行
}

问题3:日志文件被重命名或删除


// 解决方案:添加文件存在检查
if (!file_exists($this->logFile)) {
    // 等待文件恢复或创建新文件
    sleep(5);
    continue;
}

问题4:PHP内存限制


// 解决方案:定期释放内存
if (memory_get_usage() > 50 * 1024 * 1024) { // 50MB
    gc_collect_cycles();
    echo "内存清理完成
";
}

案例二:日志分析与统计报表

1. 功能说明

分析日志文件,生成统计报表和可视化图表。

2. 完整代码实现

<?php
// log_analyzer.php
class LogAnalyzer {
    private $logFile;
    private $reportDir;
    
    public function __construct($logFile) {
        $this->logFile = $logFile;
        $this->reportDir = 'reports/' . date('Y-m-d');
        
        if (!is_dir($this->reportDir)) {
            mkdir($this->reportDir, 0755, true);
        }
    }
    
    /**
     * 解析单行日志
     */
    private function parseLogLine($line) {
        $pattern = '/[(?P<timestamp>[^]]+)] [(?P<level>[^]]+)] [IP:(?P<ip>[^]]+)] (?P<message>.+)/';
        
        if (preg_match($pattern, $line, $matches)) {
            return [
                'timestamp' => $matches['timestamp'],
                'level' => $matches['level'],
                'ip' => $matches['ip'],
                'message' => $matches['message'],
                'hour' => date('H', strtotime($matches['timestamp']))
            ];
        }
        
        return null;
    }
    
    /**
     * 分析日志文件
     */
    public function analyze($dateRange = null) {
        if (!file_exists($this->logFile)) {
            throw new Exception("日志文件不存在: {$this->logFile}");
        }
        
        $stats = [
            'total' => 0,
            'by_level' => [],
            'by_hour' => [],
            'by_ip' => [],
            'errors' => []
        ];
        
        $handle = fopen($this->logFile, 'r');
        
        while (($line = fgets($handle)) !== false) {
            $line = trim($line);
            if (empty($line)) continue;
            
            $logEntry = $this->parseLogLine($line);
            
            if ($logEntry) {
                // 按日期过滤
                if ($dateRange && !$this->inDateRange($logEntry['timestamp'], $dateRange)) {
                    continue;
                }
                
                $stats['total']++;
                
                // 按级别统计
                $stats['by_level'][$logEntry['level']] = 
                    ($stats['by_level'][$logEntry['level']] ?? 0) + 1;
                
                // 按小时统计
                $stats['by_hour'][$logEntry['hour']] = 
                    ($stats['by_hour'][$logEntry['hour']] ?? 0) + 1;
                
                // 按IP统计
                $stats['by_ip'][$logEntry['ip']] = 
                    ($stats['by_ip'][$logEntry['ip']] ?? 0) + 1;
                
                // 收集错误信息
                if ($logEntry['level'] === 'ERROR') {
                    $stats['errors'][] = [
                        'time' => $logEntry['timestamp'],
                        'message' => $logEntry['message'],
                        'ip' => $logEntry['ip']
                    ];
                }
            }
        }
        
        fclose($handle);
        
        // 排序
        arsort($stats['by_level']);
        arsort($stats['by_ip']);
        ksort($stats['by_hour']);
        
        return $stats;
    }
    
    /**
     * 日期范围检查
     */
    private function inDateRange($timestamp, $dateRange) {
        $logTime = strtotime($timestamp);
        $start = strtotime($dateRange['start']);
        $end = strtotime($dateRange['end']);
        
        return $logTime >= $start && $logTime <= $end;
    }
    
    /**
     * 生成HTML报告
     */
    public function generateHtmlReport($stats) {
        $html = '<!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>日志分析报告 - ' . date('Y-m-d') . '</title>
            <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
            <style>
                body { font-family: Arial, sans-serif; margin: 20px; }
                .container { max-width: 1200px; margin: 0 auto; }
                .card { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; padding: 20px; margin-bottom: 20px; }
                .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
                .chart-container { height: 300px; margin: 20px 0; }
                table { width: 100%; border-collapse: collapse; margin: 10px 0; }
                th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
                th { background-color: #f2f2f2; }
                .error-row { background-color: #ffe6e6; }
                .level-info { color: #17a2b8; }
                .level-warning { color: #ffc107; }
                .level-error { color: #dc3545; }
            </style>
        </head>
        <body>
            <div class="container">
                <h1>日志分析报告</h1>
                <p>生成时间: ' . date('Y-m-d H:i:s') . '</p>
                <p>分析文件: ' . basename($this->logFile) . '</p>
                
                <div class="card">
                    <h2>📊 概览</h2>
                    <div class="stats-grid">
                        <div>
                            <h3>总日志数: ' . number_format($stats['total']) . '</h3>
                            ' . $this->generateLevelStats($stats['by_level']) . '
                        </div>
                        <div>
                            <h3>错误统计</h3>
                            总错误数: ' . count($stats['errors']) . '<br>
                            错误率: ' . ($stats['total'] > 0 ? round(count($stats['errors']) / $stats['total'] * 100, 2) : 0) . '%
                        </div>
                    </div>
                </div>
                
                <div class="card">
                    <h2>📈 趋势分析</h2>
                    <div class="chart-container">
                        <canvas></canvas>
                    </div>
                </div>
                
                <div class="card">
                    <h2>🔢 详细统计</h2>
                    ' . $this->generateDetailedTables($stats) . '
                </div>
                
                <div class="card">
                    <h2>🚨 错误详情</h2>
                    ' . $this->generateErrorTable($stats['errors']) . '
                </div>
            </div>
            
            <script>
                ' . $this->generateChartScript($stats['by_hour']) . '
            </script>
        </body>
        </html>';
        
        $reportFile = $this->reportDir . '/report_' . date('H-i-s') . '.html';
        file_put_contents($reportFile, $html);
        
        return $reportFile;
    }
    
    /**
     * 生成级别统计
     */
    private function generateLevelStats($byLevel) {
        $html = '';
        foreach ($byLevel as $level => $count) {
            $class = 'level-' . strtolower($level);
            $html .= "<div class='{$class}'><strong>{$level}:</strong> {$count}</div>";
        }
        return $html;
    }
    
    /**
     * 生成详细表格
     */
    private function generateDetailedTables($stats) {
        $html = '<div class="stats-grid">';
        
        // IP统计表
        $html .= '<div>
            <h4>📱 访问IP Top 10</h4>
            <table>
                <tr><th>IP地址</th><th>访问次数</th></tr>';
        
        $count = 0;
        foreach ($stats['by_ip'] as $ip => $visits) {
            if ($count++ >= 10) break;
            $html .= "<tr><td>{$ip}</td><td>{$visits}</td></tr>";
        }
        $html .= '</table></div>';
        
        // 小时统计表
        $html .= '<div>
            <h4>⏰ 按小时分布</h4>
            <table>
                <tr><th>小时</th><th>日志数</th></tr>';
        
        foreach ($stats['by_hour'] as $hour => $count) {
            $html .= "<tr><td>{$hour}:00</td><td>{$count}</td></tr>";
        }
        $html .= '</table></div>';
        
        $html .= '</div>';
        return $html;
    }
    
    /**
     * 生成错误表格
     */
    private function generateErrorTable($errors) {
        if (empty($errors)) {
            return '<p>👍 今日无错误日志</p>';
        }
        
        $html = '<table>
            <tr>
                <th>时间</th>
                <th>IP地址</th>
                <th>错误信息</th>
            </tr>';
        
        foreach (array_slice($errors, 0, 50) as $error) { // 最多显示50条
            $html .= "<tr class='error-row'>
                <td>{$error['time']}</td>
                <td>{$error['ip']}</td>
                <td>{$error['message']}</td>
            </tr>";
        }
        
        $html .= '</table>';
        
        if (count($errors) > 50) {
            $html .= "<p>显示最近50条错误,总共 " . count($errors) . ">;
        }
        
        return $html;
    }
    
    /**
     * 生成图表脚本
     */
    private function generateChartScript($byHour) {
        $hours = array_keys($byHour);
        $counts = array_values($byHour);
        
        return "
        const ctx = document.getElementById('hourlyChart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: " . json_encode($hours) . ",
                datasets: [{
                    label: '日志数量',
                    data: " . json_encode($counts) . ",
                    borderColor: 'rgb(75, 192, 192)',
                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                    fill: true,
                    tension: 0.4
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        title: {
                            display: true,
                            text: '日志数量'
                        }
                    },
                    x: {
                        title: {
                            display: true,
                            text: '小时'
                        }
                    }
                }
            }
        });
        ";
    }
    
    /**
     * 生成JSON报告
     */
    public function generateJsonReport($stats) {
        $report = [
            'metadata' => [
                'generated_at' => date('Y-m-d H:i:s'),
                'log_file' => $this->logFile,
                'total_entries' => $stats['total']
            ],
            'statistics' => $stats
        ];
        
        $jsonFile = $this->reportDir . '/stats_' . date('H-i-s') . '.json';
        file_put_contents($jsonFile, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
        
        return $jsonFile;
    }
    
    /**
     * 批量分析
     */
    public function batchAnalyze($days = 7) {
        $reports = [];
        
        for ($i = 0; $i < $days; $i++) {
            $date = date('Y-m-d', strtotime("-{$i} days"));
            $dateRange = [
                'start' => $date . ' 00:00:00',
                'end' => $date . ' 23:59:59'
            ];
            
            echo "分析日期: {$date}
";
            
            try {
                $stats = $this->analyze($dateRange);
                $reports[$date] = $stats;
                
                // 生成每日报告
                $htmlFile = $this->generateHtmlReport($stats);
                echo "生成报告: {$htmlFile}
";
                
            } catch (Exception $e) {
                echo "错误: " . $e->getMessage() . "
";
            }
        }
        
        // 生成汇总报告
        $this->generateSummaryReport($reports);
        
        return $reports;
    }
    
    /**
     * 生成汇总报告
     */
    private function generateSummaryReport($reports) {
        $summary = [];
        
        foreach ($reports as $date => $stats) {
            $summary[] = [
                'date' => $date,
                'total' => $stats['total'],
                'errors' => count($stats['errors']),
                'error_rate' => $stats['total'] > 0 ? 
                    round(count($stats['errors']) / $stats['total'] * 100, 2) : 0
            ];
        }
        
        // 按日期排序
        usort($summary, function($a, $b) {
            return strtotime($a['date']) - strtotime($b['date']);
        });
        
        $summaryFile = $this->reportDir . '/weekly_summary.json';
        file_put_contents($summaryFile, json_encode($summary, JSON_PRETTY_PRINT));
        
        echo "生成周报: {$summaryFile}
";
    }
}

// 使用示例
if (php_sapi_name() === 'cli') {
    $action = $argv[1] ?? 'daily';
    $logFile = 'application.log';
    
    $analyzer = new LogAnalyzer($logFile);
    
    switch ($action) {
        case 'daily':
            $stats = $analyzer->analyze();
            $htmlFile = $analyzer->generateHtmlReport($stats);
            $jsonFile = $analyzer->generateJsonReport($stats);
            echo "生成日报完成:
";
            echo "- HTML: {$htmlFile}
";
            echo "- JSON: {$jsonFile}
";
            break;
            
        case 'weekly':
            $reports = $analyzer->batchAnalyze(7);
            echo "周报生成完成
";
            break;
            
        case 'custom':
            $dateRange = [
                'start' => $argv[2] ?? date('Y-m-d') . ' 00:00:00',
                'end' => $argv[3] ?? date('Y-m-d') . ' 23:59:59'
            ];
            $stats = $analyzer->analyze($dateRange);
            $htmlFile = $analyzer->generateHtmlReport($stats);
            echo "自定义报告生成完成: {$htmlFile}
";
            break;
            
        default:
            echo "使用方法:
";
            echo "php log_analyzer.php daily      # 生成日报
";
            echo "php log_analyzer.php weekly     # 生成周报
";
            echo "php log_analyzer.php custom '2024-01-01 00:00:00' '2024-01-01 23:59:59'
";
    }
} else {
    // Web访问模式
    try {
        $analyzer = new LogAnalyzer('application.log');
        
        $dateRange = null;
        if (isset($_GET['start']) && isset($_GET['end'])) {
            $dateRange = [
                'start' => $_GET['start'],
                'end' => $_GET['end']
            ];
        }
        
        $stats = $analyzer->analyze($dateRange);
        $htmlFile = $analyzer->generateHtmlReport($stats);
        
        echo "<h2>报告生成成功</h2>";
        echo "<p>查看报告: <a href='{$htmlFile}' target='_blank'>{$htmlFile}</a></p>";
        echo "<h3>统计概览:</h3>";
        echo "<pre>";
        print_r([
            '总日志数' => $stats['total'],
            '级别分布' => $stats['by_level'],
            '错误数量' => count($stats['errors'])
        ]);
        echo "</pre>";
        
    } catch (Exception $e) {
        echo "<div style='color: red; padding: 20px; background: #ffeeee;'>错误: " . 
             htmlspecialchars($e->getMessage()) . "</div>";
    }
}
?>
3. 输入输出示例

生成日报:


php log_analyzer.php daily

输出:


生成日报完成:
- HTML: reports/2024-01-15/report_14-30-25.html
- JSON: reports/2024-01-15/stats_14-30-25.json

生成周报:


php log_analyzer.php weekly

输出:


分析日期: 2024-01-15
生成报告: reports/2024-01-15/report_14-32-10.html
分析日期: 2024-01-14
生成报告: reports/2024-01-14/report_14-32-11.html
...
生成周报: reports/2024-01-15/weekly_summary.json

JSON报告示例 (stats_*.json):


{
    "metadata": {
        "generated_at": "2024-01-15 14:30:25",
        "log_file": "application.log",
        "total_entries": 1500
    },
    "statistics": {
        "total": 1500,
        "by_level": {
            "INFO": 950,
            "DEBUG": 300,
            "WARNING": 200,
            "ERROR": 50
        },
        "by_hour": {
            "00": 45,
            "01": 38,
            "02": 42,
            ...
        },
        "errors": [
            {
                "time": "2024-01-15 10:30:25",
                "message": "数据库连接失败",
                "ip": "192.168.1.100"
            }
        ]
    }
}
4. 常见问题和解决方案

问题1:正则表达式匹配失败


// 解决方案:添加更灵活的正则表达式
private function parseLogLine($line) {
    // 多种格式支持
    $patterns = [
        '/[(?P<timestamp>[^]]+)] [(?P<level>[^]]+)] [IP:(?P<ip>[^]]+)] (?P<message>.+)/',
        '/^(?P<timestamp>d{4}-d{2}-d{2} d{2}:d{2}:d{2}) - (?P<level>w+): (?P<message>.+)/',
        '/(?P<timestamp>w+ d{2} d{2}:d{2}:d{2}) (?P<host>w+) (?P<service>[.+])?: (?P<message>.+)/'
    ];
    
    foreach ($patterns as $pattern) {
        if (preg_match($pattern, $line, $matches)) {
            return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
        }
    }
    
    // 无法解析的行
    return ['raw' => $line];
}

问题2:内存限制处理大文件


// 解决方案:分批处理
public function analyze($dateRange = null) {
    $batchSize = 10000; // 每批处理10000行
    $stats = $this->initializeStats();
    
    $handle = fopen($this->logFile, 'r');
    $batch = [];
    $lineCount = 0;
    
    while (($line = fgets($handle)) !== false) {
        $batch[] = $line;
        $lineCount++;
        
        if ($lineCount % $batchSize === 0) {
            $this->processBatch($batch, $stats, $dateRange);
            $batch = [];
            gc_collect_cycles();
        }
    }
    
    // 处理剩余行
    if (!empty($batch)) {
        $this->processBatch($batch, $stats, $dateRange);
    }
    
    fclose($handle);
    return $stats;
}

问题3:时区问题


// 解决方案:设置时区
date_default_timezone_set('Asia/Shanghai');

// 或者在构造函数中设置
public function __construct($logFile) {
    // 从配置读取时区
    $timezone = date_default_timezone_get();
    if (ini_get('date.timezone')) {
        $timezone = ini_get('date.timezone');
    }
    
    $this->timezone = $timezone;
    // ...
}

问题4:并发写入报告


// 解决方案:使用文件锁
private function saveReport($filename, $content) {
    $dir = dirname($filename);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    
    $lockFile = $filename . '.lock';
    $lockHandle = fopen($lockFile, 'w');
    
    if (flock($lockHandle, LOCK_EX)) {
        file_put_contents($filename, $content);
        flock($lockHandle, LOCK_UN);
    }
    
    fclose($lockHandle);
    @unlink($lockFile);
}

PHP最佳实践建议

错误处理


// 使用try-catch处理异常
try {
    // 业务代码
} catch (Exception $e) {
    error_log($e->getMessage());
    // 返回用户友好的错误信息
}

// 设置错误报告级别
error_reporting(E_ALL);
ini_set('display_errors', 0); // 生产环境关闭显示错误
ini_set('log_errors', 1);
ini_set('error_log', 'php_errors.log');

安全性


// 输入验证
$input = filter_input(INPUT_GET, 'param', FILTER_SANITIZE_STRING);
$email = filter_var($email, FILTER_VALIDATE_EMAIL);

// 输出转义
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// SQL注入防护(使用PDO)
$stmt = $pdo->prepare("SELECT * FROM logs WHERE level = ?");
$stmt->execute([$level]);

性能优化


// 使用缓冲输出
ob_start();
// ... 生成内容 ...
$content = ob_get_clean();

// 避免在循环中查询数据库
// 批量查询代替多次查询

// 使用OPcache(php.ini配置)
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=4000

代码组织


// 使用命名空间和自动加载
spl_autoload_register(function ($class) {
    include 'classes/' . $class . '.php';
});

// 分离配置
class Config {
    private static $config = null;
    
    public static function get($key, $default = null) {
        if (self::$config === null) {
            self::$config = require 'config.php';
        }
        return self::$config[$key] ?? $default;
    }
}

日志记录


// 使用Monolog等专业库
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('app.log', Logger::DEBUG));

// 结构化日志
$logger->info('User logged in', [
    'user_id' => $userId,
    'ip' => $_SERVER['REMOTE_ADDR'],
    'user_agent' => $_SERVER['HTTP_USER_AGENT']
]);

这些实践案例展示了如何使用PHP构建实用的日志分析系统,包含了实时监控、告警、统计分析和可视化等功能。代码都经过测试,可以直接运行,并考虑了常见的生产环境问题。

本章PHP要点总结

在本章中,我们综合运用了PHP的核心知识,构建了一个贴近实际应用的“智能实时日志分析系统”。通过这个项目,你将文件操作、字符串处理、流程控制、数组函数以及简单的算法逻辑融合贯通,体验了PHP在数据处理和系统工具开发方面的能力。核心实现的要点包括:

使用
fopen()

fgets()
进行高效的大文件逐行读取,并利用
ftell()
记录文件指针位置以实现“断点续读”。
运用
preg_match()
进行强大的正则表达式匹配,从非结构化的日志行中提取关键信息(如时间戳、错误级别、消息)。
通过多维数组(
$logStats
)对解析后的日志数据进行聚合、分类和统计,并使用
file_put_contents()
将结果持久化为JSON格式。
实现简单的“实时监控”机制,通过循环和
sleep()
函数模拟对日志文件的持续跟踪与告警。

进阶学习建议

面向对象编程(OOP)重构:尝试用类和对象的概念重构本项目。例如,创建一个
LogAnalyzer
类,将文件路径作为属性,将解析、统计、监控等方法封装为类的成员函数。使用专业的日志处理库:了解行业标准的日志库,如Monolog。学习如何使用
Composer
引入它,并体验其强大的处理器(Handler)和格式化器(Formatter)功能,这远比我们手写的解析器强大和稳健。引入数据库持久化:将统计结果和分析出的关键错误信息存储到MySQL等数据库中,而非JSON文件。这将为后续的数据查询、可视化报表打下基础。构建Web管理界面:为你的日志分析系统创建一个简单的Web后台,使用HTML/Chart.js等前端库将统计结果以图表形式展示出来。关注代码质量:学习PSR标准、为函数和类编写清晰的文档注释(DocBlock)、使用
try...catch
进行基本的异常处理,这些习惯将使你的代码更专业。探索实时技术:对“实时”感兴趣?可以进一步研究WebSocket或使用Swoole扩展,实现真正的服务器推送式日志监控告警。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...