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,577 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker;
use Fendx\Service\CircuitBreaker\State\CircuitState;
use Fendx\Service\CircuitBreaker\State\ClosedState;
use Fendx\Service\CircuitBreaker\State\OpenState;
use Fendx\Service\CircuitBreaker\State\HalfOpenState;
use Fendx\Service\CircuitBreaker\Storage\CircuitStorage;
use Fendx\Service\CircuitBreaker\Monitor\CircuitMonitor;
class CircuitBreaker
{
protected string $name;
protected array $config;
protected CircuitState $state;
protected CircuitStorage $storage;
protected CircuitMonitor $monitor;
protected array $statistics = [];
protected float $lastStateChange = 0;
protected int $failureCount = 0;
protected int $successCount = 0;
protected float $lastFailureTime = 0;
public function __construct(string $name, array $config = [])
{
$this->name = $name;
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->storage = new CircuitStorage($this->config);
$this->monitor = new CircuitMonitor($this->config);
$this->initialize();
}
/**
* Execute function with circuit breaker protection.
*/
public function call(callable $function, ...$args)
{
if (!$this->canExecute()) {
throw new CircuitBreakerOpenException("Circuit breaker '{$this->name}' is open");
}
$startTime = microtime(true);
try {
$result = $function(...$args);
$duration = microtime(true) - $startTime;
$this->onSuccess($duration);
return $result;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
$this->onFailure($e, $duration);
throw $e;
}
}
/**
* Execute function with fallback.
*/
public function callWithFallback(callable $function, callable $fallback, ...$args)
{
try {
return $this->call($function, ...$args);
} catch (CircuitBreakerOpenException $e) {
return $fallback(...$args);
}
}
/**
* Check if circuit breaker allows execution.
*/
public function canExecute(): bool
{
return $this->state->canExecute();
}
/**
* Get current state.
*/
public function getState(): string
{
return $this->state->getName();
}
/**
* Get circuit breaker name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Force open circuit breaker.
*/
public function forceOpen(): void
{
$this->transitionTo(new OpenState($this->config));
$this->logInfo("Circuit breaker '{$this->name}' forced open");
}
/**
* Force close circuit breaker.
*/
public function forceClose(): void
{
$this->transitionTo(new ClosedState($this->config));
$this->logInfo("Circuit breaker '{$this->name}' forced closed");
}
/**
* Reset circuit breaker to initial state.
*/
public function reset(): void
{
$this->failureCount = 0;
$this->successCount = 0;
$this->lastFailureTime = 0;
$this->statistics = [];
$this->transitionTo(new ClosedState($this->config));
$this->logInfo("Circuit breaker '{$this->name}' reset");
}
/**
* Get circuit breaker statistics.
*/
public function getStatistics(): array
{
return array_merge($this->statistics, [
'name' => $this->name,
'state' => $this->getState(),
'failure_count' => $this->failureCount,
'success_count' => $this->successCount,
'last_failure_time' => $this->lastFailureTime,
'last_state_change' => $this->lastStateChange,
'uptime_percentage' => $this->calculateUptimePercentage(),
'average_response_time' => $this->calculateAverageResponseTime(),
'error_rate' => $this->calculateErrorRate()
]);
}
/**
* Get detailed status.
*/
public function getStatus(): array
{
return [
'name' => $this->name,
'state' => $this->getState(),
'config' => $this->config,
'statistics' => $this->getStatistics(),
'state_details' => $this->state->getDetails(),
'monitoring' => $this->monitor->getStatus()
];
}
/**
* Update configuration.
*/
public function updateConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
$this->state->updateConfig($this->config);
$this->logInfo("Configuration updated for circuit breaker '{$this->name}'");
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Enable/disable circuit breaker.
*/
public function setEnabled(bool $enabled): void
{
$this->config['enabled'] = $enabled;
if (!$enabled) {
$this->forceClose();
}
$this->logInfo("Circuit breaker '{$this->name}' " . ($enabled ? 'enabled' : 'disabled'));
}
/**
* Check if circuit breaker is enabled.
*/
public function isEnabled(): bool
{
return $this->config['enabled'] ?? true;
}
/**
* Handle successful execution.
*/
protected function onSuccess(float $duration): void
{
$this->successCount++;
$this->recordExecution(true, $duration);
$this->state->onSuccess();
$this->monitor->recordSuccess($duration);
// Persist state
$this->persistState();
}
/**
* Handle failed execution.
*/
protected function onFailure(\Exception $exception, float $duration): void
{
$this->failureCount++;
$this->lastFailureTime = microtime(true);
$this->recordExecution(false, $duration, $exception);
$this->state->onFailure();
$this->monitor->recordFailure($exception, $duration);
// Persist state
$this->persistState();
}
/**
* Transition to new state.
*/
protected function transitionTo(CircuitState $newState): void
{
$previousState = $this->getState();
$this->state = $newState;
$this->lastStateChange = microtime(true);
// Record state transition
$this->recordStateTransition($previousState, $newState->getName());
// Notify monitor
$this->monitor->recordStateTransition($previousState, $newState->getName());
$this->logInfo("State transition: {$previousState} -> {$newState->getName()}");
}
/**
* Record execution statistics.
*/
protected function recordExecution(bool $success, float $duration, \Exception $exception = null): void
{
$timestamp = microtime(true);
$this->statistics['executions'][] = [
'timestamp' => $timestamp,
'success' => $success,
'duration' => $duration,
'exception' => $exception ? [
'class' => get_class($exception),
'message' => $exception->getMessage()
] : null
];
// Limit execution history
$maxHistory = $this->config['max_execution_history'] ?? 1000;
if (count($this->statistics['executions']) > $maxHistory) {
$this->statistics['executions'] = array_slice(
$this->statistics['executions'],
-$maxHistory
);
}
// Update aggregates
$this->updateAggregates();
}
/**
* Record state transition.
*/
protected function recordStateTransition(string $from, string $to): void
{
if (!isset($this->statistics['state_transitions'])) {
$this->statistics['state_transitions'] = [];
}
$this->statistics['state_transitions'][] = [
'timestamp' => microtime(true),
'from' => $from,
'to' => $to
];
// Limit transition history
$maxHistory = $this->config['max_transition_history'] ?? 100;
if (count($this->statistics['state_transitions']) > $maxHistory) {
$this->statistics['state_transitions'] = array_slice(
$this->statistics['state_transitions'],
-$maxHistory
);
}
}
/**
* Update aggregate statistics.
*/
protected function updateAggregates(): void
{
$executions = $this->statistics['executions'] ?? [];
if (empty($executions)) {
return;
}
$recentExecutions = array_slice($executions, -$this->config['window_size']);
$totalDuration = 0;
$successCount = 0;
foreach ($recentExecutions as $execution) {
$totalDuration += $execution['duration'];
if ($execution['success']) {
$successCount++;
}
}
$this->statistics['recent_average_duration'] = $totalDuration / count($recentExecutions);
$this->statistics['recent_success_rate'] = ($successCount / count($recentExecutions)) * 100;
$this->statistics['recent_error_rate'] = 100 - $this->statistics['recent_success_rate'];
}
/**
* Calculate uptime percentage.
*/
protected function calculateUptimePercentage(): float
{
$executions = $this->statistics['executions'] ?? [];
if (empty($executions)) {
return 100.0;
}
$successCount = 0;
foreach ($executions as $execution) {
if ($execution['success']) {
$successCount++;
}
}
return ($successCount / count($executions)) * 100;
}
/**
* Calculate average response time.
*/
protected function calculateAverageResponseTime(): float
{
$executions = $this->statistics['executions'] ?? [];
if (empty($executions)) {
return 0.0;
}
$totalDuration = 0;
foreach ($executions as $execution) {
$totalDuration += $execution['duration'];
}
return $totalDuration / count($executions);
}
/**
* Calculate error rate.
*/
protected function calculateErrorRate(): float
{
return 100.0 - $this->calculateUptimePercentage();
}
/**
* Persist circuit breaker state.
*/
protected function persistState(): void
{
if (!$this->config['persist_state']) {
return;
}
$state = [
'name' => $this->name,
'state' => $this->getState(),
'failure_count' => $this->failureCount,
'success_count' => $this->successCount,
'last_failure_time' => $this->lastFailureTime,
'last_state_change' => $this->lastStateChange,
'statistics' => $this->statistics
];
$this->storage->save($this->name, $state);
}
/**
* Load circuit breaker state.
*/
protected function loadState(): void
{
if (!$this->config['persist_state']) {
return;
}
$state = $this->storage->load($this->name);
if (!$state) {
return;
}
$this->failureCount = $state['failure_count'] ?? 0;
$this->successCount = $state['success_count'] ?? 0;
$this->lastFailureTime = $state['last_failure_time'] ?? 0;
$this->lastStateChange = $state['last_state_change'] ?? 0;
$this->statistics = $state['statistics'] ?? [];
// Restore state
switch ($state['state']) {
case 'open':
$this->state = new OpenState($this->config);
break;
case 'half_open':
$this->state = new HalfOpenState($this->config);
break;
default:
$this->state = new ClosedState($this->config);
break;
}
$this->logInfo("State loaded for circuit breaker '{$this->name}': {$state['state']}");
}
/**
* Initialize circuit breaker.
*/
protected function initialize(): void
{
// Load persisted state
$this->loadState();
// Initialize state if not loaded
if (!isset($this->state)) {
$this->state = new ClosedState($this->config);
$this->lastStateChange = microtime(true);
}
// Start monitoring
$this->monitor->start($this->name);
$this->logInfo("Circuit breaker '{$this->name}' initialized in {$this->getState()} state");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CircuitBreaker:{$this->name}] {$message}");
}
}
/**
* Log warning message.
*/
protected function logWarning(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CircuitBreaker:{$this->name}] WARNING: {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'failure_threshold' => 5,
'success_threshold' => 3,
'timeout' => 60,
'half_open_max_calls' => 3,
'window_size' => 100,
'enabled' => true,
'persist_state' => true,
'logging_enabled' => true,
'max_execution_history' => 1000,
'max_transition_history' => 100,
'monitoring' => [
'enabled' => true,
'metrics_interval' => 60
],
'storage' => [
'type' => 'file',
'path' => __DIR__ . '/../../../storage/circuit_breaker'
]
];
}
/**
* Create circuit breaker instance.
*/
public static function create(string $name, array $config = []): self
{
return new self($name, $config);
}
/**
* Create for high availability.
*/
public static function forHighAvailability(string $name): self
{
return new self($name, [
'failure_threshold' => 3,
'success_threshold' => 2,
'timeout' => 30,
'half_open_max_calls' => 5,
'window_size' => 50
]);
}
/**
* Create for development.
*/
public static function forDevelopment(string $name): self
{
return new self($name, [
'failure_threshold' => 10,
'success_threshold' => 5,
'timeout' => 120,
'persist_state' => false,
'logging_enabled' => true
]);
}
/**
* Create for testing.
*/
public static function forTesting(string $name): self
{
return new self($name, [
'failure_threshold' => 2,
'success_threshold' => 1,
'timeout' => 5,
'persist_state' => false,
'logging_enabled' => false
]);
}
}
/**
* Circuit breaker open exception.
*/
class CircuitBreakerOpenException extends \Exception
{
public function __construct(string $message = "Circuit breaker is open", int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,662 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker\Detector;
use Fendx\Service\CircuitBreaker\Detector\Strategy\ErrorRateStrategy;
use Fendx\Service\CircuitBreaker\Detector\Strategy\ResponseTimeStrategy;
use Fendx\Service\CircuitBreaker\Detector\Strategy\TimeoutStrategy;
use Fendx\Service\CircuitBreaker\Detector\Strategy\CustomStrategy;
class FailureDetector
{
protected array $strategies = [];
protected array $config = [];
protected array $metrics = [];
protected array $detectionHistory = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->initializeStrategies();
}
/**
* Detect failure based on metrics.
*/
public function detectFailure(array $metrics): array
{
$results = [];
$overallFailure = false;
$failureReasons = [];
foreach ($this->strategies as $name => $strategy) {
$strategyResult = $strategy->detect($metrics);
$results[$name] = $strategyResult;
if ($strategyResult['failure']) {
$overallFailure = true;
$failureReasons[] = $strategyResult['reason'];
}
}
$detection = [
'timestamp' => microtime(true),
'failure' => $overallFailure,
'reasons' => $failureReasons,
'strategies' => $results,
'metrics' => $metrics
];
// Record detection
$this->recordDetection($detection);
return $detection;
}
/**
* Add custom detection strategy.
*/
public function addStrategy(string $name, callable $detector): void
{
$this->strategies[$name] = new CustomStrategy($detector);
$this->logInfo("Added custom detection strategy: {$name}");
}
/**
* Remove detection strategy.
*/
public function removeStrategy(string $name): bool
{
if (!isset($this->strategies[$name])) {
return false;
}
unset($this->strategies[$name]);
$this->logInfo("Removed detection strategy: {$name}");
return true;
}
/**
* Get all strategies.
*/
public function getStrategies(): array
{
return array_keys($this->strategies);
}
/**
* Enable/disable strategy.
*/
public function setStrategyEnabled(string $name, bool $enabled): bool
{
if (!isset($this->strategies[$name])) {
return false;
}
$this->strategies[$name]->setEnabled($enabled);
$this->logInfo("Strategy '{$name}' " . ($enabled ? 'enabled' : 'disabled'));
return true;
}
/**
* Update strategy configuration.
*/
public function updateStrategyConfig(string $name, array $config): bool
{
if (!isset($this->strategies[$name])) {
return false;
}
$this->strategies[$name]->updateConfig($config);
$this->logInfo("Configuration updated for strategy: {$name}");
return true;
}
/**
* Record metrics for analysis.
*/
public function recordMetrics(array $metrics): void
{
$timestamp = microtime(true);
$this->metrics[] = array_merge($metrics, [
'timestamp' => $timestamp
]);
// Limit metrics history
$maxHistory = $this->config['max_metrics_history'] ?? 10000;
if (count($this->metrics) > $maxHistory) {
$this->metrics = array_slice($this->metrics, -$maxHistory);
}
}
/**
* Get recent metrics.
*/
public function getRecentMetrics(int $windowSize = null): array
{
$windowSize = $windowSize ?? $this->config['window_size'] ?? 100;
return array_slice($this->metrics, -$windowSize);
}
/**
* Get metrics in time range.
*/
public function getMetricsInRange(float $startTime, float $endTime): array
{
return array_filter($this->metrics, function($metric) use ($startTime, $endTime) {
return $metric['timestamp'] >= $startTime && $metric['timestamp'] <= $endTime;
});
}
/**
* Calculate aggregated metrics.
*/
public function calculateAggregates(array $metrics = null): array
{
$metrics = $metrics ?? $this->getRecentMetrics();
if (empty($metrics)) {
return [
'count' => 0,
'success_rate' => 0,
'error_rate' => 0,
'average_response_time' => 0,
'min_response_time' => 0,
'max_response_time' => 0,
'timeout_rate' => 0
];
}
$totalCount = count($metrics);
$successCount = 0;
$errorCount = 0;
$timeoutCount = 0;
$totalResponseTime = 0;
$minResponseTime = PHP_FLOAT_MAX;
$maxResponseTime = 0;
foreach ($metrics as $metric) {
$success = $metric['success'] ?? false;
$responseTime = $metric['response_time'] ?? 0;
$timeout = $metric['timeout'] ?? false;
if ($success) {
$successCount++;
} else {
$errorCount++;
}
if ($timeout) {
$timeoutCount++;
}
$totalResponseTime += $responseTime;
$minResponseTime = min($minResponseTime, $responseTime);
$maxResponseTime = max($maxResponseTime, $responseTime);
}
return [
'count' => $totalCount,
'success_rate' => ($successCount / $totalCount) * 100,
'error_rate' => ($errorCount / $totalCount) * 100,
'timeout_rate' => ($timeoutCount / $totalCount) * 100,
'average_response_time' => $totalResponseTime / $totalCount,
'min_response_time' => $minResponseTime === PHP_FLOAT_MAX ? 0 : $minResponseTime,
'max_response_time' => $maxResponseTime,
'success_count' => $successCount,
'error_count' => $errorCount,
'timeout_count' => $timeoutCount
];
}
/**
* Get detection statistics.
*/
public function getStatistics(): array
{
$aggregates = $this->calculateAggregates();
$detectionStats = [
'total_detections' => count($this->detectionHistory),
'failure_detections' => 0,
'strategy_performance' => []
];
foreach ($this->detectionHistory as $detection) {
if ($detection['failure']) {
$detectionStats['failure_detections']++;
}
foreach ($detection['strategies'] as $strategyName => $result) {
if (!isset($detectionStats['strategy_performance'][$strategyName])) {
$detectionStats['strategy_performance'][$strategyName] = [
'detections' => 0,
'failures' => 0
];
}
$detectionStats['strategy_performance'][$strategyName]['detections']++;
if ($result['failure']) {
$detectionStats['strategy_performance'][$strategyName]['failures']++;
}
}
}
// Calculate strategy effectiveness
foreach ($detectionStats['strategy_performance'] as $strategyName => &$stats) {
$stats['failure_rate'] = $stats['detections'] > 0 ?
($stats['failures'] / $stats['detections']) * 100 : 0;
}
return array_merge($aggregates, $detectionStats);
}
/**
* Get detection history.
*/
public function getDetectionHistory(int $limit = 100): array
{
return array_slice($this->detectionHistory, -$limit);
}
/**
* Analyze failure patterns.
*/
public function analyzeFailurePatterns(): array
{
$patterns = [];
// Time-based patterns
$hourlyFailures = [];
$dailyFailures = [];
foreach ($this->detectionHistory as $detection) {
if (!$detection['failure']) {
continue;
}
$timestamp = $detection['timestamp'];
$hour = date('H', (int) $timestamp);
$day = date('Y-m-d', (int) $timestamp);
$hourlyFailures[$hour] = ($hourlyFailures[$hour] ?? 0) + 1;
$dailyFailures[$day] = ($dailyFailures[$day] ?? 0) + 1;
}
// Strategy correlation
$strategyCorrelations = [];
foreach ($this->detectionHistory as $detection) {
foreach ($detection['strategies'] as $strategyName => $result) {
if (!isset($strategyCorrelations[$strategyName])) {
$strategyCorrelations[$strategyName] = [
'total' => 0,
'failures' => 0
];
}
$strategyCorrelations[$strategyName]['total']++;
if ($result['failure']) {
$strategyCorrelations[$strategyName]['failures']++;
}
}
}
// Calculate correlation rates
foreach ($strategyCorrelations as $strategyName => &$correlation) {
$correlation['correlation_rate'] = $correlation['total'] > 0 ?
($correlation['failures'] / $correlation['total']) * 100 : 0;
}
return [
'hourly_pattern' => $hourlyFailures,
'daily_pattern' => $dailyFailures,
'strategy_correlations' => $strategyCorrelations,
'peak_failure_hour' => $this->findPeakHour($hourlyFailures),
'most_correlated_strategy' => $this->findMostCorrelatedStrategy($strategyCorrelations)
];
}
/**
* Predict failure probability.
*/
public function predictFailureProbability(): float
{
$recentMetrics = $this->getRecentMetrics($this->config['prediction_window'] ?? 50);
if (count($recentMetrics) < $this->config['min_prediction_samples'] ?? 10) {
return 0.0;
}
$aggregates = $this->calculateAggregates($recentMetrics);
// Simple probability calculation based on recent trends
$errorRate = $aggregates['error_rate'];
$timeoutRate = $aggregates['timeout_rate'];
// Weight factors
$errorWeight = $this->config['error_rate_weight'] ?? 0.6;
$timeoutWeight = $this->config['timeout_rate_weight'] ?? 0.4;
$probability = ($errorRate * $errorWeight) + ($timeoutRate * $timeoutWeight);
// Apply trend analysis
$trendFactor = $this->calculateTrendFactor($recentMetrics);
$probability *= $trendFactor;
return min(100.0, max(0.0, $probability));
}
/**
* Get health score.
*/
public function getHealthScore(): float
{
$failureProbability = $this->predictFailureProbability();
// Convert failure probability to health score (0-100)
$healthScore = 100.0 - $failureProbability;
// Apply stability factor
$stabilityFactor = $this->calculateStabilityFactor();
$healthScore *= $stabilityFactor;
return min(100.0, max(0.0, $healthScore));
}
/**
* Reset detector state.
*/
public function reset(): void
{
$this->metrics = [];
$this->detectionHistory = [];
$this->logInfo("Failure detector reset");
}
/**
* Export detector configuration and data.
*/
public function exportData(): array
{
return [
'config' => $this->config,
'strategies' => $this->getStrategyConfigs(),
'metrics' => $this->metrics,
'detection_history' => $this->detectionHistory,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s')
];
}
/**
* Record detection result.
*/
protected function recordDetection(array $detection): void
{
$this->detectionHistory[] = $detection;
// Limit history size
$maxHistory = $this->config['max_detection_history'] ?? 1000;
if (count($this->detectionHistory) > $maxHistory) {
$this->detectionHistory = array_slice($this->detectionHistory, -$maxHistory);
}
}
/**
* Initialize built-in strategies.
*/
protected function initializeStrategies(): void
{
if ($this->config['strategies']['error_rate']['enabled'] ?? true) {
$this->strategies['error_rate'] = new ErrorRateStrategy(
$this->config['strategies']['error_rate'] ?? []
);
}
if ($this->config['strategies']['response_time']['enabled'] ?? true) {
$this->strategies['response_time'] = new ResponseTimeStrategy(
$this->config['strategies']['response_time'] ?? []
);
}
if ($this->config['strategies']['timeout']['enabled'] ?? true) {
$this->strategies['timeout'] = new TimeoutStrategy(
$this->config['strategies']['timeout'] ?? []
);
}
}
/**
* Get strategy configurations.
*/
protected function getStrategyConfigs(): array
{
$configs = [];
foreach ($this->strategies as $name => $strategy) {
$configs[$name] = $strategy->getConfig();
}
return $configs;
}
/**
* Find peak failure hour.
*/
protected function findPeakHour(array $hourlyFailures): ?int
{
if (empty($hourlyFailures)) {
return null;
}
return array_keys($hourlyFailures, max($hourlyFailures))[0];
}
/**
* Find most correlated strategy.
*/
protected function findMostCorrelatedStrategy(array $correlations): ?string
{
if (empty($correlations)) {
return null;
}
$maxCorrelation = 0;
$mostCorrelated = null;
foreach ($correlations as $strategy => $data) {
if ($data['correlation_rate'] > $maxCorrelation) {
$maxCorrelation = $data['correlation_rate'];
$mostCorrelated = $strategy;
}
}
return $mostCorrelated;
}
/**
* Calculate trend factor.
*/
protected function calculateTrendFactor(array $recentMetrics): float
{
if (count($recentMetrics) < 10) {
return 1.0;
}
// Split into two halves
$mid = count($recentMetrics) / 2;
$firstHalf = array_slice($recentMetrics, 0, $mid);
$secondHalf = array_slice($recentMetrics, $mid);
$firstHalfAggregates = $this->calculateAggregates($firstHalf);
$secondHalfAggregates = $this->calculateAggregates($secondHalf);
// Compare error rates
$errorRateChange = $secondHalfAggregates['error_rate'] - $firstHalfAggregates['error_rate'];
// Convert to factor (1.0 = no change, >1.0 = worsening, <1.0 = improving)
$factor = 1.0 + ($errorRateChange / 100.0);
return max(0.5, min(2.0, $factor));
}
/**
* Calculate stability factor.
*/
protected function calculateStabilityFactor(): float
{
if (count($this->detectionHistory) < 10) {
return 1.0;
}
$recentDetections = array_slice($this->detectionHistory, -20);
$fluctuations = 0;
for ($i = 1; $i < count($recentDetections); $i++) {
$current = $recentDetections[$i]['failure'];
$previous = $recentDetections[$i - 1]['failure'];
if ($current !== $previous) {
$fluctuations++;
}
}
$fluctuationRate = $fluctuations / (count($recentDetections) - 1);
// Convert fluctuation rate to stability factor
return max(0.7, 1.0 - ($fluctuationRate * 0.5));
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[FailureDetector] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'window_size' => 100,
'prediction_window' => 50,
'min_prediction_samples' => 10,
'max_metrics_history' => 10000,
'max_detection_history' => 1000,
'error_rate_weight' => 0.6,
'timeout_rate_weight' => 0.4,
'logging_enabled' => true,
'strategies' => [
'error_rate' => [
'enabled' => true,
'threshold' => 50.0,
'window_size' => 20
],
'response_time' => [
'enabled' => true,
'threshold' => 5000.0,
'window_size' => 10
],
'timeout' => [
'enabled' => true,
'threshold' => 10.0,
'window_size' => 30
]
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
// Reinitialize strategies
$this->initializeStrategies();
}
/**
* Create failure detector instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'window_size' => 200,
'prediction_window' => 100,
'min_prediction_samples' => 20,
'strategies' => [
'error_rate' => [
'threshold' => 30.0,
'window_size' => 50
],
'response_time' => [
'threshold' => 2000.0,
'window_size' => 25
],
'timeout' => [
'threshold' => 5.0,
'window_size' => 40
]
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'window_size' => 50,
'prediction_window' => 25,
'min_prediction_samples' => 5,
'logging_enabled' => true,
'strategies' => [
'error_rate' => [
'threshold' => 70.0
],
'response_time' => [
'threshold' => 10000.0
]
]
]);
}
}

View File

@@ -0,0 +1,874 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker\Monitor;
use Fendx\Service\CircuitBreaker\Monitor\Collector\MetricsCollector;
use Fendx\Service\CircuitBreaker\Monitor\Analyzer\StateAnalyzer;
use Fendx\Service\CircuitBreaker\Monitor\Alert\AlertManager;
class CircuitMonitor
{
protected array $config = [];
protected MetricsCollector $collector;
protected StateAnalyzer $analyzer;
protected AlertManager $alertManager;
protected array $circuitStates = [];
protected array $monitoringData = [];
protected array $alerts = [];
protected bool $isRunning = false;
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->collector = new MetricsCollector($this->config);
$this->analyzer = new StateAnalyzer($this->config);
$this->alertManager = new AlertManager($this->config);
}
/**
* Start monitoring for a circuit breaker.
*/
public function start(string $circuitBreakerName): void
{
$this->circuitStates[$circuitBreakerName] = [
'name' => $circuitBreakerName,
'started_at' => microtime(true),
'last_update' => microtime(true),
'state_transitions' => [],
'metrics' => [],
'alerts' => []
];
if (!$this->isRunning) {
$this->isRunning = true;
$this->startMonitoringLoop();
}
$this->logInfo("Started monitoring for circuit breaker: {$circuitBreakerName}");
}
/**
* Stop monitoring for a circuit breaker.
*/
public function stop(string $circuitBreakerName): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return;
}
unset($this->circuitStates[$circuitBreakerName]);
if (empty($this->circuitStates)) {
$this->isRunning = false;
}
$this->logInfo("Stopped monitoring for circuit breaker: {$circuitBreakerName}");
}
/**
* Record state transition.
*/
public function recordStateTransition(string $circuitBreakerName, string $fromState, string $toState): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
$this->start($circuitBreakerName);
}
$transition = [
'timestamp' => microtime(true),
'from' => $fromState,
'to' => $toState,
'duration' => 0
];
$this->circuitStates[$circuitBreakerName]['state_transitions'][] = $transition;
$this->circuitStates[$circuitBreakerName]['last_update'] = microtime(true);
// Analyze transition
$this->analyzeTransition($circuitBreakerName, $transition);
// Check for alerts
$this->checkTransitionAlerts($circuitBreakerName, $transition);
$this->logInfo("State transition recorded for {$circuitBreakerName}: {$fromState} -> {$toState}");
}
/**
* Record successful execution.
*/
public function recordSuccess(string $circuitBreakerName, float $duration): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
$this->start($circuitBreakerName);
}
$metric = [
'timestamp' => microtime(true),
'type' => 'success',
'duration' => $duration
];
$this->circuitStates[$circuitBreakerName]['metrics'][] = $metric;
$this->circuitStates[$circuitBreakerName]['last_update'] = microtime(true);
// Collect metrics
$this->collector->record($circuitBreakerName, $metric);
// Check performance alerts
$this->checkPerformanceAlerts($circuitBreakerName, $metric);
}
/**
* Record failed execution.
*/
public function recordFailure(string $circuitBreakerName, \Exception $exception, float $duration): void
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
$this->start($circuitBreakerName);
}
$metric = [
'timestamp' => microtime(true),
'type' => 'failure',
'duration' => $duration,
'exception' => [
'class' => get_class($exception),
'message' => $exception->getMessage(),
'code' => $exception->getCode()
]
];
$this->circuitStates[$circuitBreakerName]['metrics'][] = $metric;
$this->circuitStates[$circuitBreakerName]['last_update'] = microtime(true);
// Collect metrics
$this->collector->record($circuitBreakerName, $metric);
// Check failure alerts
$this->checkFailureAlerts($circuitBreakerName, $metric);
}
/**
* Get monitoring status.
*/
public function getStatus(): array
{
return [
'is_running' => $this->isRunning,
'monitored_circuits' => count($this->circuitStates),
'circuit_names' => array_keys($this->circuitStates),
'total_metrics' => $this->getTotalMetricsCount(),
'total_transitions' => $this->getTotalTransitionsCount(),
'active_alerts' => count($this->alerts)
];
}
/**
* Get circuit breaker monitoring data.
*/
public function getCircuitData(string $circuitBreakerName): ?array
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return null;
}
$circuitData = $this->circuitStates[$circuitBreakerName];
// Add calculated metrics
$circuitData['calculated_metrics'] = $this->calculateCircuitMetrics($circuitBreakerName);
// Add analysis results
$circuitData['analysis'] = $this->analyzer->analyze($circuitBreakerName, $circuitData);
return $circuitData;
}
/**
* Get all circuit breaker data.
*/
public function getAllCircuitData(): array
{
$allData = [];
foreach (array_keys($this->circuitStates) as $circuitName) {
$allData[$circuitName] = $this->getCircuitData($circuitName);
}
return $allData;
}
/**
* Get monitoring dashboard data.
*/
public function getDashboardData(): array
{
$dashboard = [
'overview' => [
'total_circuits' => count($this->circuitStates),
'healthy_circuits' => 0,
'degraded_circuits' => 0,
'failed_circuits' => 0,
'total_requests' => 0,
'success_rate' => 0,
'average_response_time' => 0
],
'circuits' => [],
'alerts' => $this->getRecentAlerts(10),
'trends' => $this->getTrends()
];
foreach ($this->circuitStates as $circuitName => $circuitData) {
$metrics = $this->calculateCircuitMetrics($circuitName);
$analysis = $this->analyzer->analyze($circuitName, $circuitData);
$circuitInfo = [
'name' => $circuitName,
'status' => $analysis['health_status'],
'success_rate' => $metrics['success_rate'],
'average_response_time' => $metrics['average_response_time'],
'last_state_change' => $this->getLastStateChange($circuitName),
'uptime_percentage' => $metrics['uptime_percentage'],
'error_rate' => $metrics['error_rate']
];
$dashboard['circuits'][] = $circuitInfo;
// Update overview
switch ($analysis['health_status']) {
case 'healthy':
$dashboard['overview']['healthy_circuits']++;
break;
case 'degraded':
$dashboard['overview']['degraded_circuits']++;
break;
case 'failed':
$dashboard['overview']['failed_circuits']++;
break;
}
$dashboard['overview']['total_requests'] += $metrics['total_requests'];
}
// Calculate overview averages
if (count($this->circuitStates) > 0) {
$totalSuccessRate = 0;
$totalResponseTime = 0;
foreach ($dashboard['circuits'] as $circuit) {
$totalSuccessRate += $circuit['success_rate'];
$totalResponseTime += $circuit['average_response_time'];
}
$dashboard['overview']['success_rate'] = $totalSuccessRate / count($dashboard['circuits']);
$dashboard['overview']['average_response_time'] = $totalResponseTime / count($dashboard['circuits']);
}
return $dashboard;
}
/**
* Get recent alerts.
*/
public function getRecentAlerts(int $limit = 50): array
{
$alerts = $this->alertManager->getRecentAlerts($limit);
// Add circuit-specific alerts
foreach ($this->circuitStates as $circuitName => $circuitData) {
$circuitAlerts = $circuitData['alerts'] ?? [];
foreach ($circuitAlerts as $alert) {
$alerts[] = array_merge($alert, ['circuit' => $circuitName]);
}
}
// Sort by timestamp
usort($alerts, function($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return array_slice($alerts, 0, $limit);
}
/**
* Get trends data.
*/
public function getTrends(): array
{
$trends = [
'success_rate_trend' => [],
'response_time_trend' => [],
'error_rate_trend' => [],
'state_change_frequency' => []
];
foreach ($this->circuitStates as $circuitName => $circuitData) {
$circuitTrends = $this->calculateCircuitTrends($circuitName);
foreach ($trends as $trendType => &$trendData) {
if (isset($circuitTrends[$trendType])) {
$trendData[$circuitName] = $circuitTrends[$trendType];
}
}
}
return $trends;
}
/**
* Get performance report.
*/
public function getPerformanceReport(string $circuitBreakerName = null): array
{
if ($circuitBreakerName) {
return $this->generateCircuitReport($circuitBreakerName);
}
$report = [
'generated_at' => date('Y-m-d H:i:s'),
'period' => 'last_24_hours',
'summary' => $this->generateSummaryReport(),
'circuits' => []
];
foreach (array_keys($this->circuitStates) as $circuitName) {
$report['circuits'][$circuitName] = $this->generateCircuitReport($circuitName);
}
return $report;
}
/**
* Set alert threshold.
*/
public function setAlertThreshold(string $metric, float $value): void
{
$this->alertManager->setThreshold($metric, $value);
$this->logInfo("Alert threshold set for {$metric}: {$value}");
}
/**
* Enable/disable alert.
*/
public function setAlertEnabled(string $alertType, bool $enabled): void
{
$this->alertManager->setEnabled($alertType, $enabled);
$this->logInfo("Alert '{$alertType}' " . ($enabled ? 'enabled' : 'disabled'));
}
/**
* Clear monitoring data.
*/
public function clearData(string $circuitBreakerName = null): void
{
if ($circuitBreakerName) {
if (isset($this->circuitStates[$circuitBreakerName])) {
$this->circuitStates[$circuitBreakerName]['metrics'] = [];
$this->circuitStates[$circuitBreakerName]['state_transitions'] = [];
$this->circuitStates[$circuitBreakerName]['alerts'] = [];
}
} else {
foreach ($this->circuitStates as $circuitName => &$circuitData) {
$circuitData['metrics'] = [];
$circuitData['state_transitions'] = [];
$circuitData['alerts'] = [];
}
}
$this->logInfo("Monitoring data cleared" . ($circuitBreakerName ? " for {$circuitBreakerName}" : ""));
}
/**
* Export monitoring data.
*/
public function exportData(string $format = 'json'): string
{
$data = [
'config' => $this->config,
'circuit_states' => $this->circuitStates,
'alerts' => $this->alerts,
'dashboard' => $this->getDashboardData(),
'exported_at' => date('Y-m-d H:i:s')
];
switch ($format) {
case 'json':
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
case 'php':
return '<?php return ' . var_export($data, true) . ';';
default:
throw new \InvalidArgumentException("Unsupported export format: {$format}");
}
}
/**
* Calculate circuit metrics.
*/
protected function calculateCircuitMetrics(string $circuitBreakerName): array
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return [];
}
$metrics = $this->circuitStates[$circuitBreakerName]['metrics'];
if (empty($metrics)) {
return [
'total_requests' => 0,
'success_count' => 0,
'failure_count' => 0,
'success_rate' => 100,
'error_rate' => 0,
'average_response_time' => 0,
'min_response_time' => 0,
'max_response_time' => 0,
'uptime_percentage' => 100
];
}
$totalRequests = count($metrics);
$successCount = 0;
$failureCount = 0;
$totalResponseTime = 0;
$minResponseTime = PHP_FLOAT_MAX;
$maxResponseTime = 0;
foreach ($metrics as $metric) {
$responseTime = $metric['duration'];
if ($metric['type'] === 'success') {
$successCount++;
} else {
$failureCount++;
}
$totalResponseTime += $responseTime;
$minResponseTime = min($minResponseTime, $responseTime);
$maxResponseTime = max($maxResponseTime, $responseTime);
}
return [
'total_requests' => $totalRequests,
'success_count' => $successCount,
'failure_count' => $failureCount,
'success_rate' => ($successCount / $totalRequests) * 100,
'error_rate' => ($failureCount / $totalRequests) * 100,
'average_response_time' => $totalResponseTime / $totalRequests,
'min_response_time' => $minResponseTime === PHP_FLOAT_MAX ? 0 : $minResponseTime,
'max_response_time' => $maxResponseTime,
'uptime_percentage' => ($successCount / $totalRequests) * 100
];
}
/**
* Calculate circuit trends.
*/
protected function calculateCircuitTrends(string $circuitBreakerName): array
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return [];
}
$metrics = $this->circuitStates[$circuitBreakerName]['metrics'];
// Group metrics by time windows (e.g., hourly)
$timeWindows = [];
$windowSize = 3600; // 1 hour
foreach ($metrics as $metric) {
$window = floor($metric['timestamp'] / $windowSize) * $windowSize;
if (!isset($timeWindows[$window])) {
$timeWindows[$window] = [];
}
$timeWindows[$window][] = $metric;
}
// Calculate trends for each window
$trends = [
'success_rate_trend' => [],
'response_time_trend' => [],
'error_rate_trend' => []
];
ksort($timeWindows);
foreach ($timeWindows as $window => $windowMetrics) {
$successCount = 0;
$totalResponseTime = 0;
foreach ($windowMetrics as $metric) {
if ($metric['type'] === 'success') {
$successCount++;
}
$totalResponseTime += $metric['duration'];
}
$totalCount = count($windowMetrics);
$successRate = ($successCount / $totalCount) * 100;
$averageResponseTime = $totalResponseTime / $totalCount;
$errorRate = 100 - $successRate;
$trends['success_rate_trend'][] = [
'timestamp' => $window,
'value' => $successRate
];
$trends['response_time_trend'][] = [
'timestamp' => $window,
'value' => $averageResponseTime
];
$trends['error_rate_trend'][] = [
'timestamp' => $window,
'value' => $errorRate
];
}
// Calculate state change frequency
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
$trends['state_change_frequency'] = count($transitions);
return $trends;
}
/**
* Analyze state transition.
*/
protected function analyzeTransition(string $circuitBreakerName, array $transition): void
{
$analysis = [
'circuit' => $circuitBreakerName,
'transition' => $transition,
'analysis' => []
];
// Analyze transition patterns
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
if (count($transitions) > 1) {
$previousTransition = $transitions[count($transitions) - 2];
$timeSincePrevious = $transition['timestamp'] - $previousTransition['timestamp'];
$analysis['analysis']['time_since_previous'] = $timeSincePrevious;
// Check for rapid transitions
if ($timeSincePrevious < 60) { // Less than 1 minute
$analysis['analysis']['rapid_transition'] = true;
}
}
// Store analysis
$this->monitoringData[] = $analysis;
}
/**
* Check transition alerts.
*/
protected function checkTransitionAlerts(string $circuitBreakerName, array $transition): void
{
// Check for frequent state changes
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
if (count($transitions) >= 10) { // 10 transitions in monitoring period
$this->createAlert($circuitBreakerName, 'frequent_state_changes', [
'transition_count' => count($transitions),
'latest_transition' => $transition
]);
}
// Check for unwanted transitions (e.g., closed -> open)
if ($transition['from'] === 'closed' && $transition['to'] === 'open') {
$this->createAlert($circuitBreakerName, 'circuit_opened', [
'transition' => $transition
]);
}
}
/**
* Check performance alerts.
*/
protected function checkPerformanceAlerts(string $circuitBreakerName, array $metric): void
{
// Check response time threshold
$responseTimeThreshold = $this->config['alerts']['response_time_threshold'] ?? 5000; // 5 seconds
if ($metric['duration'] > $responseTimeThreshold) {
$this->createAlert($circuitBreakerName, 'slow_response', [
'duration' => $metric['duration'],
'threshold' => $responseTimeThreshold
]);
}
}
/**
* Check failure alerts.
*/
protected function checkFailureAlerts(string $circuitBreakerName, array $metric): void
{
// Check error rate threshold
$metrics = $this->circuitStates[$circuitBreakerName]['metrics'];
$recentMetrics = array_slice($metrics, -100); // Last 100 requests
if (count($recentMetrics) >= 10) {
$failureCount = 0;
foreach ($recentMetrics as $recentMetric) {
if ($recentMetric['type'] === 'failure') {
$failureCount++;
}
}
$errorRate = ($failureCount / count($recentMetrics)) * 100;
$errorRateThreshold = $this->config['alerts']['error_rate_threshold'] ?? 50; // 50%
if ($errorRate > $errorRateThreshold) {
$this->createAlert($circuitBreakerName, 'high_error_rate', [
'error_rate' => $errorRate,
'threshold' => $errorRateThreshold,
'sample_size' => count($recentMetrics)
]);
}
}
}
/**
* Create alert.
*/
protected function createAlert(string $circuitBreakerName, string $type, array $data): void
{
$alert = [
'id' => uniqid('alert_'),
'circuit' => $circuitBreakerName,
'type' => $type,
'timestamp' => microtime(true),
'data' => $data,
'acknowledged' => false
];
$this->alerts[] = $alert;
$this->circuitStates[$circuitBreakerName]['alerts'][] = $alert;
// Send to alert manager
$this->alertManager->process($alert);
$this->logInfo("Alert created for {$circuitBreakerName}: {$type}");
}
/**
* Get last state change.
*/
protected function getLastStateChange(string $circuitBreakerName): ?float
{
if (!isset($this->circuitStates[$circuitBreakerName])) {
return null;
}
$transitions = $this->circuitStates[$circuitBreakerName]['state_transitions'];
if (empty($transitions)) {
return null;
}
$lastTransition = end($transitions);
return $lastTransition['timestamp'];
}
/**
* Generate circuit report.
*/
protected function generateCircuitReport(string $circuitBreakerName): array
{
$circuitData = $this->getCircuitData($circuitBreakerName);
return [
'circuit' => $circuitBreakerName,
'period' => [
'start' => $circuitData['started_at'],
'end' => microtime(true)
],
'metrics' => $circuitData['calculated_metrics'],
'analysis' => $circuitData['analysis'],
'state_transitions' => $circuitData['state_transitions'],
'alerts' => $circuitData['alerts']
];
}
/**
* Generate summary report.
*/
protected function generateSummaryReport(): array
{
$summary = [
'total_circuits' => count($this->circuitStates),
'healthy_circuits' => 0,
'degraded_circuits' => 0,
'failed_circuits' => 0,
'total_requests' => 0,
'total_successes' => 0,
'total_failures' => 0,
'overall_success_rate' => 0,
'overall_average_response_time' => 0
];
foreach ($this->circuitStates as $circuitName => $circuitData) {
$metrics = $this->calculateCircuitMetrics($circuitName);
$analysis = $this->analyzer->analyze($circuitName, $circuitData);
switch ($analysis['health_status']) {
case 'healthy':
$summary['healthy_circuits']++;
break;
case 'degraded':
$summary['degraded_circuits']++;
break;
case 'failed':
$summary['failed_circuits']++;
break;
}
$summary['total_requests'] += $metrics['total_requests'];
$summary['total_successes'] += $metrics['success_count'];
$summary['total_failures'] += $metrics['failure_count'];
}
if ($summary['total_requests'] > 0) {
$summary['overall_success_rate'] = ($summary['total_successes'] / $summary['total_requests']) * 100;
}
return $summary;
}
/**
* Get total metrics count.
*/
protected function getTotalMetricsCount(): int
{
$total = 0;
foreach ($this->circuitStates as $circuitData) {
$total += count($circuitData['metrics']);
}
return $total;
}
/**
* Get total transitions count.
*/
protected function getTotalTransitionsCount(): int
{
$total = 0;
foreach ($this->circuitStates as $circuitData) {
$total += count($circuitData['state_transitions']);
}
return $total;
}
/**
* Start monitoring loop.
*/
protected function startMonitoringLoop(): void
{
// This would typically run as a background process
// For now, we'll just log that it would start
$this->logInfo("Monitoring loop started");
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[CircuitMonitor] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'metrics_retention' => 86400, // 24 hours
'alerts_retention' => 604800, // 7 days
'monitoring_interval' => 60, // 1 minute
'logging_enabled' => true,
'alerts' => [
'response_time_threshold' => 5000, // 5 seconds
'error_rate_threshold' => 50, // 50%
'state_change_threshold' => 10, // 10 transitions
'enabled' => true
],
'dashboard' => [
'refresh_interval' => 30, // 30 seconds
'trend_window_size' => 24 // 24 hours
]
];
}
/**
* 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 circuit monitor instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'metrics_retention' => 604800, // 7 days
'alerts_retention' => 2592000, // 30 days
'monitoring_interval' => 30, // 30 seconds
'logging_enabled' => false,
'alerts' => [
'response_time_threshold' => 2000, // 2 seconds
'error_rate_threshold' => 30, // 30%
'enabled' => true
]
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'metrics_retention' => 3600, // 1 hour
'alerts_retention' => 86400, // 24 hours
'monitoring_interval' => 10, // 10 seconds
'logging_enabled' => true,
'alerts' => [
'response_time_threshold' => 10000, // 10 seconds
'error_rate_threshold' => 70, // 70%
'enabled' => true
]
]);
}
}

View File

@@ -0,0 +1,718 @@
<?php
declare(strict_types=1);
namespace Fendx\Service\CircuitBreaker\Recovery;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\ExponentialBackoffStrategy;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\LinearBackoffStrategy;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\FixedIntervalStrategy;
use Fendx\Service\CircuitBreaker\Recovery\Strategy\AdaptiveStrategy;
class AutoRecovery
{
protected array $config = [];
protected array $strategies = [];
protected array $recoveryHistory = [];
protected array $activeRecoveries = [];
protected array $recoveryMetrics = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->initializeStrategies();
}
/**
* Start auto recovery for a circuit breaker.
*/
public function startRecovery(string $circuitBreakerName, array $context = []): string
{
$recoveryId = $this->generateRecoveryId($circuitBreakerName);
$recovery = [
'id' => $recoveryId,
'circuit_breaker' => $circuitBreakerName,
'strategy' => $context['strategy'] ?? $this->config['default_strategy'],
'started_at' => microtime(true),
'attempts' => 0,
'max_attempts' => $context['max_attempts'] ?? $this->config['max_attempts'],
'context' => $context,
'status' => 'active',
'next_attempt' => null,
'last_attempt' => null,
'success_count' => 0,
'failure_count' => 0
];
$this->activeRecoveries[$recoveryId] = $recovery;
// Schedule first attempt
$this->scheduleNextAttempt($recoveryId);
$this->logInfo("Started auto recovery for {$circuitBreakerName} (ID: {$recoveryId})");
return $recoveryId;
}
/**
* Stop auto recovery.
*/
public function stopRecovery(string $recoveryId): bool
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return false;
}
$recovery = $this->activeRecoveries[$recoveryId];
$recovery['status'] = 'stopped';
$recovery['stopped_at'] = microtime(true);
// Move to history
$this->recoveryHistory[] = $recovery;
unset($this->activeRecoveries[$recoveryId]);
$this->logInfo("Stopped recovery {$recoveryId} for {$recovery['circuit_breaker']}");
return true;
}
/**
* Record recovery attempt result.
*/
public function recordAttempt(string $recoveryId, bool $success, array $result = []): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$recovery['attempts']++;
$recovery['last_attempt'] = microtime(true);
if ($success) {
$recovery['success_count']++;
// Check if recovery is complete
if ($this->isRecoveryComplete($recovery)) {
$this->completeRecovery($recoveryId, $result);
} else {
// Schedule next attempt
$this->scheduleNextAttempt($recoveryId);
}
} else {
$recovery['failure_count']++;
// Check if max attempts reached
if ($recovery['attempts'] >= $recovery['max_attempts']) {
$this->failRecovery($recoveryId, $result);
} else {
// Schedule next attempt with backoff
$this->scheduleNextAttempt($recoveryId);
}
}
// Record metrics
$this->recordRecoveryMetrics($recoveryId, $success, $result);
}
/**
* Get active recoveries.
*/
public function getActiveRecoveries(): array
{
return $this->activeRecoveries;
}
/**
* Get recovery history.
*/
public function getRecoveryHistory(int $limit = 100): array
{
return array_slice($this->recoveryHistory, -$limit);
}
/**
* Get recovery by ID.
*/
public function getRecovery(string $recoveryId): ?array
{
return $this->activeRecoveries[$recoveryId] ?? null;
}
/**
* Get recoveries for circuit breaker.
*/
public function getRecoveriesForCircuit(string $circuitBreakerName): array
{
$recoveries = [];
foreach ($this->activeRecoveries as $recoveryId => $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
$recoveries[$recoveryId] = $recovery;
}
}
return $recoveries;
}
/**
* Get recovery statistics.
*/
public function getStatistics(): array
{
$stats = [
'active_recoveries' => count($this->activeRecoveries),
'total_recoveries' => count($this->recoveryHistory) + count($this->activeRecoveries),
'completed_recoveries' => 0,
'failed_recoveries' => 0,
'stopped_recoveries' => 0,
'success_rate' => 0,
'average_attempts' => 0,
'average_duration' => 0,
'strategy_performance' => []
];
$totalAttempts = 0;
$totalDuration = 0;
$completedCount = 0;
// Analyze history
foreach ($this->recoveryHistory as $recovery) {
switch ($recovery['status']) {
case 'completed':
$stats['completed_recoveries']++;
$completedCount++;
break;
case 'failed':
$stats['failed_recoveries']++;
break;
case 'stopped':
$stats['stopped_recoveries']++;
break;
}
$totalAttempts += $recovery['attempts'];
if (isset($recovery['completed_at']) || isset($recovery['failed_at'])) {
$endTime = $recovery['completed_at'] ?? $recovery['failed_at'];
$duration = $endTime - $recovery['started_at'];
$totalDuration += $duration;
}
// Strategy performance
$strategy = $recovery['strategy'];
if (!isset($stats['strategy_performance'][$strategy])) {
$stats['strategy_performance'][$strategy] = [
'total' => 0,
'completed' => 0,
'failed' => 0
];
}
$stats['strategy_performance'][$strategy]['total']++;
if ($recovery['status'] === 'completed') {
$stats['strategy_performance'][$strategy]['completed']++;
} elseif ($recovery['status'] === 'failed') {
$stats['strategy_performance'][$strategy]['failed']++;
}
}
// Calculate averages and rates
if ($stats['total_recoveries'] > 0) {
$stats['success_rate'] = ($stats['completed_recoveries'] / $stats['total_recoveries']) * 100;
}
if ($completedCount > 0) {
$stats['average_attempts'] = $totalAttempts / $completedCount;
$stats['average_duration'] = $totalDuration / $completedCount;
}
// Calculate strategy success rates
foreach ($stats['strategy_performance'] as $strategy => &$performance) {
if ($performance['total'] > 0) {
$performance['success_rate'] = ($performance['completed'] / $performance['total']) * 100;
} else {
$performance['success_rate'] = 0;
}
}
return $stats;
}
/**
* Get recovery metrics.
*/
public function getRecoveryMetrics(string $circuitBreakerName = null): array
{
if ($circuitBreakerName) {
return $this->recoveryMetrics[$circuitBreakerName] ?? [];
}
return $this->recoveryMetrics;
}
/**
* Check if recovery should be triggered.
*/
public function shouldTriggerRecovery(string $circuitBreakerName, array $context = []): bool
{
// Check if there's already an active recovery
$activeRecoveries = $this->getRecoveriesForCircuit($circuitBreakerName);
if (!empty($activeRecoveries)) {
return false;
}
// Check cooldown period
$lastRecovery = $this->getLastRecovery($circuitBreakerName);
if ($lastRecovery && isset($lastRecovery['completed_at'])) {
$timeSinceLastRecovery = microtime(true) - $lastRecovery['completed_at'];
$cooldownPeriod = $context['cooldown_period'] ?? $this->config['cooldown_period'];
if ($timeSinceLastRecovery < $cooldownPeriod) {
return false;
}
}
// Check failure conditions
return $this->checkFailureConditions($circuitBreakerName, $context);
}
/**
* Trigger automatic recovery if conditions are met.
*/
public function triggerAutoRecovery(string $circuitBreakerName, array $context = []): ?string
{
if (!$this->shouldTriggerRecovery($circuitBreakerName, $context)) {
return null;
}
// Select best strategy
$strategy = $this->selectBestStrategy($circuitBreakerName, $context);
$context['strategy'] = $strategy;
return $this->startRecovery($circuitBreakerName, $context);
}
/**
* Add custom recovery strategy.
*/
public function addStrategy(string $name, callable $strategy): void
{
$this->strategies[$name] = $strategy;
$this->logInfo("Added custom recovery strategy: {$name}");
}
/**
* Get next attempt time for recovery.
*/
public function getNextAttemptTime(string $recoveryId): ?float
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return null;
}
return $this->activeRecoveries[$recoveryId]['next_attempt'];
}
/**
* Get time until next attempt.
*/
public function getTimeUntilNextAttempt(string $recoveryId): ?float
{
$nextAttempt = $this->getNextAttemptTime($recoveryId);
if ($nextAttempt === null) {
return null;
}
return max(0, $nextAttempt - microtime(true));
}
/**
* Cancel all recoveries for a circuit breaker.
*/
public function cancelAllRecoveries(string $circuitBreakerName): int
{
$cancelled = 0;
foreach ($this->activeRecoveries as $recoveryId => $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
$this->stopRecovery($recoveryId);
$cancelled++;
}
}
$this->logInfo("Cancelled {$cancelled} recoveries for {$circuitBreakerName}");
return $cancelled;
}
/**
* Reset all recovery state.
*/
public function reset(): void
{
// Stop all active recoveries
foreach (array_keys($this->activeRecoveries) as $recoveryId) {
$this->stopRecovery($recoveryId);
}
$this->recoveryHistory = [];
$this->recoveryMetrics = [];
$this->logInfo("Auto recovery reset");
}
/**
* Export recovery data.
*/
public function exportData(): array
{
return [
'config' => $this->config,
'active_recoveries' => $this->activeRecoveries,
'recovery_history' => $this->recoveryHistory,
'recovery_metrics' => $this->recoveryMetrics,
'statistics' => $this->getStatistics(),
'exported_at' => date('Y-m-d H:i:s')
];
}
/**
* Schedule next recovery attempt.
*/
protected function scheduleNextAttempt(string $recoveryId): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$strategy = $recovery['strategy'];
if (!isset($this->strategies[$strategy])) {
$this->logError("Unknown recovery strategy: {$strategy}");
return;
}
$delay = $this->strategies[$strategy]($recovery['attempts'], $recovery['context']);
$recovery['next_attempt'] = microtime(true) + $delay;
$this->logInfo("Scheduled next attempt for {$recoveryId} in {$delay} seconds");
}
/**
* Check if recovery is complete.
*/
protected function isRecoveryComplete(array $recovery): bool
{
$requiredSuccesses = $recovery['context']['required_successes'] ??
$this->config['required_successes'];
return $recovery['success_count'] >= $requiredSuccesses;
}
/**
* Complete recovery successfully.
*/
protected function completeRecovery(string $recoveryId, array $result): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$recovery['status'] = 'completed';
$recovery['completed_at'] = microtime(true);
$recovery['result'] = $result;
// Move to history
$this->recoveryHistory[] = $recovery;
unset($this->activeRecoveries[$recoveryId]);
$this->logInfo("Recovery {$recoveryId} completed successfully for {$recovery['circuit_breaker']}");
}
/**
* Mark recovery as failed.
*/
protected function failRecovery(string $recoveryId, array $result): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = &$this->activeRecoveries[$recoveryId];
$recovery['status'] = 'failed';
$recovery['failed_at'] = microtime(true);
$recovery['result'] = $result;
// Move to history
$this->recoveryHistory[] = $recovery;
unset($this->activeRecoveries[$recoveryId]);
$this->logInfo("Recovery {$recoveryId} failed for {$recovery['circuit_breaker']}");
}
/**
* Record recovery metrics.
*/
protected function recordRecoveryMetrics(string $recoveryId, bool $success, array $result): void
{
if (!isset($this->activeRecoveries[$recoveryId])) {
return;
}
$recovery = $this->activeRecoveries[$recoveryId];
$circuitBreakerName = $recovery['circuit_breaker'];
if (!isset($this->recoveryMetrics[$circuitBreakerName])) {
$this->recoveryMetrics[$circuitBreakerName] = [
'total_attempts' => 0,
'successful_attempts' => 0,
'failed_attempts' => 0,
'last_attempt' => null,
'average_attempt_duration' => 0
];
}
$metrics = &$this->recoveryMetrics[$circuitBreakerName];
$metrics['total_attempts']++;
$metrics['last_attempt'] = microtime(true);
if ($success) {
$metrics['successful_attempts']++;
} else {
$metrics['failed_attempts']++;
}
// Update average duration if provided
if (isset($result['duration'])) {
$currentAvg = $metrics['average_attempt_duration'];
$totalAttempts = $metrics['total_attempts'];
$metrics['average_attempt_duration'] =
(($currentAvg * ($totalAttempts - 1)) + $result['duration']) / $totalAttempts;
}
}
/**
* Get last recovery for circuit breaker.
*/
protected function getLastRecovery(string $circuitBreakerName): ?array
{
// Check active recoveries first
foreach ($this->activeRecoveries as $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
return $recovery;
}
}
// Check history
for ($i = count($this->recoveryHistory) - 1; $i >= 0; $i--) {
if ($this->recoveryHistory[$i]['circuit_breaker'] === $circuitBreakerName) {
return $this->recoveryHistory[$i];
}
}
return null;
}
/**
* Check failure conditions.
*/
protected function checkFailureConditions(string $circuitBreakerName, array $context): bool
{
$metrics = $this->recoveryMetrics[$circuitBreakerName] ?? [];
// Check failure rate
$failureRateThreshold = $context['failure_rate_threshold'] ?? 80.0;
if ($metrics['total_attempts'] > 0) {
$failureRate = ($metrics['failed_attempts'] / $metrics['total_attempts']) * 100;
if ($failureRate < $failureRateThreshold) {
return false;
}
}
// Check minimum attempts
$minAttempts = $context['min_attempts'] ?? $this->config['min_attempts'];
if ($metrics['total_attempts'] < $minAttempts) {
return false;
}
return true;
}
/**
* Select best recovery strategy.
*/
protected function selectBestStrategy(string $circuitBreakerName, array $context): string
{
$metrics = $this->recoveryMetrics[$circuitBreakerName] ?? [];
// Analyze past performance
$strategyPerformance = [];
foreach ($this->recoveryHistory as $recovery) {
if ($recovery['circuit_breaker'] === $circuitBreakerName) {
$strategy = $recovery['strategy'];
if (!isset($strategyPerformance[$strategy])) {
$strategyPerformance[$strategy] = [
'total' => 0,
'successful' => 0
];
}
$strategyPerformance[$strategy]['total']++;
if ($recovery['status'] === 'completed') {
$strategyPerformance[$strategy]['successful']++;
}
}
}
// Select strategy with best success rate
$bestStrategy = $this->config['default_strategy'];
$bestSuccessRate = 0;
foreach ($strategyPerformance as $strategy => $performance) {
if ($performance['total'] > 0) {
$successRate = ($performance['successful'] / $performance['total']) * 100;
if ($successRate > $bestSuccessRate) {
$bestSuccessRate = $successRate;
$bestStrategy = $strategy;
}
}
}
return $bestStrategy;
}
/**
* Generate unique recovery ID.
*/
protected function generateRecoveryId(string $circuitBreakerName): string
{
return $circuitBreakerName . '_' . uniqid() . '_' . time();
}
/**
* Initialize built-in strategies.
*/
protected function initializeStrategies(): void
{
$this->strategies['exponential_backoff'] = new ExponentialBackoffStrategy($this->config);
$this->strategies['linear_backoff'] = new LinearBackoffStrategy($this->config);
$this->strategies['fixed_interval'] = new FixedIntervalStrategy($this->config);
$this->strategies['adaptive'] = new AdaptiveStrategy($this->config);
}
/**
* Log error message.
*/
protected function logError(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[AutoRecovery] ERROR: {$message}");
}
}
/**
* Log info message.
*/
protected function logInfo(string $message): void
{
if ($this->config['logging_enabled']) {
error_log("[AutoRecovery] {$message}");
}
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'default_strategy' => 'exponential_backoff',
'max_attempts' => 5,
'required_successes' => 3,
'cooldown_period' => 300, // 5 minutes
'min_attempts' => 3,
'logging_enabled' => true,
'strategies' => [
'exponential_backoff' => [
'base_delay' => 1.0,
'max_delay' => 300.0,
'multiplier' => 2.0
],
'linear_backoff' => [
'base_delay' => 5.0,
'max_delay' => 300.0,
'increment' => 10.0
],
'fixed_interval' => [
'delay' => 30.0
],
'adaptive' => [
'min_delay' => 1.0,
'max_delay' => 300.0,
'success_factor' => 0.8,
'failure_factor' => 1.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);
$this->initializeStrategies();
}
/**
* Create auto recovery instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for production.
*/
public static function forProduction(): self
{
return new self([
'max_attempts' => 10,
'required_successes' => 5,
'cooldown_period' => 600, // 10 minutes
'min_attempts' => 5,
'logging_enabled' => false
]);
}
/**
* Create for development.
*/
public static function forDevelopment(): self
{
return new self([
'max_attempts' => 3,
'required_successes' => 2,
'cooldown_period' => 60, // 1 minute
'min_attempts' => 2,
'logging_enabled' => true
]);
}
}