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