feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -0,0 +1,629 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\Health;
use Fendx\Service\Health\Checker\HttpHealthChecker;
use Fendx\Service\Health\Checker\TcpHealthChecker;
use Fendx\Service\Health\Checker\CustomHealthChecker;
use Fendx\Service\Health\Storage\HealthStorage;
use Fendx\Service\Health\Notifier\HealthNotifier;
class HealthChecker
{
protected HttpHealthChecker $httpChecker;
protected TcpHealthChecker $tcpChecker;
protected CustomHealthChecker $customChecker;
protected HealthStorage $storage;
protected HealthNotifier $notifier;
protected array $config = [];
protected array $monitoredServices = [];
protected array $healthStatuses = [];
protected array $checkHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->httpChecker = new HttpHealthChecker($this->config);
$this->tcpChecker = new TcpHealthChecker($this->config);
$this->customChecker = new CustomHealthChecker($this->config);
$this->storage = new HealthStorage($this->config);
$this->notifier = new HealthNotifier($this->config);
$this->initialize();
}
/**
* Start monitoring a service.
*/
public function startMonitoring(string $serviceId, array $healthConfig): void
{
$this->monitoredServices[$serviceId] = [
'id' => $serviceId,
'config' => $healthConfig,
'status' => 'unknown',
'last_check' => null,
'last_success' => null,
'last_failure' => null,
'consecutive_failures' => 0,
'consecutive_successes' => 0,
'total_checks' => 0,
'total_failures' => 0,
'total_successes' => 0,
'created_at' => time(),
'updated_at' => time()
];
// Perform initial health check
$this->checkHealth($serviceId);
$this->logInfo("Started health monitoring for service: {$serviceId}");
}
/**
* Stop monitoring a service.
*/
public function stopMonitoring(string $serviceId): void
{
if (isset($this->monitoredServices[$serviceId])) {
unset($this->monitoredServices[$serviceId]);
unset($this->healthStatuses[$serviceId]);
$this->logInfo("Stopped health monitoring for service: {$serviceId}");
}
}
/**
* Check health of a specific service.
*/
public function checkHealth(string $serviceId): array
{
if (!isset($this->monitoredServices[$serviceId])) {
throw new \InvalidArgumentException("Service not being monitored: {$serviceId}");
}
$service = &$this->monitoredServices[$serviceId];
$config = $service['config'];
$startTime = microtime(true);
$result = $this->performHealthCheck($config);
$duration = microtime(true) - $startTime;
// Update service statistics
$service['last_check'] = time();
$service['total_checks']++;
$service['updated_at'] = time();
if ($result['healthy']) {
$service['status'] = 'healthy';
$service['last_success'] = time();
$service['consecutive_successes']++;
$service['consecutive_failures'] = 0;
$service['total_successes']++;
} else {
$service['status'] = 'unhealthy';
$service['last_failure'] = time();
$service['consecutive_failures']++;
$service['consecutive_successes'] = 0;
$service['total_failures']++;
}
// Store health status
$this->healthStatuses[$serviceId] = array_merge($result, [
'service_id' => $serviceId,
'check_duration' => $duration,
'timestamp' => time()
]);
// Store in persistent storage
$this->storage->storeHealthCheck($serviceId, $this->healthStatuses[$serviceId]);
// Add to history
$this->addToHistory($serviceId, $this->healthStatuses[$serviceId]);
// Send notification if needed
$this->handleHealthChange($serviceId, $service, $result);
return $this->healthStatuses[$serviceId];
}
/**
* Check health of all monitored services.
*/
public function checkAllHealth(): array
{
$results = [];
foreach (array_keys($this->monitoredServices) as $serviceId) {
try {
$results[$serviceId] = $this->checkHealth($serviceId);
} catch (\Exception $e) {
$results[$serviceId] = [
'service_id' => $serviceId,
'healthy' => false,
'error' => $e->getMessage(),
'timestamp' => time()
];
}
}
return $results;
}
/**
* Get health status of a service.
*/
public function getHealthStatus(string $serviceId): ?array
{
return $this->healthStatuses[$serviceId] ?? null;
}
/**
* Check if service is healthy.
*/
public function isHealthy(string $serviceId): bool
{
$status = $this->getHealthStatus($serviceId);
if (!$status) {
return false;
}
return $status['healthy'] ?? false;
}
/**
* Get all monitored services.
*/
public function getMonitoredServices(): array
{
return $this->monitoredServices;
}
/**
* Get service health statistics.
*/
public function getServiceStatistics(string $serviceId): array
{
if (!isset($this->monitoredServices[$serviceId])) {
return [];
}
$service = $this->monitoredServices[$serviceId];
$uptime = $this->calculateUptime($serviceId);
return [
'service_id' => $serviceId,
'status' => $service['status'],
'total_checks' => $service['total_checks'],
'total_successes' => $service['total_successes'],
'total_failures' => $service['total_failures'],
'success_rate' => $service['total_checks'] > 0 ?
($service['total_successes'] / $service['total_checks']) * 100 : 0,
'consecutive_successes' => $service['consecutive_successes'],
'consecutive_failures' => $service['consecutive_failures'],
'last_check' => $service['last_check'],
'last_success' => $service['last_success'],
'last_failure' => $service['last_failure'],
'uptime_percentage' => $uptime,
'monitored_since' => $service['created_at']
];
}
/**
* Get overall health statistics.
*/
public function getOverallStatistics(): array
{
$totalServices = count($this->monitoredServices);
$healthyServices = 0;
$unhealthyServices = 0;
$totalChecks = 0;
$totalSuccesses = 0;
$totalFailures = 0;
foreach ($this->monitoredServices as $service) {
if ($service['status'] === 'healthy') {
$healthyServices++;
} else {
$unhealthyServices++;
}
$totalChecks += $service['total_checks'];
$totalSuccesses += $service['total_successes'];
$totalFailures += $service['total_failures'];
}
return [
'total_services' => $totalServices,
'healthy_services' => $healthyServices,
'unhealthy_services' => $unhealthyServices,
'unknown_services' => $totalServices - $healthyServices - $unhealthyServices,
'health_percentage' => $totalServices > 0 ? ($healthyServices / $totalServices) * 100 : 0,
'total_checks' => $totalChecks,
'total_successes' => $totalSuccesses,
'total_failures' => $totalFailures,
'overall_success_rate' => $totalChecks > 0 ?
($totalSuccesses / $totalChecks) * 100 : 0
];
}
/**
* Get health history for a service.
*/
public function getHealthHistory(string $serviceId, int $limit = 100): array
{
if (!isset($this->checkHistory[$serviceId])) {
return [];
}
$history = $this->checkHistory[$serviceId];
usort($history, function ($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return array_slice($history, 0, $limit);
}
/**
* Get services with consecutive failures.
*/
public function getServicesWithFailures(int $threshold = 3): array
{
$services = [];
foreach ($this->monitoredServices as $serviceId => $service) {
if ($service['consecutive_failures'] >= $threshold) {
$services[$serviceId] = $service;
}
}
return $services;
}
/**
* Get services that need attention.
*/
public function getServicesNeedingAttention(): array
{
$services = [];
$failureThreshold = $this->config['failure_threshold'] ?? 3;
foreach ($this->monitoredServices as $serviceId => $service) {
$needsAttention = false;
$reason = [];
if ($service['consecutive_failures'] >= $failureThreshold) {
$needsAttention = true;
$reason[] = "Consecutive failures: {$service['consecutive_failures']}";
}
if ($service['status'] === 'unhealthy') {
$needsAttention = true;
$reason[] = "Currently unhealthy";
}
// Check if service hasn't been checked recently
$timeSinceLastCheck = time() - $service['last_check'];
$staleThreshold = $this->config['stale_threshold'] ?? 300;
if ($timeSinceLastCheck > $staleThreshold) {
$needsAttention = true;
$reason[] = "Last check was {$timeSinceLastCheck} seconds ago";
}
if ($needsAttention) {
$services[$serviceId] = array_merge($service, [
'attention_reasons' => $reason
]);
}
}
return $services;
}
/**
* Reset service statistics.
*/
public function resetStatistics(string $serviceId): bool
{
if (!isset($this->monitoredServices[$serviceId])) {
return false;
}
$service = &$this->monitoredServices[$serviceId];
$service['consecutive_failures'] = 0;
$service['consecutive_successes'] = 0;
$service['total_checks'] = 0;
$service['total_failures'] = 0;
$service['total_successes'] = 0;
$service['updated_at'] = time();
// Clear history
unset($this->checkHistory[$serviceId]);
$this->logInfo("Reset statistics for service: {$serviceId}");
return true;
}
/**
* Enable/disable service monitoring.
*/
public function setMonitoringEnabled(string $serviceId, bool $enabled): bool
{
if (!isset($this->monitoredServices[$serviceId])) {
return false;
}
$this->monitoredServices[$serviceId]['enabled'] = $enabled;
$this->monitoredServices[$serviceId]['updated_at'] = time();
$this->logInfo("Monitoring " . ($enabled ? 'enabled' : 'disabled') . " for service: {$serviceId}");
return true;
}
/**
* Update health check configuration.
*/
public function updateHealthConfig(string $serviceId, array $config): bool
{
if (!isset($this->monitoredServices[$serviceId])) {
return false;
}
$this->monitoredServices[$serviceId]['config'] = array_merge(
$this->monitoredServices[$serviceId]['config'],
$config
);
$this->monitoredServices[$serviceId]['updated_at'] = time();
// Perform health check with new config
$this->checkHealth($serviceId);
$this->logInfo("Updated health config for service: {$serviceId}");
return true;
}
/**
* Perform health check based on configuration.
*/
protected function performHealthCheck(array $config): array
{
$type = $config['type'] ?? 'http';
switch ($type) {
case 'http':
return $this->httpChecker->check($config);
case 'tcp':
return $this->tcpChecker->check($config);
case 'custom':
return $this->customChecker->check($config);
default:
throw new \InvalidArgumentException("Unsupported health check type: {$type}");
}
}
/**
* Add check to history.
*/
protected function addToHistory(string $serviceId, array $result): void
{
if (!isset($this->checkHistory[$serviceId])) {
$this->checkHistory[$serviceId] = [];
}
$this->checkHistory[$serviceId][] = $result;
// Limit history size
$maxHistory = $this->config['max_history'] ?? 1000;
if (count($this->checkHistory[$serviceId]) > $maxHistory) {
$this->checkHistory[$serviceId] = array_slice(
$this->checkHistory[$serviceId],
-$maxHistory
);
}
}
/**
* Handle health status changes.
*/
protected function handleHealthChange(string $serviceId, array $service, array $result): void
{
$previousStatus = $service['status'];
$currentStatus = $result['healthy'] ? 'healthy' : 'unhealthy';
// Status changed
if ($previousStatus !== $currentStatus) {
$this->logInfo("Service {$serviceId} status changed: {$previousStatus} -> {$currentStatus}");
// Send notification
$this->notifier->notifyStatusChange($serviceId, $previousStatus, $currentStatus, $result);
}
// Check for consecutive failures threshold
$failureThreshold = $this->config['failure_threshold'] ?? 3;
if ($service['consecutive_failures'] === $failureThreshold) {
$this->notifier->notifyFailureThreshold($serviceId, $failureThreshold, $result);
}
// Check for recovery
$recoveryThreshold = $this->config['recovery_threshold'] ?? 3;
if ($service['consecutive_successes'] === $recoveryThreshold &&
$service['consecutive_failures'] > 0) {
$this->notifier->notifyRecovery($serviceId, $service['consecutive_failures'], $result);
}
}
/**
* Calculate service uptime.
*/
protected function calculateUptime(string $serviceId): float
{
if (!isset($this->checkHistory[$serviceId]) || empty($this->checkHistory[$serviceId])) {
return 0.0;
}
$history = $this->checkHistory[$serviceId];
$healthyCount = 0;
foreach ($history as $check) {
if ($check['healthy']) {
$healthyCount++;
}
}
return ($healthyCount / count($history)) * 100;
}
/**
* Initialize health checker.
*/
protected function initialize(): void
{
// Load existing services from storage
$this->loadMonitoredServices();
// Start periodic health checks
if ($this->config['periodic_checks']) {
$this->startPeriodicChecks();
}
$this->logInfo("Health checker initialized");
}
/**
* Load monitored services from storage.
*/
protected function loadMonitoredServices(): void
{
$services = $this->storage->loadMonitoredServices();
foreach ($services as $service) {
$this->monitoredServices[$service['id']] = $service;
}
$this->logInfo("Loaded " . count($services) . " monitored services from storage");
}
/**
* Start periodic health checks.
*/
protected function startPeriodicChecks(): void
{
// This would typically be run as a background process
// For now, we'll just log that it would start
$this->logInfo("Periodic health checks started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[HealthChecker] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'periodic_checks' => true,
'check_interval' => 30,
'failure_threshold' => 3,
'recovery_threshold' => 3,
'stale_threshold' => 300,
'max_history' => 1000,
'logging_enabled' => true,
'notifications' => [
'enabled' => true,
'channels' => ['email', 'slack'],
'on_status_change' => true,
'on_failure_threshold' => true,
'on_recovery' => true
],
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/health'
],
'http' => [
'timeout' => 5,
'follow_redirects' => true,
'verify_ssl' => true
],
'tcp' => [
'timeout' => 5
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create health checker instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'periodic_checks' => false,
'check_interval' => 10,
'failure_threshold' => 2,
'logging_enabled' => true,
'notifications' => [
'enabled' => false
]
]);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'periodic_checks' => true,
'check_interval' => 30,
'failure_threshold' => 3,
'recovery_threshold' => 3,
'stale_threshold' => 180,
'logging_enabled' => false,
'notifications' => [
'enabled' => true,
'channels' => ['email', 'slack', 'webhook']
],
'storage' => [
'type' => 'redis',
'host' => 'localhost',
'port' => 6379
]
]);
}
}