- 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.
500 lines
17 KiB
PHP
500 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\LiveComponents\Tracing\LiveComponentTracer;
|
|
use App\Framework\Telemetry\UnifiedTelemetryService;
|
|
use App\Framework\Telemetry\OperationHandle;
|
|
use App\Framework\Telemetry\Config\TelemetryConfig;
|
|
use App\Framework\Tracing\TraceSpan;
|
|
use App\Framework\Tracing\SpanStatus;
|
|
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
|
use App\Framework\CircuitBreaker\CircuitBreaker;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Cache\Driver\InMemoryCache;
|
|
use App\Framework\Cache\GeneralCache;
|
|
use App\Framework\Serializer\Serializer;
|
|
use App\Framework\Logging\DefaultLogger;
|
|
|
|
describe('LiveComponentTracer', function () {
|
|
beforeEach(function () {
|
|
// Create telemetry service with enabled configuration
|
|
$this->config = new TelemetryConfig(
|
|
serviceName: 'test-service',
|
|
serviceVersion: '1.0.0',
|
|
environment: 'test',
|
|
enabled: true
|
|
);
|
|
|
|
// Create real instances for final classes
|
|
$this->clock = new SystemClock();
|
|
$serializer = Mockery::mock(Serializer::class);
|
|
$serializer->shouldReceive('serialize')->andReturnUsing(fn($val) => serialize($val));
|
|
$serializer->shouldReceive('unserialize')->andReturnUsing(fn($val) => unserialize($val));
|
|
$this->cache = new GeneralCache(new InMemoryCache(), $serializer);
|
|
|
|
// Create CircuitBreaker with test dependencies
|
|
$this->circuitBreaker = new CircuitBreaker($this->cache, $this->clock, null, null, null, 'test');
|
|
|
|
// Mock interfaces
|
|
$this->performanceCollector = Mockery::mock(PerformanceCollectorInterface::class);
|
|
|
|
// Configure mocks
|
|
$this->performanceCollector->shouldReceive('startTiming')->andReturnNull();
|
|
$this->performanceCollector->shouldReceive('endTiming')->andReturnNull();
|
|
|
|
// Create simple logger for tests
|
|
$this->logger = new DefaultLogger();
|
|
|
|
$this->telemetry = new UnifiedTelemetryService(
|
|
$this->performanceCollector,
|
|
$this->circuitBreaker,
|
|
$this->logger,
|
|
$this->clock,
|
|
$this->config
|
|
);
|
|
|
|
$this->tracer = new LiveComponentTracer($this->telemetry);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
describe('Resolve Operation Tracing', function () {
|
|
it('creates trace span for component resolution', function () {
|
|
$operation = $this->tracer->traceResolve('test-component', [
|
|
'data_provider' => 'UserProvider',
|
|
]);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
// End the operation
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('includes component id in resolve attributes', function () {
|
|
$operation = $this->tracer->traceResolve('user-list', [
|
|
'initial_data' => ['page' => 1],
|
|
]);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('handles resolve operation errors gracefully', function () {
|
|
$operation = $this->tracer->traceResolve('failing-component');
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
// End with error
|
|
$operation->fail('Component resolution failed');
|
|
});
|
|
});
|
|
|
|
describe('Render Operation Tracing', function () {
|
|
it('creates trace span for component rendering', function () {
|
|
$operation = $this->tracer->traceRender('test-component', [
|
|
'cached' => false,
|
|
'template' => 'components/user-card',
|
|
]);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('tracks render operation with view type', function () {
|
|
$operation = $this->tracer->traceRender('product-list', [
|
|
'items_count' => 50,
|
|
]);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('handles render failures with error status', function () {
|
|
$operation = $this->tracer->traceRender('broken-template');
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->fail('Template rendering failed');
|
|
});
|
|
});
|
|
|
|
describe('Handle Operation Tracing', function () {
|
|
it('creates trace span for action handling', function () {
|
|
$operation = $this->tracer->traceHandle(
|
|
'form-component',
|
|
'submitForm',
|
|
['validation' => 'passed']
|
|
);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('includes action name in handle attributes', function () {
|
|
$operation = $this->tracer->traceHandle(
|
|
'cart-component',
|
|
'addItem',
|
|
['item_id' => 123]
|
|
);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('tracks action execution time', function () {
|
|
$operation = $this->tracer->traceHandle('data-table', 'sortColumn');
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
// Simulate some work
|
|
usleep(1000); // 1ms
|
|
|
|
$operation->end('success');
|
|
});
|
|
});
|
|
|
|
describe('Upload Operation Tracing', function () {
|
|
it('creates trace span for file upload', function () {
|
|
$operation = $this->tracer->traceUpload(
|
|
'file-uploader',
|
|
'upload-session-123',
|
|
['chunk' => 1, 'total_chunks' => 5]
|
|
);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('includes upload session id in attributes', function () {
|
|
$operation = $this->tracer->traceUpload(
|
|
'avatar-upload',
|
|
'session-abc-def',
|
|
['file_size' => 2048000]
|
|
);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('handles upload failures', function () {
|
|
$operation = $this->tracer->traceUpload('doc-uploader', 'session-xyz');
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->fail('Upload failed: file too large');
|
|
});
|
|
});
|
|
|
|
describe('Event Recording', function () {
|
|
it('records component lifecycle events', function () {
|
|
// Events are fire-and-forget, no return value
|
|
$this->tracer->recordEvent('mounted', 'test-component', [
|
|
'mount_time_ms' => 45.2,
|
|
]);
|
|
|
|
// Should not throw
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('records multiple events for component', function () {
|
|
$componentId = 'lifecycle-test';
|
|
|
|
$this->tracer->recordEvent('mounted', $componentId);
|
|
$this->tracer->recordEvent('updated', $componentId, ['updates' => 3]);
|
|
$this->tracer->recordEvent('destroyed', $componentId);
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('records custom component events', function () {
|
|
$this->tracer->recordEvent('form_submitted', 'contact-form', [
|
|
'fields_count' => 5,
|
|
'validation_passed' => true,
|
|
]);
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Metric Recording', function () {
|
|
it('records component metrics', function () {
|
|
$this->tracer->recordMetric('render_duration', 25.5, 'ms', [
|
|
'component_id' => 'test-component',
|
|
]);
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('records multiple metrics with different units', function () {
|
|
$this->tracer->recordMetric('cache_size', 1024, 'bytes');
|
|
$this->tracer->recordMetric('action_count', 15, 'count');
|
|
$this->tracer->recordMetric('response_time', 150.3, 'ms');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('records metrics without unit', function () {
|
|
$this->tracer->recordMetric('items_rendered', 50.0);
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Traced Execution', function () {
|
|
it('executes callback within traced operation', function () {
|
|
$result = $this->tracer->trace(
|
|
'resolve',
|
|
'test-component',
|
|
fn() => ['data' => 'loaded'],
|
|
['source' => 'database']
|
|
);
|
|
|
|
expect($result)->toBe(['data' => 'loaded']);
|
|
});
|
|
|
|
it('executes render operation with trace', function () {
|
|
$html = $this->tracer->trace(
|
|
'render',
|
|
'user-card',
|
|
fn() => '<div>User Card</div>'
|
|
);
|
|
|
|
expect($html)->toBe('<div>User Card</div>');
|
|
});
|
|
|
|
it('executes handle operation with trace', function () {
|
|
$result = $this->tracer->trace(
|
|
'handle',
|
|
'form-component',
|
|
fn() => ['success' => true, 'message' => 'Form submitted']
|
|
);
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
expect($result['message'])->toBe('Form submitted');
|
|
});
|
|
|
|
it('executes upload operation with trace', function () {
|
|
$uploadResult = $this->tracer->trace(
|
|
'upload',
|
|
'file-uploader',
|
|
fn() => ['uploaded' => true, 'file_id' => 123]
|
|
);
|
|
|
|
expect($uploadResult['uploaded'])->toBeTrue();
|
|
expect($uploadResult['file_id'])->toBe(123);
|
|
});
|
|
|
|
it('propagates exceptions from traced callbacks', function () {
|
|
expect(fn() => $this->tracer->trace(
|
|
'render',
|
|
'broken-component',
|
|
fn() => throw new \RuntimeException('Template not found')
|
|
))->toThrow(\RuntimeException::class, 'Template not found');
|
|
});
|
|
});
|
|
|
|
describe('Complex Tracing Scenarios', function () {
|
|
it('handles nested tracing operations', function () {
|
|
$result = $this->tracer->trace('resolve', 'parent-component', function() {
|
|
// Nested render operation
|
|
return $this->tracer->trace('render', 'child-component', function() {
|
|
// Nested action handling
|
|
return $this->tracer->trace('handle', 'nested-action', function() {
|
|
return ['nested' => true];
|
|
});
|
|
});
|
|
});
|
|
|
|
expect($result['nested'])->toBeTrue();
|
|
});
|
|
|
|
it('traces complete component lifecycle', function () {
|
|
$componentId = 'full-lifecycle';
|
|
|
|
// 1. Resolve
|
|
$resolveOp = $this->tracer->traceResolve($componentId);
|
|
$resolveOp->end('success');
|
|
|
|
// 2. Render
|
|
$renderOp = $this->tracer->traceRender($componentId);
|
|
$renderOp->end('success');
|
|
|
|
// 3. Handle action
|
|
$handleOp = $this->tracer->traceHandle($componentId, 'onClick');
|
|
$handleOp->end('success');
|
|
|
|
// 4. Re-render
|
|
$rerenderOp = $this->tracer->traceRender($componentId);
|
|
$rerenderOp->end('success');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('traces concurrent operations on different components', function () {
|
|
$op1 = $this->tracer->traceRender('component-1');
|
|
$op2 = $this->tracer->traceRender('component-2');
|
|
$op3 = $this->tracer->traceHandle('component-3', 'action');
|
|
|
|
// End in different order
|
|
$op2->end('success');
|
|
$op1->end('success');
|
|
$op3->end('success');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Telemetry Integration', function () {
|
|
it('checks if telemetry is enabled', function () {
|
|
expect($this->tracer->isEnabled())->toBeTrue();
|
|
});
|
|
|
|
it('provides operation stack for debugging', function () {
|
|
$this->tracer->trace('resolve', 'test-component', function() {
|
|
$stack = $this->tracer->getOperationStack();
|
|
expect($stack)->toBeString();
|
|
});
|
|
});
|
|
|
|
it('handles telemetry disabled gracefully', function () {
|
|
// Create tracer with disabled telemetry
|
|
$disabledConfig = new TelemetryConfig(
|
|
serviceName: 'test-service',
|
|
serviceVersion: '1.0.0',
|
|
environment: 'test',
|
|
enabled: false
|
|
);
|
|
$disabledTelemetry = new UnifiedTelemetryService(
|
|
$this->performanceCollector,
|
|
$this->circuitBreaker,
|
|
$this->logger,
|
|
$this->clock,
|
|
$disabledConfig
|
|
);
|
|
$disabledTracer = new LiveComponentTracer($disabledTelemetry);
|
|
|
|
expect($disabledTracer->isEnabled())->toBeFalse();
|
|
|
|
// Operations should still work without throwing
|
|
$op = $disabledTracer->traceResolve('test-component');
|
|
$op->end('success');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Performance Characteristics', function () {
|
|
it('has minimal overhead for tracing', function () {
|
|
$start = microtime(true);
|
|
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$op = $this->tracer->traceRender("component-{$i}");
|
|
$op->end('success');
|
|
}
|
|
|
|
$duration = (microtime(true) - $start) * 1000; // ms
|
|
|
|
// 100 operations should complete in reasonable time
|
|
expect($duration)->toBeLessThan(100.0); // < 1ms per operation
|
|
});
|
|
|
|
it('handles high-frequency event recording', function () {
|
|
$start = microtime(true);
|
|
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
$this->tracer->recordEvent('click', 'button-component');
|
|
}
|
|
|
|
$duration = (microtime(true) - $start) * 1000; // ms
|
|
|
|
// 1000 events should record quickly
|
|
expect($duration)->toBeLessThan(200.0); // < 0.2ms per event
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', function () {
|
|
it('records operation failures correctly', function () {
|
|
$operation = $this->tracer->traceHandle('error-component', 'failingAction');
|
|
|
|
// Simulate failure
|
|
$operation->fail('Action failed: validation error');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('continues tracing after operation errors', function () {
|
|
// First operation fails
|
|
$op1 = $this->tracer->traceResolve('failing-component');
|
|
$op1->fail('Resolution failed');
|
|
|
|
// Subsequent operations should still work
|
|
$op2 = $this->tracer->traceRender('working-component');
|
|
$op2->end('success');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('handles exceptions in traced callbacks', function () {
|
|
try {
|
|
$this->tracer->trace('handle', 'exception-component', function() {
|
|
throw new \RuntimeException('Business logic error');
|
|
});
|
|
} catch (\RuntimeException $e) {
|
|
expect($e->getMessage())->toBe('Business logic error');
|
|
}
|
|
|
|
// Tracing should still work after exception
|
|
$op = $this->tracer->traceRender('recovery-component');
|
|
$op->end('success');
|
|
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Attributes and Context', function () {
|
|
it('merges custom attributes with operation attributes', function () {
|
|
$operation = $this->tracer->traceResolve('test-component', [
|
|
'custom_attr1' => 'value1',
|
|
'custom_attr2' => 123,
|
|
]);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('preserves attribute types', function () {
|
|
$operation = $this->tracer->traceHandle('type-test', 'action', [
|
|
'string_attr' => 'text',
|
|
'int_attr' => 42,
|
|
'float_attr' => 3.14,
|
|
'bool_attr' => true,
|
|
'array_attr' => ['a', 'b', 'c'],
|
|
]);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
|
|
it('handles empty attributes gracefully', function () {
|
|
$operation = $this->tracer->traceRender('minimal-component', []);
|
|
|
|
expect($operation)->toBeInstanceOf(OperationHandle::class);
|
|
|
|
$operation->end('success');
|
|
});
|
|
});
|
|
});
|