Files
FendxPHP/fendx-framework/fendx-monitor/src/Alert/AlertManager.php

409 lines
13 KiB
PHP
Raw Normal View History

<?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']);
}
}