Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
#[MiddlewarePriorityAttribute(MiddlewarePriority::CONTROLLER, -10)]
final readonly class ControllerPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::CONTROLLER)) {
return $next($context);
}
$request = $context->request;
// Try to get controller info from state manager or request
$controllerInfo = $this->extractControllerInfo($request, $stateManager);
$controllerKey = $controllerInfo['controller'] ?? 'unknown_controller';
$actionKey = $controllerInfo['action'] ?? 'unknown_action';
$performanceContext = [
'controller' => $controllerKey,
'action' => $actionKey,
'method' => $request->method,
'path' => $request->path,
];
$timingKey = "controller_{$controllerKey}_{$actionKey}";
$this->collector->startTiming($timingKey, PerformanceCategory::CONTROLLER, $performanceContext);
// Count controller invocations
$this->collector->increment(
"controller_calls_{$controllerKey}",
PerformanceCategory::CONTROLLER,
1,
$performanceContext
);
try {
$result = $next($context);
// Record successful controller execution
$this->collector->increment(
"controller_success_{$controllerKey}",
PerformanceCategory::CONTROLLER,
1,
$performanceContext
);
return $result;
} catch (\Throwable $e) {
// Record controller error
$this->collector->increment(
"controller_errors_{$controllerKey}",
PerformanceCategory::CONTROLLER,
1,
array_merge($performanceContext, ['error' => $e::class])
);
throw $e;
} finally {
$this->collector->endTiming($timingKey);
}
}
private function extractControllerInfo(mixed $request, RequestStateManager $stateManager): array
{
// Try to get controller info from various sources
// Method 1: From state manager
$controllerData = $stateManager->get('controller_info');
if ($controllerData) {
return $controllerData;
}
// Method 2: From request attributes/route
if (isset($request->route)) {
$handler = $request->route['handler'] ?? null;
if ($handler && is_string($handler)) {
return $this->parseControllerString($handler);
}
}
// Method 3: From request path (fallback)
$path = $request->path ?? '/';
$segments = explode('/', trim($path, '/'));
return [
'controller' => $segments[0] ?? 'home',
'action' => $segments[1] ?? 'index',
];
}
private function parseControllerString(string $handler): array
{
// Parse strings like "App\Controller\HomeController@index"
if (str_contains($handler, '@')) {
[$controller, $action] = explode('@', $handler, 2);
$controller = basename(str_replace('\\', '/', $controller));
return ['controller' => $controller, 'action' => $action];
}
// Parse class names
if (str_contains($handler, '\\')) {
$controller = basename(str_replace('\\', '/', $handler));
return ['controller' => $controller, 'action' => '__invoke'];
}
return ['controller' => $handler, 'action' => 'unknown'];
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Database\Middleware\QueryContext;
use App\Framework\Database\Middleware\QueryMiddleware;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
final readonly class DatabasePerformanceMiddleware implements QueryMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function process(QueryContext $context, callable $next): mixed
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::DATABASE)) {
return $next();
}
$query = $context->sql;
$bindings = $context->parameters;
$queryType = $this->getQueryType($query);
$queryKey = "db_query_{$queryType}";
$performanceContext = [
'query_type' => $queryType,
'bindings_count' => count($bindings),
'query_hash' => md5($query),
];
// Add query text for detailed reports (careful with sensitive data)
if ($this->config->detailedReports) {
$performanceContext['query'] = $this->sanitizeQuery($query);
$performanceContext['bindings'] = $this->sanitizeBindings($bindings);
}
$this->collector->startTiming($queryKey, PerformanceCategory::DATABASE, $performanceContext);
// Count database queries
$this->collector->increment('database_queries_total', PerformanceCategory::DATABASE, 1, $performanceContext);
$this->collector->increment("database_queries_{$queryType}", PerformanceCategory::DATABASE, 1, $performanceContext);
try {
$result = $next();
// Record successful query
$this->collector->increment('database_queries_success', PerformanceCategory::DATABASE, 1, $performanceContext);
return $result;
} catch (\Throwable $e) {
// Record failed query
$this->collector->increment(
'database_queries_failed',
PerformanceCategory::DATABASE,
1,
array_merge($performanceContext, ['error' => $e::class])
);
throw $e;
} finally {
$duration = $this->collector->endTiming($queryKey);
// Check for slow queries
if ($this->config->isSlowQuery($duration)) {
$this->collector->increment(
'database_slow_queries',
PerformanceCategory::DATABASE,
1,
array_merge($performanceContext, ['duration_ms' => $duration])
);
}
}
}
public function getPriority(): int
{
return 100; // Performance middleware should run with moderate priority
}
private function getQueryType(string $query): string
{
$query = trim(strtoupper($query));
if (str_starts_with($query, 'SELECT')) {
return 'select';
}
if (str_starts_with($query, 'INSERT')) {
return 'insert';
}
if (str_starts_with($query, 'UPDATE')) {
return 'update';
}
if (str_starts_with($query, 'DELETE')) {
return 'delete';
}
if (str_starts_with($query, 'CREATE')) {
return 'create';
}
if (str_starts_with($query, 'DROP')) {
return 'drop';
}
if (str_starts_with($query, 'ALTER')) {
return 'alter';
}
if (str_starts_with($query, 'SHOW')) {
return 'show';
}
if (str_starts_with($query, 'DESCRIBE') || str_starts_with($query, 'DESC')) {
return 'describe';
}
return 'other';
}
private function sanitizeQuery(string $query): string
{
// Remove potential sensitive data from queries for logging
$query = preg_replace('/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/', '****-****-****-****', $query);
$query = preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '***@***.***', $query);
// Truncate very long queries
if (strlen($query) > 500) {
$query = substr($query, 0, 500) . '...';
}
return $query;
}
private function sanitizeBindings(array $bindings): array
{
// Sanitize binding values to prevent sensitive data exposure
return array_map(function ($value) {
if (is_string($value)) {
// Hide potential sensitive strings
if (strlen($value) > 50) {
return '[LONG_STRING:' . strlen($value) . '_chars]';
}
if (preg_match('/^\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}$/', $value)) {
return '[CREDIT_CARD]';
}
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
return '[EMAIL]';
}
}
return $value;
}, $bindings);
}
}

View File

@@ -0,0 +1,696 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\EnhancedPerformanceCollector;
use App\Framework\Performance\PerformanceConfig;
use App\Framework\Performance\PerformanceReporter;
#[MiddlewarePriorityAttribute(MiddlewarePriority::LAST)]
final readonly class PerformanceDebugMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config,
private PerformanceReporter $reporter
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
// Always execute the request first
$context = $next($context);
// Handle performance output and headers
return $this->handlePerformanceOutput($context, $stateManager);
}
private function handlePerformanceOutput(MiddlewareContext $context, RequestStateManager $stateManager): MiddlewareContext
{
// Check if performance tracking is enabled
if (! $this->config->enabled) {
return $context;
}
$request = $context->request;
$response = $context->response ?? null;
// Skip if no response
if (! $response) {
return $context;
}
// Skip for excluded paths
$path = $request->path ?? '/';
if ($this->config->shouldExcludePath($path)) {
return $context;
}
// Check content type - only add to HTML responses
$contentType = $response->headers->get('Content-Type', '');
if (is_string($contentType) && ! empty($contentType) && ! str_contains($contentType, 'text/html')) {
// Add performance headers for non-HTML responses
$headersWithPerformance = $this->addPerformanceHeaders($response->headers);
$newResponse = new HttpResponse(
status: $response->status,
headers: $headersWithPerformance,
body: $response->body
);
return $context->withResponse($newResponse);
}
// Skip for AJAX requests unless specifically enabled
if ($this->isAjaxRequest($request) && ! $this->config->detailedReports) {
return $context;
}
if (! $context->response instanceof HttpResponse) {
return $context;
}
// Add full performance output to HTML responses
return $this->addPerformanceOutput($context, $stateManager);
}
private function isAjaxRequest(mixed $request): bool
{
$xhr = $request->headers->get('X-Requested-With', '');
return is_string($xhr) && strtolower($xhr) === 'xmlhttprequest';
}
private function addPerformanceOutput(MiddlewareContext $context, RequestStateManager $stateManager): MiddlewareContext
{
$response = $context->response;
if ($context->response->headers->getFirst('Content-Type') === 'application/json') {
return $context;
}
if (! $response || ! isset($response->body)) {
return $context;
}
$body = $response->body;
// Generate performance report
$reportHtml = $this->reporter->generateReport('html');
assert(is_string($reportHtml), 'HTML format should return string');
// Add hierarchical performance report if using EnhancedPerformanceCollector
if ($this->collector instanceof EnhancedPerformanceCollector) {
$hierarchicalHtml = $this->generateHierarchicalReport();
$reportHtml .= $hierarchicalHtml;
}
// Always add session debug information as collapsible details
if ($this->config->detailedReports) {
$sessionDebugHtml = $this->generateSessionDebugInfo($stateManager);
$reportHtml .= $sessionDebugHtml;
}
// Try to find closing body tag
$closingBodyPos = strripos($body, '</body>');
if ($closingBodyPos !== false) {
// Insert before closing body tag
$newBody = substr($body, 0, $closingBodyPos);
$newBody .= $this->wrapReportForInsertion($reportHtml);
$newBody .= substr($body, $closingBodyPos);
} else {
// Append to end if no body tag found
$newBody = $body . $this->wrapReportForInsertion($reportHtml);
}
// Add performance headers to the headers
$headersWithPerformance = $this->addPerformanceHeaders($response->headers);
// Create new response with modified body and headers
$newResponse = new HttpResponse(
status: $response->status,
headers: $headersWithPerformance,
body: $newBody
);
// Return new context with new response
return $context->withResponse($newResponse);
}
private function wrapReportForInsertion(string $reportHtml): string
{
return "
<!-- Performance Debug Report -->
<style>
.perf-debug-theme-light {
--perf-bg-primary: #ffffff;
--perf-bg-secondary: #f8f9fa;
--perf-bg-tertiary: #e9ecef;
--perf-text-primary: #212529;
--perf-text-secondary: #6c757d;
--perf-border: #dee2e6;
--perf-accent: #007bff;
--perf-success: #28a745;
--perf-warning: #ffc107;
--perf-danger: #dc3545;
}
.perf-debug-theme-dark {
--perf-bg-primary: #1a1a1a;
--perf-bg-secondary: #2d2d2d;
--perf-bg-tertiary: #404040;
--perf-text-primary: #ffffff;
--perf-text-secondary: #b0b0b0;
--perf-border: #404040;
--perf-accent: #0d6efd;
--perf-success: #198754;
--perf-warning: #fd7e14;
--perf-danger: #dc3545;
}
.perf-debug-container {
color-scheme: light dark;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
color: var(--perf-text-primary, black);
}
.perf-debug-container * {
box-sizing: border-box;
}
.perf-debug-toggle {
background: var(--perf-accent);
color: var(--perf-bg-primary);
border: none;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.perf-debug-toggle:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
}
.perf-debug-modal {
background: var(--perf-bg-primary);
border: 1px solid var(--perf-border);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
color: var(--perf-text-primary);
backdrop-filter: blur(10px);
min-width: 400px;
max-width: 80vw;
}
.perf-debug-header {
background: var(--perf-bg-secondary);
padding: 16px 20px;
border-radius: 12px 12px 0 0;
border-bottom: 1px solid var(--perf-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.perf-debug-title {
font-weight: 700;
font-size: 14px;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.perf-debug-controls {
display: flex;
align-items: center;
gap: 8px;
}
.perf-debug-theme-toggle {
background: var(--perf-bg-tertiary);
color: var(--perf-text-primary);
border: 1px solid var(--perf-border);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.perf-debug-theme-toggle:hover {
background: var(--perf-accent);
color: var(--perf-bg-primary);
}
.perf-debug-close {
background: var(--perf-danger);
color: white;
border: none;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s ease;
}
.perf-debug-close:hover {
background: #c82333;
transform: scale(1.05);
}
.perf-debug-content {
color: var(--perf-text-primary);
}
.perf-debug-content table {
color: var(--perf-text-primary);
background: var(--perf-bg-primary);
}
.perf-debug-content th {
background: var(--perf-bg-secondary) !important;
color: var(--perf-text-primary) !important;
border-color: var(--perf-border) !important;
}
.perf-debug-content td {
background: var(--perf-bg-primary) !important;
color: var(--perf-text-primary) !important;
border-color: var(--perf-border) !important;
}
.perf-debug-content tr:nth-child(even) td {
background: var(--perf-bg-secondary) !important;
}
</style>
<div id=\"performance-debug-report\" class=\"perf-debug-container perf-debug-theme-light\" style=\"position: relative; z-index: 999999;\">
<div style=\"position: fixed; bottom: 20px; right: 20px; z-index: 1000000;\">
<button onclick=\"togglePerformanceReport()\" class=\"perf-debug-toggle\">
<span>📊</span>
<span>Performance</span>
</button>
</div>
<div id=\"performance-report-content\" class=\"perf-debug-modal\" style=\"display: none; position: fixed; top: 20px; right: 20px; z-index: 1000000; max-height: 80vh; overflow-y: auto;\">
<div class=\"perf-debug-header\">
<h3 class=\"perf-debug-title\">
<span>⚡</span>
<span>Performance Debug</span>
</h3>
<div class=\"perf-debug-controls\">
<button onclick=\"toggleTheme()\" class=\"perf-debug-theme-toggle\" title=\"Toggle Dark/Light Mode\">
<span id=\"theme-icon\">🌙</span>
</button>
<button onclick=\"closePerformanceReport()\" class=\"perf-debug-close\">
</button>
</div>
</div>
<div class=\"perf-debug-content\">
{$reportHtml}
</div>
</div>
</div>
<script>
function togglePerformanceReport() {
const content = document.getElementById('performance-report-content');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
}
function closePerformanceReport() {
document.getElementById('performance-report-content').style.display = 'none';
}
function toggleTheme() {
const container = document.getElementById('performance-debug-report');
const themeIcon = document.getElementById('theme-icon');
const isDark = container.classList.contains('perf-debug-theme-dark');
if (isDark) {
container.classList.remove('perf-debug-theme-dark');
container.classList.add('perf-debug-theme-light');
themeIcon.textContent = '🌙';
localStorage.setItem('perf-debug-theme', 'light');
} else {
container.classList.remove('perf-debug-theme-light');
container.classList.add('perf-debug-theme-dark');
themeIcon.textContent = '☀️';
localStorage.setItem('perf-debug-theme', 'dark');
}
}
// Load saved theme preference
(function() {
const savedTheme = localStorage.getItem('perf-debug-theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const container = document.getElementById('performance-debug-report');
const themeIcon = document.getElementById('theme-icon');
const shouldUseDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
if (shouldUseDark) {
container.classList.remove('perf-debug-theme-light');
container.classList.add('perf-debug-theme-dark');
themeIcon.textContent = '☀️';
}
})();
</script>
<!-- End Performance Debug Report -->
";
}
private function addPerformanceHeaders(Headers $headers): Headers
{
$data = $this->reporter->generateReport('array');
$summary = $data['summary'];
$newHeaders = $headers
->with('X-Performance-Time', sprintf('%.2f ms', $summary['total_request_time_ms']))
->with('X-Performance-Memory', $this->formatBytes($summary['total_request_memory_bytes']))
->with('X-Performance-Peak-Memory', $this->formatBytes($summary['peak_memory_bytes']))
->with('X-Performance-Metrics-Count', (string) $summary['metrics_count']);
// Add category summaries
foreach ($data['categories'] as $categoryName => $categoryData) {
$headerName = 'X-Performance-' . ucfirst($categoryName);
$headerValue = sprintf('%.2f ms (%d calls)', $categoryData['total_time_ms'], $categoryData['total_calls']);
$newHeaders = $newHeaders->with($headerName, $headerValue);
}
return $newHeaders;
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
private function generateSessionDebugInfo(RequestStateManager $stateManager): string
{
$session = $stateManager->get('session');
if (! $session instanceof SessionInterface) {
return '<details style="margin: 20px;">
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
<span style="margin-right: 8px;">🔐</span>Session Debug Information
</summary>
<div style="padding: 20px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px; margin-top: 10px;">
<strong>⚠️ Session Debug:</strong> No session found in RequestStateManager
</div>
</details>';
}
$sessionData = $session->all();
$sessionId = $session->id->toString();
// Count form data and validation errors
$formDataCount = isset($sessionData['__form']) ? count($sessionData['__form']) : 0;
$validationErrorCount = isset($sessionData['__validation']) ? count($sessionData['__validation']) : 0;
// Build summary text
$summaryText = '🔐 Session Debug';
if ($formDataCount > 0) {
$summaryText .= ' • 📝 ' . $formDataCount . ' form(s)';
}
if ($validationErrorCount > 0) {
$summaryText .= ' • ❌ ' . $validationErrorCount . ' error(s)';
}
// Format session data as HTML
$html = '<details style="margin: 20px;">
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
' . $summaryText . '
</summary>
<div style="padding: 20px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 0 0 8px 8px; margin-top: -1px;">
<table style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px;">
<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; width: 200px;">Session ID</td>
<td style="padding: 8px; background: var(--perf-bg-primary); word-break: break-all;">' . htmlspecialchars($sessionId) . '</td>
</tr>
<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">Session Data</td>
<td style="padding: 8px; background: var(--perf-bg-primary);">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData, JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
</td>
</tr>';
// Check for form data specifically
if (isset($sessionData['__form'])) {
$html .= '<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">📝 Form Data</td>
<td style="padding: 8px; background: var(--perf-bg-primary);">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData['__form'], JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
</td>
</tr>';
}
// Check for validation errors
if (isset($sessionData['__validation'])) {
$html .= '<tr>
<td style="padding: 8px; background: var(--perf-bg-tertiary); font-weight: bold; vertical-align: top;">❌ Validation Errors</td>
<td style="padding: 8px; background: var(--perf-bg-primary);">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">' . htmlspecialchars(json_encode($sessionData['__validation'], JSON_PRETTY_PRINT) ?: 'Unable to encode') . '</pre>
</td>
</tr>';
}
$html .= '</table>
</div>
</details>';
return $html;
}
/**
* Generate hierarchical performance report HTML
*/
private function generateHierarchicalReport(): string
{
if (! $this->collector instanceof EnhancedPerformanceCollector) {
return '';
}
$hierarchicalData = $this->collector->getHierarchicalReport();
$executionTree = $this->collector->getExecutionTree();
$summary = $this->collector->getNestedSummary();
$html = '
<details style="margin: 20px;" open>
<summary style="cursor: pointer; padding: 12px 16px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 8px; font-weight: 600; color: var(--perf-text-primary);">
<span style="margin-right: 8px;">🌳</span>Performance Execution Tree
<span style="font-size: 11px; opacity: 0.7; margin-left: 8px;">
• Max Depth: ' . $summary['max_depth'] . '
• Total: ' . $summary['total_operations'] . ' ops
• Active: ' . $summary['active_operations'] . ' ops
</span>
</summary>
<div style="padding: 20px; background: var(--perf-bg-secondary); border: 1px solid var(--perf-border); border-radius: 0 0 8px 8px; margin-top: -1px;">
<!-- Execution Tree -->
<div style="margin-bottom: 20px;">
<h4 style="margin: 0 0 10px 0; color: var(--perf-text-primary); font-size: 13px; font-weight: 600;">
📊 Execution Tree (Text View)
</h4>
<pre style="
background: var(--perf-bg-primary);
border: 1px solid var(--perf-border);
border-radius: 6px;
padding: 16px;
margin: 0;
font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace;
font-size: 11px;
line-height: 1.4;
color: var(--perf-text-primary);
overflow-x: auto;
white-space: pre;
">' . htmlspecialchars($executionTree) . '</pre>
</div>
<!-- Hierarchical Table -->
<div>
<h4 style="margin: 0 0 10px 0; color: var(--perf-text-primary); font-size: 13px; font-weight: 600;">
📋 Hierarchical Operations (Table View)
</h4>
<div style="overflow-x: auto;">
' . $this->buildHierarchicalTable($hierarchicalData['operations'] ?? []) . '
</div>
</div>
<!-- Call Stack Info -->
' . $this->buildCallStackInfo() . '
</div>
</details>';
return $html;
}
/**
* Build hierarchical table from operations data
*/
private function buildHierarchicalTable(array $operations): string
{
if (empty($operations)) {
return '<p style="color: var(--perf-text-secondary); font-style: italic;">No hierarchical operations recorded.</p>';
}
$html = '
<table style="
width: 100%;
border-collapse: collapse;
font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace;
font-size: 11px;
background: var(--perf-bg-primary);
border: 1px solid var(--perf-border);
border-radius: 6px;
overflow: hidden;
">
<thead>
<tr style="background: var(--perf-bg-tertiary);">
<th style="padding: 10px 12px; text-align: left; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Operation</th>
<th style="padding: 10px 12px; text-align: right; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Total Time</th>
<th style="padding: 10px 12px; text-align: right; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Self Time</th>
<th style="padding: 10px 12px; text-align: center; font-weight: 600; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">Children</th>
</tr>
</thead>
<tbody>';
$html .= $this->buildHierarchicalRows($operations);
$html .= '
</tbody>
</table>';
return $html;
}
/**
* Build hierarchical table rows recursively
*/
private function buildHierarchicalRows(array $operations, int $depth = 0): string
{
$html = '';
foreach ($operations as $operation) {
$indent = str_repeat('&nbsp;&nbsp;', $depth * 2);
$depthIndicator = $depth > 0 ? str_repeat('┗', 1) . '━ ' : '';
$totalTime = $operation['total_time_ms'] ?? 0;
$selfTime = $operation['self_time_ms'] ?? 0;
$childrenCount = count($operation['children'] ?? []);
// Color coding for performance
$timeColor = $this->getTimeColor($totalTime);
$selfTimeColor = $this->getTimeColor($selfTime);
$html .= '
<tr style="' . ($depth % 2 === 1 ? 'background: var(--perf-bg-secondary);' : 'background: var(--perf-bg-primary);') . '">
<td style="padding: 8px 12px; color: var(--perf-text-primary); border-bottom: 1px solid var(--perf-border);">
<span style="font-family: monospace;">' . $indent . $depthIndicator . '</span>
<strong style="color: var(--perf-text-primary);">' . htmlspecialchars($operation['key']) . '</strong>
' . ($depth > 0 ? '<span style="opacity: 0.6; font-size: 10px; margin-left: 4px;">(depth: ' . $depth . ')</span>' : '') . '
</td>
<td style="padding: 8px 12px; text-align: right; color: ' . $timeColor . '; font-weight: 600; border-bottom: 1px solid var(--perf-border);">
' . ($totalTime > 0 ? number_format($totalTime, 2) . 'ms' : '—') . '
</td>
<td style="padding: 8px 12px; text-align: right; color: ' . $selfTimeColor . '; font-weight: 600; border-bottom: 1px solid var(--perf-border);">
' . ($selfTime > 0 ? number_format($selfTime, 2) . 'ms' : '—') . '
</td>
<td style="padding: 8px 12px; text-align: center; color: var(--perf-text-secondary); border-bottom: 1px solid var(--perf-border);">
' . ($childrenCount > 0 ? $childrenCount : '—') . '
</td>
</tr>';
// Recursively add children
if (! empty($operation['children'])) {
$html .= $this->buildHierarchicalRows($operation['children'], $depth + 1);
}
}
return $html;
}
/**
* Get color for time values based on performance thresholds
*/
private function getTimeColor(float $timeMs): string
{
if ($timeMs >= 1000) {
return 'var(--perf-danger)'; // Red for >= 1s
} elseif ($timeMs >= 500) {
return 'var(--perf-warning)'; // Orange for >= 500ms
} elseif ($timeMs >= 100) {
return '#fd7e14'; // Yellow for >= 100ms
} else {
return 'var(--perf-success)'; // Green for < 100ms
}
}
/**
* Build call stack information
*/
private function buildCallStackInfo(): string
{
if (! $this->collector instanceof EnhancedPerformanceCollector) {
return '';
}
$callStack = $this->collector->getCallStack();
$currentOp = $this->collector->getCurrentOperation();
$depth = $this->collector->getCallStackDepth();
if (empty($callStack)) {
return '
<div style="margin-top: 20px; padding: 12px 16px; background: var(--perf-bg-tertiary); border: 1px solid var(--perf-border); border-radius: 6px;">
<h4 style="margin: 0 0 8px 0; color: var(--perf-text-primary); font-size: 12px; font-weight: 600;">
📚 Call Stack Status
</h4>
<p style="margin: 0; color: var(--perf-success); font-size: 11px;">
✅ All operations completed (stack is empty)
</p>
</div>';
}
$stackHtml = '
<div style="margin-top: 20px; padding: 12px 16px; background: var(--perf-bg-tertiary); border: 1px solid var(--perf-border); border-radius: 6px;">
<h4 style="margin: 0 0 8px 0; color: var(--perf-text-primary); font-size: 12px; font-weight: 600;">
📚 Active Call Stack (Depth: ' . $depth . ')
</h4>
<div style="font-family: \'SF Mono\', Monaco, \'Cascadia Code\', \'Roboto Mono\', Consolas, \'Courier New\', monospace; font-size: 11px; line-height: 1.4;">';
foreach ($callStack as $i => $operation) {
$indent = str_repeat('&nbsp;&nbsp;', $i);
$isCurrentOp = $operation === $currentOp;
$stackHtml .= '
<div style="margin: 4px 0; color: var(--perf-text-primary);">
' . $indent . '
<span style="color: var(--perf-text-secondary);">' . ($i + 1) . '.</span>
<strong style="color: ' . ($isCurrentOp ? 'var(--perf-accent)' : 'var(--perf-text-primary)') . ';">' . htmlspecialchars($operation) . '</strong>
' . ($isCurrentOp ? '<span style="color: var(--perf-accent); font-size: 10px;"> ← CURRENT</span>' : '') . '
<span style="opacity: 0.6; font-size: 10px; margin-left: 8px;">(active)</span>
</div>';
}
$stackHtml .= '</div></div>';
return $stackHtml;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
#[MiddlewarePriorityAttribute(MiddlewarePriority::FIRST)]
final readonly class RequestPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::SYSTEM)) {
return $next($context);
}
$request = $context->request;
$path = $request->path ?? '/';
if ($this->config->shouldExcludePath($path)) {
return $next($context);
}
$requestKey = 'http_request';
$performanceContext = [
'method' => $request->method,
'path' => $path,
'user_agent' => $request->headers->getFirst('User-Agent', ''),
'ip' => $request->server->getClientIp()->value,
];
$this->collector->startTiming($requestKey, PerformanceCategory::SYSTEM, $performanceContext);
// Initialize memory monitoring
$memoryMonitor = new MemoryMonitor();
// Memory usage before request
$memoryBefore = $memoryMonitor->getCurrentMemory();
$this->collector->recordMetric(
'memory_before_request',
PerformanceCategory::SYSTEM,
$memoryBefore->toBytes(),
array_merge($performanceContext, [
'memory_human' => $memoryBefore->toHumanReadable(),
'memory_percentage' => $memoryMonitor->getMemoryUsagePercentage(),
])
);
try {
$result = $next($context);
// Record successful request
$this->collector->increment('requests_total', PerformanceCategory::SYSTEM, 1, $performanceContext);
$this->collector->increment('requests_success', PerformanceCategory::SYSTEM, 1, $performanceContext);
return $result;
} catch (\Throwable $e) {
// Record failed request
$this->collector->increment('requests_total', PerformanceCategory::SYSTEM, 1, $performanceContext);
$this->collector->increment('requests_failed', PerformanceCategory::SYSTEM, 1, $performanceContext);
$this->collector->recordMetric(
'request_error',
PerformanceCategory::SYSTEM,
1,
array_merge($performanceContext, ['error' => $e::class])
);
throw $e;
} finally {
$this->collector->endTiming($requestKey);
// Memory monitoring after request
$memoryAfter = $memoryMonitor->getCurrentMemory();
$memoryPeak = $memoryMonitor->getPeakMemory();
$memoryDiff = $memoryAfter->greaterThan($memoryBefore)
? $memoryAfter->subtract($memoryBefore)
: Byte::zero();
$this->collector->recordMetric(
'memory_after_request',
PerformanceCategory::SYSTEM,
$memoryAfter->toBytes(),
array_merge($performanceContext, [
'memory_human' => $memoryAfter->toHumanReadable(),
'memory_percentage' => $memoryMonitor->getMemoryUsagePercentage(),
])
);
if ($memoryDiff->isNotEmpty()) {
$this->collector->recordMetric(
'memory_usage_request',
PerformanceCategory::SYSTEM,
$memoryDiff->toBytes(),
array_merge($performanceContext, [
'memory_diff_human' => $memoryDiff->toHumanReadable(),
])
);
}
// Peak memory
$this->collector->recordMetric(
'memory_peak',
PerformanceCategory::SYSTEM,
$memoryPeak->toBytes(),
array_merge($performanceContext, [
'memory_peak_human' => $memoryPeak->toHumanReadable(),
'is_approaching_limit' => $memoryMonitor->isMemoryLimitApproaching(),
])
);
}
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace App\Framework\Performance\Middleware;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\PerformanceConfig;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ROUTING, 10)]
final readonly class RoutingPerformanceMiddleware implements HttpMiddleware
{
public function __construct(
private PerformanceCollectorInterface $collector,
private PerformanceConfig $config
) {
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
if (! $this->config->isTrackingEnabled(PerformanceCategory::ROUTING)) {
return $next($context);
}
$request = $context->request;
$path = $request->path ?? '/';
$method = $request->method;
$routingContext = [
'method' => $method,
'path' => $path,
];
$routingKey = 'route_resolution';
$this->collector->startTiming($routingKey, PerformanceCategory::ROUTING, $routingContext);
// Count routing attempts
$this->collector->increment('routing_attempts', PerformanceCategory::ROUTING, 1, $routingContext);
try {
$result = $next($context);
// Check if route was found (this is framework-specific logic)
$routeFound = $this->isRouteFound($result, $stateManager);
if ($routeFound) {
$this->collector->increment('routes_found', PerformanceCategory::ROUTING, 1, $routingContext);
// Get route pattern if available
$routePattern = $this->getRoutePattern($result, $stateManager);
if ($routePattern) {
$this->collector->increment(
"route_pattern_" . md5($routePattern),
PerformanceCategory::ROUTING,
1,
array_merge($routingContext, ['pattern' => $routePattern])
);
}
} else {
$this->collector->increment('routes_not_found', PerformanceCategory::ROUTING, 1, $routingContext);
}
return $result;
} catch (\Throwable $e) {
$this->collector->increment(
'routing_errors',
PerformanceCategory::ROUTING,
1,
array_merge($routingContext, ['error' => $e::class])
);
throw $e;
} finally {
$this->collector->endTiming($routingKey);
}
}
private function isRouteFound(MiddlewareContext $context, RequestStateManager $stateManager): bool
{
// Check various indicators that a route was found
// Method 1: Check state manager
if ($stateManager->has('route_found')) {
return $stateManager->get('route_found');
}
// Method 2: Check if controller info is available
if ($stateManager->has('controller_info')) {
return true;
}
// Method 3: Check response status (if not 404)
if (isset($context->response) && $context->response->status->value !== 404) {
return true;
}
// Default: assume found if we got here without exceptions
return true;
}
private function getRoutePattern(MiddlewareContext $context, RequestStateManager $stateManager): ?string
{
// Try to get route pattern from various sources
// Method 1: From state manager
$routeData = $stateManager->get('route_data');
if ($routeData && isset($routeData['pattern'])) {
return $routeData['pattern'];
}
// Method 2: From request route
if (isset($context->request->route['pattern'])) {
return $context->request->route['pattern'];
}
// Method 3: From matched route
$matchedRoute = $stateManager->get('matched_route');
if ($matchedRoute && isset($matchedRoute['pattern'])) {
return $matchedRoute['pattern'];
}
return null;
}
}