- 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.
307 lines
11 KiB
PHP
307 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Observability\ComponentMetricsCollector;
|
|
use App\Framework\Performance\PerformanceCollector;
|
|
|
|
describe('ComponentMetricsCollector', function () {
|
|
it('records component render with metrics', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordRender('test-component-123', 45.5, false);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect(count($metrics))->toBeGreaterThan(0);
|
|
expect(isset($metrics['livecomponent_renders_total{component_id=test-component-123,cached=false}']))->toBeTrue();
|
|
});
|
|
|
|
it('records cached vs non-cached renders separately', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordRender('comp-1', 45.5, false); // non-cached
|
|
$collector->recordRender('comp-2', 10.2, true); // cached
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
// Should have separate metrics for cached and non-cached
|
|
$renderMetrics = array_filter(
|
|
$metrics,
|
|
fn($m) => str_contains($m->name, 'livecomponent_renders_total')
|
|
);
|
|
|
|
expect(count($renderMetrics))->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('records action execution with duration', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordAction('comp-123', 'handleClick', 25.5, true);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect($metrics)->toHaveKey('livecomponent_actions_total');
|
|
expect($metrics['livecomponent_actions_total']->value)->toBe(1.0);
|
|
});
|
|
|
|
it('tracks action errors separately', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordAction('comp-123', 'handleClick', 25.5, false);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect($metrics)->toHaveKey('livecomponent_action_errors_total');
|
|
expect($metrics['livecomponent_action_errors_total']->value)->toBe(1.0);
|
|
});
|
|
|
|
it('records cache hits and misses', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordCacheHit('comp-1', true);
|
|
$collector->recordCacheHit('comp-2', false);
|
|
$collector->recordCacheHit('comp-3', true);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect($metrics)->toHaveKey('livecomponent_cache_hits_total');
|
|
expect($metrics)->toHaveKey('livecomponent_cache_misses_total');
|
|
expect($metrics['livecomponent_cache_hits_total']->value)->toBe(2.0);
|
|
expect($metrics['livecomponent_cache_misses_total']->value)->toBe(1.0);
|
|
});
|
|
|
|
it('records event dispatching', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordEventDispatched('comp-1', 'user:updated');
|
|
$collector->recordEventDispatched('comp-2', 'data:loaded');
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
$eventMetrics = array_filter(
|
|
$metrics,
|
|
fn($m) => str_contains($m->name, 'livecomponent_events_dispatched_total')
|
|
);
|
|
|
|
expect(count($eventMetrics))->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('records event receiving', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordEventReceived('comp-1', 'user:updated');
|
|
$collector->recordEventReceived('comp-2', 'data:loaded');
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
$eventMetrics = array_filter(
|
|
$metrics,
|
|
fn($m) => str_contains($m->name, 'livecomponent_events_received_total')
|
|
);
|
|
|
|
expect(count($eventMetrics))->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('records hydration time', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordHydration('comp-123', 15.5);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
$hydrationMetrics = array_filter(
|
|
$metrics,
|
|
fn($m) => str_contains($m->name, 'livecomponent_hydration_duration_ms')
|
|
);
|
|
|
|
expect(count($hydrationMetrics))->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('records batch operations', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordBatch(10, 125.5, 8, 2);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect($metrics)->toHaveKey('livecomponent_batch_operations_total');
|
|
expect($metrics)->toHaveKey('livecomponent_batch_success_total');
|
|
expect($metrics)->toHaveKey('livecomponent_batch_failure_total');
|
|
expect($metrics['livecomponent_batch_success_total']->value)->toBe(8.0);
|
|
expect($metrics['livecomponent_batch_failure_total']->value)->toBe(2.0);
|
|
});
|
|
|
|
it('records fragment updates', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordFragmentUpdate('comp-123', 3, 45.5);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect($metrics)->toHaveKey('livecomponent_fragment_updates_total');
|
|
});
|
|
|
|
it('records upload chunks', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordUploadChunk('session-abc', 0, 125.5, true);
|
|
$collector->recordUploadChunk('session-abc', 1, 130.2, true);
|
|
$collector->recordUploadChunk('session-abc', 2, 128.8, false);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
$uploadMetrics = array_filter(
|
|
$metrics,
|
|
fn($m) => str_contains($m->name, 'livecomponent_upload_chunks_total')
|
|
);
|
|
|
|
expect(count($uploadMetrics))->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('records upload completion', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordUploadComplete('session-abc', 1500.5, 5);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
expect($metrics)->toHaveKey('livecomponent_uploads_completed_total');
|
|
});
|
|
|
|
it('calculates summary statistics', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
// Record various operations
|
|
$collector->recordRender('comp-1', 45.5, false);
|
|
$collector->recordRender('comp-2', 30.2, true);
|
|
$collector->recordAction('comp-1', 'handleClick', 25.5, true);
|
|
$collector->recordAction('comp-2', 'handleSubmit', 35.8, false);
|
|
$collector->recordCacheHit('comp-1', true);
|
|
$collector->recordCacheHit('comp-2', false);
|
|
$collector->recordCacheHit('comp-3', true);
|
|
|
|
$summary = $collector->getSummary();
|
|
|
|
expect($summary)->toHaveKey('total_renders');
|
|
expect($summary)->toHaveKey('total_actions');
|
|
expect($summary)->toHaveKey('cache_hits');
|
|
expect($summary)->toHaveKey('cache_misses');
|
|
expect($summary)->toHaveKey('cache_hit_rate');
|
|
expect($summary)->toHaveKey('action_errors');
|
|
|
|
expect($summary['total_renders'])->toBe(2);
|
|
expect($summary['total_actions'])->toBe(2);
|
|
expect($summary['cache_hits'])->toBe(2);
|
|
expect($summary['cache_misses'])->toBe(1);
|
|
expect($summary['action_errors'])->toBe(1);
|
|
expect($summary['cache_hit_rate'])->toBeGreaterThan(60.0);
|
|
});
|
|
|
|
it('exports metrics in Prometheus format', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordRender('comp-1', 45.5, false);
|
|
$collector->recordAction('comp-1', 'handleClick', 25.5, true);
|
|
|
|
$prometheus = $collector->exportPrometheus();
|
|
|
|
expect($prometheus)->toBeString();
|
|
expect($prometheus)->toContain('# HELP LiveComponents metrics');
|
|
expect($prometheus)->toContain('livecomponent_');
|
|
});
|
|
|
|
it('resets all metrics', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordRender('comp-1', 45.5, false);
|
|
$collector->recordAction('comp-1', 'handleClick', 25.5, true);
|
|
|
|
expect($collector->getMetrics())->not->toBeEmpty();
|
|
|
|
$collector->reset();
|
|
|
|
expect($collector->getMetrics())->toBeEmpty();
|
|
});
|
|
|
|
it('integrates with PerformanceCollector', function () {
|
|
$performanceCollector = $this->createMock(PerformanceCollector::class);
|
|
$performanceCollector->expects($this->once())
|
|
->method('recordMetric')
|
|
->with(
|
|
$this->stringContains('livecomponent.render'),
|
|
$this->anything(),
|
|
$this->equalTo(45.5),
|
|
$this->anything()
|
|
);
|
|
|
|
$collector = new ComponentMetricsCollector($performanceCollector);
|
|
|
|
$collector->recordRender('comp-1', 45.5, false);
|
|
});
|
|
|
|
it('tracks multiple components independently', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
// Component 1: 3 renders, 2 actions
|
|
$collector->recordRender('comp-1', 45.5, false);
|
|
$collector->recordRender('comp-1', 40.2, false);
|
|
$collector->recordRender('comp-1', 35.8, false);
|
|
$collector->recordAction('comp-1', 'handleClick', 25.5, true);
|
|
$collector->recordAction('comp-1', 'handleSubmit', 30.2, true);
|
|
|
|
// Component 2: 2 renders, 1 action
|
|
$collector->recordRender('comp-2', 20.5, true);
|
|
$collector->recordRender('comp-2', 18.2, true);
|
|
$collector->recordAction('comp-2', 'handleClick', 15.5, true);
|
|
|
|
$metrics = $collector->getMetrics();
|
|
|
|
// Should have metrics for both components
|
|
$renderMetrics = array_filter(
|
|
$metrics,
|
|
fn($m) => str_contains($m->name, 'livecomponent_renders_total')
|
|
);
|
|
|
|
expect(count($renderMetrics))->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('handles edge case with zero operations', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$summary = $collector->getSummary();
|
|
|
|
expect($summary['total_renders'])->toBe(0);
|
|
expect($summary['total_actions'])->toBe(0);
|
|
expect($summary['cache_hit_rate'])->toBe(0.0);
|
|
});
|
|
|
|
it('handles edge case with all cache misses', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordCacheHit('comp-1', false);
|
|
$collector->recordCacheHit('comp-2', false);
|
|
$collector->recordCacheHit('comp-3', false);
|
|
|
|
$summary = $collector->getSummary();
|
|
|
|
expect($summary['cache_hits'])->toBe(0);
|
|
expect($summary['cache_misses'])->toBe(3);
|
|
expect($summary['cache_hit_rate'])->toBe(0.0);
|
|
});
|
|
|
|
it('handles edge case with all cache hits', function () {
|
|
$collector = new ComponentMetricsCollector();
|
|
|
|
$collector->recordCacheHit('comp-1', true);
|
|
$collector->recordCacheHit('comp-2', true);
|
|
$collector->recordCacheHit('comp-3', true);
|
|
|
|
$summary = $collector->getSummary();
|
|
|
|
expect($summary['cache_hits'])->toBe(3);
|
|
expect($summary['cache_misses'])->toBe(0);
|
|
expect($summary['cache_hit_rate'])->toBe(100.0);
|
|
});
|
|
});
|