Skip to content

Commit 86837fa

Browse files
committed
feat(plugin-health): add plugin health monitoring system
Implement comprehensive plugin health monitoring with metrics collection, error tracking, and alerting. Includes: - PluginHealthMonitor service for metrics and error tracking - Middleware for request metrics collection - Console command for health status checks - Alert system with multiple notification channels - Interface for health monitoring functionality
1 parent 316ae9e commit 86837fa

File tree

6 files changed

+1022
-0
lines changed

6 files changed

+1022
-0
lines changed

pint.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"preset": "laravel",
3+
"rules": {
4+
"not_operator_with_successor_space": false,
5+
"not_operator_with_space": false,
6+
"logical_operators": false
7+
}
8+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
3+
namespace SoysalTan\LaravelPluginSystem\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use SoysalTan\LaravelPluginSystem\Services\PluginHealthMonitor;
7+
use Symfony\Component\Console\Command\Command as CommandAlias;
8+
9+
class PluginHealthCommand extends Command
10+
{
11+
protected $signature = 'plugin:health
12+
{plugin? : Specific plugin name to check}
13+
{--watch : Watch mode - continuously monitor health}
14+
{--json : Output in JSON format}
15+
{--detailed : Show detailed metrics}
16+
{--errors : Show recent errors}
17+
{--clear-errors= : Clear errors for specific plugin}';
18+
19+
protected $description = 'Monitor plugin health status and metrics';
20+
21+
protected PluginHealthMonitor $healthMonitor;
22+
23+
public function __construct(PluginHealthMonitor $healthMonitor)
24+
{
25+
parent::__construct();
26+
$this->healthMonitor = $healthMonitor;
27+
}
28+
29+
public function handle(): int
30+
{
31+
if ($this->option('clear-errors')) {
32+
return $this->clearErrors();
33+
}
34+
35+
if ($this->option('watch')) {
36+
return $this->watchMode();
37+
}
38+
39+
$pluginName = $this->argument('plugin');
40+
41+
if ($pluginName) {
42+
return $this->showPluginHealth($pluginName);
43+
}
44+
45+
return $this->showAllPluginsHealth();
46+
}
47+
48+
protected function showPluginHealth(string $pluginName): int
49+
{
50+
$health = $this->healthMonitor->checkPluginHealth($pluginName);
51+
52+
if (empty($health)) {
53+
$this->error("Plugin '{$pluginName}' not found or not enabled.");
54+
return CommandAlias::FAILURE;
55+
}
56+
57+
if ($this->option('json')) {
58+
$this->line(json_encode($health, JSON_PRETTY_PRINT));
59+
return CommandAlias::SUCCESS;
60+
}
61+
62+
$this->displayPluginHealth($health);
63+
return CommandAlias::SUCCESS;
64+
}
65+
66+
protected function showAllPluginsHealth(): int
67+
{
68+
$healthReport = $this->healthMonitor->getHealthReport();
69+
70+
if ($this->option('json')) {
71+
$this->line(json_encode($healthReport, JSON_PRETTY_PRINT));
72+
return CommandAlias::SUCCESS;
73+
}
74+
75+
$this->displayHealthSummary($healthReport['summary']);
76+
$this->newLine();
77+
78+
foreach ($healthReport['plugins'] as $pluginHealth) {
79+
$this->displayPluginHealth($pluginHealth, false);
80+
$this->newLine();
81+
}
82+
83+
return CommandAlias::SUCCESS;
84+
}
85+
86+
protected function displayHealthSummary(array $summary): void
87+
{
88+
$this->info('=== Plugin Health Summary ===');
89+
90+
$statusColor = match($summary['overall_status']) {
91+
'healthy' => 'green',
92+
'warning' => 'yellow',
93+
'critical' => 'red',
94+
default => 'white'
95+
};
96+
97+
$this->line("Overall Status: <fg={$statusColor}>" . strtoupper($summary['overall_status']) . "</>");
98+
$this->line("Total Plugins: {$summary['total_plugins']}");
99+
$this->line("<fg=green>Healthy: {$summary['healthy_plugins']}</>");
100+
$this->line("<fg=yellow>Warning: {$summary['warning_plugins']}</>");
101+
$this->line("<fg=red>Critical: {$summary['critical_plugins']}</>");
102+
$this->line("Last Check: {$summary['last_check']}");
103+
}
104+
105+
protected function displayPluginHealth(array $health, bool $detailed = null): void
106+
{
107+
$detailed = $detailed ?? $this->option('detailed');
108+
109+
$statusColor = match($health['status']) {
110+
'healthy' => 'green',
111+
'warning' => 'yellow',
112+
'critical' => 'red',
113+
default => 'white'
114+
};
115+
116+
$this->line("Plugin: <fg=cyan>{$health['plugin_name']}</>");
117+
$this->line("Status: <fg={$statusColor}>" . strtoupper($health['status']) . "</>");
118+
$this->line("Uptime: {$health['uptime']}%");
119+
120+
if ($detailed) {
121+
$this->displayDetailedMetrics($health['metrics']);
122+
}
123+
124+
if ($this->option('errors') && !empty($health['recent_errors'])) {
125+
$this->displayRecentErrors($health['recent_errors']);
126+
}
127+
128+
if (!empty($health['recommendations'])) {
129+
$this->displayRecommendations($health['recommendations']);
130+
}
131+
}
132+
133+
protected function displayDetailedMetrics(array $metrics): void
134+
{
135+
$this->line("Metrics:");
136+
$this->line(" Memory Usage: " . $this->formatBytes($metrics['memory_usage'] ?? 0));
137+
$this->line(" Execution Time: " . ($metrics['execution_time'] ?? 0) . "ms");
138+
$this->line(" Request Count: " . ($metrics['request_count'] ?? 0));
139+
$this->line(" Error Count: " . ($metrics['error_count'] ?? 0));
140+
$this->line(" Response Time: " . ($metrics['response_time'] ?? 0) . "ms");
141+
$this->line(" Database Queries: " . ($metrics['database_queries'] ?? 0));
142+
$this->line(" Cache Hits: " . ($metrics['cache_hits'] ?? 0));
143+
$this->line(" Cache Misses: " . ($metrics['cache_misses'] ?? 0));
144+
145+
if ($metrics['last_activity']) {
146+
$this->line(" Last Activity: {$metrics['last_activity']}");
147+
}
148+
}
149+
150+
protected function displayRecentErrors(array $errors): void
151+
{
152+
$this->line("<fg=red>Recent Errors:</>");
153+
154+
foreach ($errors as $index => $error) {
155+
$severityColor = match($error['severity']) {
156+
'critical' => 'red',
157+
'warning' => 'yellow',
158+
'info' => 'blue',
159+
default => 'white'
160+
};
161+
162+
$this->line(" " . ($index + 1) . ". <fg={$severityColor}>[{$error['severity']}]</> {$error['message']}");
163+
$this->line(" File: {$error['file']}:{$error['line']}");
164+
$this->line(" Time: {$error['timestamp']}");
165+
}
166+
}
167+
168+
protected function displayRecommendations(array $recommendations): void
169+
{
170+
$this->line("<fg=yellow>Recommendations:</>");
171+
172+
foreach ($recommendations as $index => $recommendation) {
173+
$this->line(" " . ($index + 1) . ". {$recommendation}");
174+
}
175+
}
176+
177+
protected function watchMode(): int
178+
{
179+
$this->info('Starting health monitoring in watch mode. Press Ctrl+C to stop.');
180+
181+
try {
182+
while (true) {
183+
$this->call('clear');
184+
$this->line('Plugin Health Monitor - ' . now()->format('Y-m-d H:i:s'));
185+
$this->line(str_repeat('=', 60));
186+
187+
$this->showAllPluginsHealth();
188+
189+
sleep(5); // Refresh every 5 seconds
190+
}
191+
} catch (\Exception $e) {
192+
$this->error('Watch mode interrupted: ' . $e->getMessage());
193+
return CommandAlias::FAILURE;
194+
}
195+
196+
return CommandAlias::SUCCESS;
197+
}
198+
199+
protected function clearErrors(): int
200+
{
201+
$pluginName = $this->option('clear-errors');
202+
203+
if ($pluginName === 'all') {
204+
// Clear errors for all plugins
205+
$healthReport = $this->healthMonitor->getHealthReport();
206+
207+
foreach ($healthReport['plugins'] as $plugin) {
208+
$this->healthMonitor->clearPluginErrors($plugin['plugin_name']);
209+
}
210+
211+
$this->info('Cleared errors for all plugins.');
212+
} else {
213+
$this->healthMonitor->clearPluginErrors($pluginName);
214+
$this->info("Cleared errors for plugin: {$pluginName}");
215+
}
216+
217+
return CommandAlias::SUCCESS;
218+
}
219+
220+
protected function formatBytes(int $bytes): string
221+
{
222+
$units = ['B', 'KB', 'MB', 'GB'];
223+
$bytes = max($bytes, 0);
224+
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
225+
$pow = min($pow, count($units) - 1);
226+
227+
$bytes /= (1 << (10 * $pow));
228+
229+
return round($bytes, 2) . ' ' . $units[$pow];
230+
}
231+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace SoysalTan\LaravelPluginSystem\Contracts;
4+
5+
interface PluginHealthMonitorInterface
6+
{
7+
public function checkPluginHealth(string $pluginName): array;
8+
9+
public function checkAllPluginsHealth(): array;
10+
11+
public function getPluginMetrics(string $pluginName): array;
12+
13+
public function isPluginHealthy(string $pluginName): bool;
14+
15+
public function getHealthThresholds(): array;
16+
17+
public function setHealthThreshold(string $metric, $value): void;
18+
19+
public function getPluginErrors(string $pluginName, int $limit = 10): array;
20+
21+
public function clearPluginErrors(string $pluginName): void;
22+
23+
public function getPluginUptime(string $pluginName): float;
24+
25+
public function recordPluginError(string $pluginName, \Throwable $exception): void;
26+
27+
public function recordPluginMetric(string $pluginName, string $metric, $value): void;
28+
29+
public function getHealthReport(): array;
30+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace SoysalTan\LaravelPluginSystem\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\App;
8+
use SoysalTan\LaravelPluginSystem\Services\PluginHealthMonitor;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class PluginHealthCollector
12+
{
13+
protected PluginHealthMonitor $healthMonitor;
14+
15+
public function __construct(PluginHealthMonitor $healthMonitor)
16+
{
17+
$this->healthMonitor = $healthMonitor;
18+
}
19+
20+
public function handle(Request $request, Closure $next): Response
21+
{
22+
$startTime = microtime(true);
23+
$startMemory = memory_get_usage(true);
24+
25+
// Detect which plugin is handling this request
26+
$pluginName = $this->detectPluginFromRequest($request);
27+
28+
if (!$pluginName) {
29+
return $next($request);
30+
}
31+
32+
try {
33+
$response = $next($request);
34+
35+
// Record successful request metrics
36+
$this->recordMetrics($pluginName, $startTime, $startMemory, true);
37+
38+
return $response;
39+
40+
} catch (\Throwable $exception) {
41+
// Record error metrics
42+
$this->recordMetrics($pluginName, $startTime, $startMemory, false);
43+
$this->healthMonitor->recordPluginError($pluginName, $exception);
44+
45+
throw $exception;
46+
}
47+
}
48+
49+
protected function detectPluginFromRequest(Request $request): ?string
50+
{
51+
$path = $request->path();
52+
53+
// Check if request matches plugin route pattern
54+
if (preg_match('/^(?:plugins\/)?([^\/]+)/', $path, $matches)) {
55+
$potentialPlugin = ucfirst($matches[1]);
56+
57+
// Verify plugin exists
58+
$pluginsPath = config('laravel-plugin-system.plugins_path', app_path('Plugins'));
59+
if (is_dir($pluginsPath . '/' . $potentialPlugin)) {
60+
return $potentialPlugin;
61+
}
62+
}
63+
64+
return null;
65+
}
66+
67+
protected function recordMetrics(string $pluginName, float $startTime, int $startMemory, bool $success): void
68+
{
69+
$executionTime = (microtime(true) - $startTime) * 1000; // Convert to milliseconds
70+
$memoryUsage = memory_get_usage(true) - $startMemory;
71+
72+
// Record execution time
73+
$this->healthMonitor->recordPluginMetric($pluginName, 'execution_time', $executionTime);
74+
75+
// Record memory usage
76+
$this->healthMonitor->recordPluginMetric($pluginName, 'memory_usage', $memoryUsage);
77+
78+
// Increment request count
79+
$currentMetrics = $this->healthMonitor->getPluginMetrics($pluginName);
80+
$this->healthMonitor->recordPluginMetric($pluginName, 'request_count', ($currentMetrics['request_count'] ?? 0) + 1);
81+
82+
// Record response time
83+
$this->healthMonitor->recordPluginMetric($pluginName, 'response_time', $executionTime);
84+
85+
if (!$success) {
86+
// Increment error count
87+
$this->healthMonitor->recordPluginMetric($pluginName, 'error_count', ($currentMetrics['error_count'] ?? 0) + 1);
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)