subscription/includes/class-yoone-log-analyzer.php

488 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 日志分析器类
*
* 用于分析日志数据,生成统计报告和趋势分析
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* 日志分析器类
*/
class Yoone_Log_Analyzer {
/**
* 分析最近的日志
*
* @param int $days 分析天数
* @return array 分析结果
*/
public static function analyze_recent_logs($days = 7) {
$logs = Yoone_Logger::get_recent_logs(1000);
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$analysis = array(
'total_logs' => 0,
'error_count' => 0,
'warning_count' => 0,
'info_count' => 0,
'debug_count' => 0,
'daily_stats' => array(),
'hourly_stats' => array(),
'top_errors' => array(),
'subscription_stats' => array(),
'payment_stats' => array(),
'performance_issues' => array()
);
foreach ($logs as $log_line) {
$parsed_log = self::parse_log_line($log_line);
if (!$parsed_log || $parsed_log['timestamp'] < $cutoff_date) {
continue;
}
$analysis['total_logs']++;
// 统计日志级别
$level = $parsed_log['level'];
if (isset($analysis[$level . '_count'])) {
$analysis[$level . '_count']++;
}
// 按日统计
$date = date('Y-m-d', strtotime($parsed_log['timestamp']));
if (!isset($analysis['daily_stats'][$date])) {
$analysis['daily_stats'][$date] = array(
'total' => 0,
'error' => 0,
'warning' => 0,
'info' => 0,
'debug' => 0
);
}
$analysis['daily_stats'][$date]['total']++;
$analysis['daily_stats'][$date][$level]++;
// 按小时统计
$hour = date('H', strtotime($parsed_log['timestamp']));
if (!isset($analysis['hourly_stats'][$hour])) {
$analysis['hourly_stats'][$hour] = 0;
}
$analysis['hourly_stats'][$hour]++;
// 收集错误信息
if ($level === 'error') {
$error_key = md5($parsed_log['message']);
if (!isset($analysis['top_errors'][$error_key])) {
$analysis['top_errors'][$error_key] = array(
'message' => $parsed_log['message'],
'count' => 0,
'first_seen' => $parsed_log['timestamp'],
'last_seen' => $parsed_log['timestamp']
);
}
$analysis['top_errors'][$error_key]['count']++;
$analysis['top_errors'][$error_key]['last_seen'] = $parsed_log['timestamp'];
}
// 订阅相关统计
if (strpos($parsed_log['message'], 'subscription') !== false) {
$analysis['subscription_stats']['total'] = ($analysis['subscription_stats']['total'] ?? 0) + 1;
if (strpos($parsed_log['message'], 'created') !== false) {
$analysis['subscription_stats']['created'] = ($analysis['subscription_stats']['created'] ?? 0) + 1;
} elseif (strpos($parsed_log['message'], 'cancelled') !== false) {
$analysis['subscription_stats']['cancelled'] = ($analysis['subscription_stats']['cancelled'] ?? 0) + 1;
} elseif (strpos($parsed_log['message'], 'renewed') !== false) {
$analysis['subscription_stats']['renewed'] = ($analysis['subscription_stats']['renewed'] ?? 0) + 1;
}
}
// 支付相关统计
if (strpos($parsed_log['message'], 'payment') !== false) {
$analysis['payment_stats']['total'] = ($analysis['payment_stats']['total'] ?? 0) + 1;
if (strpos($parsed_log['message'], 'success') !== false) {
$analysis['payment_stats']['success'] = ($analysis['payment_stats']['success'] ?? 0) + 1;
} elseif (strpos($parsed_log['message'], 'failed') !== false) {
$analysis['payment_stats']['failed'] = ($analysis['payment_stats']['failed'] ?? 0) + 1;
}
}
// 性能问题检测
if (strpos($parsed_log['message'], 'timeout') !== false ||
strpos($parsed_log['message'], 'slow') !== false ||
strpos($parsed_log['message'], 'memory') !== false) {
$analysis['performance_issues'][] = array(
'timestamp' => $parsed_log['timestamp'],
'message' => $parsed_log['message'],
'level' => $level
);
}
}
// 排序错误统计
uasort($analysis['top_errors'], function($a, $b) {
return $b['count'] - $a['count'];
});
// 只保留前10个错误
$analysis['top_errors'] = array_slice($analysis['top_errors'], 0, 10, true);
return $analysis;
}
/**
* 生成健康报告
*
* @return array 健康报告
*/
public static function generate_health_report() {
$analysis = self::analyze_recent_logs(7);
$health_score = 100;
$issues = array();
$recommendations = array();
// 错误率检查
$error_rate = $analysis['total_logs'] > 0 ? ($analysis['error_count'] / $analysis['total_logs']) * 100 : 0;
if ($error_rate > 10) {
$health_score -= 30;
$issues[] = sprintf(__('错误率过高: %.1f%%', 'yoone-subscriptions'), $error_rate);
$recommendations[] = __('检查并修复频繁出现的错误', 'yoone-subscriptions');
} elseif ($error_rate > 5) {
$health_score -= 15;
$issues[] = sprintf(__('错误率较高: %.1f%%', 'yoone-subscriptions'), $error_rate);
}
// 警告率检查
$warning_rate = $analysis['total_logs'] > 0 ? ($analysis['warning_count'] / $analysis['total_logs']) * 100 : 0;
if ($warning_rate > 20) {
$health_score -= 20;
$issues[] = sprintf(__('警告率过高: %.1f%%', 'yoone-subscriptions'), $warning_rate);
$recommendations[] = __('关注警告信息,预防潜在问题', 'yoone-subscriptions');
}
// 支付失败率检查
if (isset($analysis['payment_stats']['total']) && $analysis['payment_stats']['total'] > 0) {
$payment_failure_rate = (($analysis['payment_stats']['failed'] ?? 0) / $analysis['payment_stats']['total']) * 100;
if ($payment_failure_rate > 15) {
$health_score -= 25;
$issues[] = sprintf(__('支付失败率过高: %.1f%%', 'yoone-subscriptions'), $payment_failure_rate);
$recommendations[] = __('检查支付网关配置和网络连接', 'yoone-subscriptions');
}
}
// 性能问题检查
if (count($analysis['performance_issues']) > 5) {
$health_score -= 20;
$issues[] = sprintf(__('发现 %d 个性能问题', 'yoone-subscriptions'), count($analysis['performance_issues']));
$recommendations[] = __('优化代码性能,检查服务器资源', 'yoone-subscriptions');
}
// 频繁错误检查
foreach ($analysis['top_errors'] as $error) {
if ($error['count'] > 10) {
$health_score -= 10;
$issues[] = sprintf(__('频繁错误: %s (出现 %d 次)', 'yoone-subscriptions'),
substr($error['message'], 0, 50) . '...', $error['count']);
break;
}
}
// 确保分数不低于0
$health_score = max(0, $health_score);
// 健康等级
if ($health_score >= 90) {
$health_level = 'excellent';
$health_text = __('优秀', 'yoone-subscriptions');
} elseif ($health_score >= 70) {
$health_level = 'good';
$health_text = __('良好', 'yoone-subscriptions');
} elseif ($health_score >= 50) {
$health_level = 'fair';
$health_text = __('一般', 'yoone-subscriptions');
} else {
$health_level = 'poor';
$health_text = __('较差', 'yoone-subscriptions');
}
return array(
'score' => $health_score,
'level' => $health_level,
'text' => $health_text,
'issues' => $issues,
'recommendations' => $recommendations,
'analysis' => $analysis
);
}
/**
* 获取趋势数据
*
* @param int $days 天数
* @return array 趋势数据
*/
public static function get_trend_data($days = 30) {
$analysis = self::analyze_recent_logs($days);
$trends = array(
'daily_errors' => array(),
'daily_warnings' => array(),
'daily_total' => array(),
'subscription_activity' => array(),
'payment_activity' => array()
);
// 填充每日数据
for ($i = $days - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$trends['daily_errors'][$date] = $analysis['daily_stats'][$date]['error'] ?? 0;
$trends['daily_warnings'][$date] = $analysis['daily_stats'][$date]['warning'] ?? 0;
$trends['daily_total'][$date] = $analysis['daily_stats'][$date]['total'] ?? 0;
}
return $trends;
}
/**
* 检测异常模式
*
* @return array 异常模式
*/
public static function detect_anomalies() {
$analysis = self::analyze_recent_logs(7);
$anomalies = array();
// 检测错误激增
$daily_errors = array();
foreach ($analysis['daily_stats'] as $date => $stats) {
$daily_errors[] = $stats['error'];
}
if (count($daily_errors) >= 3) {
$avg_errors = array_sum($daily_errors) / count($daily_errors);
$latest_errors = end($daily_errors);
if ($latest_errors > $avg_errors * 2 && $latest_errors > 5) {
$anomalies[] = array(
'type' => 'error_spike',
'severity' => 'high',
'message' => sprintf(__('今日错误数量异常增加: %d (平均: %.1f)', 'yoone-subscriptions'),
$latest_errors, $avg_errors),
'timestamp' => current_time('mysql')
);
}
}
// 检测重复错误
foreach ($analysis['top_errors'] as $error) {
if ($error['count'] > 20) {
$anomalies[] = array(
'type' => 'repeated_error',
'severity' => 'medium',
'message' => sprintf(__('重复错误: %s (出现 %d 次)', 'yoone-subscriptions'),
substr($error['message'], 0, 100), $error['count']),
'timestamp' => $error['last_seen']
);
}
}
// 检测支付问题
if (isset($analysis['payment_stats']['failed']) && $analysis['payment_stats']['failed'] > 10) {
$anomalies[] = array(
'type' => 'payment_issues',
'severity' => 'high',
'message' => sprintf(__('支付失败次数过多: %d', 'yoone-subscriptions'),
$analysis['payment_stats']['failed']),
'timestamp' => current_time('mysql')
);
}
return $anomalies;
}
/**
* 生成报告摘要
*
* @param array $analysis 分析数据
* @return string 报告摘要
*/
public static function generate_summary($analysis) {
$summary = array();
$summary[] = sprintf(__('总计 %d 条日志记录', 'yoone-subscriptions'), $analysis['total_logs']);
if ($analysis['error_count'] > 0) {
$summary[] = sprintf(__('%d 个错误', 'yoone-subscriptions'), $analysis['error_count']);
}
if ($analysis['warning_count'] > 0) {
$summary[] = sprintf(__('%d 个警告', 'yoone-subscriptions'), $analysis['warning_count']);
}
if (isset($analysis['subscription_stats']['total'])) {
$summary[] = sprintf(__('%d 个订阅相关事件', 'yoone-subscriptions'),
$analysis['subscription_stats']['total']);
}
if (isset($analysis['payment_stats']['total'])) {
$summary[] = sprintf(__('%d 个支付相关事件', 'yoone-subscriptions'),
$analysis['payment_stats']['total']);
}
return implode('', $summary);
}
/**
* 解析日志行
*
* @param string $log_line 日志行
* @return array|false 解析结果
*/
private static function parse_log_line($log_line) {
// 匹配WooCommerce日志格式
if (preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})\s+(\w+)\s+(.+)$/', $log_line, $matches)) {
return array(
'timestamp' => $matches[1],
'level' => strtolower($matches[2]),
'message' => $matches[3]
);
}
return false;
}
/**
* 导出分析报告
*
* @param string $format 格式 (json|csv|html)
* @param int $days 分析天数
* @return string 报告内容
*/
public static function export_report($format = 'json', $days = 7) {
$health_report = self::generate_health_report();
$trends = self::get_trend_data($days);
$anomalies = self::detect_anomalies();
$report_data = array(
'generated_at' => current_time('mysql'),
'period_days' => $days,
'health' => $health_report,
'trends' => $trends,
'anomalies' => $anomalies
);
switch ($format) {
case 'json':
return json_encode($report_data, JSON_PRETTY_PRINT);
case 'csv':
return self::convert_to_csv($report_data);
case 'html':
return self::convert_to_html($report_data);
default:
return json_encode($report_data);
}
}
/**
* 转换为CSV格式
*
* @param array $data 数据
* @return string CSV内容
*/
private static function convert_to_csv($data) {
$csv = "Yoone Subscriptions Log Analysis Report\n";
$csv .= "Generated: " . $data['generated_at'] . "\n";
$csv .= "Period: " . $data['period_days'] . " days\n\n";
$csv .= "Health Score," . $data['health']['score'] . "\n";
$csv .= "Health Level," . $data['health']['text'] . "\n\n";
if (!empty($data['health']['issues'])) {
$csv .= "Issues:\n";
foreach ($data['health']['issues'] as $issue) {
$csv .= "," . $issue . "\n";
}
$csv .= "\n";
}
if (!empty($data['anomalies'])) {
$csv .= "Anomalies:\n";
$csv .= "Type,Severity,Message,Timestamp\n";
foreach ($data['anomalies'] as $anomaly) {
$csv .= $anomaly['type'] . "," . $anomaly['severity'] . "," .
$anomaly['message'] . "," . $anomaly['timestamp'] . "\n";
}
}
return $csv;
}
/**
* 转换为HTML格式
*
* @param array $data 数据
* @return string HTML内容
*/
private static function convert_to_html($data) {
$html = '<html><head><title>Yoone Subscriptions Log Analysis Report</title>';
$html .= '<style>body{font-family:Arial,sans-serif;margin:20px;}';
$html .= '.health-score{font-size:24px;font-weight:bold;margin:20px 0;}';
$html .= '.excellent{color:#28a745;}.good{color:#17a2b8;}.fair{color:#ffc107;}.poor{color:#dc3545;}';
$html .= 'table{border-collapse:collapse;width:100%;margin:20px 0;}';
$html .= 'th,td{border:1px solid #ddd;padding:8px;text-align:left;}';
$html .= 'th{background-color:#f2f2f2;}</style></head><body>';
$html .= '<h1>Yoone Subscriptions 日志分析报告</h1>';
$html .= '<p>生成时间: ' . $data['generated_at'] . '</p>';
$html .= '<p>分析周期: ' . $data['period_days'] . ' 天</p>';
$html .= '<div class="health-score ' . $data['health']['level'] . '">';
$html .= '健康评分: ' . $data['health']['score'] . '/100 (' . $data['health']['text'] . ')';
$html .= '</div>';
if (!empty($data['health']['issues'])) {
$html .= '<h2>发现的问题</h2><ul>';
foreach ($data['health']['issues'] as $issue) {
$html .= '<li>' . htmlspecialchars($issue) . '</li>';
}
$html .= '</ul>';
}
if (!empty($data['health']['recommendations'])) {
$html .= '<h2>建议</h2><ul>';
foreach ($data['health']['recommendations'] as $rec) {
$html .= '<li>' . htmlspecialchars($rec) . '</li>';
}
$html .= '</ul>';
}
if (!empty($data['anomalies'])) {
$html .= '<h2>异常检测</h2><table>';
$html .= '<tr><th>类型</th><th>严重程度</th><th>描述</th><th>时间</th></tr>';
foreach ($data['anomalies'] as $anomaly) {
$html .= '<tr>';
$html .= '<td>' . htmlspecialchars($anomaly['type']) . '</td>';
$html .= '<td>' . htmlspecialchars($anomaly['severity']) . '</td>';
$html .= '<td>' . htmlspecialchars($anomaly['message']) . '</td>';
$html .= '<td>' . htmlspecialchars($anomaly['timestamp']) . '</td>';
$html .= '</tr>';
}
$html .= '</table>';
}
$html .= '</body></html>';
return $html;
}
}