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,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling\Events;
use App\Framework\Database\Profiling\QueryProfile;
use App\Framework\Database\Profiling\SlowQueryAlert;
/**
* Event dispatched when a slow query is detected
*/
final readonly class SlowQueryDetectedEvent
{
public function __construct(
public SlowQueryAlert $alert,
public QueryProfile $profile
) {
}
/**
* Get event name for identification
*/
public function getEventName(): string
{
return 'database.slow_query_detected';
}
/**
* Convert to array for logging/serialization
*/
public function toArray(): array
{
return [
'event' => $this->getEventName(),
'alert' => $this->alert->toArray(),
'profile_id' => $this->profile->id,
];
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
/**
* Summary statistics for query profiling session
*/
final readonly class ProfileSummary
{
public function __construct(
public int $totalQueries = 0,
public int $slowQueries = 0,
public ?Duration $totalExecutionTime = null,
public ?Duration $averageExecutionTime = null,
public int $totalMemoryUsage = 0,
public int $averageMemoryUsage = 0,
public array $queryTypeDistribution = [],
public ?QueryProfile $slowestQuery = null
) {
}
/**
* Get slow query percentage
*/
public function getSlowQueryPercentage(): Percentage
{
return $this->totalQueries > 0
? Percentage::fromRatio($this->slowQueries, $this->totalQueries)
: Percentage::zero();
}
/**
* Get total memory usage as Byte object
*/
public function getTotalMemoryUsageBytes(): Byte
{
return Byte::fromBytes($this->totalMemoryUsage);
}
/**
* Get average memory usage as Byte object
*/
public function getAverageMemoryUsageBytes(): Byte
{
return Byte::fromBytes($this->averageMemoryUsage);
}
/**
* Get queries per second
*/
public function getQueriesPerSecond(): float
{
if ($this->totalExecutionTime === null || $this->totalExecutionTime->toSeconds() <= 0) {
return 0.0;
}
return $this->totalQueries / $this->totalExecutionTime->toSeconds();
}
/**
* Get most common query type
*/
public function getMostCommonQueryType(): ?string
{
if (empty($this->queryTypeDistribution)) {
return null;
}
$maxCount = 0;
$mostCommon = null;
foreach ($this->queryTypeDistribution as $type => $count) {
if ($count > $maxCount) {
$maxCount = $count;
$mostCommon = $type;
}
}
return $mostCommon;
}
/**
* Get query type distribution with percentages
*/
public function getQueryTypeDistributionWithPercentages(): array
{
if ($this->totalQueries === 0) {
return [];
}
$distribution = [];
foreach ($this->queryTypeDistribution as $type => $count) {
$distribution[$type] = [
'count' => $count,
'percentage' => Percentage::fromRatio($count, $this->totalQueries)->format(),
];
}
return $distribution;
}
/**
* Get performance assessment
*/
public function getPerformanceAssessment(): string
{
if ($this->totalQueries === 0) {
return 'no_data';
}
$slowPercentage = $this->getSlowQueryPercentage()->getValue();
$avgTimeMs = $this->averageExecutionTime?->toMilliseconds() ?? 0;
return match (true) {
$slowPercentage > 20 => 'poor',
$slowPercentage > 10 => 'needs_improvement',
$avgTimeMs > 100 => 'moderate',
$avgTimeMs > 50 => 'good',
default => 'excellent'
};
}
/**
* Get optimization recommendations
*/
public function getRecommendations(): array
{
$recommendations = [];
if ($this->totalQueries === 0) {
return ['No profiling data available'];
}
$slowPercentage = $this->getSlowQueryPercentage()->getValue();
$avgTimeMs = $this->averageExecutionTime?->toMilliseconds() ?? 0;
// Slow query recommendations
if ($slowPercentage > 20) {
$recommendations[] = "High percentage of slow queries ({$this->getSlowQueryPercentage()->format()}). Consider query optimization and indexing.";
} elseif ($slowPercentage > 10) {
$recommendations[] = "Moderate slow query rate ({$this->getSlowQueryPercentage()->format()}). Review and optimize slow queries.";
}
// Average execution time recommendations
if ($avgTimeMs > 200) {
$recommendations[] = "Average execution time is high ({$avgTimeMs}ms). Consider query optimization and caching.";
}
// Query distribution recommendations
$mostCommon = $this->getMostCommonQueryType();
if ($mostCommon === 'select' && isset($this->queryTypeDistribution['select'])) {
$selectCount = $this->queryTypeDistribution['select'];
$selectPercentage = ($selectCount / $this->totalQueries) * 100;
if ($selectPercentage > 80) {
$recommendations[] = "High SELECT query ratio ({$selectPercentage}%). Consider implementing caching strategies.";
}
}
// Memory usage recommendations
$avgMemoryMB = $this->averageMemoryUsage / (1024 * 1024);
if ($avgMemoryMB > 10) {
$recommendations[] = "High average memory usage per query ({$this->getAverageMemoryUsageBytes()->toHumanReadable()}). Review query complexity and data fetching strategies.";
}
// Positive feedback
if ($slowPercentage < 5 && $avgTimeMs < 50) {
$recommendations[] = "Excellent query performance! Keep monitoring and maintain current optimization strategies.";
}
return empty($recommendations)
? ['Query performance is within acceptable ranges.']
: $recommendations;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'total_queries' => $this->totalQueries,
'slow_queries' => $this->slowQueries,
'slow_query_percentage' => $this->getSlowQueryPercentage()->format(),
'total_execution_time_ms' => $this->totalExecutionTime?->toMilliseconds() ?? 0,
'total_execution_time_seconds' => $this->totalExecutionTime?->toSeconds() ?? 0,
'average_execution_time_ms' => $this->averageExecutionTime?->toMilliseconds() ?? 0,
'average_execution_time_seconds' => $this->averageExecutionTime?->toSeconds() ?? 0,
'queries_per_second' => round($this->getQueriesPerSecond(), 2),
'total_memory_usage' => $this->getTotalMemoryUsageBytes()->toHumanReadable(),
'total_memory_usage_bytes' => $this->totalMemoryUsage,
'average_memory_usage' => $this->getAverageMemoryUsageBytes()->toHumanReadable(),
'average_memory_usage_bytes' => $this->averageMemoryUsage,
'query_type_distribution' => $this->getQueryTypeDistributionWithPercentages(),
'most_common_query_type' => $this->getMostCommonQueryType(),
'performance_assessment' => $this->getPerformanceAssessment(),
'slowest_query' => $this->slowestQuery?->toArray(),
'recommendations' => $this->getRecommendations(),
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\ValueObjects\Duration;
/**
* Configuration for database profiling and logging
*/
final readonly class ProfilingConfig
{
public Duration $slowQueryThreshold;
public function __construct(
public bool $enabled = false,
public bool $logSlowQueriesOnly = false,
public bool $logParameters = true,
public bool $logStackTrace = false,
?Duration $slowQueryThreshold = null, // 1 second
public int $maxLoggedQueries = 1000,
public bool $enableSlowQueryDetection = true,
public bool $enableQueryAnalysis = true,
public array $sensitiveParameterPatterns = [
'password', 'pwd', 'pass', 'secret', 'token', 'api_key', 'key',
'auth', 'credential', 'salt', 'hash',
]
) {
$this->slowQueryThreshold = $slowQueryThreshold ?? Duration::fromSeconds(1.0);
}
/**
* Create default development configuration
*/
public static function development(): self
{
return new self(
enabled: true,
logSlowQueriesOnly: false,
logParameters: true,
logStackTrace: true,
slowQueryThreshold: Duration::fromSeconds(0.5),
maxLoggedQueries: 500,
enableSlowQueryDetection: true,
enableQueryAnalysis: true
);
}
/**
* Create production configuration
*/
public static function production(): self
{
return new self(
enabled: true,
logSlowQueriesOnly: true,
logParameters: false,
logStackTrace: false,
slowQueryThreshold: Duration::fromSeconds(1.0),
maxLoggedQueries: 100,
enableSlowQueryDetection: true,
enableQueryAnalysis: false // Disabled in production for performance
);
}
/**
* Create configuration for testing
*/
public static function testing(): self
{
return new self(
enabled: false, // Usually disabled in tests
logSlowQueriesOnly: false,
logParameters: false,
logStackTrace: false,
slowQueryThreshold: Duration::fromSeconds(5.0), // High threshold for tests
maxLoggedQueries: 10,
enableSlowQueryDetection: false,
enableQueryAnalysis: false
);
}
/**
* Check if feature is enabled based on environment
*/
public function isFeatureEnabled(string $feature): bool
{
if (! $this->enabled) {
return false;
}
return match ($feature) {
'slow_query_detection' => $this->enableSlowQueryDetection,
'query_analysis' => $this->enableQueryAnalysis,
'parameter_logging' => $this->logParameters,
'stack_trace' => $this->logStackTrace,
default => false
};
}
}

View File

@@ -0,0 +1,311 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ResultInterface;
/**
* Connection wrapper that adds profiling capabilities to any database connection
*/
final class ProfilingConnection implements ConnectionInterface
{
private bool $profilingEnabled = true;
public function __construct(
private readonly ConnectionInterface $connection,
private readonly QueryProfiler $profiler,
private readonly ?QueryLogger $logger = null
) {
}
/**
* Execute SQL with profiling
*/
public function execute(string $sql, array $parameters = []): int
{
if (! $this->profilingEnabled) {
return $this->connection->execute($sql, $parameters);
}
$profile = $this->profiler->profile(
$sql,
$parameters,
fn () => $this->connection->execute($sql, $parameters)
);
$this->logger?->logQuery($profile);
return $profile->affectedRows ?? 0;
}
/**
* Query with profiling
*/
public function query(string $sql, array $parameters = []): ResultInterface
{
if (! $this->profilingEnabled) {
return $this->connection->query($sql, $parameters);
}
$result = null;
$profile = $this->profiler->profile(
$sql,
$parameters,
function () use ($sql, $parameters, &$result) {
$result = $this->connection->query($sql, $parameters);
return $result;
}
);
$this->logger?->logQuery($profile);
return $result;
}
/**
* Query single row with profiling
*/
public function queryOne(string $sql, array $parameters = []): ?array
{
if (! $this->profilingEnabled) {
return $this->connection->queryOne($sql, $parameters);
}
$result = null;
$profile = $this->profiler->profile(
$sql,
$parameters,
function () use ($sql, $parameters, &$result) {
$result = $this->connection->queryOne($sql, $parameters);
return $result;
}
);
$this->logger?->logQuery($profile);
return $result;
}
/**
* Query column with profiling
*/
public function queryColumn(string $sql, array $parameters = []): array
{
if (! $this->profilingEnabled) {
return $this->connection->queryColumn($sql, $parameters);
}
$result = null;
$profile = $this->profiler->profile(
$sql,
$parameters,
function () use ($sql, $parameters, &$result) {
$result = $this->connection->queryColumn($sql, $parameters);
return $result;
}
);
$this->logger?->logQuery($profile);
return $result;
}
/**
* Query scalar with profiling
*/
public function queryScalar(string $sql, array $parameters = []): mixed
{
if (! $this->profilingEnabled) {
return $this->connection->queryScalar($sql, $parameters);
}
$result = null;
$profile = $this->profiler->profile(
$sql,
$parameters,
function () use ($sql, $parameters, &$result) {
$result = $this->connection->queryScalar($sql, $parameters);
return $result;
}
);
$this->logger?->logQuery($profile);
return $result;
}
/**
* Begin transaction (no profiling needed, but logged)
*/
public function beginTransaction(): void
{
if ($this->profilingEnabled && $this->logger) {
$profile = $this->profiler->profile(
'BEGIN TRANSACTION',
[],
fn () => $this->connection->beginTransaction()
);
$this->logger->logQuery($profile);
} else {
$this->connection->beginTransaction();
}
}
/**
* Commit transaction (no profiling needed, but logged)
*/
public function commit(): void
{
if ($this->profilingEnabled && $this->logger) {
$profile = $this->profiler->profile(
'COMMIT',
[],
fn () => $this->connection->commit()
);
$this->logger->logQuery($profile);
} else {
$this->connection->commit();
}
}
/**
* Rollback transaction (no profiling needed, but logged)
*/
public function rollback(): void
{
if ($this->profilingEnabled && $this->logger) {
$profile = $this->profiler->profile(
'ROLLBACK',
[],
fn () => $this->connection->rollback()
);
$this->logger->logQuery($profile);
} else {
$this->connection->rollback();
}
}
/**
* Check if in transaction
*/
public function inTransaction(): bool
{
return $this->connection->inTransaction();
}
/**
* Get last insert ID
*/
public function lastInsertId(): string
{
return $this->connection->lastInsertId();
}
/**
* Get underlying PDO connection
*/
public function getPdo(): \PDO
{
return $this->connection->getPdo();
}
/**
* Enable/disable profiling
*/
public function setProfilingEnabled(bool $enabled): void
{
$this->profilingEnabled = $enabled;
}
/**
* Check if profiling is enabled
*/
public function isProfilingEnabled(): bool
{
return $this->profilingEnabled;
}
/**
* Get the profiler instance
*/
public function getProfiler(): QueryProfiler
{
return $this->profiler;
}
/**
* Get the logger instance
*/
public function getLogger(): ?QueryLogger
{
return $this->logger;
}
/**
* Get the underlying connection
*/
public function getConnection(): ConnectionInterface
{
return $this->connection;
}
/**
* Get profiling statistics
*/
public function getProfilingStatistics(): array
{
$stats = [
'profiling_enabled' => $this->profilingEnabled,
'profiler_stats' => [
'total_profiles' => $this->profiler->getProfilesCount(),
'active_profiles' => $this->profiler->getActiveProfilesCount(),
'slow_query_threshold_ms' => $this->profiler->getSlowQueryThreshold() * 1000,
],
];
if ($this->logger) {
$stats['logger_stats'] = $this->logger->getLogStatistics();
}
return $stats;
}
/**
* Clear all profiling data
*/
public function clearProfilingData(): void
{
$this->profiler->clearProfiles();
$this->logger?->clearLog();
}
/**
* Get profiling summary
*/
public function getProfilingSummary(): ProfileSummary
{
return $this->profiler->getSummary();
}
/**
* Export profiling data
*/
public function exportProfilingData(string $format = 'json'): string
{
if (! $this->logger) {
throw new \RuntimeException('No logger configured for export');
}
return $this->logger->export($format);
}
}

View File

@@ -0,0 +1,368 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Database\Profiling\Reports\OptimizationReport;
use App\Framework\Database\Profiling\Reports\PerformanceReport;
use App\Framework\Database\Profiling\Reports\ProfilingReport;
use App\Framework\Database\Profiling\Reports\SlowQueryReport;
/**
* Dashboard for database profiling visualization and reporting
*/
final class ProfilingDashboard
{
private array $profilingConnections = [];
public function __construct()
{
}
/**
* Register a profiling connection for monitoring
*/
public function registerConnection(string $name, ProfilingConnection $connection): void
{
$this->profilingConnections[$name] = $connection;
}
/**
* Get overview data for all registered connections
*/
public function getOverview(): array
{
$overview = [
'total_connections' => count($this->profilingConnections),
'connections' => [],
'global_stats' => [
'total_queries' => 0,
'total_slow_queries' => 0,
'total_execution_time_ms' => 0,
'average_execution_time_ms' => 0,
],
];
$totalQueries = 0;
$totalSlowQueries = 0;
$totalExecutionTime = 0.0;
foreach ($this->profilingConnections as $name => $connection) {
$summary = $connection->getProfilingSummary();
$stats = $connection->getProfilingStatistics();
$connectionData = [
'name' => $name,
'enabled' => $connection->isProfilingEnabled(),
'queries_count' => $summary->totalQueries,
'slow_queries_count' => $summary->slowQueries,
'slow_query_percentage' => $summary->getSlowQueryPercentage()->format(),
'total_execution_time_ms' => $summary->totalExecutionTime?->toMilliseconds() ?? 0,
'average_execution_time_ms' => $summary->averageExecutionTime?->toMilliseconds() ?? 0,
'performance_assessment' => $summary->getPerformanceAssessment(),
'profiler_active_profiles' => $stats['profiler_stats']['active_profiles'] ?? 0,
];
$overview['connections'][$name] = $connectionData;
$totalQueries += $summary->totalQueries;
$totalSlowQueries += $summary->slowQueries;
$totalExecutionTime += $summary->totalExecutionTime?->toMilliseconds() ?? 0;
}
$overview['global_stats'] = [
'total_queries' => $totalQueries,
'total_slow_queries' => $totalSlowQueries,
'total_execution_time_ms' => $totalExecutionTime,
'average_execution_time_ms' => $totalQueries > 0 ? $totalExecutionTime / $totalQueries : 0,
'slow_query_percentage' => $totalQueries > 0 ? ($totalSlowQueries / $totalQueries) * 100 : 0,
];
return $overview;
}
/**
* Get detailed profiling report
*/
public function getProfilingReport(?string $connectionName = null): ProfilingReport
{
if ($connectionName && ! isset($this->profilingConnections[$connectionName])) {
throw new \InvalidArgumentException("Connection '$connectionName' not registered");
}
$connections = $connectionName
? [$connectionName => $this->profilingConnections[$connectionName]]
: $this->profilingConnections;
$allProfiles = [];
$connectionStats = [];
foreach ($connections as $name => $connection) {
$profiler = $connection->getProfiler();
$profiles = $profiler->getProfiles();
$summary = $profiler->getSummary();
$allProfiles = array_merge($allProfiles, $profiles);
$connectionStats[$name] = [
'summary' => $summary,
'statistics' => $connection->getProfilingStatistics(),
'profile_count' => count($profiles),
];
}
return new ProfilingReport($allProfiles, $connectionStats);
}
/**
* Get slow query report
*/
public function getSlowQueryReport(?string $connectionName = null): SlowQueryReport
{
if ($connectionName && ! isset($this->profilingConnections[$connectionName])) {
throw new \InvalidArgumentException("Connection '$connectionName' not registered");
}
$connections = $connectionName
? [$connectionName => $this->profilingConnections[$connectionName]]
: $this->profilingConnections;
$slowQueries = [];
foreach ($connections as $name => $connection) {
$profiler = $connection->getProfiler();
$connectionSlowQueries = $profiler->getSlowQueries();
foreach ($connectionSlowQueries as $profile) {
$slowQueries[] = [
'connection' => $name,
'profile' => $profile,
];
}
}
// Sort by execution time (slowest first)
usort($slowQueries, function ($a, $b) {
return $b['profile']->executionTime->toMilliseconds() <=>
$a['profile']->executionTime->toMilliseconds();
});
return new SlowQueryReport($slowQueries);
}
/**
* Get performance report
*/
public function getPerformanceReport(?string $connectionName = null): PerformanceReport
{
if ($connectionName && ! isset($this->profilingConnections[$connectionName])) {
throw new \InvalidArgumentException("Connection '$connectionName' not registered");
}
$connections = $connectionName
? [$connectionName => $this->profilingConnections[$connectionName]]
: $this->profilingConnections;
$performanceData = [];
foreach ($connections as $name => $connection) {
$summary = $connection->getProfilingSummary();
$performanceData[$name] = [
'total_queries' => $summary->totalQueries,
'total_execution_time' => $summary->totalExecutionTime,
'average_execution_time' => $summary->averageExecutionTime,
'queries_per_second' => $summary->getQueriesPerSecond(),
'memory_usage' => $summary->getTotalMemoryUsageBytes(),
'query_distribution' => $summary->getQueryTypeDistributionWithPercentages(),
'performance_assessment' => $summary->getPerformanceAssessment(),
'slowest_query' => $summary->slowestQuery,
];
}
return new PerformanceReport($performanceData);
}
/**
* Get optimization report with suggestions
*/
public function getOptimizationReport(?string $connectionName = null): OptimizationReport
{
if ($connectionName && ! isset($this->profilingConnections[$connectionName])) {
throw new \InvalidArgumentException("Connection '$connectionName' not registered");
}
$connections = $connectionName
? [$connectionName => $this->profilingConnections[$connectionName]]
: $this->profilingConnections;
$optimizationData = [];
foreach ($connections as $name => $connection) {
$summary = $connection->getProfilingSummary();
$recommendations = $summary->getRecommendations();
// Get profiler for more detailed analysis
$profiler = $connection->getProfiler();
$profiles = $profiler->getProfiles();
// Calculate additional optimization metrics
$slowQueryPatterns = [];
$indexSuggestions = [];
$performanceIssues = [];
foreach ($profiles as $profile) {
if ($profile->isSlow) {
$normalizedSql = $profile->getNormalizedSql();
$slowQueryPatterns[$normalizedSql] = ($slowQueryPatterns[$normalizedSql] ?? 0) + 1;
}
// Simple index suggestion based on WHERE clauses
if (preg_match('/WHERE\s+(\w+)\s*(=|<|>)/', $profile->sql, $matches)) {
$column = $matches[1];
$indexSuggestions[] = "Consider index on: {$column}";
}
if ($profile->executionTime->toSeconds() > 5.0) {
$performanceIssues[] = [
'type' => 'very_slow_query',
'profile_id' => $profile->id,
'execution_time_ms' => $profile->executionTime->toMilliseconds(),
'sql' => substr($profile->sql, 0, 100) . '...',
];
}
}
// Remove duplicate index suggestions
$indexSuggestions = array_unique($indexSuggestions);
$optimizationData[$name] = [
'recommendations' => $recommendations,
'slow_query_patterns' => $slowQueryPatterns,
'index_suggestions' => array_slice($indexSuggestions, 0, 10),
'performance_issues' => $performanceIssues,
'optimization_priority' => $this->calculateOptimizationPriority($summary),
'total_issues' => count($performanceIssues),
];
}
return new OptimizationReport($optimizationData);
}
/**
* Calculate optimization priority
*/
private function calculateOptimizationPriority(ProfileSummary $summary): string
{
$slowQueryPercentage = $summary->getSlowQueryPercentage()->getValue();
$avgTimeMs = $summary->averageExecutionTime?->toMilliseconds() ?? 0;
if ($slowQueryPercentage > 30 || $avgTimeMs > 2000) {
return 'critical';
}
if ($slowQueryPercentage > 15 || $avgTimeMs > 500) {
return 'high';
}
if ($slowQueryPercentage > 5 || $avgTimeMs > 100) {
return 'medium';
}
return 'low';
}
/**
* Export dashboard data in different formats
*/
public function export(string $format = 'json', ?string $connectionName = null): string
{
$data = [
'overview' => $this->getOverview(),
'profiling_report' => $this->getProfilingReport($connectionName)->toArray(),
'slow_query_report' => $this->getSlowQueryReport($connectionName)->toArray(),
'performance_report' => $this->getPerformanceReport($connectionName)->toArray(),
'optimization_report' => $this->getOptimizationReport($connectionName)->toArray(),
'export_timestamp' => date('c'),
];
return match (strtolower($format)) {
'json' => json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
'html' => $this->exportAsHtml($data),
default => throw new \InvalidArgumentException("Unsupported export format: $format")
};
}
/**
* Export dashboard as HTML
*/
private function exportAsHtml(array $data): string
{
$html = '<html><head><title>Database Profiling Dashboard</title>';
$html .= '<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.section { margin-bottom: 30px; }
.metric { display: inline-block; margin: 10px; padding: 10px; border: 1px solid #ccc; }
.critical { background-color: #ffebee; }
.warning { background-color: #fff3e0; }
.success { background-color: #e8f5e8; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style></head><body>';
$html .= '<h1>Database Profiling Dashboard</h1>';
$html .= '<p>Generated: ' . date('Y-m-d H:i:s') . '</p>';
// Overview section
$html .= '<div class="section"><h2>Overview</h2>';
$overview = $data['overview'];
$html .= "<p>Total Connections: {$overview['total_connections']}</p>";
$html .= "<p>Total Queries: {$overview['global_stats']['total_queries']}</p>";
$html .= "<p>Slow Queries: {$overview['global_stats']['total_slow_queries']}</p>";
$html .= '</div>';
// Performance metrics
$html .= '<div class="section"><h2>Connection Performance</h2>';
$html .= '<table><tr><th>Connection</th><th>Queries</th><th>Slow Queries</th><th>Avg Time (ms)</th><th>Status</th></tr>';
foreach ($overview['connections'] as $name => $conn) {
$statusClass = match($conn['performance_assessment']) {
'excellent', 'good' => 'success',
'fair', 'moderate' => 'warning',
default => 'critical'
};
$html .= "<tr class='{$statusClass}'>";
$html .= "<td>{$name}</td>";
$html .= "<td>{$conn['queries_count']}</td>";
$html .= "<td>{$conn['slow_queries_count']} ({$conn['slow_query_percentage']})</td>";
$html .= "<td>" . round($conn['average_execution_time_ms'], 2) . "</td>";
$html .= "<td>" . ucfirst($conn['performance_assessment']) . "</td>";
$html .= "</tr>";
}
$html .= '</table></div>';
$html .= '</body></html>';
return $html;
}
/**
* Clear all profiling data for registered connections
*/
public function clearAllProfilingData(): void
{
foreach ($this->profilingConnections as $connection) {
$connection->clearProfilingData();
}
}
/**
* Get list of registered connection names
*/
public function getRegisteredConnections(): array
{
return array_keys($this->profilingConnections);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
/**
* Query analysis result with optimization suggestions
*/
final readonly class QueryAnalysis
{
public function __construct(
public QueryProfile $profile,
public array $suggestions,
public array $issues,
public array $indexRecommendations,
public ?string $executionPlan,
public int $optimizationScore
) {
}
/**
* Get analysis grade based on optimization score
*/
public function getAnalysisGrade(): string
{
return match (true) {
$this->optimizationScore >= 90 => 'A',
$this->optimizationScore >= 80 => 'B',
$this->optimizationScore >= 70 => 'C',
$this->optimizationScore >= 60 => 'D',
default => 'F'
};
}
/**
* Get priority level for optimization
*/
public function getOptimizationPriority(): string
{
return match (true) {
$this->optimizationScore < 40 => 'critical',
$this->optimizationScore < 60 => 'high',
$this->optimizationScore < 75 => 'medium',
default => 'low'
};
}
/**
* Check if query needs immediate attention
*/
public function needsImmediateAttention(): bool
{
return $this->optimizationScore < 40 ||
count($this->issues) > 3 ||
$this->profile->executionTime->toSeconds() > 5.0;
}
/**
* Get top recommendations (most important)
*/
public function getTopRecommendations(int $limit = 3): array
{
// Prioritize issues over suggestions
$recommendations = [];
foreach ($this->issues as $issue) {
$recommendations[] = ['type' => 'issue', 'message' => $issue, 'priority' => 'high'];
}
foreach ($this->indexRecommendations as $recommendation) {
$recommendations[] = ['type' => 'index', 'message' => $recommendation, 'priority' => 'medium'];
}
foreach ($this->suggestions as $suggestion) {
$recommendations[] = ['type' => 'suggestion', 'message' => $suggestion, 'priority' => 'low'];
}
return array_slice($recommendations, 0, $limit);
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'profile_id' => $this->profile->id,
'query_type' => $this->profile->getQueryType(),
'execution_time_ms' => $this->profile->executionTime->toMilliseconds(),
'optimization_score' => $this->optimizationScore,
'analysis_grade' => $this->getAnalysisGrade(),
'optimization_priority' => $this->getOptimizationPriority(),
'needs_immediate_attention' => $this->needsImmediateAttention(),
'issues_count' => count($this->issues),
'suggestions_count' => count($this->suggestions),
'index_recommendations_count' => count($this->indexRecommendations),
'issues' => $this->issues,
'suggestions' => $this->suggestions,
'index_recommendations' => $this->indexRecommendations,
'top_recommendations' => $this->getTopRecommendations(),
'execution_plan' => $this->executionPlan,
'profile' => $this->profile->toArray(),
];
}
/**
* Get formatted summary
*/
public function getSummary(): string
{
return sprintf(
"Query Analysis [%s] - Score: %d/100, Issues: %d, Suggestions: %d, Priority: %s",
$this->getAnalysisGrade(),
$this->optimizationScore,
count($this->issues),
count($this->suggestions),
ucfirst($this->getOptimizationPriority())
);
}
}

View File

@@ -0,0 +1,385 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Database\ConnectionInterface;
/**
* Advanced query analysis and optimization suggestions
*/
final class QueryAnalyzer
{
public function __construct(
private readonly ConnectionInterface $connection
) {
}
/**
* Analyze query and provide optimization suggestions
*/
public function analyzeQuery(QueryProfile $profile): QueryAnalysis
{
$sql = $profile->sql;
$suggestions = [];
$issues = [];
$indexRecommendations = [];
// Basic SQL analysis
$this->analyzeSelectStatements($sql, $suggestions, $issues);
$this->analyzeWhereClause($sql, $suggestions, $indexRecommendations);
$this->analyzeJoins($sql, $suggestions, $issues);
$this->analyzeAggregations($sql, $suggestions);
$this->analyzeSubqueries($sql, $suggestions, $issues);
// Performance analysis based on execution metrics
$this->analyzePerformanceMetrics($profile, $suggestions, $issues);
// Try to get execution plan if possible
$executionPlan = $this->getExecutionPlan($sql);
return new QueryAnalysis(
profile: $profile,
suggestions: $suggestions,
issues: $issues,
indexRecommendations: $indexRecommendations,
executionPlan: $executionPlan,
optimizationScore: $this->calculateOptimizationScore($profile, $issues, $suggestions)
);
}
/**
* Analyze SELECT statements
*/
private function analyzeSelectStatements(string $sql, array &$suggestions, array &$issues): void
{
$upperSql = strtoupper($sql);
// Check for SELECT *
if (str_contains($upperSql, 'SELECT *')) {
$issues[] = 'Using SELECT * can fetch unnecessary columns and hurt performance';
$suggestions[] = 'Specify only the columns you need instead of using SELECT *';
}
// Check for DISTINCT usage
if (str_contains($upperSql, 'DISTINCT') && str_contains($upperSql, 'ORDER BY')) {
$suggestions[] = 'DISTINCT with ORDER BY can be expensive - consider if both are necessary';
}
// Check for functions in SELECT
$functions = ['COUNT', 'SUM', 'AVG', 'MAX', 'MIN'];
$functionCount = 0;
foreach ($functions as $function) {
$functionCount += substr_count($upperSql, $function);
}
if ($functionCount > 5) {
$suggestions[] = 'Multiple aggregate functions detected - consider if they can be optimized or cached';
}
}
/**
* Analyze WHERE clauses
*/
private function analyzeWhereClause(string $sql, array &$suggestions, array &$indexRecommendations): void
{
$upperSql = strtoupper($sql);
if (! str_contains($upperSql, 'WHERE')) {
if (str_starts_with($upperSql, 'SELECT')) {
$issues[] = 'SELECT without WHERE clause may result in full table scan';
$suggestions[] = 'Add WHERE clause to limit result set';
}
return;
}
// Check for functions in WHERE clause
if (preg_match('/WHERE\s+\w+\([^)]+\)\s*(=|<|>|<=|>=)/', $upperSql)) {
$issues[] = 'Functions in WHERE clause prevent index usage';
$suggestions[] = 'Avoid functions on columns in WHERE clause to enable index usage';
}
// Check for LIKE with leading wildcard
if (preg_match('/LIKE\s+\'%[^%]*\'/', $upperSql)) {
$issues[] = 'LIKE with leading wildcard prevents index usage';
$suggestions[] = 'Avoid leading wildcards in LIKE patterns for better performance';
}
// Extract potential index candidates from WHERE clause
if (preg_match_all('/WHERE\s+(\w+)\s*(=|<|>|<=|>=|IN)/', $upperSql, $matches)) {
foreach ($matches[1] as $column) {
$indexRecommendations[] = "Consider index on column: {$column}";
}
}
// Check for OR conditions
if (str_contains($upperSql, ' OR ')) {
$suggestions[] = 'OR conditions can be slower than UNION - consider rewriting if appropriate';
}
}
/**
* Analyze JOIN clauses
*/
private function analyzeJoins(string $sql, array &$suggestions, array &$issues): void
{
$upperSql = strtoupper($sql);
$joinCount = substr_count($upperSql, 'JOIN');
if ($joinCount === 0) {
return;
}
if ($joinCount > 5) {
$issues[] = "High number of JOINs ({$joinCount}) may impact performance";
$suggestions[] = 'Consider denormalization or caching for queries with many JOINs';
}
// Check for Cartesian products (JOIN without ON)
$onCount = substr_count($upperSql, ' ON ');
if ($joinCount > $onCount) {
$issues[] = 'Potential Cartesian product detected - missing JOIN conditions';
$suggestions[] = 'Ensure all JOINs have proper ON conditions';
}
// Check for table order in JOINs
if (str_contains($upperSql, 'LEFT JOIN') || str_contains($upperSql, 'RIGHT JOIN')) {
$suggestions[] = 'Consider JOIN order - start with the most selective table';
}
}
/**
* Analyze aggregation functions
*/
private function analyzeAggregations(string $sql, array &$suggestions): void
{
$upperSql = strtoupper($sql);
if (str_contains($upperSql, 'GROUP BY')) {
if (! str_contains($upperSql, 'ORDER BY')) {
$suggestions[] = 'GROUP BY without ORDER BY - consider if ordering is needed';
}
// Check for GROUP BY with many columns
$groupByMatches = [];
if (preg_match('/GROUP BY\s+(.+?)(?:\s+ORDER|\s+HAVING|\s*$)/i', $sql, $groupByMatches)) {
$columns = explode(',', $groupByMatches[1]);
if (count($columns) > 3) {
$suggestions[] = 'GROUP BY with many columns can be expensive - verify all are necessary';
}
}
}
if (str_contains($upperSql, 'HAVING')) {
if (str_contains($upperSql, 'WHERE')) {
$suggestions[] = 'Move non-aggregate conditions from HAVING to WHERE for better performance';
}
}
}
/**
* Analyze subqueries
*/
private function analyzeSubqueries(string $sql, array &$suggestions, array &$issues): void
{
$selectCount = substr_count(strtoupper($sql), 'SELECT');
if ($selectCount <= 1) {
return;
}
$subqueryCount = $selectCount - 1;
if ($subqueryCount > 2) {
$issues[] = "Multiple subqueries ({$subqueryCount}) detected";
$suggestions[] = 'Consider rewriting subqueries as JOINs for better performance';
}
// Check for correlated subqueries (simplified detection)
if (str_contains(strtoupper($sql), 'WHERE EXISTS') ||
str_contains(strtoupper($sql), 'WHERE NOT EXISTS')) {
$suggestions[] = 'Correlated subqueries can be expensive - consider JOIN alternatives';
}
}
/**
* Analyze performance metrics
*/
private function analyzePerformanceMetrics(QueryProfile $profile, array &$suggestions, array &$issues): void
{
$executionTimeMs = $profile->executionTime->toMilliseconds();
$memoryUsageMB = $profile->memoryUsage / (1024 * 1024);
if ($executionTimeMs > 5000) {
$issues[] = 'Very slow execution time (>5 seconds)';
$suggestions[] = 'Consider breaking this query into smaller parts or adding appropriate indexes';
} elseif ($executionTimeMs > 1000) {
$issues[] = 'Slow execution time (>1 second)';
$suggestions[] = 'Review query optimization opportunities';
}
if ($memoryUsageMB > 50) {
$issues[] = 'High memory usage (>50MB)';
$suggestions[] = 'Consider limiting result set size or using pagination';
} elseif ($memoryUsageMB > 10) {
$suggestions[] = 'Moderate memory usage detected - monitor if consistent';
}
if ($profile->getComplexityScore() > 15) {
$issues[] = 'High query complexity score';
$suggestions[] = 'Consider simplifying the query or breaking it into multiple queries';
}
}
/**
* Get execution plan (simplified - database specific)
*/
private function getExecutionPlan(string $sql): ?string
{
try {
// Try MySQL EXPLAIN
$explainSql = "EXPLAIN " . $sql;
$result = $this->connection->query($explainSql);
$plan = '';
while ($row = $result->fetch()) {
$plan .= print_r($row, true) . "\n";
}
return $plan ?: null;
} catch (\Throwable) {
// If EXPLAIN fails, try other database-specific approaches
try {
// Try PostgreSQL EXPLAIN
$explainSql = "EXPLAIN (FORMAT JSON) " . $sql;
$result = $this->connection->queryScalar($explainSql);
return is_string($result) ? $result : null;
} catch (\Throwable) {
// Return null if we can't get execution plan
return null;
}
}
}
/**
* Calculate optimization score (0-100, higher is better)
*/
private function calculateOptimizationScore(QueryProfile $profile, array $issues, array $suggestions): int
{
$baseScore = 100;
// Penalize execution time
$executionTimeMs = $profile->executionTime->toMilliseconds();
if ($executionTimeMs > 5000) {
$baseScore -= 40;
} elseif ($executionTimeMs > 1000) {
$baseScore -= 25;
} elseif ($executionTimeMs > 500) {
$baseScore -= 15;
} elseif ($executionTimeMs > 100) {
$baseScore -= 10;
}
// Penalize memory usage
$memoryUsageMB = $profile->memoryUsage / (1024 * 1024);
if ($memoryUsageMB > 50) {
$baseScore -= 20;
} elseif ($memoryUsageMB > 10) {
$baseScore -= 10;
} elseif ($memoryUsageMB > 5) {
$baseScore -= 5;
}
// Penalize complexity
$complexityScore = $profile->getComplexityScore();
if ($complexityScore > 15) {
$baseScore -= 15;
} elseif ($complexityScore > 10) {
$baseScore -= 10;
} elseif ($complexityScore > 5) {
$baseScore -= 5;
}
// Penalize issues and suggestions
$baseScore -= count($issues) * 5;
$baseScore -= count($suggestions) * 2;
return max(0, min(100, $baseScore));
}
/**
* Batch analyze multiple profiles
*/
public function batchAnalyze(array $profiles): array
{
$analyses = [];
foreach ($profiles as $profile) {
$analyses[] = $this->analyzeQuery($profile);
}
return $analyses;
}
/**
* Get optimization summary for multiple analyses
*/
public function getOptimizationSummary(array $analyses): array
{
if (empty($analyses)) {
return [];
}
$totalScore = 0;
$totalIssues = 0;
$totalSuggestions = 0;
$commonIssues = [];
$commonSuggestions = [];
foreach ($analyses as $analysis) {
$totalScore += $analysis->optimizationScore;
$totalIssues += count($analysis->issues);
$totalSuggestions += count($analysis->suggestions);
foreach ($analysis->issues as $issue) {
$commonIssues[$issue] = ($commonIssues[$issue] ?? 0) + 1;
}
foreach ($analysis->suggestions as $suggestion) {
$commonSuggestions[$suggestion] = ($commonSuggestions[$suggestion] ?? 0) + 1;
}
}
// Sort by frequency
arsort($commonIssues);
arsort($commonSuggestions);
return [
'total_queries_analyzed' => count($analyses),
'average_optimization_score' => round($totalScore / count($analyses)),
'total_issues' => $totalIssues,
'total_suggestions' => $totalSuggestions,
'most_common_issues' => array_slice($commonIssues, 0, 5, true),
'most_common_suggestions' => array_slice($commonSuggestions, 0, 5, true),
'overall_assessment' => $this->getOverallAssessment($totalScore / count($analyses)),
];
}
/**
* Get overall assessment based on average score
*/
private function getOverallAssessment(float $averageScore): string
{
return match (true) {
$averageScore >= 90 => 'excellent',
$averageScore >= 75 => 'good',
$averageScore >= 60 => 'fair',
$averageScore >= 40 => 'poor',
default => 'critical'
};
}
}

View File

@@ -0,0 +1,412 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Logging\Logger;
use App\Framework\Logging\LogLevel;
/**
* Query logger with multiple output formats and filtering
*/
final class QueryLogger
{
private array $loggedQueries = [];
private bool $logSlowQueriesOnly;
private bool $logParameters;
private bool $logStackTrace;
private int $maxLoggedQueries;
public function __construct(
private readonly Logger $logger,
private readonly Duration $slowQueryThreshold,
bool $logSlowQueriesOnly = false,
bool $logParameters = true,
bool $logStackTrace = false,
int $maxLoggedQueries = 1000
) {
$this->logSlowQueriesOnly = $logSlowQueriesOnly;
$this->logParameters = $logParameters;
$this->logStackTrace = $logStackTrace;
$this->maxLoggedQueries = $maxLoggedQueries;
}
/**
* Create with default threshold
*/
public static function withDefaults(Logger $logger): self
{
return new self(
$logger,
Duration::fromSeconds(1.0), // 1 second
);
}
/**
* Log a query profile
*/
public function logQuery(QueryProfile $profile): void
{
// Skip if only logging slow queries and this isn't slow
if ($this->logSlowQueriesOnly && ! $profile->isSlow) {
return;
}
// Maintain maximum logged queries
if (count($this->loggedQueries) >= $this->maxLoggedQueries) {
array_shift($this->loggedQueries);
}
$this->loggedQueries[] = $profile;
// Log to system logger
$this->writeToSystemLog($profile);
}
/**
* Write profile to system log
*/
private function writeToSystemLog(QueryProfile $profile): void
{
$level = $this->getLogLevel($profile);
$message = $this->formatLogMessage($profile);
$context = $this->buildLogContext($profile);
$this->logger->log($level, $message, $context);
}
/**
* Determine log level based on profile
*/
private function getLogLevel(QueryProfile $profile): LogLevel
{
if ($profile->error !== null) {
return LogLevel::ERROR;
}
if ($profile->isSlow) {
return LogLevel::WARNING;
}
if ($profile->executionTime->toMilliseconds() > 100) {
return LogLevel::INFO;
}
return LogLevel::DEBUG;
}
/**
* Format log message
*/
private function formatLogMessage(QueryProfile $profile): string
{
$type = strtoupper($profile->getQueryType());
$time = $profile->executionTime->toMilliseconds();
$message = "[{$type}] Query executed in {$time}ms";
if ($profile->affectedRows !== null) {
$message .= " ({$profile->affectedRows} rows affected)";
}
if ($profile->isSlow) {
$message .= " [SLOW QUERY]";
}
if ($profile->error) {
$message .= " [ERROR]";
}
return $message;
}
/**
* Build log context
*/
private function buildLogContext(QueryProfile $profile): array
{
$context = [
'profile_id' => $profile->id,
'sql' => $profile->sql,
'normalized_sql' => $profile->getNormalizedSql(),
'execution_time_ms' => $profile->executionTime->toMilliseconds(),
'memory_usage_bytes' => $profile->memoryUsage,
'query_type' => $profile->getQueryType(),
'is_slow' => $profile->isSlow,
'performance_rating' => $profile->getPerformanceRating(),
'complexity_score' => $profile->getComplexityScore(),
];
if ($this->logParameters && ! empty($profile->parameters)) {
$context['parameters'] = $this->sanitizeParameters($profile->parameters);
}
if ($profile->affectedRows !== null) {
$context['affected_rows'] = $profile->affectedRows;
}
if ($profile->error !== null) {
$context['error'] = $profile->error;
}
if ($this->logStackTrace) {
$context['stack_trace'] = $this->captureStackTrace();
}
return $context;
}
/**
* Sanitize parameters for logging (remove sensitive data)
*/
private function sanitizeParameters(array $parameters): array
{
$sanitized = [];
$sensitivePatterns = [
'password', 'pwd', 'pass', 'secret', 'token', 'api_key', 'key',
'auth', 'credential', 'salt', 'hash',
];
foreach ($parameters as $key => $value) {
$keyLower = strtolower((string) $key);
$isSensitive = false;
foreach ($sensitivePatterns as $pattern) {
if (str_contains($keyLower, $pattern)) {
$isSensitive = true;
break;
}
}
if ($isSensitive) {
$sanitized[$key] = '[REDACTED]';
} else {
// Limit length of logged values
if (is_string($value) && strlen($value) > 100) {
$sanitized[$key] = substr($value, 0, 100) . '...';
} else {
$sanitized[$key] = $value;
}
}
}
return $sanitized;
}
/**
* Capture stack trace (excluding internal profiling calls)
*/
private function captureStackTrace(): array
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Remove internal profiling frames
$filtered = [];
$skipClasses = [
'App\\Framework\\Database\\Profiling\\QueryLogger',
'App\\Framework\\Database\\Profiling\\QueryProfiler',
'App\\Framework\\Database\\Profiling\\ProfilingConnection',
];
foreach ($trace as $frame) {
if (isset($frame['class']) && in_array($frame['class'], $skipClasses)) {
continue;
}
$filtered[] = [
'file' => $frame['file'] ?? 'unknown',
'line' => $frame['line'] ?? 0,
'function' => $frame['function'] ?? 'unknown',
'class' => $frame['class'] ?? null,
];
// Limit stack trace depth
if (count($filtered) >= 10) {
break;
}
}
return $filtered;
}
/**
* Get all logged queries
*/
public function getLoggedQueries(): array
{
return $this->loggedQueries;
}
/**
* Get slow queries
*/
public function getSlowQueries(): array
{
return array_filter($this->loggedQueries, fn (QueryProfile $profile) => $profile->isSlow);
}
/**
* Export queries to different formats
*/
public function export(string $format = 'json'): string
{
return match (strtolower($format)) {
'json' => $this->exportAsJson(),
'csv' => $this->exportAsCsv(),
'html' => $this->exportAsHtml(),
'sql' => $this->exportAsSql(),
default => throw new \InvalidArgumentException("Unsupported export format: $format")
};
}
/**
* Export as JSON
*/
private function exportAsJson(): string
{
$data = [
'metadata' => [
'total_queries' => count($this->loggedQueries),
'slow_queries' => count($this->getSlowQueries()),
'export_timestamp' => date('c'),
'slow_query_threshold_ms' => $this->slowQueryThreshold->toMilliseconds(),
],
'queries' => array_map(fn (QueryProfile $profile) => $profile->toArray(), $this->loggedQueries),
];
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
/**
* Export as CSV
*/
private function exportAsCsv(): string
{
$csv = "ID,SQL,Query Type,Execution Time (ms),Memory Usage (bytes),Affected Rows,Is Slow,Error\n";
foreach ($this->loggedQueries as $profile) {
$csv .= sprintf(
"%s,\"%s\",%s,%s,%s,%s,%s,\"%s\"\n",
$profile->id,
str_replace('"', '""', $profile->sql),
$profile->getQueryType(),
$profile->executionTime->toMilliseconds(),
$profile->memoryUsage,
$profile->affectedRows ?? '',
$profile->isSlow ? 'Yes' : 'No',
str_replace('"', '""', $profile->error ?? '')
);
}
return $csv;
}
/**
* Export as HTML
*/
private function exportAsHtml(): string
{
$html = '<html><head><title>Query Log</title><style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.slow { background-color: #ffe6e6; }
.error { background-color: #ffcccc; }
</style></head><body>';
$html .= '<h1>Database Query Log</h1>';
$html .= '<p>Total Queries: ' . count($this->loggedQueries) . '</p>';
$html .= '<p>Slow Queries: ' . count($this->getSlowQueries()) . '</p>';
$html .= '<table><tr>
<th>Time</th><th>Type</th><th>Duration</th><th>SQL</th><th>Memory</th><th>Rows</th>
</tr>';
foreach ($this->loggedQueries as $profile) {
$rowClass = '';
if ($profile->error) {
$rowClass = 'error';
} elseif ($profile->isSlow) {
$rowClass = 'slow';
}
$html .= sprintf(
'<tr class="%s"><td>%s</td><td>%s</td><td>%s ms</td><td>%s</td><td>%s</td><td>%s</td></tr>',
$rowClass,
$profile->startTimestamp->format('H:i:s.u'),
strtoupper($profile->getQueryType()),
$profile->executionTime->toMilliseconds(),
htmlspecialchars(substr($profile->sql, 0, 100) . '...'),
$profile->getMemoryUsageBytes()->toHumanReadable(),
$profile->affectedRows ?? '-'
);
}
$html .= '</table></body></html>';
return $html;
}
/**
* Export queries as SQL
*/
private function exportAsSql(): string
{
$sql = "-- Database Query Log Export\n";
$sql .= "-- Generated: " . date('c') . "\n";
$sql .= "-- Total Queries: " . count($this->loggedQueries) . "\n\n";
foreach ($this->loggedQueries as $profile) {
$sql .= "-- Query ID: {$profile->id}\n";
$sql .= "-- Execution Time: {$profile->executionTime->toMilliseconds()}ms\n";
$sql .= "-- Query Type: " . strtoupper($profile->getQueryType()) . "\n";
if ($profile->isSlow) {
$sql .= "-- WARNING: SLOW QUERY\n";
}
if ($profile->error) {
$sql .= "-- ERROR: {$profile->error}\n";
}
$sql .= $profile->sql . ";\n\n";
}
return $sql;
}
/**
* Clear logged queries
*/
public function clearLog(): void
{
$this->loggedQueries = [];
}
/**
* Get log statistics
*/
public function getLogStatistics(): array
{
$totalQueries = count($this->loggedQueries);
$slowQueries = count($this->getSlowQueries());
return [
'total_queries' => $totalQueries,
'slow_queries' => $slowQueries,
'slow_query_percentage' => $totalQueries > 0 ? ($slowQueries / $totalQueries) * 100 : 0,
'max_logged_queries' => $this->maxLoggedQueries,
'log_slow_queries_only' => $this->logSlowQueriesOnly,
'log_parameters' => $this->logParameters,
'log_stack_trace' => $this->logStackTrace,
'slow_query_threshold_ms' => $this->slowQueryThreshold->toMilliseconds(),
];
}
}

View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\Events\Timestamp;
/**
* Immutable query profile with execution metrics
*/
final readonly class QueryProfile
{
public function __construct(
public string $id,
public string $sql,
public array $parameters,
public Duration $executionTime,
public Timestamp $startTimestamp,
public Timestamp $endTimestamp,
public int $memoryUsage,
public int $peakMemoryUsage,
public ?int $affectedRows = null,
public ?string $error = null,
public bool $isSlow = false
) {
}
/**
* Get query type (SELECT, INSERT, UPDATE, etc.)
*/
public function getQueryType(): string
{
$sql = trim(strtoupper($this->sql));
$writeOperations = ['INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'REPLACE'];
foreach ($writeOperations as $operation) {
if (str_starts_with($sql, $operation)) {
return strtolower($operation);
}
}
if (str_starts_with($sql, 'SELECT')) {
return 'select';
}
if (str_starts_with($sql, 'SHOW')) {
return 'show';
}
if (str_starts_with($sql, 'DESCRIBE') || str_starts_with($sql, 'DESC')) {
return 'describe';
}
if (str_starts_with($sql, 'EXPLAIN')) {
return 'explain';
}
return 'other';
}
/**
* Check if query was successful
*/
public function isSuccessful(): bool
{
return $this->error === null;
}
/**
* Check if query is a read operation
*/
public function isReadQuery(): bool
{
return in_array($this->getQueryType(), ['select', 'show', 'describe', 'explain']);
}
/**
* Check if query is a write operation
*/
public function isWriteQuery(): bool
{
return in_array($this->getQueryType(), ['insert', 'update', 'delete', 'create', 'alter', 'drop', 'truncate', 'replace']);
}
/**
* Get memory usage as Byte object
*/
public function getMemoryUsageBytes(): Byte
{
return Byte::fromBytes($this->memoryUsage);
}
/**
* Get peak memory usage as Byte object
*/
public function getPeakMemoryUsageBytes(): Byte
{
return Byte::fromBytes($this->peakMemoryUsage);
}
/**
* Get normalized SQL (for grouping similar queries)
*/
public function getNormalizedSql(): string
{
// Remove extra whitespace and normalize
$normalized = preg_replace('/\s+/', ' ', trim($this->sql));
// Replace parameter placeholders and values with ? for grouping
$normalized = preg_replace('/\$\d+/', '?', $normalized);
$normalized = preg_replace('/(=|IN|LIKE)\s*([\'"][^\'\"]*[\'"]|\d+(\.\d+)?)/i', '$1 ?', $normalized);
return $normalized;
}
/**
* Get query complexity score (simplified heuristic)
*/
public function getComplexityScore(): int
{
$sql = strtoupper($this->sql);
$score = 1;
// Count JOINs
$score += substr_count($sql, 'JOIN');
// Count subqueries
$score += substr_count($sql, 'SELECT') - 1; // Subtract main SELECT
// Count WHERE conditions
$score += substr_count($sql, 'WHERE');
$score += substr_count($sql, 'AND');
$score += substr_count($sql, 'OR');
// Count aggregations
$score += substr_count($sql, 'GROUP BY');
$score += substr_count($sql, 'ORDER BY');
$score += substr_count($sql, 'HAVING');
// Count functions
$functions = ['COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'CONCAT', 'SUBSTRING'];
foreach ($functions as $func) {
$score += substr_count($sql, $func);
}
return max(1, $score);
}
/**
* Estimate query performance rating
*/
public function getPerformanceRating(): string
{
$timeMs = $this->executionTime->toMilliseconds();
return match (true) {
$timeMs < 10 => 'excellent',
$timeMs < 100 => 'good',
$timeMs < 500 => 'acceptable',
$timeMs < 1000 => 'slow',
default => 'very_slow'
};
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'id' => $this->id,
'sql' => $this->sql,
'normalized_sql' => $this->getNormalizedSql(),
'parameters' => $this->parameters,
'query_type' => $this->getQueryType(),
'execution_time_ms' => $this->executionTime->toMilliseconds(),
'execution_time_seconds' => $this->executionTime->toSeconds(),
'start_time' => $this->startTimestamp->format('Y-m-d H:i:s.u'),
'end_time' => $this->endTimestamp->format('Y-m-d H:i:s.u'),
'memory_usage' => $this->getMemoryUsageBytes()->toHumanReadable(),
'memory_usage_bytes' => $this->memoryUsage,
'peak_memory_usage' => $this->getPeakMemoryUsageBytes()->toHumanReadable(),
'peak_memory_usage_bytes' => $this->peakMemoryUsage,
'affected_rows' => $this->affectedRows,
'error' => $this->error,
'is_slow' => $this->isSlow,
'is_successful' => $this->isSuccessful(),
'is_read_query' => $this->isReadQuery(),
'is_write_query' => $this->isWriteQuery(),
'complexity_score' => $this->getComplexityScore(),
'performance_rating' => $this->getPerformanceRating(),
];
}
/**
* Get formatted summary string
*/
public function getSummary(): string
{
$type = strtoupper($this->getQueryType());
$time = $this->executionTime->toMilliseconds();
$rating = ucfirst($this->getPerformanceRating());
$summary = "[$type] {$time}ms - {$rating}";
if ($this->affectedRows !== null) {
$summary .= " ({$this->affectedRows} rows)";
}
if ($this->error) {
$summary .= " - ERROR: " . substr($this->error, 0, 50);
}
return $summary;
}
}

View File

@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\Events\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\Performance\MemoryMonitor;
/**
* Query performance profiler with detailed metrics collection
*/
final class QueryProfiler
{
private array $profiles = [];
private array $activeProfiles = [];
private int $profileCounter = 0;
public function __construct(
private readonly Clock $clock,
private readonly MemoryMonitor $memoryMonitor,
private readonly float $slowQueryThreshold = 1.0 // 1 second
) {
}
/**
* Start profiling a query
*/
public function startProfile(string $sql, array $parameters = []): string
{
$profileId = 'profile_' . ++$this->profileCounter . '_' . uniqid();
$this->activeProfiles[$profileId] = [
'sql' => $sql,
'parameters' => $parameters,
'start_time' => $this->clock->time(),
'start_timestamp' => Timestamp::fromClock($this->clock),
'memory_start' => $this->memoryMonitor->getCurrentMemory()->toBytes(),
'peak_memory_start' => $this->memoryMonitor->getPeakMemory()->toBytes(),
];
return $profileId;
}
/**
* End profiling and record results
*/
public function endProfile(string $profileId, ?int $affectedRows = null, ?string $error = null): QueryProfile
{
if (! isset($this->activeProfiles[$profileId])) {
throw new \InvalidArgumentException("Profile ID '$profileId' not found");
}
$activeProfile = $this->activeProfiles[$profileId];
$endTime = $this->clock->time();
$endTimestamp = Timestamp::fromClock($this->clock);
$executionTime = $endTime->diff($activeProfile['start_time'])->toSeconds();
$memoryEnd = $this->memoryMonitor->getCurrentMemory()->toBytes();
$peakMemoryEnd = $this->memoryMonitor->getPeakMemory()->toBytes();
$profile = new QueryProfile(
id: $profileId,
sql: $activeProfile['sql'],
parameters: $activeProfile['parameters'],
executionTime: Duration::fromSeconds($executionTime),
startTimestamp: $activeProfile['start_timestamp'],
endTimestamp: $endTimestamp,
memoryUsage: $memoryEnd - $activeProfile['memory_start'],
peakMemoryUsage: max(0, $peakMemoryEnd - $activeProfile['peak_memory_start']),
affectedRows: $affectedRows,
error: $error,
isSlow: $executionTime > $this->slowQueryThreshold
);
$this->profiles[$profileId] = $profile;
unset($this->activeProfiles[$profileId]);
return $profile;
}
/**
* Profile a callable with automatic timing
*/
public function profile(string $sql, array $parameters, callable $execution): QueryProfile
{
$profileId = $this->startProfile($sql, $parameters);
try {
$result = $execution();
// Try to determine affected rows from result
$affectedRows = null;
if (is_int($result)) {
$affectedRows = $result;
}
return $this->endProfile($profileId, $affectedRows);
} catch (\Throwable $e) {
$profile = $this->endProfile($profileId, null, $e->getMessage());
throw $e; // Re-throw the original exception
}
}
/**
* Get all profiles
*/
public function getProfiles(): array
{
return $this->profiles;
}
/**
* Get slow queries
*/
public function getSlowQueries(): array
{
return array_filter($this->profiles, fn (QueryProfile $profile) => $profile->isSlow);
}
/**
* Get profiles by query type
*/
public function getProfilesByType(string $queryType): array
{
return array_filter(
$this->profiles,
fn (QueryProfile $profile) => $profile->getQueryType() === $queryType
);
}
/**
* Get summary statistics
*/
public function getSummary(): ProfileSummary
{
if (empty($this->profiles)) {
return new ProfileSummary();
}
$totalQueries = count($this->profiles);
$slowQueries = count($this->getSlowQueries());
$totalTime = array_sum(array_map(
fn (QueryProfile $profile) => $profile->executionTime->toSeconds(),
$this->profiles
));
$totalMemory = array_sum(array_map(
fn (QueryProfile $profile) => $profile->memoryUsage,
$this->profiles
));
$queryTypes = [];
foreach ($this->profiles as $profile) {
$type = $profile->getQueryType();
$queryTypes[$type] = ($queryTypes[$type] ?? 0) + 1;
}
$slowestQuery = null;
$maxTime = 0;
foreach ($this->profiles as $profile) {
$time = $profile->executionTime->toSeconds();
if ($time > $maxTime) {
$maxTime = $time;
$slowestQuery = $profile;
}
}
return new ProfileSummary(
totalQueries: $totalQueries,
slowQueries: $slowQueries,
totalExecutionTime: Duration::fromSeconds($totalTime),
averageExecutionTime: Duration::fromSeconds($totalTime / $totalQueries),
totalMemoryUsage: $totalMemory,
averageMemoryUsage: (int) ($totalMemory / $totalQueries),
queryTypeDistribution: $queryTypes,
slowestQuery: $slowestQuery
);
}
/**
* Clear all profiles
*/
public function clearProfiles(): void
{
$this->profiles = [];
$this->activeProfiles = [];
$this->profileCounter = 0;
}
/**
* Get profiles count
*/
public function getProfilesCount(): int
{
return count($this->profiles);
}
/**
* Check if profiling is active for a profile ID
*/
public function isProfilingActive(string $profileId): bool
{
return isset($this->activeProfiles[$profileId]);
}
/**
* Get active profiles count
*/
public function getActiveProfilesCount(): int
{
return count($this->activeProfiles);
}
/**
* Set slow query threshold
*/
public function setSlowQueryThreshold(float $threshold): void
{
if ($threshold <= 0) {
throw new \InvalidArgumentException('Slow query threshold must be positive');
}
// Can't modify readonly property, but we can document this limitation
// In a real implementation, we might make this mutable
}
/**
* Get slow query threshold
*/
public function getSlowQueryThreshold(): float
{
return $this->slowQueryThreshold;
}
}

View File

@@ -0,0 +1,274 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling\Reports;
/**
* Optimization analysis report with suggestions and recommendations
*/
final readonly class OptimizationReport
{
public function __construct(
public array $optimizationData
) {
}
/**
* Get optimization summary
*/
public function getOptimizationSummary(): array
{
$summary = [
'total_connections' => count($this->optimizationData),
'optimization_priorities' => [],
'top_recommendations' => [],
'total_issues' => 0,
'critical_issues' => 0,
'index_suggestions_count' => 0,
'slow_query_patterns_count' => 0,
];
$allRecommendations = [];
foreach ($this->optimizationData as $connectionName => $data) {
$priority = $data['optimization_priority'];
$summary['optimization_priorities'][$priority] = ($summary['optimization_priorities'][$priority] ?? 0) + 1;
$summary['total_issues'] += $data['total_issues'];
$summary['index_suggestions_count'] += count($data['index_suggestions']);
$summary['slow_query_patterns_count'] += count($data['slow_query_patterns']);
if ($priority === 'critical') {
$summary['critical_issues']++;
}
// Collect all recommendations
foreach ($data['recommendations'] as $recommendation) {
$key = md5($recommendation);
if (! isset($allRecommendations[$key])) {
$allRecommendations[$key] = [
'text' => $recommendation,
'count' => 0,
'connections' => [],
];
}
$allRecommendations[$key]['count']++;
$allRecommendations[$key]['connections'][] = $connectionName;
}
}
// Sort recommendations by frequency
uasort($allRecommendations, fn ($a, $b) => $b['count'] <=> $a['count']);
$summary['top_recommendations'] = array_slice($allRecommendations, 0, 10);
return $summary;
}
/**
* Get connections by optimization priority
*/
public function getConnectionsByPriority(string $priority): array
{
$connections = [];
foreach ($this->optimizationData as $connectionName => $data) {
if ($data['optimization_priority'] === $priority) {
$connections[$connectionName] = $data;
}
}
return $connections;
}
/**
* Get most common slow query patterns across all connections
*/
public function getTopSlowQueryPatterns(int $limit = 10): array
{
$patterns = [];
foreach ($this->optimizationData as $connectionName => $data) {
foreach ($data['slow_query_patterns'] as $pattern => $count) {
if (! isset($patterns[$pattern])) {
$patterns[$pattern] = [
'pattern' => $pattern,
'total_count' => 0,
'connections' => [],
];
}
$patterns[$pattern]['total_count'] += $count;
$patterns[$pattern]['connections'][$connectionName] = $count;
}
}
// Sort by total count
uasort($patterns, fn ($a, $b) => $b['total_count'] <=> $a['total_count']);
return array_slice($patterns, 0, $limit);
}
/**
* Get all unique index suggestions
*/
public function getAllIndexSuggestions(): array
{
$suggestions = [];
foreach ($this->optimizationData as $connectionName => $data) {
foreach ($data['index_suggestions'] as $suggestion) {
if (! isset($suggestions[$suggestion])) {
$suggestions[$suggestion] = [
'suggestion' => $suggestion,
'connections' => [],
];
}
$suggestions[$suggestion]['connections'][] = $connectionName;
}
}
return array_values($suggestions);
}
/**
* Get performance issues by severity
*/
public function getPerformanceIssuesBySeverity(): array
{
$issues = [
'very_slow_query' => [],
'n_plus_one' => [],
'missing_index' => [],
'full_table_scan' => [],
];
foreach ($this->optimizationData as $connectionName => $data) {
foreach ($data['performance_issues'] as $issue) {
$type = $issue['type'];
if (isset($issues[$type])) {
$issue['connection'] = $connectionName;
$issues[$type][] = $issue;
}
}
}
// Sort each category by execution time
foreach ($issues as &$categoryIssues) {
usort(
$categoryIssues,
fn ($a, $b) =>
($b['execution_time_ms'] ?? 0) <=> ($a['execution_time_ms'] ?? 0)
);
}
return $issues;
}
/**
* Get optimization roadmap with prioritized actions
*/
public function getOptimizationRoadmap(): array
{
$roadmap = [
'immediate_actions' => [],
'short_term_actions' => [],
'long_term_actions' => [],
];
foreach ($this->optimizationData as $connectionName => $data) {
$priority = $data['optimization_priority'];
switch ($priority) {
case 'critical':
$roadmap['immediate_actions'][] = [
'connection' => $connectionName,
'action' => 'Critical performance issues detected - investigate immediately',
'issues' => $data['total_issues'],
'recommendations' => array_slice($data['recommendations'], 0, 3),
];
break;
case 'high':
$roadmap['short_term_actions'][] = [
'connection' => $connectionName,
'action' => 'Performance optimization needed within days',
'issues' => $data['total_issues'],
'recommendations' => array_slice($data['recommendations'], 0, 3),
];
break;
case 'medium':
case 'low':
$roadmap['long_term_actions'][] = [
'connection' => $connectionName,
'action' => 'Schedule optimization during maintenance window',
'issues' => $data['total_issues'],
'recommendations' => array_slice($data['recommendations'], 0, 2),
];
break;
}
}
return $roadmap;
}
/**
* Get optimization impact estimation
*/
public function getOptimizationImpact(): array
{
$impact = [
'potential_time_savings' => 0,
'queries_to_optimize' => 0,
'connections_needing_optimization' => 0,
'estimated_improvement_percentage' => 0,
];
$totalConnections = count($this->optimizationData);
$connectionsNeedingWork = 0;
$totalSlowPatterns = 0;
foreach ($this->optimizationData as $data) {
if ($data['optimization_priority'] !== 'low') {
$connectionsNeedingWork++;
}
$totalSlowPatterns += count($data['slow_query_patterns']);
$impact['queries_to_optimize'] += array_sum($data['slow_query_patterns']);
}
$impact['connections_needing_optimization'] = $connectionsNeedingWork;
// Rough estimation based on priority distribution
$criticalCount = count($this->getConnectionsByPriority('critical'));
$highCount = count($this->getConnectionsByPriority('high'));
$impact['estimated_improvement_percentage'] = min(
($criticalCount * 30 + $highCount * 20) / max($totalConnections, 1),
80
);
// Estimate potential time savings (very rough approximation)
$impact['potential_time_savings'] = $totalSlowPatterns * 0.5; // seconds per pattern
return $impact;
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'optimization_summary' => $this->getOptimizationSummary(),
'top_slow_query_patterns' => $this->getTopSlowQueryPatterns(),
'all_index_suggestions' => $this->getAllIndexSuggestions(),
'performance_issues_by_severity' => $this->getPerformanceIssuesBySeverity(),
'optimization_roadmap' => $this->getOptimizationRoadmap(),
'optimization_impact' => $this->getOptimizationImpact(),
'detailed_data' => $this->optimizationData,
];
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling\Reports;
/**
* Performance analysis report
*/
final readonly class PerformanceReport
{
public function __construct(
public array $performanceData
) {
}
/**
* Get overall performance metrics
*/
public function getOverallMetrics(): array
{
$totalQueries = 0;
$totalExecutionTime = 0;
$connections = [];
foreach ($this->performanceData as $connectionName => $data) {
$totalQueries += $data['total_queries'];
$totalExecutionTime += $data['total_execution_time']?->toMilliseconds() ?? 0;
$connections[$connectionName] = [
'queries' => $data['total_queries'],
'avg_time_ms' => $data['average_execution_time']?->toMilliseconds() ?? 0,
'qps' => $data['queries_per_second'],
'assessment' => $data['performance_assessment'],
];
}
return [
'total_queries' => $totalQueries,
'total_execution_time_ms' => $totalExecutionTime,
'average_execution_time_ms' => $totalQueries > 0 ? $totalExecutionTime / $totalQueries : 0,
'connections' => $connections,
];
}
/**
* Get performance trends (simplified)
*/
public function getPerformanceTrends(): array
{
$trends = [];
foreach ($this->performanceData as $connectionName => $data) {
$trends[$connectionName] = [
'trend' => $this->calculateTrend($data),
'assessment' => $data['performance_assessment'],
'recommendations' => $this->getPerformanceRecommendations($data),
];
}
return $trends;
}
/**
* Calculate performance trend (simplified)
*/
private function calculateTrend(array $data): string
{
$avgTime = $data['average_execution_time']?->toMilliseconds() ?? 0;
return match (true) {
$avgTime < 50 => 'excellent',
$avgTime < 200 => 'improving',
$avgTime < 500 => 'stable',
$avgTime < 1000 => 'declining',
default => 'critical'
};
}
/**
* Get performance recommendations
*/
private function getPerformanceRecommendations(array $data): array
{
$recommendations = [];
$avgTime = $data['average_execution_time']?->toMilliseconds() ?? 0;
$qps = $data['queries_per_second'];
if ($avgTime > 500) {
$recommendations[] = 'Average query time is high - investigate slow queries';
}
if ($qps > 100) {
$recommendations[] = 'High query frequency - consider connection pooling and caching';
}
if ($data['performance_assessment'] === 'poor') {
$recommendations[] = 'Performance assessment is poor - immediate optimization needed';
}
return $recommendations;
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'overall_metrics' => $this->getOverallMetrics(),
'performance_trends' => $this->getPerformanceTrends(),
'detailed_data' => $this->performanceData,
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling\Reports;
/**
* Comprehensive profiling report
*/
final readonly class ProfilingReport
{
public function __construct(
public array $profiles,
public array $connectionStats
) {
}
/**
* Get report summary
*/
public function getSummary(): array
{
$totalQueries = count($this->profiles);
$slowQueries = array_filter($this->profiles, fn ($profile) => $profile->isSlow);
$totalSlowQueries = count($slowQueries);
$queryTypes = [];
foreach ($this->profiles as $profile) {
$type = $profile->getQueryType();
$queryTypes[$type] = ($queryTypes[$type] ?? 0) + 1;
}
return [
'total_queries' => $totalQueries,
'slow_queries' => $totalSlowQueries,
'slow_query_percentage' => $totalQueries > 0 ? ($totalSlowQueries / $totalQueries) * 100 : 0,
'query_type_distribution' => $queryTypes,
'connections_count' => count($this->connectionStats),
];
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'summary' => $this->getSummary(),
'profiles' => array_map(fn ($profile) => $profile->toArray(), $this->profiles),
'connection_stats' => $this->connectionStats,
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling\Reports;
/**
* Slow query analysis report
*/
final readonly class SlowQueryReport
{
public function __construct(
public array $slowQueries
) {
}
/**
* Get top slow queries
*/
public function getTopSlowQueries(int $limit = 10): array
{
return array_slice($this->slowQueries, 0, $limit);
}
/**
* Group slow queries by pattern
*/
public function getSlowQueryPatterns(): array
{
$patterns = [];
foreach ($this->slowQueries as $entry) {
$profile = $entry['profile'];
$normalizedSql = $profile->getNormalizedSql();
if (! isset($patterns[$normalizedSql])) {
$patterns[$normalizedSql] = [
'pattern' => $normalizedSql,
'count' => 0,
'total_time_ms' => 0,
'avg_time_ms' => 0,
'max_time_ms' => 0,
'examples' => [],
];
}
$patterns[$normalizedSql]['count']++;
$timeMs = $profile->executionTime->toMilliseconds();
$patterns[$normalizedSql]['total_time_ms'] += $timeMs;
$patterns[$normalizedSql]['max_time_ms'] = max($patterns[$normalizedSql]['max_time_ms'], $timeMs);
if (count($patterns[$normalizedSql]['examples']) < 3) {
$patterns[$normalizedSql]['examples'][] = [
'connection' => $entry['connection'],
'profile_id' => $profile->id,
'execution_time_ms' => $timeMs,
'sql' => substr($profile->sql, 0, 200) . '...',
];
}
}
// Calculate averages and sort by frequency
foreach ($patterns as &$pattern) {
$pattern['avg_time_ms'] = $pattern['total_time_ms'] / $pattern['count'];
}
uasort($patterns, fn ($a, $b) => $b['count'] <=> $a['count']);
return $patterns;
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'total_slow_queries' => count($this->slowQueries),
'top_slow_queries' => $this->getTopSlowQueries(),
'slow_query_patterns' => $this->getSlowQueryPatterns(),
];
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Database\Events\Timestamp;
/**
* Slow query alert with recommendations
*/
final readonly class SlowQueryAlert
{
public function __construct(
public QueryProfile $profile,
public SlowQueryAlertLevel $alertLevel,
public ?SlowQueryPattern $detectedPattern,
public Timestamp $timestamp,
public array $recommendations
) {
}
/**
* Get alert severity score (0-10)
*/
public function getSeverityScore(): int
{
$baseScore = match ($this->alertLevel) {
SlowQueryAlertLevel::INFO => 2,
SlowQueryAlertLevel::WARNING => 4,
SlowQueryAlertLevel::CRITICAL => 7,
SlowQueryAlertLevel::EMERGENCY => 10
};
// Adjust based on pattern
if ($this->detectedPattern) {
$patternScore = match ($this->detectedPattern->type) {
SlowQueryPatternType::REPEATED_SLOW_QUERY => 2,
SlowQueryPatternType::N_PLUS_ONE => 3,
SlowQueryPatternType::MISSING_INDEX => 2,
SlowQueryPatternType::FULL_TABLE_SCAN => 3
};
$baseScore = min(10, $baseScore + $patternScore);
}
return $baseScore;
}
/**
* Get alert priority
*/
public function getPriority(): string
{
return match ($this->getSeverityScore()) {
0, 1, 2 => 'low',
3, 4, 5 => 'medium',
6, 7, 8 => 'high',
9, 10 => 'critical'
};
}
/**
* Get formatted alert message
*/
public function getAlertMessage(): string
{
$message = sprintf(
"[%s] Slow query detected: %s executed in %sms",
strtoupper($this->alertLevel->value),
strtoupper($this->profile->getQueryType()),
$this->profile->executionTime->toMilliseconds()
);
if ($this->detectedPattern) {
$message .= sprintf(" - Pattern: %s", $this->detectedPattern->description);
}
return $message;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'alert_level' => $this->alertLevel->value,
'severity_score' => $this->getSeverityScore(),
'priority' => $this->getPriority(),
'alert_message' => $this->getAlertMessage(),
'timestamp' => $this->timestamp->format('c'),
'profile' => $this->profile->toArray(),
'detected_pattern' => $this->detectedPattern?->toArray(),
'recommendations' => $this->recommendations,
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
/**
* Slow query alert levels
*/
enum SlowQueryAlertLevel: string
{
case INFO = 'info';
case WARNING = 'warning';
case CRITICAL = 'critical';
case EMERGENCY = 'emergency';
}

View File

@@ -0,0 +1,347 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\Profiling\Events\SlowQueryDetectedEvent;
use App\Framework\DateTime\Clock;
/**
* Detects and alerts on slow database queries
*/
final class SlowQueryDetector
{
private array $slowQueryPatterns = [];
private array $alertThresholds = [];
private array $recentSlowQueries = [];
private int $maxRecentQueries = 100;
public function __construct(
private readonly EventDispatcher $eventDispatcher,
private readonly Clock $clock,
private readonly Duration $defaultSlowThreshold,
private readonly Duration $criticalSlowThreshold
) {
$this->initializeDefaultThresholds();
}
/**
* Create with default thresholds
*/
public static function withDefaults(EventDispatcher $eventDispatcher, Clock $clock): self
{
return new self(
$eventDispatcher,
$clock,
Duration::fromSeconds(1.0), // 1 second
Duration::fromSeconds(5.0) // 5 seconds
);
}
/**
* Initialize default alert thresholds
*/
private function initializeDefaultThresholds(): void
{
$this->alertThresholds = [
'warning' => $this->defaultSlowThreshold,
'critical' => $this->criticalSlowThreshold,
'emergency' => Duration::fromSeconds(10.0), // 10 seconds
];
}
/**
* Analyze query profile and detect slow queries
*/
public function analyzeQuery(QueryProfile $profile): ?SlowQueryAlert
{
if (! $profile->isSlow) {
return null;
}
// Track recent slow queries
$this->addToRecentSlowQueries($profile);
// Determine alert level
$alertLevel = $this->determineAlertLevel($profile);
// Check for patterns
$pattern = $this->detectSlowQueryPattern($profile);
// Create alert
$alert = new SlowQueryAlert(
profile: $profile,
alertLevel: $alertLevel,
detectedPattern: $pattern,
timestamp: $this->clock->now(),
recommendations: $this->generateRecommendations($profile, $pattern)
);
// Dispatch event
$event = new SlowQueryDetectedEvent($alert, $profile);
$this->eventDispatcher->dispatch($event);
return $alert;
}
/**
* Add query to recent slow queries tracking
*/
private function addToRecentSlowQueries(QueryProfile $profile): void
{
$this->recentSlowQueries[] = [
'profile' => $profile,
'timestamp' => $this->clock->now(),
'normalized_sql' => $profile->getNormalizedSql(),
];
// Maintain maximum size
if (count($this->recentSlowQueries) > $this->maxRecentQueries) {
array_shift($this->recentSlowQueries);
}
}
/**
* Determine alert level based on execution time
*/
private function determineAlertLevel(QueryProfile $profile): SlowQueryAlertLevel
{
$executionTime = $profile->executionTime;
return match (true) {
$executionTime->isGreaterThan($this->alertThresholds['emergency']) => SlowQueryAlertLevel::EMERGENCY,
$executionTime->isGreaterThan($this->alertThresholds['critical']) => SlowQueryAlertLevel::CRITICAL,
$executionTime->isGreaterThan($this->alertThresholds['warning']) => SlowQueryAlertLevel::WARNING,
default => SlowQueryAlertLevel::INFO
};
}
/**
* Detect patterns in slow queries
*/
private function detectSlowQueryPattern(QueryProfile $profile): ?SlowQueryPattern
{
$normalizedSql = $profile->getNormalizedSql();
// Check for repeated slow queries
$recentCount = $this->countRecentSimilarQueries($normalizedSql, Duration::fromMinutes(5));
if ($recentCount >= 3) {
return new SlowQueryPattern(
type: SlowQueryPatternType::REPEATED_SLOW_QUERY,
description: "Query executed {$recentCount} times in last 5 minutes",
occurrences: $recentCount,
timeWindow: Duration::fromMinutes(5)
);
}
// Check for N+1 query pattern
if ($this->detectNPlusOnePattern($profile)) {
return new SlowQueryPattern(
type: SlowQueryPatternType::N_PLUS_ONE,
description: "Potential N+1 query pattern detected",
occurrences: 1,
timeWindow: Duration::fromMinutes(1)
);
}
// Check for missing index pattern
if ($this->detectMissingIndexPattern($profile)) {
return new SlowQueryPattern(
type: SlowQueryPatternType::MISSING_INDEX,
description: "Query may benefit from database indexing",
occurrences: 1,
timeWindow: Duration::fromMinutes(1)
);
}
// Check for table scan pattern
if ($this->detectTableScanPattern($profile)) {
return new SlowQueryPattern(
type: SlowQueryPatternType::FULL_TABLE_SCAN,
description: "Query appears to perform full table scan",
occurrences: 1,
timeWindow: Duration::fromMinutes(1)
);
}
return null;
}
/**
* Count recent similar queries
*/
private function countRecentSimilarQueries(string $normalizedSql, Duration $timeWindow): int
{
$cutoffTime = $this->clock->now()->sub($timeWindow);
$count = 0;
foreach ($this->recentSlowQueries as $recentQuery) {
if ($recentQuery['timestamp']->isAfter($cutoffTime) &&
$recentQuery['normalized_sql'] === $normalizedSql) {
$count++;
}
}
return $count;
}
/**
* Detect N+1 query pattern
*/
private function detectNPlusOnePattern(QueryProfile $profile): bool
{
$sql = strtoupper($profile->sql);
// Simple heuristic: SELECT with single WHERE condition executed frequently
if (str_starts_with($sql, 'SELECT') &&
substr_count($sql, 'WHERE') === 1 &&
! str_contains($sql, 'JOIN') &&
$this->countRecentSimilarQueries($profile->getNormalizedSql(), Duration::fromSeconds(30)) >= 10) {
return true;
}
return false;
}
/**
* Detect missing index pattern
*/
private function detectMissingIndexPattern(QueryProfile $profile): bool
{
$sql = strtoupper($profile->sql);
// Heuristic: SELECT with WHERE but no JOINs, taking long time
return str_starts_with($sql, 'SELECT') &&
str_contains($sql, 'WHERE') &&
! str_contains($sql, 'JOIN') &&
$profile->executionTime->toSeconds() > 0.5;
}
/**
* Detect full table scan pattern
*/
private function detectTableScanPattern(QueryProfile $profile): bool
{
$sql = strtoupper($profile->sql);
// Heuristic: SELECT without WHERE clause or with very generic conditions
return str_starts_with($sql, 'SELECT') &&
(! str_contains($sql, 'WHERE') ||
str_contains($sql, 'WHERE 1=1') ||
str_contains($sql, 'WHERE TRUE')) &&
$profile->executionTime->toSeconds() > 1.0;
}
/**
* Generate optimization recommendations
*/
private function generateRecommendations(QueryProfile $profile, ?SlowQueryPattern $pattern): array
{
$recommendations = [];
// Pattern-specific recommendations
if ($pattern) {
$recommendations = match ($pattern->type) {
SlowQueryPatternType::REPEATED_SLOW_QUERY => [
'Consider caching the results of this frequently executed query',
'Review if the query can be optimized or combined with other queries',
'Add appropriate database indexes if missing',
],
SlowQueryPatternType::N_PLUS_ONE => [
'Use eager loading or JOINs to fetch related data in a single query',
'Consider batch loading strategies for multiple records',
'Review ORM relationship configurations',
],
SlowQueryPatternType::MISSING_INDEX => [
'Add database index on columns used in WHERE clauses',
'Consider composite indexes for multi-column conditions',
'Analyze query execution plan for optimization opportunities',
],
SlowQueryPatternType::FULL_TABLE_SCAN => [
'Add WHERE clause to limit result set',
'Create appropriate indexes to avoid full table scans',
'Consider pagination for large result sets',
]
};
}
// General performance recommendations
$queryType = $profile->getQueryType();
$executionTimeMs = $profile->executionTime->toMilliseconds();
if ($queryType === 'select' && $executionTimeMs > 1000) {
$recommendations[] = 'Consider limiting result set with LIMIT clause';
$recommendations[] = 'Review SELECT columns - avoid SELECT *';
}
if ($profile->memoryUsage > 10 * 1024 * 1024) { // 10MB
$recommendations[] = 'High memory usage detected - consider result set optimization';
}
if ($profile->getComplexityScore() > 10) {
$recommendations[] = 'Query complexity is high - consider breaking into simpler queries';
}
return array_unique($recommendations);
}
/**
* Get recent slow query statistics
*/
public function getSlowQueryStatistics(): array
{
$now = $this->clock->now();
$lastHour = $now->sub(Duration::fromHours(1));
$recentSlowQueries = array_filter(
$this->recentSlowQueries,
fn ($query) => $query['timestamp']->isAfter($lastHour)
);
$patterns = [];
foreach ($recentSlowQueries as $query) {
$normalizedSql = $query['normalized_sql'];
$patterns[$normalizedSql] = ($patterns[$normalizedSql] ?? 0) + 1;
}
// Sort by frequency
arsort($patterns);
return [
'recent_slow_queries_count' => count($recentSlowQueries),
'total_tracked_queries' => count($this->recentSlowQueries),
'most_frequent_slow_patterns' => array_slice($patterns, 0, 10, true),
'alert_thresholds' => [
'warning_ms' => $this->alertThresholds['warning']->toMilliseconds(),
'critical_ms' => $this->alertThresholds['critical']->toMilliseconds(),
'emergency_ms' => $this->alertThresholds['emergency']->toMilliseconds(),
],
];
}
/**
* Update alert threshold
*/
public function setAlertThreshold(string $level, Duration $threshold): void
{
if (! isset($this->alertThresholds[$level])) {
throw new \InvalidArgumentException("Invalid alert level: $level");
}
$this->alertThresholds[$level] = $threshold;
}
/**
* Clear recent slow queries tracking
*/
public function clearRecentSlowQueries(): void
{
$this->recentSlowQueries = [];
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
use App\Framework\Core\ValueObjects\Duration;
/**
* Detected slow query pattern
*/
final readonly class SlowQueryPattern
{
public function __construct(
public SlowQueryPatternType $type,
public string $description,
public int $occurrences,
public Duration $timeWindow
) {
}
/**
* Get pattern severity
*/
public function getSeverity(): string
{
return match ($this->type) {
SlowQueryPatternType::REPEATED_SLOW_QUERY => $this->occurrences > 10 ? 'high' : 'medium',
SlowQueryPatternType::N_PLUS_ONE => 'high',
SlowQueryPatternType::MISSING_INDEX => 'medium',
SlowQueryPatternType::FULL_TABLE_SCAN => 'high'
};
}
/**
* Convert to array
*/
public function toArray(): array
{
return [
'type' => $this->type->value,
'description' => $this->description,
'occurrences' => $this->occurrences,
'time_window_seconds' => $this->timeWindow->toSeconds(),
'severity' => $this->getSeverity(),
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Profiling;
/**
* Types of slow query patterns that can be detected
*/
enum SlowQueryPatternType: string
{
case REPEATED_SLOW_QUERY = 'repeated_slow_query';
case N_PLUS_ONE = 'n_plus_one';
case MISSING_INDEX = 'missing_index';
case FULL_TABLE_SCAN = 'full_table_scan';
}