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,333 @@
<?php
declare(strict_types=1);
use App\Framework\LiveComponents\Profiling\LiveComponentProfiler;
use App\Framework\LiveComponents\Profiling\ProfileSession;
use App\Framework\LiveComponents\Profiling\ProfileTimeline;
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfileResult;
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfileSessionId;
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfilePhase;
use App\Framework\LiveComponents\Profiling\ValueObjects\MemorySnapshot;
use App\Framework\LiveComponents\Observability\ComponentMetricsCollector;
use App\Framework\Telemetry\UnifiedTelemetryService;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Timestamp;
use Tests\Unit\Framework\LiveComponents\Profiling\SimpleTelemetryService;
describe('LiveComponentProfiler', function () {
beforeEach(function () {
// Use simple test implementations
$randomGenerator = new \App\Framework\Random\SecureRandomGenerator();
$this->telemetryService = new SimpleTelemetryService($randomGenerator);
$this->metricsCollector = new ComponentMetricsCollector();
$this->memoryMonitor = new MemoryMonitor();
$this->profiler = new LiveComponentProfiler(
$this->telemetryService,
$this->metricsCollector,
$this->memoryMonitor
);
});
it('starts profiling session', function () {
$session = $this->profiler->startSession('UserCard');
expect($session)->toBeInstanceOf(ProfileSession::class);
expect($session->componentId)->toBe('UserCard');
expect($session->sessionId)->toBeInstanceOf(ProfileSessionId::class);
expect($session->startTime)->toBeInstanceOf(Timestamp::class);
expect($session->startMemory)->toBeInstanceOf(Byte::class);
expect($session->operation)->toBeInstanceOf(\App\Framework\Telemetry\OperationHandle::class);
});
it('profiles resolve phase', function () {
$session = $this->profiler->startSession('UserCard');
$result = $this->profiler->profileResolve($session, function () {
return ['resolved' => true];
});
expect($result)->toBe(['resolved' => true]);
expect($session->getPhases())->toHaveCount(1);
$resolvePhase = $session->getPhase('resolve');
expect($resolvePhase)->toBeInstanceOf(ProfilePhase::class);
expect($resolvePhase->name)->toBe('resolve');
expect($resolvePhase->isSuccessful())->toBeTrue();
});
it('profiles render phase with cache flag', function () {
$session = $this->profiler->startSession('UserCard');
$html = $this->profiler->profileRender($session, function () {
return '<div>User Card</div>';
}, cached: true);
expect($html)->toBe('<div>User Card</div>');
expect($session->getPhases())->toHaveCount(1);
$renderPhase = $session->getPhase('render');
expect($renderPhase)->toBeInstanceOf(ProfilePhase::class);
expect($renderPhase->attributes['cached'])->toBeTrue();
expect($renderPhase->isSuccessful())->toBeTrue();
});
it('profiles action execution', function () {
$session = $this->profiler->startSession('UserCard');
$result = $this->profiler->profileAction($session, 'submit', function () {
return ['success' => true];
});
expect($result)->toBe(['success' => true]);
expect($session->getPhases())->toHaveCount(1);
$actionPhase = $session->getPhase('action.submit');
expect($actionPhase)->toBeInstanceOf(ProfilePhase::class);
expect($actionPhase->name)->toBe('action.submit');
expect($actionPhase->isSuccessful())->toBeTrue();
});
it('handles action errors', function () {
$session = $this->profiler->startSession('UserCard');
expect(fn() => $this->profiler->profileAction($session, 'submit', function () {
throw new \RuntimeException('Action failed');
}))->toThrow(\RuntimeException::class);
$actionPhase = $session->getPhase('action.submit');
expect($actionPhase)->toBeInstanceOf(ProfilePhase::class);
expect($actionPhase->isSuccessful())->toBeFalse();
expect($actionPhase->getError())->toBe('Action failed');
});
it('profiles cache operations', function () {
$session = $this->profiler->startSession('UserCard');
$result = $this->profiler->profileCache($session, 'get', function () {
return ['cached_data' => true];
});
expect($result)->toBe(['cached_data' => true]);
expect($session->getPhases())->toHaveCount(1);
$cachePhase = $session->getPhase('cache.get');
expect($cachePhase)->toBeInstanceOf(ProfilePhase::class);
expect($cachePhase->name)->toBe('cache.get');
expect($cachePhase->attributes['hit'])->toBeTrue();
});
it('takes memory snapshots', function () {
$session = $this->profiler->startSession('UserCard');
$snapshot = $this->profiler->takeMemorySnapshot($session, 'after_render');
expect($snapshot)->toBeInstanceOf(MemorySnapshot::class);
expect($snapshot->label)->toBe('after_render');
expect($snapshot->timestamp)->toBeInstanceOf(Timestamp::class);
expect($session->getMemorySnapshots())->toHaveCount(1);
});
it('ends session and returns result', function () {
$session = $this->profiler->startSession('UserCard');
// Profile some phases
$this->profiler->profileResolve($session, fn() => ['resolved' => true]);
$this->profiler->profileRender($session, fn() => '<div>Test</div>');
$result = $this->profiler->endSession($session);
expect($result)->toBeInstanceOf(ProfileResult::class);
expect($result->componentId)->toBe('UserCard');
expect($result->sessionId)->toBeInstanceOf(ProfileSessionId::class);
expect($result->totalDuration)->toBeInstanceOf(Duration::class);
expect($result->totalMemory)->toBeInstanceOf(Byte::class);
expect($result->phases)->toHaveCount(2);
});
it('provides timeline access', function () {
$timeline = $this->profiler->getTimeline();
expect($timeline)->toBeInstanceOf(ProfileTimeline::class);
});
});
describe('ProfileSessionId', function () {
it('generates unique session IDs', function () {
$id1 = ProfileSessionId::generate('UserCard');
$id2 = ProfileSessionId::generate('UserCard');
expect($id1->toString())->not->toBe($id2->toString());
expect($id1->toString())->toStartWith('UserCard_');
});
it('converts to string', function () {
$id = ProfileSessionId::generate('UserCard');
expect($id->toString())->toBeString();
expect(strlen($id->toString()))->toBeGreaterThan(10);
});
});
describe('ProfilePhase', function () {
it('creates from raw values', function () {
$phase = ProfilePhase::create(
name: 'render',
durationMs: 15.5,
memoryBytes: 2048,
attributes: ['cached' => true]
);
expect($phase)->toBeInstanceOf(ProfilePhase::class);
expect($phase->name)->toBe('render');
expect($phase->duration)->toBeInstanceOf(Duration::class);
expect($phase->memoryDelta)->toBeInstanceOf(Byte::class);
expect($phase->getDurationMs())->toBeFloat();
expect($phase->getMemoryBytes())->toBe(2048);
expect($phase->attributes)->toBe(['cached' => true]);
});
it('gets memory in megabytes', function () {
$phase = ProfilePhase::create(
name: 'render',
durationMs: 10.0,
memoryBytes: 1048576 // 1 MB
);
expect($phase->getMemoryMB())->toBe(1.0);
});
it('checks if phase was successful', function () {
$success = ProfilePhase::create(
name: 'render',
durationMs: 10.0,
memoryBytes: 1024,
attributes: ['success' => true]
);
$failure = ProfilePhase::create(
name: 'render',
durationMs: 10.0,
memoryBytes: 1024,
attributes: ['success' => false, 'error' => 'Render failed']
);
expect($success->isSuccessful())->toBeTrue();
expect($failure->isSuccessful())->toBeFalse();
expect($failure->getError())->toBe('Render failed');
});
it('converts to array', function () {
$phase = ProfilePhase::create(
name: 'render',
durationMs: 15.5,
memoryBytes: 2048
);
$array = $phase->toArray();
expect($array)->toHaveKeys([
'name',
'duration_ms',
'memory_bytes',
'memory_mb',
'attributes',
]);
});
});
describe('MemorySnapshot', function () {
it('creates from MemoryMonitor', function () {
$monitor = new MemoryMonitor();
$timestamp = Timestamp::now();
$snapshot = MemorySnapshot::fromMonitor('checkpoint', $monitor, $timestamp);
expect($snapshot)->toBeInstanceOf(MemorySnapshot::class);
expect($snapshot->label)->toBe('checkpoint');
expect($snapshot->currentUsage)->toBeInstanceOf(Byte::class);
expect($snapshot->peakUsage)->toBeInstanceOf(Byte::class);
expect($snapshot->timestamp)->toBe($timestamp);
});
it('takes snapshot now', function () {
$snapshot = MemorySnapshot::now('test');
expect($snapshot)->toBeInstanceOf(MemorySnapshot::class);
expect($snapshot->label)->toBe('test');
});
it('gets memory in MB', function () {
$snapshot = new MemorySnapshot(
label: 'test',
currentUsage: Byte::fromBytes(1048576), // 1 MB
peakUsage: Byte::fromBytes(2097152), // 2 MB
allocatedObjects: 100,
timestamp: Timestamp::now()
);
expect($snapshot->getCurrentMemoryMB())->toBe(1.0);
expect($snapshot->getPeakMemoryMB())->toBe(2.0);
});
it('calculates memory delta', function () {
$snapshot1 = new MemorySnapshot(
label: 'before',
currentUsage: Byte::fromBytes(1048576),
peakUsage: Byte::fromBytes(1048576),
allocatedObjects: 100,
timestamp: Timestamp::now()
);
$snapshot2 = new MemorySnapshot(
label: 'after',
currentUsage: Byte::fromBytes(2097152),
peakUsage: Byte::fromBytes(2097152),
allocatedObjects: 100,
timestamp: Timestamp::now()
);
$delta = $snapshot2->deltaFrom($snapshot1);
expect($delta->toBytes())->toBe(1048576); // 1 MB increase
});
it('checks if memory increased', function () {
$snapshot1 = new MemorySnapshot(
label: 'before',
currentUsage: Byte::fromBytes(1048576),
peakUsage: Byte::fromBytes(1048576),
allocatedObjects: 100,
timestamp: Timestamp::now()
);
$snapshot2 = new MemorySnapshot(
label: 'after',
currentUsage: Byte::fromBytes(2097152),
peakUsage: Byte::fromBytes(2097152),
allocatedObjects: 100,
timestamp: Timestamp::now()
);
expect($snapshot2->memoryIncreasedFrom($snapshot1))->toBeTrue();
expect($snapshot1->memoryIncreasedFrom($snapshot2))->toBeFalse();
});
it('converts to array', function () {
$snapshot = MemorySnapshot::now('test');
$array = $snapshot->toArray();
expect($array)->toHaveKeys([
'label',
'current_usage_bytes',
'current_usage_mb',
'peak_usage_bytes',
'peak_usage_mb',
'allocated_objects',
'timestamp',
]);
});
});

View File

@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
use App\Framework\LiveComponents\Profiling\ProfileTimeline;
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfileResult;
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfileSessionId;
use App\Framework\LiveComponents\Profiling\ValueObjects\ProfilePhase;
use App\Framework\LiveComponents\Profiling\ValueObjects\MemorySnapshot;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Timestamp;
describe('ProfileTimeline', function () {
beforeEach(function () {
$this->timeline = new ProfileTimeline();
// Create test ProfileResult
$this->result = new ProfileResult(
sessionId: ProfileSessionId::generate('UserCard'),
componentId: 'UserCard',
totalDuration: Duration::fromMilliseconds(50.0),
totalMemory: Byte::fromBytes(4096),
phases: [
ProfilePhase::create('resolve', 10.0, 1024, ['success' => true]),
ProfilePhase::create('render', 30.0, 2048, ['cached' => false, 'success' => true]),
ProfilePhase::create('action.submit', 10.0, 1024, ['success' => true]),
],
memorySnapshots: [
new MemorySnapshot(
label: 'after_resolve',
currentUsage: Byte::fromBytes(1048576),
peakUsage: Byte::fromBytes(1048576),
allocatedObjects: 100,
timestamp: Timestamp::now()
),
new MemorySnapshot(
label: 'after_render',
currentUsage: Byte::fromBytes(2097152),
peakUsage: Byte::fromBytes(2097152),
allocatedObjects: 150,
timestamp: Timestamp::now()
),
],
startTime: Timestamp::now(),
endTime: Timestamp::now()
);
});
it('generates DevTools timeline', function () {
$timeline = $this->timeline->generateDevToolsTimeline($this->result);
expect($timeline)->toBeArray();
expect($timeline)->not->toBeEmpty();
// Should have metadata event
$metadataEvent = $timeline[0];
expect($metadataEvent['ph'])->toBe('M'); // Metadata
expect($metadataEvent['name'])->toBe('thread_name');
// Should have complete events for phases
$completeEvents = array_filter($timeline, fn($e) => $e['ph'] === 'X');
expect($completeEvents)->toHaveCount(3); // 3 phases
// Should have instant events for memory snapshots
$instantEvents = array_filter($timeline, fn($e) => $e['ph'] === 'i');
expect($instantEvents)->toHaveCount(2); // 2 snapshots
});
it('generates simple timeline', function () {
$timeline = $this->timeline->generateSimpleTimeline($this->result);
expect($timeline)->toBeArray();
expect($timeline)->toHaveKeys([
'component_id',
'session_id',
'total_duration_ms',
'total_memory_mb',
'start_time',
'end_time',
'phases',
'memory_snapshots',
]);
expect($timeline['component_id'])->toBe('UserCard');
expect($timeline['phases'])->toHaveCount(3);
expect($timeline['memory_snapshots'])->toHaveCount(2);
// Verify phase structure
$firstPhase = $timeline['phases'][0];
expect($firstPhase)->toHaveKeys([
'name',
'start_ms',
'end_ms',
'duration_ms',
'memory_mb',
'success',
'error',
]);
});
it('generates flamegraph data', function () {
$flamegraph = $this->timeline->generateFlamegraph($this->result);
expect($flamegraph)->toBeString();
expect($flamegraph)->toContain('livecomponent.UserCard;resolve');
expect($flamegraph)->toContain('livecomponent.UserCard;render');
expect($flamegraph)->toContain('livecomponent.UserCard;action.submit');
// Should have stack format: "stack_name count"
$lines = explode("\n", $flamegraph);
foreach ($lines as $line) {
if (!empty($line)) {
expect($line)->toMatch('/^[^;]+;[^\s]+ \d+$/');
}
}
});
it('generates Gantt chart data', function () {
$gantt = $this->timeline->generateGanttChart($this->result);
expect($gantt)->toBeArray();
expect($gantt)->toHaveKeys(['component_id', 'total_duration_ms', 'tasks']);
expect($gantt['tasks'])->toHaveCount(3);
// Verify task structure
$firstTask = $gantt['tasks'][0];
expect($firstTask)->toHaveKeys([
'id',
'name',
'start',
'end',
'duration',
'memory',
'success',
'type',
]);
expect($firstTask['name'])->toBe('Resolve');
expect($firstTask['type'])->toBe('initialization');
});
it('generates waterfall diagram', function () {
$waterfall = $this->timeline->generateWaterfall($this->result);
expect($waterfall)->toBeArray();
expect($waterfall)->toHaveKeys(['component_id', 'total_time', 'entries']);
expect($waterfall['entries'])->toHaveCount(3);
// Verify entry structure
$firstEntry = $waterfall['entries'][0];
expect($firstEntry)->toHaveKeys([
'name',
'start_time',
'duration',
'memory_delta',
'timing',
'status',
]);
expect($firstEntry['timing'])->toHaveKeys([
'blocked',
'execution',
'total',
]);
});
it('categorizes phases by type', function () {
$gantt = $this->timeline->generateGanttChart($this->result);
$resolveTask = $gantt['tasks'][0];
$renderTask = $gantt['tasks'][1];
$actionTask = $gantt['tasks'][2];
expect($resolveTask['type'])->toBe('initialization');
expect($renderTask['type'])->toBe('rendering');
expect($actionTask['type'])->toBe('interaction');
});
it('exports as JSON in different formats', function () {
$formats = ['devtools', 'simple', 'gantt', 'waterfall'];
foreach ($formats as $format) {
$json = $this->timeline->exportAsJson($this->result, $format);
expect($json)->toBeString();
$decoded = json_decode($json, true);
expect($decoded)->not->toBeNull();
expect(json_last_error())->toBe(JSON_ERROR_NONE);
}
});
it('throws exception for unknown format', function () {
expect(fn() => $this->timeline->exportAsJson($this->result, 'unknown'))
->toThrow(\InvalidArgumentException::class);
});
it('includes error stacks in flamegraph for failed phases', function () {
$resultWithError = new ProfileResult(
sessionId: ProfileSessionId::generate('UserCard'),
componentId: 'UserCard',
totalDuration: Duration::fromMilliseconds(20.0),
totalMemory: Byte::fromBytes(2048),
phases: [
ProfilePhase::create('render', 20.0, 2048, [
'success' => false,
'error' => 'Render failed'
]),
],
memorySnapshots: [],
startTime: Timestamp::now(),
endTime: Timestamp::now()
);
$flamegraph = $this->timeline->generateFlamegraph($resultWithError);
expect($flamegraph)->toContain('livecomponent.UserCard;render');
expect($flamegraph)->toContain('livecomponent.UserCard;render;error');
});
it('generates counter events for memory tracking in DevTools format', function () {
$timeline = $this->timeline->generateDevToolsTimeline($this->result);
$counterEvents = array_filter($timeline, fn($e) => $e['ph'] === 'C');
expect(count($counterEvents))->toBeGreaterThan(0);
foreach ($counterEvents as $event) {
expect($event)->toHaveKey('args');
expect($event['args'])->toHaveKey('memory_mb');
}
});
it('preserves phase timing in simple timeline', function () {
$timeline = $this->timeline->generateSimpleTimeline($this->result);
$phases = $timeline['phases'];
// First phase starts at 0
expect($phases[0]['start_ms'])->toBe(0.0);
expect($phases[0]['end_ms'])->toBe(10.0);
// Second phase starts where first ended
expect($phases[1]['start_ms'])->toBe(10.0);
expect($phases[1]['end_ms'])->toBe(40.0);
// Third phase starts where second ended
expect($phases[2]['start_ms'])->toBe(40.0);
expect($phases[2]['end_ms'])->toBe(50.0);
});
});

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\LiveComponents\Profiling;
use App\Framework\Random\RandomGenerator;
use App\Framework\Telemetry\OperationHandle;
use App\Framework\Telemetry\TelemetryService;
/**
* Simple Test Implementation of TelemetryService
*
* Minimal implementation for testing without complex dependencies.
* Provides valid OperationHandle instances but doesn't track anything.
*/
final class SimpleTelemetryService implements TelemetryService
{
/** @var array<string, mixed> */
private array $recordedMetrics = [];
/** @var array<string, mixed> */
private array $recordedEvents = [];
public function __construct(
private readonly RandomGenerator $randomGenerator
) {}
public function startOperation(
string $name,
string $type,
array $attributes = []
): OperationHandle {
// Create a valid OperationHandle with this service instance
$operationId = 'test-op-' . bin2hex($this->randomGenerator->bytes(4));
return new OperationHandle($operationId, $this);
}
public function trace(
string $name,
string $type,
callable $callback,
array $attributes = []
): mixed {
// Just execute the callback without tracing
return $callback();
}
public function recordMetric(
string $name,
float $value,
string $unit = '',
array $attributes = []
): void {
$this->recordedMetrics[] = [
'name' => $name,
'value' => $value,
'unit' => $unit,
'attributes' => $attributes,
];
}
public function recordEvent(
string $name,
array $attributes = [],
string $severity = 'info'
): void {
$this->recordedEvents[] = [
'name' => $name,
'attributes' => $attributes,
'severity' => $severity,
];
}
/**
* End an operation (called by OperationHandle)
*/
public function endOperation(string $operationId, ?string $status = null, ?string $errorMessage = null): void
{
// No-op for testing
}
/**
* Add an attribute to an operation
*/
public function addOperationAttribute(string $operationId, string $key, mixed $value): void
{
// No-op for testing
}
/**
* Get recorded metrics for test assertions
*/
public function getRecordedMetrics(): array
{
return $this->recordedMetrics;
}
/**
* Get recorded events for test assertions
*/
public function getRecordedEvents(): array
{
return $this->recordedEvents;
}
/**
* Clear all recorded data
*/
public function clear(): void
{
$this->recordedMetrics = [];
$this->recordedEvents = [];
}
}