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:
629
fendx-framework/fendx-service/src/Health/HealthChecker.php
Normal file
629
fendx-framework/fendx-service/src/Health/HealthChecker.php
Normal 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
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user