|
|
@@ -0,0 +1,298 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Http\Middleware;
|
|
|
+
|
|
|
+use Closure;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Symfony\Component\HttpFoundation\Response;
|
|
|
+
|
|
|
+class PerformanceMonitor
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * Initial performance markers
|
|
|
+ */
|
|
|
+ private float $startTime;
|
|
|
+ private int $startMemory;
|
|
|
+ private int $startPeakMemory;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Output buffer
|
|
|
+ */
|
|
|
+ private string $buffer = '';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ANSI color codes for console output
|
|
|
+ */
|
|
|
+ private const COLORS = [
|
|
|
+ 'green' => "\033[32m",
|
|
|
+ 'yellow' => "\033[33m",
|
|
|
+ 'magenta' => "\033[35m",
|
|
|
+ 'cyan' => "\033[36m",
|
|
|
+ 'gray' => "\033[90m",
|
|
|
+ 'reset' => "\033[0m"
|
|
|
+ ];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Performance thresholds (in milliseconds)
|
|
|
+ */
|
|
|
+ private const SLOW_REQUEST_THRESHOLD = 1000;
|
|
|
+ private const SLOW_QUERY_THRESHOLD = 100;
|
|
|
+
|
|
|
+ public function __construct()
|
|
|
+ {
|
|
|
+ $this->startTime = defined('LARAVEL_START') ? LARAVEL_START : microtime(true);
|
|
|
+ $this->startMemory = memory_get_usage();
|
|
|
+ $this->startPeakMemory = memory_get_peak_usage();
|
|
|
+ }
|
|
|
+
|
|
|
+ public function handle(Request $request, Closure $next): Response
|
|
|
+ {
|
|
|
+ if (app()->environment('local', 'development')) {
|
|
|
+ DB::enableQueryLog();
|
|
|
+ }
|
|
|
+
|
|
|
+ $response = $next($request);
|
|
|
+
|
|
|
+ if (app()->environment('local', 'development')) {
|
|
|
+ $this->logPerformanceMetrics($request);
|
|
|
+ $this->flushBuffer();
|
|
|
+ }
|
|
|
+
|
|
|
+ return $response;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function gatherMetrics(): array
|
|
|
+ {
|
|
|
+ $memoryLimit = $this->getMemoryLimit();
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'time' => $this->getExecutionTime(),
|
|
|
+ 'memory' => $this->getMemoryUsage(),
|
|
|
+ 'peakMemory' => $this->getPeakMemoryUsage(),
|
|
|
+ 'memoryLimit' => $memoryLimit,
|
|
|
+ 'memoryPercentage' => $this->calculateMemoryPercentage($memoryLimit),
|
|
|
+ 'queries' => $this->getQueryMetrics(),
|
|
|
+ 'cpu' => $this->getCpuMetrics(),
|
|
|
+ 'cache' => $this->getCacheMetrics(),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getExecutionTime(): float
|
|
|
+ {
|
|
|
+ return (microtime(true) - $this->startTime) * 1000;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getMemoryUsage(): float
|
|
|
+ {
|
|
|
+ return (memory_get_usage() - $this->startMemory) / 1024 / 1024;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getPeakMemoryUsage(): float
|
|
|
+ {
|
|
|
+ return (memory_get_peak_usage() - $this->startPeakMemory) / 1024 / 1024;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getMemoryLimit(): int
|
|
|
+ {
|
|
|
+ $limit = ini_get('memory_limit');
|
|
|
+ if (!$limit) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ preg_match('/^(\d+)(K|M|G)?$/i', $limit, $matches);
|
|
|
+ $value = (int)($matches[1] ?? 0);
|
|
|
+ $unit = strtoupper($matches[2] ?? '');
|
|
|
+
|
|
|
+ return match ($unit) {
|
|
|
+ 'G' => $value * 1024 * 1024 * 1024,
|
|
|
+ 'M' => $value * 1024 * 1024,
|
|
|
+ 'K' => $value * 1024,
|
|
|
+ default => $value,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private function calculateMemoryPercentage(int $limit): float
|
|
|
+ {
|
|
|
+ if ($limit === 0) {
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (memory_get_usage(true) / $limit) * 100;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getQueryMetrics(): ?array
|
|
|
+ {
|
|
|
+ $queries = DB::getQueryLog();
|
|
|
+ if (empty($queries)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $totalTime = 0;
|
|
|
+ $formattedQueries = [];
|
|
|
+ foreach ($queries as $query) {
|
|
|
+ $totalTime += $query['time'];
|
|
|
+ if ($query['time'] > self::SLOW_QUERY_THRESHOLD) {
|
|
|
+ $formattedQueries[] = [
|
|
|
+ 'sql' => $this->formatSql($query['query'], $query['bindings']),
|
|
|
+ 'time' => $query['time'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'count' => count($queries),
|
|
|
+ 'time' => $totalTime,
|
|
|
+ 'slow_queries' => $formattedQueries,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function formatSql(string $sql, array $bindings): string
|
|
|
+ {
|
|
|
+ return vsprintf(str_replace(['%', '?'], ['%%', "'%s'"], $sql), $bindings);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getCpuMetrics(): ?array
|
|
|
+ {
|
|
|
+ if (!function_exists('getrusage')) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $usage = getrusage();
|
|
|
+ return [
|
|
|
+ 'user' => ($usage['ru_utime.tv_sec'] * 1000 + intval($usage['ru_utime.tv_usec'] / 1000)),
|
|
|
+ 'system' => ($usage['ru_stime.tv_sec'] * 1000 + intval($usage['ru_stime.tv_usec'] / 1000)),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function getCacheMetrics(): ?array
|
|
|
+ {
|
|
|
+ if (!function_exists('opcache_get_status')) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $status = opcache_get_status(false);
|
|
|
+ if (!$status) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $stats = $status['opcache_statistics'];
|
|
|
+ $hits = $stats['hits'];
|
|
|
+ $misses = $stats['misses'];
|
|
|
+ $total = $hits + $misses;
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'hit_rate' => $total > 0 ? ($hits / $total * 100) : 0,
|
|
|
+ 'memory_usage' => $status['memory_usage'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function appendToBuffer(string $text): void
|
|
|
+ {
|
|
|
+ $this->buffer .= $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function flushBuffer(): void
|
|
|
+ {
|
|
|
+ if (empty($this->buffer)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $written = file_put_contents('php://stderr', $this->buffer);
|
|
|
+ if ($written === false) {
|
|
|
+ error_log('Failed to write performance metrics to stderr');
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ error_log("Error writing performance metrics: {$e->getMessage()}");
|
|
|
+ } finally {
|
|
|
+ $this->buffer = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private function colorize(string $text, string $color): string
|
|
|
+ {
|
|
|
+ return self::COLORS[$color] . $text . self::COLORS['reset'];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function logPerformanceMetrics(Request $request): void
|
|
|
+ {
|
|
|
+ $metrics = $this->gatherMetrics();
|
|
|
+
|
|
|
+ // Request information
|
|
|
+ $this->appendToBuffer(sprintf(
|
|
|
+ "\n%s %s %s[%s]%s\n",
|
|
|
+ $this->colorize($request->method(), 'green'),
|
|
|
+ $this->colorize($request->getRequestUri(), 'green'),
|
|
|
+ self::COLORS['gray'],
|
|
|
+ date('Y-m-d H:i:s'),
|
|
|
+ self::COLORS['reset']
|
|
|
+ ));
|
|
|
+
|
|
|
+ // Core metrics
|
|
|
+ $this->appendToBuffer(sprintf(
|
|
|
+ "➜ Time: %s%.2fms%s | Memory: %s%.2fMB%s (%.1f%%) | Peak: %s%.2fMB%s",
|
|
|
+ self::COLORS['yellow'],
|
|
|
+ $metrics['time'],
|
|
|
+ self::COLORS['reset'],
|
|
|
+ self::COLORS['magenta'],
|
|
|
+ $metrics['memory'],
|
|
|
+ self::COLORS['reset'],
|
|
|
+ $metrics['memoryPercentage'],
|
|
|
+ self::COLORS['magenta'],
|
|
|
+ $metrics['peakMemory'],
|
|
|
+ self::COLORS['reset']
|
|
|
+ ));
|
|
|
+
|
|
|
+ // Query metrics
|
|
|
+ if ($metrics['queries']) {
|
|
|
+ $this->appendToBuffer(sprintf(
|
|
|
+ " | Queries: %s%d%s (%s%.2fms%s)",
|
|
|
+ self::COLORS['cyan'],
|
|
|
+ $metrics['queries']['count'],
|
|
|
+ self::COLORS['reset'],
|
|
|
+ self::COLORS['cyan'],
|
|
|
+ $metrics['queries']['time'],
|
|
|
+ self::COLORS['reset']
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ // CPU metrics
|
|
|
+ if ($metrics['cpu']) {
|
|
|
+ $totalCpu = $metrics['cpu']['user'] + $metrics['cpu']['system'];
|
|
|
+ $this->appendToBuffer(sprintf(
|
|
|
+ " | CPU: %s%.2fms%s",
|
|
|
+ self::COLORS['cyan'],
|
|
|
+ $totalCpu,
|
|
|
+ self::COLORS['reset']
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cache metrics
|
|
|
+ if ($metrics['cache']) {
|
|
|
+ $this->appendToBuffer(sprintf(
|
|
|
+ " | OPcache: %s%.1f%%%s",
|
|
|
+ self::COLORS['cyan'],
|
|
|
+ $metrics['cache']['hit_rate'],
|
|
|
+ self::COLORS['reset']
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->appendToBuffer("\n");
|
|
|
+
|
|
|
+ // Log slow queries
|
|
|
+ if ($metrics['time'] > self::SLOW_REQUEST_THRESHOLD && !empty($metrics['queries']['slow_queries'])) {
|
|
|
+ $this->appendToBuffer("\n" . $this->colorize("Slow Queries Detected:", 'yellow') . "\n");
|
|
|
+ foreach ($metrics['queries']['slow_queries'] as $query) {
|
|
|
+ $this->appendToBuffer(sprintf(
|
|
|
+ "➜ %s%.2fms%s: %s\n",
|
|
|
+ self::COLORS['yellow'],
|
|
|
+ $query['time'],
|
|
|
+ self::COLORS['reset'],
|
|
|
+ $query['sql']
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|