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,96 @@
<?php
declare(strict_types=1);
use App\Framework\Database\Indexing\IndexAnalyzer;
use App\Framework\Database\Indexing\ValueObjects\IndexName;
use App\Framework\Database\Indexing\ValueObjects\IndexType;
use App\Framework\Database\PdoConnection;
describe('IndexAnalyzer', function () {
beforeEach(function () {
// Create in-memory SQLite database for testing
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create test table with indexes
$pdo->exec('
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
status TEXT,
created_at TEXT
)
');
$pdo->exec('CREATE INDEX idx_users_status ON users(status)');
$pdo->exec('CREATE INDEX idx_users_created_at ON users(created_at)');
$pdo->exec('CREATE INDEX idx_users_email_status ON users(email, status)');
$this->connection = new PdoConnection($pdo, 'sqlite');
$this->analyzer = new IndexAnalyzer($this->connection);
});
it('can get all indexes for a table', function () {
$indexes = $this->analyzer->getTableIndexes('users');
expect($indexes)->toBeArray();
expect(count($indexes))->toBeGreaterThan(0);
// Should have at least: PRIMARY, UNIQUE, and our custom indexes
$indexNames = array_column($indexes, 'name');
expect($indexNames)->toContain('idx_users_status');
expect($indexNames)->toContain('idx_users_created_at');
});
it('detects index usage in query', function () {
$sql = 'SELECT * FROM users WHERE status = "active"';
$analysis = $this->analyzer->analyzeQuery($sql);
expect($analysis)->toBeArray();
expect($analysis)->toHaveKey('query');
expect($analysis)->toHaveKey('indexes_used');
expect($analysis)->toHaveKey('key_type');
expect($analysis['query'])->toBe($sql);
});
it('detects composite index columns correctly', function () {
$indexes = $this->analyzer->getTableIndexes('users');
$compositeIndex = array_values(array_filter($indexes, function ($index) {
return $index['name'] === 'idx_users_email_status';
}))[0] ?? null;
expect($compositeIndex)->not->toBeNull();
expect($compositeIndex['columns'])->toBe(['email', 'status']);
});
it('identifies index types correctly', function () {
$indexes = $this->analyzer->getTableIndexes('users');
foreach ($indexes as $index) {
expect($index['type'])->toBeInstanceOf(IndexType::class);
}
});
it('marks unique indexes correctly', function () {
$indexes = $this->analyzer->getTableIndexes('users');
$uniqueIndex = array_values(array_filter($indexes, function ($index) {
return str_contains($index['name'], 'email');
}))[0] ?? null;
expect($uniqueIndex)->not->toBeNull();
expect($uniqueIndex['is_unique'])->toBeTrue();
});
it('analyzes queries without indexes', function () {
$sql = 'SELECT * FROM users WHERE name LIKE "%John%"';
$analysis = $this->analyzer->analyzeQuery($sql);
expect($analysis['indexes_used'])->toBeArray();
// SQLite might use table scan for LIKE queries
expect($analysis['key_type'])->toBeIn(['scan', 'ALL', 'index']);
});
});

View File

@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
use App\Framework\Database\Indexing\IndexMigrationGenerator;
use App\Framework\Database\Indexing\ValueObjects\IndexRecommendation;
use App\Framework\Database\Indexing\ValueObjects\IndexType;
use App\Framework\Database\Indexing\ValueObjects\RecommendationPriority;
use App\Framework\Database\PdoConnection;
describe('IndexMigrationGenerator', function () {
beforeEach(function () {
$pdo = new PDO('sqlite::memory:');
$connection = new PdoConnection($pdo, 'sqlite');
$this->generator = new IndexMigrationGenerator($connection);
});
it('generates migration for adding indexes', function () {
$recommendations = [
new IndexRecommendation(
tableName: 'users',
columns: ['email'],
indexType: IndexType::BTREE,
reason: 'Frequent WHERE email lookups',
priority: RecommendationPriority::HIGH,
estimatedSpeedup: 5.0,
affectedQueries: 100
),
new IndexRecommendation(
tableName: 'users',
columns: ['status', 'created_at'],
indexType: IndexType::BTREE,
reason: 'WHERE status + ORDER BY created_at',
priority: RecommendationPriority::CRITICAL,
estimatedSpeedup: 10.0,
affectedQueries: 250
)
];
$migration = $this->generator->generateAddIndexMigration($recommendations, 'users');
expect($migration)->toBeString();
expect($migration)->toContain('extends Migration');
expect($migration)->toContain('public function up(Schema $schema)');
expect($migration)->toContain('public function down(Schema $schema)');
expect($migration)->toContain('users');
expect($migration)->toContain('$table->index');
});
it('generates migration for removing indexes', function () {
$unusedIndexes = [
[
'index_name' => 'idx_users_old_column',
'columns' => ['old_column']
],
[
'index_name' => 'idx_users_deprecated',
'columns' => ['deprecated_field']
]
];
$migration = $this->generator->generateRemoveIndexMigration($unusedIndexes, 'users');
expect($migration)->toBeString();
expect($migration)->toContain('extends Migration');
expect($migration)->toContain('$table->dropIndex');
expect($migration)->toContain('idx_users_old_column');
expect($migration)->toContain('idx_users_deprecated');
});
it('generates optimization migration (add + remove)', function () {
$toAdd = [
new IndexRecommendation(
tableName: 'orders',
columns: ['customer_id', 'status'],
indexType: IndexType::BTREE,
reason: 'WHERE customer_id + status filter',
priority: RecommendationPriority::HIGH,
estimatedSpeedup: 8.0,
affectedQueries: 150
)
];
$toRemove = [
[
'index_name' => 'idx_orders_unused',
'columns' => ['unused_column']
]
];
$migration = $this->generator->generateOptimizationMigration($toAdd, $toRemove, 'orders');
expect($migration)->toBeString();
expect($migration)->toContain('extends Migration');
expect($migration)->toContain('$table->dropIndex(\'idx_orders_unused\')');
expect($migration)->toContain('$table->index');
});
it('includes proper up/down migration methods', function () {
$recommendations = [
new IndexRecommendation(
tableName: 'products',
columns: ['category'],
indexType: IndexType::BTREE,
reason: 'Category filtering',
priority: RecommendationPriority::MEDIUM,
estimatedSpeedup: 3.0,
affectedQueries: 50
)
];
$migration = $this->generator->generateAddIndexMigration($recommendations, 'products');
// Check UP method
expect($migration)->toContain('public function up(Schema $schema)');
expect($migration)->toContain('$schema->table(\'products\'');
// Check DOWN method (should reverse the UP actions)
expect($migration)->toContain('public function down(Schema $schema)');
expect($migration)->toContain('$table->dropIndex');
});
it('validates migration content structure', function () {
$recommendations = [
new IndexRecommendation(
tableName: 'test_table',
columns: ['test_column'],
indexType: IndexType::BTREE,
reason: 'Test',
priority: RecommendationPriority::LOW,
estimatedSpeedup: 1.5,
affectedQueries: 10
)
];
$migration = $this->generator->generateAddIndexMigration($recommendations, 'test_table');
// Validate required components
expect($migration)->toContain('declare(strict_types=1)');
expect($migration)->toContain('use App\Framework\Database\Migration\Migration');
expect($migration)->toContain('use App\Framework\Database\Schema\Schema');
expect($migration)->toContain('final class');
});
it('handles composite indexes correctly', function () {
$recommendations = [
new IndexRecommendation(
tableName: 'posts',
columns: ['author_id', 'published_at', 'status'],
indexType: IndexType::BTREE,
reason: 'Complex WHERE + ORDER BY',
priority: RecommendationPriority::CRITICAL,
estimatedSpeedup: 15.0,
affectedQueries: 300
)
];
$migration = $this->generator->generateAddIndexMigration($recommendations, 'posts');
expect($migration)->toContain('author_id');
expect($migration)->toContain('published_at');
expect($migration)->toContain('status');
});
});

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
use App\Framework\Database\Indexing\UnusedIndexDetector;
use App\Framework\Database\Indexing\IndexAnalyzer;
use App\Framework\Database\Indexing\IndexUsageTracker;
use App\Framework\Database\PdoConnection;
describe('UnusedIndexDetector', function () {
beforeEach(function () {
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE,
category TEXT,
price REAL,
stock INTEGER,
created_at TEXT
)
');
$pdo->exec('CREATE INDEX idx_products_category ON products(category)');
$pdo->exec('CREATE INDEX idx_products_price ON products(price)');
$pdo->exec('CREATE INDEX idx_products_stock ON products(stock)');
$pdo->exec('CREATE INDEX idx_products_category_price ON products(category, price)');
$connection = new PdoConnection($pdo, 'sqlite');
$analyzer = new IndexAnalyzer($connection);
$cache = new \App\Framework\Cache\Driver\InMemoryCache();
$usageTracker = new IndexUsageTracker($cache, $analyzer);
$this->detector = new UnusedIndexDetector($analyzer, $usageTracker);
$this->tableName = 'products';
});
it('finds unused indexes', function () {
$unusedIndexes = $this->detector->findUnusedIndexes($this->tableName);
expect($unusedIndexes)->toBeArray();
// Since we haven't tracked any usage, all non-unique indexes should be marked as unused
expect(count($unusedIndexes))->toBeGreaterThan(0);
});
it('detects duplicate indexes', function () {
// Create duplicate index in test
// (In real scenario, we'd have actual duplicates)
$duplicates = $this->detector->findDuplicateIndexes($this->tableName);
expect($duplicates)->toBeArray();
});
it('finds redundant indexes (prefix pattern)', function () {
$redundant = $this->detector->findRedundantIndexes($this->tableName);
expect($redundant)->toBeArray();
// idx_products_category is redundant with idx_products_category_price
// because category is a prefix of [category, price]
if (!empty($redundant)) {
expect($redundant[0])->toHaveKey('redundant_index');
expect($redundant[0])->toHaveKey('covered_by');
}
});
it('generates comprehensive unused index report', function () {
$report = $this->detector->getUnusedIndexReport($this->tableName);
expect($report)->toHaveKey('unused');
expect($report)->toHaveKey('duplicates');
expect($report)->toHaveKey('redundant');
expect($report)->toHaveKey('total_removable');
expect($report)->toHaveKey('estimated_space_savings');
expect($report['unused'])->toBeArray();
expect($report['total_removable'])->toBeInt();
});
it('generates DROP statements for unused indexes', function () {
$statements = $this->detector->generateDropStatements($this->tableName);
expect($statements)->toBeArray();
foreach ($statements as $statement) {
expect($statement)->toContain('DROP INDEX');
expect($statement)->toContain('ON products');
}
});
it('never suggests dropping PRIMARY or UNIQUE indexes', function () {
$unusedIndexes = $this->detector->findUnusedIndexes($this->tableName);
// Filter for any PRIMARY or UNIQUE suggestions
$primaryOrUnique = array_filter($unusedIndexes, function ($index) {
return str_contains(strtolower($index['index_name']), 'primary') ||
str_contains(strtolower($index['index_name']), 'unique');
});
expect($primaryOrUnique)->toBeEmpty();
});
it('estimates space savings correctly', function () {
$report = $this->detector->getUnusedIndexReport($this->tableName);
expect($report['estimated_space_savings'])->toBeString();
expect($report['estimated_space_savings'])->toMatch('/(MB|GB|< 1 MB)/');
});
});