feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Database\NPlusOneDetection\MachineLearning\Extractors;
use App\Framework\Database\NPlusOneDetection\MachineLearning\Extractors\QueryFeatureExtractor;
use App\Framework\Database\NPlusOneDetection\QueryExecutionContext;
use App\Framework\MachineLearning\ValueObjects\FeatureType;
use App\Framework\Core\ValueObjects\Duration;
describe('QueryFeatureExtractor', function () {
it('is enabled by default', function () {
$extractor = new QueryFeatureExtractor();
expect($extractor->isEnabled())->toBeTrue();
});
it('can be disabled via constructor', function () {
$extractor = new QueryFeatureExtractor(enabled: false);
expect($extractor->isEnabled())->toBeFalse();
});
it('returns FREQUENCY as feature type', function () {
$extractor = new QueryFeatureExtractor();
expect($extractor->getFeatureType())->toBe(FeatureType::FREQUENCY);
});
it('has default priority of 10', function () {
$extractor = new QueryFeatureExtractor();
expect($extractor->getPriority())->toBe(10);
});
it('can extract from QueryExecutionContext', function () {
$extractor = new QueryFeatureExtractor();
$context = QueryExecutionContext::minimal();
expect($extractor->canExtract($context))->toBeTrue();
});
it('cannot extract from non-QueryExecutionContext', function () {
$extractor = new QueryFeatureExtractor();
expect($extractor->canExtract(['not' => 'context']))->toBeFalse();
expect($extractor->canExtract('string'))->toBeFalse();
expect($extractor->canExtract(null))->toBeFalse();
});
it('extracts 8 features from minimal context', function () {
$extractor = new QueryFeatureExtractor();
$context = QueryExecutionContext::minimal(
queryCount: 10,
durationMs: 100.0,
uniqueQueries: 5
);
$features = $extractor->extractFeatures($context);
expect($features)->toHaveCount(8);
// Verify all expected features are present
$featureNames = array_map(fn($f) => $f->name, $features);
expect($featureNames)->toContain('query_frequency');
expect($featureNames)->toContain('query_repetition_rate');
expect($featureNames)->toContain('avg_query_execution_time');
expect($featureNames)->toContain('timing_pattern_regularity');
expect($featureNames)->toContain('avg_query_complexity');
expect($featureNames)->toContain('avg_join_count');
expect($featureNames)->toContain('loop_execution_detected');
expect($featureNames)->toContain('query_similarity_score');
});
it('calculates query frequency correctly', function () {
$extractor = new QueryFeatureExtractor();
// 10 queries in 1 second = 10 queries/second
$context = QueryExecutionContext::minimal(
queryCount: 10,
durationMs: 1000.0,
uniqueQueries: 5
);
$features = $extractor->extractFeatures($context);
$frequencyFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'query_frequency'
))[0];
expect($frequencyFeature->value)->toBe(10.0);
expect($frequencyFeature->unit)->toBe('queries/second');
});
it('calculates query repetition rate correctly', function () {
$extractor = new QueryFeatureExtractor();
// 10 total queries, 3 unique = 70% repetition rate
$context = QueryExecutionContext::minimal(
queryCount: 10,
durationMs: 100.0,
uniqueQueries: 3
);
$features = $extractor->extractFeatures($context);
$repetitionFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'query_repetition_rate'
))[0];
expect($repetitionFeature->value)->toBe(70.0);
expect($repetitionFeature->unit)->toBe('percentage');
});
it('calculates average execution time correctly', function () {
$extractor = new QueryFeatureExtractor();
// 100ms total for 10 queries = 10ms average
$context = QueryExecutionContext::minimal(
queryCount: 10,
durationMs: 100.0,
uniqueQueries: 5
);
$features = $extractor->extractFeatures($context);
$avgTimeFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'avg_query_execution_time'
))[0];
expect($avgTimeFeature->value)->toBe(10.0);
expect($avgTimeFeature->unit)->toBe('milliseconds');
});
it('detects loop execution pattern', function () {
$extractor = new QueryFeatureExtractor();
$context = new QueryExecutionContext(
queryCount: 10,
duration: Duration::fromMilliseconds(100),
uniqueQueryHashes: ['hash1', 'hash2'],
queryTimings: array_fill(0, 10, 10.0),
queryComplexityScores: array_fill(0, 10, 0.5),
totalJoinCount: 5,
executedInLoop: true,
loopDepth: 2
);
$features = $extractor->extractFeatures($context);
$loopFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'loop_execution_detected'
))[0];
expect($loopFeature->value)->toBe(1.0);
expect($loopFeature->unit)->toBe('binary');
expect($loopFeature->metadata['loop_depth'])->toBe(2);
});
it('calculates high query similarity for N+1 pattern', function () {
$extractor = new QueryFeatureExtractor();
// 20 total queries, only 2 unique = 90% similarity
$context = QueryExecutionContext::minimal(
queryCount: 20,
durationMs: 200.0,
uniqueQueries: 2
);
$features = $extractor->extractFeatures($context);
$similarityFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'query_similarity_score'
))[0];
expect($similarityFeature->value)->toBe(0.9); // 1.0 - (2/20)
expect($similarityFeature->unit)->toBe('score');
});
it('calculates timing pattern regularity from consistent timings', function () {
$extractor = new QueryFeatureExtractor();
// Very consistent timings (low CV = high regularity)
$context = new QueryExecutionContext(
queryCount: 10,
duration: Duration::fromMilliseconds(100),
uniqueQueryHashes: ['hash1'],
queryTimings: [10.0, 10.1, 9.9, 10.0, 10.2, 9.8, 10.0, 10.1, 9.9, 10.0],
queryComplexityScores: array_fill(0, 10, 0.5),
totalJoinCount: 0,
executedInLoop: true
);
$features = $extractor->extractFeatures($context);
$regularityFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'timing_pattern_regularity'
))[0];
// High regularity expected (>0.9)
expect($regularityFeature->value)->toBeGreaterThan(0.9);
expect($regularityFeature->unit)->toBe('score');
});
it('calculates low timing pattern regularity from irregular timings', function () {
$extractor = new QueryFeatureExtractor();
// Very irregular timings (high CV = low regularity)
$context = new QueryExecutionContext(
queryCount: 10,
duration: Duration::fromMilliseconds(500),
uniqueQueryHashes: ['hash1', 'hash2', 'hash3'],
queryTimings: [5.0, 50.0, 10.0, 100.0, 8.0, 75.0, 12.0, 90.0, 6.0, 80.0],
queryComplexityScores: array_fill(0, 10, 0.5),
totalJoinCount: 0,
executedInLoop: false
);
$features = $extractor->extractFeatures($context);
$regularityFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'timing_pattern_regularity'
))[0];
// Low regularity expected (<0.5)
expect($regularityFeature->value)->toBeLessThan(0.5);
});
it('calculates average query complexity', function () {
$extractor = new QueryFeatureExtractor();
$context = new QueryExecutionContext(
queryCount: 5,
duration: Duration::fromMilliseconds(50),
uniqueQueryHashes: ['hash1', 'hash2'],
queryTimings: array_fill(0, 5, 10.0),
queryComplexityScores: [0.2, 0.4, 0.6, 0.8, 1.0],
totalJoinCount: 10,
executedInLoop: false
);
$features = $extractor->extractFeatures($context);
$complexityFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'avg_query_complexity'
))[0];
// Average of [0.2, 0.4, 0.6, 0.8, 1.0] = 0.6
expect($complexityFeature->value)->toBe(0.6);
expect($complexityFeature->unit)->toBe('score');
});
it('calculates average join count', function () {
$extractor = new QueryFeatureExtractor();
$context = new QueryExecutionContext(
queryCount: 10,
duration: Duration::fromMilliseconds(100),
uniqueQueryHashes: ['hash1'],
queryTimings: array_fill(0, 10, 10.0),
queryComplexityScores: array_fill(0, 10, 0.5),
totalJoinCount: 30, // 30 joins across 10 queries = 3.0 average
executedInLoop: false
);
$features = $extractor->extractFeatures($context);
$joinFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'avg_join_count'
))[0];
expect($joinFeature->value)->toBe(3.0);
expect($joinFeature->unit)->toBe('count');
});
it('handles zero queries gracefully', function () {
$extractor = new QueryFeatureExtractor();
$context = new QueryExecutionContext(
queryCount: 0,
duration: Duration::fromMilliseconds(0),
uniqueQueryHashes: [],
queryTimings: [],
queryComplexityScores: [],
totalJoinCount: 0,
executedInLoop: false
);
$features = $extractor->extractFeatures($context);
// Should return features with 0.0 values, not throw
expect($features)->not->toBeEmpty();
foreach ($features as $feature) {
expect($feature->value)->toBeFloat();
}
});
it('returns empty array for non-extractable data', function () {
$extractor = new QueryFeatureExtractor();
$features = $extractor->extractFeatures(['not' => 'context']);
expect($features)->toBeEmpty();
});
it('includes metadata in features', function () {
$extractor = new QueryFeatureExtractor();
$context = QueryExecutionContext::minimal(
queryCount: 5,
durationMs: 50.0,
uniqueQueries: 2
);
$features = $extractor->extractFeatures($context);
$frequencyFeature = array_values(array_filter(
$features,
fn($f) => $f->name === 'query_frequency'
))[0];
expect($frequencyFeature->metadata)->toHaveKey('query_count');
expect($frequencyFeature->metadata)->toHaveKey('duration_seconds');
expect($frequencyFeature->metadata['query_count'])->toBe(5);
});
});

View File

@@ -0,0 +1,400 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Database\NPlusOneDetection\MachineLearning;
use App\Framework\Database\NPlusOneDetection\MachineLearning\NPlusOneDetectionEngine;
use App\Framework\Database\NPlusOneDetection\MachineLearning\Extractors\QueryFeatureExtractor;
use App\Framework\Database\NPlusOneDetection\QueryExecutionContext;
use App\Framework\Waf\MachineLearning\Detectors\StatisticalAnomalyDetector;
use App\Framework\MachineLearning\ValueObjects\Feature;
use App\Framework\MachineLearning\ValueObjects\FeatureType;
use App\Framework\MachineLearning\ValueObjects\AnomalyDetection;
use App\Framework\MachineLearning\ValueObjects\AnomalyType;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\DateTime\SystemClock;
use Mockery;
use Mockery\MockInterface;
// Helper function to create mock extractor
function createMockQueryExtractor(bool $enabled = true, array $features = []): MockInterface
{
$extractor = Mockery::mock(\App\Framework\MachineLearning\Core\FeatureExtractorInterface::class);
$extractor->shouldReceive('isEnabled')->andReturn($enabled);
$extractor->shouldReceive('getFeatureType')->andReturn(FeatureType::FREQUENCY);
$extractor->shouldReceive('getPriority')->andReturn(10);
$extractor->shouldReceive('canExtract')->andReturn(true);
$extractor->shouldReceive('extractFeatures')->andReturn($features);
return $extractor;
}
// Helper function to create mock detector
function createMockQueryDetector(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface
{
$supportedTypes = $supportedTypes ?: [FeatureType::FREQUENCY];
$detector = Mockery::mock(\App\Framework\MachineLearning\Core\AnomalyDetectorInterface::class);
$detector->shouldReceive('isEnabled')->andReturn($enabled);
$detector->shouldReceive('getName')->andReturn('MockDetector');
$detector->shouldReceive('getSupportedFeatureTypes')->andReturn($supportedTypes);
$detector->shouldReceive('canAnalyze')->andReturn(true);
$detector->shouldReceive('detectAnomalies')->andReturn($anomalies);
return $detector;
}
describe('NPlusOneDetectionEngine', function () {
beforeEach(function () {
$this->clock = new SystemClock();
});
afterEach(function () {
Mockery::close();
});
it('is disabled when constructed with enabled: false', function () {
$engine = new NPlusOneDetectionEngine(
enabled: false,
extractors: [],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
expect($engine->isEnabled())->toBeFalse();
});
it('is enabled when constructed with enabled: true', function () {
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
expect($engine->isEnabled())->toBeTrue();
});
it('returns configuration', function () {
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [new QueryFeatureExtractor()],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(3),
confidenceThreshold: Percentage::from(75.0)
);
$config = $engine->getConfiguration();
expect($config)->toBeArray();
expect($config['enabled'])->toBeTrue();
expect($config['extractor_count'])->toBe(1);
expect($config['detector_count'])->toBe(0);
expect($config['analysis_timeout_ms'])->toBe(3000);
expect($config['confidence_threshold'])->toBe(75.0);
});
it('returns disabled result when engine is disabled', function () {
$engine = new NPlusOneDetectionEngine(
enabled: false,
extractors: [],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->enabled)->toBeFalse();
expect($result->features)->toBeEmpty();
expect($result->anomalies)->toBeEmpty();
expect($result->confidence->getValue())->toBe(0.0);
});
it('extracts features from query context', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 10.0,
unit: 'queries/second'
);
$extractor = createMockQueryExtractor(true, [$feature]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->features)->toHaveCount(1);
expect($result->features[0]->name)->toBe('query_frequency');
});
it('detects anomalies using configured detectors', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0, // High frequency
unit: 'queries/second'
);
$anomaly = AnomalyDetection::frequencySpike(
currentRate: 100.0,
baseline: 10.0,
threshold: 3.0
);
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->anomalies)->not->toBeEmpty();
expect($result->confidence->getValue())->toBeGreaterThan(0.0);
});
it('filters anomalies by confidence threshold', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 50.0,
unit: 'queries/second'
);
// Low confidence anomaly (should be filtered out)
$lowConfidenceAnomaly = AnomalyDetection::create(
type: AnomalyType::FREQUENCY_SPIKE,
featureType: FeatureType::FREQUENCY,
anomalyScore: 0.5, // 50% confidence
description: 'Low confidence anomaly',
features: [$feature],
evidence: []
);
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$lowConfidenceAnomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(75.0) // High threshold
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Anomaly should be filtered out due to low confidence
expect($result->anomalies)->toBeEmpty();
});
it('skips disabled extractors', function () {
$enabledExtractor = createMockQueryExtractor(true, [
new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
]);
$disabledExtractor = createMockQueryExtractor(false, [
new Feature(FeatureType::FREQUENCY, 'feature2', 20.0, 'unit')
]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$enabledExtractor, $disabledExtractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Should only have features from enabled extractor
expect($result->features)->toHaveCount(1);
expect($result->features[0]->name)->toBe('feature1');
});
it('skips disabled detectors', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0,
unit: 'queries/second'
);
$anomaly = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0);
$extractor = createMockQueryExtractor(true, [$feature]);
$enabledDetector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
$disabledDetector = createMockQueryDetector(false, [FeatureType::FREQUENCY], [$anomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$enabledDetector, $disabledDetector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Should only have anomalies from enabled detector
expect($result->detectorResults)->toHaveCount(1);
});
it('includes extractor results in output', function () {
$extractor = createMockQueryExtractor(true, [
new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->extractorResults)->not->toBeEmpty();
expect($result->extractorResults[0]->featuresExtracted)->toBe(1);
expect($result->extractorResults[0]->success)->toBeTrue();
});
it('includes detector results in output', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0,
unit: 'queries/second'
);
$anomaly = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0);
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->detectorResults)->not->toBeEmpty();
expect($result->detectorResults[0]->anomaliesDetected)->toBe(1);
expect($result->detectorResults[0]->success)->toBeTrue();
});
it('calculates overall confidence from anomalies', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0,
unit: 'queries/second'
);
$anomaly1 = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0); // ~95% confidence
$anomaly2 = AnomalyDetection::frequencySpike(90.0, 10.0, 3.0); // ~95% confidence
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly1, $anomaly2]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Overall confidence should be average of anomalies
expect($result->confidence->getValue())->toBeGreaterThan(90.0);
});
it('handles exceptions gracefully', function () {
$extractor = Mockery::mock(\App\Framework\MachineLearning\Core\FeatureExtractorInterface::class);
$extractor->shouldReceive('isEnabled')->andReturn(true);
$extractor->shouldReceive('canExtract')->andReturn(true);
$extractor->shouldReceive('extractFeatures')->andThrow(new \RuntimeException('Extraction failed'));
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Should not throw, should return error in result
expect($result->error)->not->toBeNull();
expect($result->error)->toContain('Extraction failed');
});
it('tracks analysis time', function () {
$extractor = createMockQueryExtractor(true, [
new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Analysis time should be measured
expect($result->analysisTime->toMilliseconds())->toBeGreaterThanOrEqual(0);
});
});