mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
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']);
|
||
}
|
||
}
|