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,594 @@
<?php
declare(strict_types=1);
use App\Framework\LiveComponents\Observability\ComponentMetricsCollector;
use App\Framework\Metrics\MetricType;
describe('ComponentMetricsCollector', function () {
beforeEach(function () {
$this->collector = new ComponentMetricsCollector();
});
describe('Render Metrics', function () {
it('records total renders with cached/uncached distinction', function () {
$this->collector->recordRender('user-stats', 45.5, false);
$this->collector->recordRender('user-stats', 12.3, true);
$this->collector->recordRender('dashboard', 78.9, false);
$metrics = $this->collector->getMetrics();
// Check total renders metrics exist (labels alphabetically sorted)
expect($metrics)->toHaveKey('livecomponent_renders_total{cached=false,component_id=user-stats}');
expect($metrics)->toHaveKey('livecomponent_renders_total{cached=true,component_id=user-stats}');
expect($metrics)->toHaveKey('livecomponent_renders_total{cached=false,component_id=dashboard}');
// Verify counter values
expect($metrics['livecomponent_renders_total{cached=false,component_id=user-stats}']->value)->toBe(1.0);
expect($metrics['livecomponent_renders_total{cached=true,component_id=user-stats}']->value)->toBe(1.0);
});
it('records render duration histogram', function () {
$this->collector->recordRender('counter', 23.45, false);
$metrics = $this->collector->getMetrics();
// Check duration histogram exists (labels alphabetically sorted)
expect($metrics)->toHaveKey('livecomponent_render_duration_ms{cached=false,component_id=counter}');
$durationMetric = $metrics['livecomponent_render_duration_ms{cached=false,component_id=counter}'];
expect($durationMetric->type)->toBe(MetricType::HISTOGRAM);
expect($durationMetric->value)->toBe(23.45);
expect($durationMetric->unit)->toBe('ms');
});
it('distinguishes between cached and uncached renders', function () {
$this->collector->recordRender('product-list', 120.0, false); // Uncached
$this->collector->recordRender('product-list', 5.0, true); // Cached
$metrics = $this->collector->getMetrics();
// Labels alphabetically sorted
$uncachedDuration = $metrics['livecomponent_render_duration_ms{cached=false,component_id=product-list}'];
$cachedDuration = $metrics['livecomponent_render_duration_ms{cached=true,component_id=product-list}'];
expect($uncachedDuration->value)->toBe(120.0);
expect($cachedDuration->value)->toBe(5.0);
});
});
describe('Action Metrics', function () {
it('records action executions with success/error status', function () {
$this->collector->recordAction('form', 'validate', 15.5, true);
$this->collector->recordAction('form', 'submit', 45.0, false);
$this->collector->recordAction('form', 'validate', 12.0, true);
$metrics = $this->collector->getMetrics();
// Check action totals
expect($metrics)->toHaveKey('livecomponent_actions_total{action=validate,component_id=form,status=success}');
expect($metrics)->toHaveKey('livecomponent_actions_total{action=submit,component_id=form,status=error}');
// Verify counter values
expect($metrics['livecomponent_actions_total{action=validate,component_id=form,status=success}']->value)->toBe(2.0);
expect($metrics['livecomponent_actions_total{action=submit,component_id=form,status=error}']->value)->toBe(1.0);
});
it('records action duration histogram', function () {
$this->collector->recordAction('cart', 'addItem', 34.56, true);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_action_duration_ms{action=addItem,component_id=cart}');
$durationMetric = $metrics['livecomponent_action_duration_ms{action=addItem,component_id=cart}'];
expect($durationMetric->type)->toBe(MetricType::HISTOGRAM);
expect($durationMetric->value)->toBe(34.56);
});
it('increments action error counter on failure', function () {
$this->collector->recordAction('payment', 'charge', 100.0, false);
$this->collector->recordAction('payment', 'charge', 95.0, false);
$metrics = $this->collector->getMetrics();
// Check error counter
expect($metrics)->toHaveKey('livecomponent_action_errors_total{action=charge,component_id=payment}');
expect($metrics['livecomponent_action_errors_total{action=charge,component_id=payment}']->value)->toBe(2.0);
});
it('does not increment error counter on success', function () {
$this->collector->recordAction('search', 'query', 50.0, true);
$metrics = $this->collector->getMetrics();
// Error counter should not exist
expect($metrics)->not->toHaveKey('livecomponent_action_errors_total{action=query,component_id=search}');
});
});
describe('Cache Metrics', function () {
it('records cache hits', function () {
$this->collector->recordCacheHit('user-profile', true);
$this->collector->recordCacheHit('user-profile', true);
$this->collector->recordCacheHit('dashboard', true);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_cache_hits_total{component_id=user-profile}');
expect($metrics['livecomponent_cache_hits_total{component_id=user-profile}']->value)->toBe(2.0);
expect($metrics['livecomponent_cache_hits_total{component_id=dashboard}']->value)->toBe(1.0);
});
it('records cache misses', function () {
$this->collector->recordCacheHit('stats', false);
$this->collector->recordCacheHit('stats', false);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_cache_misses_total{component_id=stats}');
expect($metrics['livecomponent_cache_misses_total{component_id=stats}']->value)->toBe(2.0);
});
it('tracks mixed cache hits and misses', function () {
$this->collector->recordCacheHit('widget', true); // Hit
$this->collector->recordCacheHit('widget', false); // Miss
$this->collector->recordCacheHit('widget', true); // Hit
$metrics = $this->collector->getMetrics();
expect($metrics['livecomponent_cache_hits_total{component_id=widget}']->value)->toBe(2.0);
expect($metrics['livecomponent_cache_misses_total{component_id=widget}']->value)->toBe(1.0);
});
});
describe('Event Metrics', function () {
it('records dispatched events', function () {
$this->collector->recordEventDispatched('chat', 'message.sent');
$this->collector->recordEventDispatched('chat', 'message.sent');
$this->collector->recordEventDispatched('chat', 'user.typing');
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_events_dispatched_total{component_id=chat,event=message.sent}');
expect($metrics['livecomponent_events_dispatched_total{component_id=chat,event=message.sent}']->value)->toBe(2.0);
expect($metrics['livecomponent_events_dispatched_total{component_id=chat,event=user.typing}']->value)->toBe(1.0);
});
it('records received events', function () {
$this->collector->recordEventReceived('notification', 'alert.received');
$this->collector->recordEventReceived('notification', 'alert.received');
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_events_received_total{component_id=notification,event=alert.received}');
expect($metrics['livecomponent_events_received_total{component_id=notification,event=alert.received}']->value)->toBe(2.0);
});
});
describe('Hydration Metrics', function () {
it('records hydration duration', function () {
$this->collector->recordHydration('interactive-map', 156.78);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_hydration_duration_ms{component_id=interactive-map}');
$metric = $metrics['livecomponent_hydration_duration_ms{component_id=interactive-map}'];
expect($metric->type)->toBe(MetricType::HISTOGRAM);
expect($metric->value)->toBe(156.78);
});
});
describe('Batch Metrics', function () {
it('records batch operations with success/failure counts', function () {
$this->collector->recordBatch(
operationCount: 5,
durationMs: 123.45,
successCount: 4,
failureCount: 1
);
$metrics = $this->collector->getMetrics();
// Check batch operation counter
expect($metrics)->toHaveKey('livecomponent_batch_operations_total{status=executed}');
expect($metrics['livecomponent_batch_operations_total{status=executed}']->value)->toBe(1.0);
// Check batch size histogram
expect($metrics)->toHaveKey('livecomponent_batch_size');
expect($metrics['livecomponent_batch_size']->value)->toBe(5.0);
// Check batch duration histogram
expect($metrics)->toHaveKey('livecomponent_batch_duration_ms');
expect($metrics['livecomponent_batch_duration_ms']->value)->toBe(123.45);
// Check success/failure counters
expect($metrics)->toHaveKey('livecomponent_batch_success_total');
expect($metrics['livecomponent_batch_success_total']->value)->toBe(4.0);
expect($metrics)->toHaveKey('livecomponent_batch_failure_total');
expect($metrics['livecomponent_batch_failure_total']->value)->toBe(1.0);
});
it('records multiple batch operations', function () {
$this->collector->recordBatch(3, 50.0, 3, 0);
$this->collector->recordBatch(7, 150.0, 5, 2);
$metrics = $this->collector->getMetrics();
// Batch operations counter should increment
expect($metrics['livecomponent_batch_operations_total{status=executed}']->value)->toBe(2.0);
// Success/failure totals should accumulate
expect($metrics['livecomponent_batch_success_total']->value)->toBe(8.0); // 3 + 5
expect($metrics['livecomponent_batch_failure_total']->value)->toBe(2.0);
});
it('handles batch with no failures', function () {
$this->collector->recordBatch(10, 200.0, 10, 0);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_batch_success_total');
expect($metrics)->not->toHaveKey('livecomponent_batch_failure_total');
});
it('handles batch with no successes', function () {
$this->collector->recordBatch(5, 100.0, 0, 5);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_batch_failure_total');
expect($metrics)->not->toHaveKey('livecomponent_batch_success_total');
});
});
describe('Fragment Metrics', function () {
it('records fragment updates with count and duration', function () {
$this->collector->recordFragmentUpdate('dashboard', 3, 45.67);
$metrics = $this->collector->getMetrics();
// Check fragment update counter
expect($metrics)->toHaveKey('livecomponent_fragment_updates_total{component_id=dashboard}');
expect($metrics['livecomponent_fragment_updates_total{component_id=dashboard}']->value)->toBe(1.0);
// Check fragment count histogram
expect($metrics)->toHaveKey('livecomponent_fragment_count');
expect($metrics['livecomponent_fragment_count']->value)->toBe(3.0);
// Check fragment duration histogram
expect($metrics)->toHaveKey('livecomponent_fragment_duration_ms');
expect($metrics['livecomponent_fragment_duration_ms']->value)->toBe(45.67);
});
it('tracks multiple fragment updates', function () {
$this->collector->recordFragmentUpdate('editor', 2, 30.0);
$this->collector->recordFragmentUpdate('editor', 5, 60.0);
$metrics = $this->collector->getMetrics();
expect($metrics['livecomponent_fragment_updates_total{component_id=editor}']->value)->toBe(2.0);
});
});
describe('Upload Metrics', function () {
it('records successful upload chunks', function () {
$this->collector->recordUploadChunk('session-123', 0, 45.6, true);
$this->collector->recordUploadChunk('session-123', 1, 42.3, true);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_upload_chunks_total{session_id=session-123,status=success}');
expect($metrics['livecomponent_upload_chunks_total{session_id=session-123,status=success}']->value)->toBe(2.0);
expect($metrics)->toHaveKey('livecomponent_upload_chunk_duration_ms{session_id=session-123}');
});
it('records failed upload chunks', function () {
$this->collector->recordUploadChunk('session-456', 0, 100.0, false);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_upload_chunks_total{session_id=session-456,status=error}');
expect($metrics['livecomponent_upload_chunks_total{session_id=session-456,status=error}']->value)->toBe(1.0);
});
it('records upload completion', function () {
$this->collector->recordUploadComplete('session-789', 5432.1, 10);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_uploads_completed_total{session_id=session-789}');
expect($metrics['livecomponent_uploads_completed_total{session_id=session-789}']->value)->toBe(1.0);
expect($metrics)->toHaveKey('livecomponent_upload_total_duration_ms');
expect($metrics['livecomponent_upload_total_duration_ms']->value)->toBe(5432.1);
expect($metrics)->toHaveKey('livecomponent_upload_chunk_count');
expect($metrics['livecomponent_upload_chunk_count']->value)->toBe(10.0);
});
});
describe('Summary Generation', function () {
it('generates summary with aggregated statistics', function () {
// Create variety of metrics
$this->collector->recordRender('comp-1', 50.0, false);
$this->collector->recordRender('comp-1', 10.0, true);
$this->collector->recordAction('comp-1', 'action1', 30.0, true);
$this->collector->recordAction('comp-1', 'action2', 40.0, false);
$this->collector->recordCacheHit('comp-1', true);
$this->collector->recordCacheHit('comp-1', true);
$this->collector->recordCacheHit('comp-1', false);
$this->collector->recordEventDispatched('comp-1', 'event1');
$summary = $this->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('total_events');
expect($summary)->toHaveKey('action_errors');
expect($summary)->toHaveKey('cache_hit_rate');
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['total_events'])->toBe(1);
expect($summary['action_errors'])->toBe(1);
});
it('calculates cache hit rate correctly', function () {
$this->collector->recordCacheHit('test', true);
$this->collector->recordCacheHit('test', true);
$this->collector->recordCacheHit('test', true);
$this->collector->recordCacheHit('test', false);
$summary = $this->collector->getSummary();
// 3 hits / 4 total = 75%
expect($summary['cache_hit_rate'])->toBe(75.0);
});
it('handles zero cache accesses gracefully', function () {
$this->collector->recordRender('test', 10.0);
$summary = $this->collector->getSummary();
expect($summary['cache_hit_rate'])->toBe(0.0);
});
it('returns zero values for empty collector', function () {
$summary = $this->collector->getSummary();
expect($summary['total_renders'])->toBe(0);
expect($summary['total_actions'])->toBe(0);
expect($summary['cache_hits'])->toBe(0);
expect($summary['cache_misses'])->toBe(0);
expect($summary['total_events'])->toBe(0);
expect($summary['action_errors'])->toBe(0);
expect($summary['cache_hit_rate'])->toBe(0.0);
});
});
describe('Prometheus Export', function () {
it('exports metrics in Prometheus format', function () {
$this->collector->recordRender('test-component', 45.5, false);
$this->collector->recordAction('test-component', 'increment', 12.3, true);
$prometheus = $this->collector->exportPrometheus();
// Check Prometheus format
expect($prometheus)->toContain('# HELP LiveComponents metrics');
expect($prometheus)->toContain('# TYPE livecomponent_* counter/histogram');
// Check metric lines exist
expect($prometheus)->toContain('livecomponent_renders_total');
expect($prometheus)->toContain('livecomponent_actions_total');
expect($prometheus)->toContain('component_id="test-component"');
});
it('includes timestamps in Prometheus format', function () {
$this->collector->recordRender('counter', 10.0);
$prometheus = $this->collector->exportPrometheus();
// Should contain timestamp (unix timestamp format)
expect($prometheus)->toMatch('/\d+\.\d+\s+\d+/');
});
it('handles labels with special characters', function () {
// Test with special characters: quote and newline
$this->collector->recordAction('form"test', "action\nname", 10.0);
$prometheus = $this->collector->exportPrometheus();
// Should escape special characters in Prometheus format
// Quote: " becomes \"
expect($prometheus)->toContain('form\\"test');
// Newline: \n becomes \n (literal backslash-n in output)
expect($prometheus)->toContain('action\nname');
});
it('exports empty string for empty collector', function () {
$prometheus = $this->collector->exportPrometheus();
expect($prometheus)->toContain('# HELP LiveComponents metrics');
expect($prometheus)->toContain('# TYPE livecomponent_* counter/histogram');
});
});
describe('Metric Key Building', function () {
it('creates unique keys for metrics with different labels', function () {
$this->collector->recordRender('component-a', 10.0, true);
$this->collector->recordRender('component-b', 20.0, true);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveKey('livecomponent_renders_total{cached=true,component_id=component-a}');
expect($metrics)->toHaveKey('livecomponent_renders_total{cached=true,component_id=component-b}');
});
it('sorts labels alphabetically in metric keys', function () {
// Record action (labels: component_id, action, status)
$this->collector->recordAction('test', 'action1', 10.0, true);
$metrics = $this->collector->getMetrics();
// Key should have labels sorted: action, component_id, status
expect($metrics)->toHaveKey('livecomponent_actions_total{action=action1,component_id=test,status=success}');
});
});
describe('Metric Retrieval', function () {
it('retrieves metric by exact key', function () {
$this->collector->recordRender('my-component', 50.0);
$metric = $this->collector->getMetric('livecomponent_renders_total{cached=false,component_id=my-component}');
expect($metric)->not->toBeNull();
expect($metric->value)->toBe(1.0);
});
it('returns null for non-existent metric', function () {
$metric = $this->collector->getMetric('non_existent_metric');
expect($metric)->toBeNull();
});
it('retrieves all metrics', function () {
$this->collector->recordRender('comp1', 10.0);
$this->collector->recordRender('comp2', 20.0);
$this->collector->recordAction('comp1', 'action1', 30.0);
$metrics = $this->collector->getMetrics();
expect($metrics)->toBeArray();
expect(count($metrics))->toBeGreaterThan(0);
});
});
describe('Reset Functionality', function () {
it('clears all metrics on reset', function () {
// Add various metrics
$this->collector->recordRender('test', 10.0);
$this->collector->recordAction('test', 'action', 20.0);
$this->collector->recordCacheHit('test', true);
expect($this->collector->getMetrics())->not->toBeEmpty();
// Reset
$this->collector->reset();
expect($this->collector->getMetrics())->toBeEmpty();
});
it('allows recording new metrics after reset', function () {
$this->collector->recordRender('test', 10.0);
$this->collector->reset();
$this->collector->recordRender('test', 20.0);
$metrics = $this->collector->getMetrics();
expect($metrics)->toHaveCount(2); // renders_total + render_duration_ms
});
it('resets summary to zero values', function () {
$this->collector->recordRender('test', 10.0);
$this->collector->reset();
$summary = $this->collector->getSummary();
expect($summary['total_renders'])->toBe(0);
expect($summary['total_actions'])->toBe(0);
expect($summary['cache_hits'])->toBe(0);
});
});
describe('Counter Incrementing', function () {
it('increments counter metric on repeated calls', function () {
$this->collector->recordRender('counter-test', 10.0);
$this->collector->recordRender('counter-test', 15.0);
$this->collector->recordRender('counter-test', 20.0);
$metrics = $this->collector->getMetrics();
$counterMetric = $metrics['livecomponent_renders_total{cached=false,component_id=counter-test}'];
expect($counterMetric->value)->toBe(3.0);
});
it('increments by custom amount in batch operations', function () {
// Batch with 5 successes
$this->collector->recordBatch(5, 100.0, 5, 0);
$metrics = $this->collector->getMetrics();
expect($metrics['livecomponent_batch_success_total']->value)->toBe(5.0);
});
});
describe('Histogram Updates', function () {
it('updates histogram value on repeated observations', function () {
$this->collector->recordRender('histogram-test', 50.0);
$this->collector->recordRender('histogram-test', 100.0);
$metrics = $this->collector->getMetrics();
$histogramMetric = $metrics['livecomponent_render_duration_ms{cached=false,component_id=histogram-test}'];
// Latest observation should be stored
expect($histogramMetric->value)->toBe(100.0);
});
});
describe('Metric Metadata', function () {
it('stores metric type correctly', function () {
$this->collector->recordRender('test', 10.0);
$metrics = $this->collector->getMetrics();
$counterMetric = $metrics['livecomponent_renders_total{cached=false,component_id=test}'];
expect($counterMetric->type)->toBe(MetricType::COUNTER);
$histogramMetric = $metrics['livecomponent_render_duration_ms{cached=false,component_id=test}'];
expect($histogramMetric->type)->toBe(MetricType::HISTOGRAM);
});
it('stores labels correctly', function () {
$this->collector->recordAction('my-component', 'my-action', 50.0, true);
$metrics = $this->collector->getMetrics();
$actionMetric = $metrics['livecomponent_actions_total{action=my-action,component_id=my-component,status=success}'];
expect($actionMetric->labels)->toHaveKey('component_id');
expect($actionMetric->labels)->toHaveKey('action');
expect($actionMetric->labels)->toHaveKey('status');
expect($actionMetric->labels['component_id'])->toBe('my-component');
expect($actionMetric->labels['action'])->toBe('my-action');
expect($actionMetric->labels['status'])->toBe('success');
});
it('stores unit for duration histograms', function () {
$this->collector->recordRender('test', 10.0);
$metrics = $this->collector->getMetrics();
$histogramMetric = $metrics['livecomponent_render_duration_ms{cached=false,component_id=test}'];
expect($histogramMetric->unit)->toBe('ms');
});
it('stores timestamp for all metrics', function () {
$this->collector->recordRender('test', 10.0);
$metrics = $this->collector->getMetrics();
foreach ($metrics as $metric) {
expect($metric->timestamp)->not->toBeNull();
}
});
});
});