mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
409 lines
13 KiB
PHP
409 lines
13 KiB
PHP
|
|
<?php
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
namespace Fendx\Monitor\Alert;
|
|||
|
|
|
|||
|
|
use Fendx\Log\Logger;
|
|||
|
|
|
|||
|
|
final class AlertManager
|
|||
|
|
{
|
|||
|
|
private static array $alerts = [];
|
|||
|
|
private static array $config = [];
|
|||
|
|
private static array $channels = [];
|
|||
|
|
private static bool $enabled = true;
|
|||
|
|
|
|||
|
|
public static function initialize(array $config): void
|
|||
|
|
{
|
|||
|
|
self::$config = array_merge([
|
|||
|
|
'enabled' => true,
|
|||
|
|
'max_alerts' => 500,
|
|||
|
|
'retention_period' => 7200,
|
|||
|
|
'channels' => ['log'],
|
|||
|
|
'thresholds' => [
|
|||
|
|
'error_rate' => 0.05,
|
|||
|
|
'memory_usage' => 0.9,
|
|||
|
|
'disk_usage' => 0.95,
|
|||
|
|
'response_time' => 2.0,
|
|||
|
|
'critical_errors' => 5
|
|||
|
|
],
|
|||
|
|
'cooldown' => [
|
|||
|
|
'error_rate' => 300,
|
|||
|
|
'memory_usage' => 600,
|
|||
|
|
'disk_usage' => 600,
|
|||
|
|
'response_time' => 300,
|
|||
|
|
'critical_errors' => 1800
|
|||
|
|
]
|
|||
|
|
], $config);
|
|||
|
|
|
|||
|
|
self::$enabled = self::$config['enabled'];
|
|||
|
|
self::initializeChannels();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function triggerAlert(string $type, string $message, array $context = [], string $severity = 'warning'): void
|
|||
|
|
{
|
|||
|
|
if (!self::$enabled) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$alert = [
|
|||
|
|
'id' => uniqid('alert_', true),
|
|||
|
|
'type' => $type,
|
|||
|
|
'message' => $message,
|
|||
|
|
'context' => $context,
|
|||
|
|
'severity' => $severity,
|
|||
|
|
'timestamp' => microtime(true),
|
|||
|
|
'datetime' => date('Y-m-d H:i:s'),
|
|||
|
|
'status' => 'active',
|
|||
|
|
'acknowledged' => false,
|
|||
|
|
'acknowledged_by' => null,
|
|||
|
|
'acknowledged_at' => null,
|
|||
|
|
'resolved' => false,
|
|||
|
|
'resolved_at' => null
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
self::$alerts[] = $alert;
|
|||
|
|
self::cleanupOldAlerts();
|
|||
|
|
|
|||
|
|
// 发送告警到各个渠道
|
|||
|
|
self::sendToChannels($alert);
|
|||
|
|
|
|||
|
|
// 记录告警
|
|||
|
|
Logger::warning("Alert triggered: {$type} - {$message}", $alert);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function checkErrorRate(float $errorRate, int $totalErrors, int $timeWindow): void
|
|||
|
|
{
|
|||
|
|
if ($errorRate > self::$config['thresholds']['error_rate']) {
|
|||
|
|
self::triggerAlert('error_rate',
|
|||
|
|
"High error rate detected: " . round($errorRate * 100, 2) . "%",
|
|||
|
|
[
|
|||
|
|
'error_rate' => $errorRate,
|
|||
|
|
'total_errors' => $totalErrors,
|
|||
|
|
'time_window' => $timeWindow,
|
|||
|
|
'threshold' => self::$config['thresholds']['error_rate']
|
|||
|
|
],
|
|||
|
|
$errorRate > 0.1 ? 'critical' : 'warning'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function checkMemoryUsage(float $usagePercent, int $usedBytes, int $totalBytes): void
|
|||
|
|
{
|
|||
|
|
if ($usagePercent > self::$config['thresholds']['memory_usage']) {
|
|||
|
|
self::triggerAlert('memory_usage',
|
|||
|
|
"High memory usage: " . round($usagePercent * 100, 2) . "%",
|
|||
|
|
[
|
|||
|
|
'usage_percent' => $usagePercent,
|
|||
|
|
'used_bytes' => $usedBytes,
|
|||
|
|
'total_bytes' => $totalBytes,
|
|||
|
|
'threshold' => self::$config['thresholds']['memory_usage']
|
|||
|
|
],
|
|||
|
|
$usagePercent > 0.95 ? 'critical' : 'warning'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function checkDiskUsage(string $path, float $usagePercent, int $usedBytes, int $totalBytes): void
|
|||
|
|
{
|
|||
|
|
if ($usagePercent > self::$config['thresholds']['disk_usage']) {
|
|||
|
|
self::triggerAlert('disk_usage',
|
|||
|
|
"Low disk space on {$path}: " . round($usagePercent * 100, 2) . "% used",
|
|||
|
|
[
|
|||
|
|
'path' => $path,
|
|||
|
|
'usage_percent' => $usagePercent,
|
|||
|
|
'used_bytes' => $usedBytes,
|
|||
|
|
'total_bytes' => $totalBytes,
|
|||
|
|
'threshold' => self::$config['thresholds']['disk_usage']
|
|||
|
|
],
|
|||
|
|
$usagePercent > 0.98 ? 'critical' : 'warning'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function checkResponseTime(string $endpoint, float $avgTime, float $threshold = null): void
|
|||
|
|
{
|
|||
|
|
$threshold = $threshold ?? self::$config['thresholds']['response_time'];
|
|||
|
|
|
|||
|
|
if ($avgTime > $threshold) {
|
|||
|
|
self::triggerAlert('response_time',
|
|||
|
|
"Slow response time for {$endpoint}: " . round($avgTime, 3) . "s",
|
|||
|
|
[
|
|||
|
|
'endpoint' => $endpoint,
|
|||
|
|
'avg_time' => $avgTime,
|
|||
|
|
'threshold' => $threshold
|
|||
|
|
],
|
|||
|
|
$avgTime > $threshold * 2 ? 'critical' : 'warning'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function checkCriticalErrors(int $criticalCount, int $timeWindow): void
|
|||
|
|
{
|
|||
|
|
if ($criticalCount >= self::$config['thresholds']['critical_errors']) {
|
|||
|
|
self::triggerAlert('critical_errors',
|
|||
|
|
"Multiple critical errors detected: {$criticalCount} in {$timeWindow}s",
|
|||
|
|
[
|
|||
|
|
'critical_count' => $criticalCount,
|
|||
|
|
'time_window' => $timeWindow,
|
|||
|
|
'threshold' => self::$config['thresholds']['critical_errors']
|
|||
|
|
],
|
|||
|
|
'critical'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function checkServiceDown(string $service, array $context = []): void
|
|||
|
|
{
|
|||
|
|
self::triggerAlert('service_down',
|
|||
|
|
"Service unavailable: {$service}",
|
|||
|
|
array_merge($context, ['service' => $service]),
|
|||
|
|
'critical'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function acknowledgeAlert(string $alertId, string $acknowledgedBy): bool
|
|||
|
|
{
|
|||
|
|
foreach (self::$alerts as &$alert) {
|
|||
|
|
if ($alert['id'] === $alertId && $alert['status'] === 'active') {
|
|||
|
|
$alert['acknowledged'] = true;
|
|||
|
|
$alert['acknowledged_by'] = $acknowledgedBy;
|
|||
|
|
$alert['acknowledged_at'] = microtime(true);
|
|||
|
|
|
|||
|
|
Logger::info("Alert acknowledged: {$alertId} by {$acknowledgedBy}");
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function resolveAlert(string $alertId): bool
|
|||
|
|
{
|
|||
|
|
foreach (self::$alerts as &$alert) {
|
|||
|
|
if ($alert['id'] === $alertId && $alert['status'] === 'active') {
|
|||
|
|
$alert['status'] = 'resolved';
|
|||
|
|
$alert['resolved_at'] = microtime(true);
|
|||
|
|
|
|||
|
|
Logger::info("Alert resolved: {$alertId}");
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function getActiveAlerts(): array
|
|||
|
|
{
|
|||
|
|
return array_filter(self::$alerts, fn($alert) => $alert['status'] === 'active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function getAlerts(array $filters = []): array
|
|||
|
|
{
|
|||
|
|
$alerts = self::$alerts;
|
|||
|
|
|
|||
|
|
// 应用过滤器
|
|||
|
|
if (!empty($filters)) {
|
|||
|
|
$alerts = array_filter($alerts, function($alert) use ($filters) {
|
|||
|
|
foreach ($filters as $key => $value) {
|
|||
|
|
if (isset($alert[$key]) && $alert[$key] !== $value) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 按时间倒序排列
|
|||
|
|
usort($alerts, function($a, $b) {
|
|||
|
|
return $b['timestamp'] <=> $a['timestamp'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return array_values($alerts);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function getAlertStatistics(): array
|
|||
|
|
{
|
|||
|
|
$stats = [
|
|||
|
|
'total_alerts' => count(self::$alerts),
|
|||
|
|
'active_alerts' => 0,
|
|||
|
|
'acknowledged_alerts' => 0,
|
|||
|
|
'resolved_alerts' => 0,
|
|||
|
|
'by_type' => [],
|
|||
|
|
'by_severity' => [],
|
|||
|
|
'by_hour' => [],
|
|||
|
|
'recent_alerts' => 0
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$now = time();
|
|||
|
|
$oneHourAgo = $now - 3600;
|
|||
|
|
|
|||
|
|
foreach (self::$alerts as $alert) {
|
|||
|
|
// 状态统计
|
|||
|
|
if ($alert['status'] === 'active') {
|
|||
|
|
$stats['active_alerts']++;
|
|||
|
|
}
|
|||
|
|
if ($alert['acknowledged']) {
|
|||
|
|
$stats['acknowledged_alerts']++;
|
|||
|
|
}
|
|||
|
|
if ($alert['resolved']) {
|
|||
|
|
$stats['resolved_alerts']++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 按类型统计
|
|||
|
|
$type = $alert['type'];
|
|||
|
|
$stats['by_type'][$type] = ($stats['by_type'][$type] ?? 0) + 1;
|
|||
|
|
|
|||
|
|
// 按严重程度统计
|
|||
|
|
$severity = $alert['severity'];
|
|||
|
|
$stats['by_severity'][$severity] = ($stats['by_severity'][$severity] ?? 0) + 1;
|
|||
|
|
|
|||
|
|
// 按小时统计
|
|||
|
|
$hour = date('Y-m-d H:00', (int)$alert['timestamp']);
|
|||
|
|
$stats['by_hour'][$hour] = ($stats['by_hour'][$hour] ?? 0) + 1;
|
|||
|
|
|
|||
|
|
// 最近告警
|
|||
|
|
if ($alert['timestamp'] > $oneHourAgo) {
|
|||
|
|
$stats['recent_alerts']++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $stats;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function clearAlerts(): void
|
|||
|
|
{
|
|||
|
|
self::$alerts = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function clearResolvedAlerts(): void
|
|||
|
|
{
|
|||
|
|
self::$alerts = array_filter(self::$alerts, fn($alert) => !$alert['resolved']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function addChannel(string $name, callable $handler): void
|
|||
|
|
{
|
|||
|
|
self::$channels[$name] = $handler;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static function removeChannel(string $name): void
|
|||
|
|
{
|
|||
|
|
unset(self::$channels[$name]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function initializeChannels(): void
|
|||
|
|
{
|
|||
|
|
// 默认日志渠道
|
|||
|
|
self::$channels['log'] = function($alert) {
|
|||
|
|
$level = match ($alert['severity']) {
|
|||
|
|
'critical' => 'critical',
|
|||
|
|
'warning' => 'warning',
|
|||
|
|
default => 'info'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
Logger::$level("ALERT [{$alert['type']}]: {$alert['message']}", $alert);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 邮件渠道(如果配置了)
|
|||
|
|
if (isset(self::$config['email']) && self::$config['email']['enabled']) {
|
|||
|
|
self::$channels['email'] = [self::class, 'sendEmailAlert'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 钉钉渠道(如果配置了)
|
|||
|
|
if (isset(self::$config['dingtalk']) && self::$config['dingtalk']['enabled']) {
|
|||
|
|
self::$channels['dingtalk'] = [self::class, 'sendDingTalkAlert'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Slack渠道(如果配置了)
|
|||
|
|
if (isset(self::$config['slack']) && self::$config['slack']['enabled']) {
|
|||
|
|
self::$channels['slack'] = [self::class, 'sendSlackAlert'];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function sendToChannels(array $alert): void
|
|||
|
|
{
|
|||
|
|
foreach (self::$config['channels'] as $channelName) {
|
|||
|
|
if (isset(self::$channels[$channelName])) {
|
|||
|
|
try {
|
|||
|
|
self::$channels[$channelName]($alert);
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
Logger::error("Failed to send alert to channel {$channelName}: " . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function sendEmailAlert(array $alert): void
|
|||
|
|
{
|
|||
|
|
$config = self::$config['email'];
|
|||
|
|
|
|||
|
|
$to = $config['to'] ?? [];
|
|||
|
|
$subject = "[Fendx Alert] {$alert['type']} - {$alert['severity']}";
|
|||
|
|
$message = self::formatAlertMessage($alert, 'email');
|
|||
|
|
|
|||
|
|
// 这里应该使用实际的邮件发送库
|
|||
|
|
Logger::info("Email alert would be sent to: " . implode(', ', $to));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function sendDingTalkAlert(array $alert): void
|
|||
|
|
{
|
|||
|
|
$config = self::$config['dingtalk'];
|
|||
|
|
$webhook = $config['webhook'] ?? '';
|
|||
|
|
|
|||
|
|
if (empty($webhook)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$message = self::formatAlertMessage($alert, 'dingtalk');
|
|||
|
|
|
|||
|
|
// 这里应该使用实际的HTTP客户端发送到钉钉
|
|||
|
|
Logger::info("DingTalk alert would be sent: {$message}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function sendSlackAlert(array $alert): void
|
|||
|
|
{
|
|||
|
|
$config = self::$config['slack'];
|
|||
|
|
$webhook = $config['webhook'] ?? '';
|
|||
|
|
|
|||
|
|
if (empty($webhook)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$message = self::formatAlertMessage($alert, 'slack');
|
|||
|
|
|
|||
|
|
// 这里应该使用实际的HTTP客户端发送到Slack
|
|||
|
|
Logger::info("Slack alert would be sent: {$message}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function formatAlertMessage(array $alert, string $format): string
|
|||
|
|
{
|
|||
|
|
$timestamp = date('Y-m-d H:i:s', (int)$alert['timestamp']);
|
|||
|
|
$severity = strtoupper($alert['severity']);
|
|||
|
|
|
|||
|
|
return match ($format) {
|
|||
|
|
'email' => "
|
|||
|
|
<h2>[{$severity}] {$alert['type']}</h2>
|
|||
|
|
<p><strong>Message:</strong> {$alert['message']}</p>
|
|||
|
|
<p><strong>Time:</strong> {$timestamp}</p>
|
|||
|
|
<p><strong>Context:</strong> <pre>" . json_encode($alert['context'], JSON_PRETTY_PRINT) . "</pre></p>
|
|||
|
|
",
|
|||
|
|
'dingtalk' => "【{$severity}】{$alert['type']}\n{$alert['message']}\n时间: {$timestamp}",
|
|||
|
|
'slack' => "*[{$severity}] {$alert['type']}*\n{$alert['message']}\nTime: {$timestamp}",
|
|||
|
|
default => "[{$severity}] {$alert['type']}: {$alert['message']} at {$timestamp}"
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function cleanupOldAlerts(): void
|
|||
|
|
{
|
|||
|
|
if (count(self::$alerts) <= self::$config['max_alerts']) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 按时间排序,保留最新的告警
|
|||
|
|
usort(self::$alerts, function($a, $b) {
|
|||
|
|
return $a['timestamp'] <=> $b['timestamp'];
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
self::$alerts = array_slice(self::$alerts, -self::$config['max_alerts']);
|
|||
|
|
}
|
|||
|
|
}
|