Files
FendxPHP/fendx-framework/fendx-monitor/src/Alert/AlertManager.php
Lawson 2782d765fb feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
2026-04-08 17:00:28 +08:00

409 lines
13 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
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']);
}
}