- 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.
401 lines
14 KiB
PHP
401 lines
14 KiB
PHP
<?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);
|
|
});
|
|
});
|