488 lines
18 KiB
PHP
488 lines
18 KiB
PHP
<?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;
|
||
}
|
||
} |