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:
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
207
src/Framework/Database/Profiling/ProfileSummary.php
Normal file
207
src/Framework/Database/Profiling/ProfileSummary.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
101
src/Framework/Database/Profiling/ProfilingConfig.php
Normal file
101
src/Framework/Database/Profiling/ProfilingConfig.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
311
src/Framework/Database/Profiling/ProfilingConnection.php
Normal file
311
src/Framework/Database/Profiling/ProfilingConnection.php
Normal 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);
|
||||
}
|
||||
}
|
||||
368
src/Framework/Database/Profiling/ProfilingDashboard.php
Normal file
368
src/Framework/Database/Profiling/ProfilingDashboard.php
Normal 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);
|
||||
}
|
||||
}
|
||||
121
src/Framework/Database/Profiling/QueryAnalysis.php
Normal file
121
src/Framework/Database/Profiling/QueryAnalysis.php
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
385
src/Framework/Database/Profiling/QueryAnalyzer.php
Normal file
385
src/Framework/Database/Profiling/QueryAnalyzer.php
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
412
src/Framework/Database/Profiling/QueryLogger.php
Normal file
412
src/Framework/Database/Profiling/QueryLogger.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
219
src/Framework/Database/Profiling/QueryProfile.php
Normal file
219
src/Framework/Database/Profiling/QueryProfile.php
Normal 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;
|
||||
}
|
||||
}
|
||||
242
src/Framework/Database/Profiling/QueryProfiler.php
Normal file
242
src/Framework/Database/Profiling/QueryProfiler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
274
src/Framework/Database/Profiling/Reports/OptimizationReport.php
Normal file
274
src/Framework/Database/Profiling/Reports/OptimizationReport.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
115
src/Framework/Database/Profiling/Reports/PerformanceReport.php
Normal file
115
src/Framework/Database/Profiling/Reports/PerformanceReport.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
53
src/Framework/Database/Profiling/Reports/ProfilingReport.php
Normal file
53
src/Framework/Database/Profiling/Reports/ProfilingReport.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
83
src/Framework/Database/Profiling/Reports/SlowQueryReport.php
Normal file
83
src/Framework/Database/Profiling/Reports/SlowQueryReport.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
98
src/Framework/Database/Profiling/SlowQueryAlert.php
Normal file
98
src/Framework/Database/Profiling/SlowQueryAlert.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
16
src/Framework/Database/Profiling/SlowQueryAlertLevel.php
Normal file
16
src/Framework/Database/Profiling/SlowQueryAlertLevel.php
Normal 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';
|
||||
}
|
||||
347
src/Framework/Database/Profiling/SlowQueryDetector.php
Normal file
347
src/Framework/Database/Profiling/SlowQueryDetector.php
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
48
src/Framework/Database/Profiling/SlowQueryPattern.php
Normal file
48
src/Framework/Database/Profiling/SlowQueryPattern.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
16
src/Framework/Database/Profiling/SlowQueryPatternType.php
Normal file
16
src/Framework/Database/Profiling/SlowQueryPatternType.php
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user