- 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.
423 lines
14 KiB
PHP
423 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
use App\Framework\Logging\ValueObjects\UserContext;
|
|
use App\Framework\Logging\ValueObjects\RequestContext;
|
|
use App\Framework\Tracing\TraceContext;
|
|
use App\Framework\Random\SecureRandomGenerator;
|
|
|
|
describe('LogContext', function () {
|
|
describe('empty()', function () {
|
|
it('creates empty LogContext', function () {
|
|
$context = LogContext::empty();
|
|
|
|
expect($context->structured)->toBe([]);
|
|
expect($context->tags)->toBe([]);
|
|
expect($context->trace)->toBeNull();
|
|
expect($context->user)->toBeNull();
|
|
expect($context->request)->toBeNull();
|
|
expect($context->metadata)->toBe([]);
|
|
});
|
|
});
|
|
|
|
describe('withData()', function () {
|
|
it('creates LogContext with structured data', function () {
|
|
$data = ['user_id' => 123, 'action' => 'login'];
|
|
$context = LogContext::withData($data);
|
|
|
|
expect($context->structured)->toBe($data);
|
|
expect($context->tags)->toBe([]);
|
|
});
|
|
});
|
|
|
|
describe('withTags()', function () {
|
|
it('creates LogContext with tags', function () {
|
|
$context = LogContext::withTags('security', 'authentication');
|
|
|
|
expect($context->tags)->toBe(['security', 'authentication']);
|
|
expect($context->structured)->toBe([]);
|
|
});
|
|
|
|
it('accepts variadic tags', function () {
|
|
$context = LogContext::withTags('tag1', 'tag2', 'tag3');
|
|
|
|
expect($context->tags)->toHaveCount(3);
|
|
expect($context->tags)->toContain('tag1', 'tag2', 'tag3');
|
|
});
|
|
});
|
|
|
|
describe('addData()', function () {
|
|
it('adds single data entry', function () {
|
|
$context = LogContext::empty()->addData('key', 'value');
|
|
|
|
expect($context->structured)->toBe(['key' => 'value']);
|
|
});
|
|
|
|
it('preserves existing data when adding new', function () {
|
|
$context = LogContext::withData(['existing' => 'data'])
|
|
->addData('new', 'value');
|
|
|
|
expect($context->structured)->toBe([
|
|
'existing' => 'data',
|
|
'new' => 'value',
|
|
]);
|
|
});
|
|
|
|
it('returns new instance (immutability)', function () {
|
|
$original = LogContext::withData(['original' => 'data']);
|
|
$modified = $original->addData('new', 'value');
|
|
|
|
expect($original->structured)->toBe(['original' => 'data']);
|
|
expect($modified->structured)->toBe([
|
|
'original' => 'data',
|
|
'new' => 'value',
|
|
]);
|
|
expect($original)->not->toBe($modified);
|
|
});
|
|
});
|
|
|
|
describe('mergeData()', function () {
|
|
it('merges multiple data entries', function () {
|
|
$context = LogContext::withData(['existing' => 'data'])
|
|
->mergeData(['new1' => 'value1', 'new2' => 'value2']);
|
|
|
|
expect($context->structured)->toBe([
|
|
'existing' => 'data',
|
|
'new1' => 'value1',
|
|
'new2' => 'value2',
|
|
]);
|
|
});
|
|
|
|
it('overwrites existing keys', function () {
|
|
$context = LogContext::withData(['key' => 'old'])
|
|
->mergeData(['key' => 'new']);
|
|
|
|
expect($context->structured['key'])->toBe('new');
|
|
});
|
|
});
|
|
|
|
describe('addTags()', function () {
|
|
it('adds tags to existing tags', function () {
|
|
$context = LogContext::withTags('tag1')
|
|
->addTags('tag2', 'tag3');
|
|
|
|
expect($context->tags)->toHaveCount(3);
|
|
expect($context->tags)->toContain('tag1', 'tag2', 'tag3');
|
|
});
|
|
|
|
it('removes duplicate tags', function () {
|
|
$context = LogContext::withTags('tag1', 'tag2')
|
|
->addTags('tag2', 'tag3');
|
|
|
|
expect($context->tags)->toHaveCount(3);
|
|
expect($context->tags)->toContain('tag1', 'tag2', 'tag3');
|
|
});
|
|
|
|
it('returns new instance (immutability)', function () {
|
|
$original = LogContext::withTags('tag1');
|
|
$modified = $original->addTags('tag2');
|
|
|
|
expect($original->tags)->toBe(['tag1']);
|
|
expect($modified->tags)->toHaveCount(2);
|
|
expect($original)->not->toBe($modified);
|
|
});
|
|
});
|
|
|
|
describe('withTrace()', function () {
|
|
it('sets trace context', function () {
|
|
$random = new SecureRandomGenerator();
|
|
$trace = TraceContext::start($random, 'trace-id-123');
|
|
$context = LogContext::empty()->withTrace($trace);
|
|
|
|
expect($context->trace)->toBe($trace);
|
|
|
|
// Cleanup
|
|
TraceContext::clear();
|
|
});
|
|
});
|
|
|
|
describe('withUser()', function () {
|
|
it('sets user context', function () {
|
|
$user = new UserContext('user-123', 'john@example.com');
|
|
$context = LogContext::empty()->withUser($user);
|
|
|
|
expect($context->user)->toBe($user);
|
|
});
|
|
});
|
|
|
|
describe('withRequest()', function () {
|
|
it('sets request context', function () {
|
|
$request = RequestContext::empty();
|
|
$context = LogContext::empty()->withRequest($request);
|
|
|
|
expect($context->request)->toBe($request);
|
|
});
|
|
});
|
|
|
|
describe('addMetadata()', function () {
|
|
it('adds metadata entry', function () {
|
|
$context = LogContext::empty()->addMetadata('key', 'value');
|
|
|
|
expect($context->metadata)->toBe(['key' => 'value']);
|
|
});
|
|
|
|
it('preserves existing metadata', function () {
|
|
$context = LogContext::empty()
|
|
->addMetadata('key1', 'value1')
|
|
->addMetadata('key2', 'value2');
|
|
|
|
expect($context->metadata)->toBe([
|
|
'key1' => 'value1',
|
|
'key2' => 'value2',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('merge()', function () {
|
|
it('merges two LogContexts', function () {
|
|
$context1 = LogContext::withData(['key1' => 'value1'])
|
|
->addTags('tag1');
|
|
|
|
$context2 = LogContext::withData(['key2' => 'value2'])
|
|
->addTags('tag2');
|
|
|
|
$merged = $context1->merge($context2);
|
|
|
|
expect($merged->structured)->toBe([
|
|
'key1' => 'value1',
|
|
'key2' => 'value2',
|
|
]);
|
|
expect($merged->tags)->toContain('tag1', 'tag2');
|
|
});
|
|
|
|
it('overwrites structured data on merge', function () {
|
|
$context1 = LogContext::withData(['key' => 'old']);
|
|
$context2 = LogContext::withData(['key' => 'new']);
|
|
|
|
$merged = $context1->merge($context2);
|
|
|
|
expect($merged->structured['key'])->toBe('new');
|
|
});
|
|
|
|
it('removes duplicate tags on merge', function () {
|
|
$context1 = LogContext::withTags('tag1', 'tag2');
|
|
$context2 = LogContext::withTags('tag2', 'tag3');
|
|
|
|
$merged = $context1->merge($context2);
|
|
|
|
expect($merged->tags)->toHaveCount(3);
|
|
expect($merged->tags)->toContain('tag1', 'tag2', 'tag3');
|
|
});
|
|
|
|
it('prefers other context for trace/user/request', function () {
|
|
$random = new SecureRandomGenerator();
|
|
$trace1 = TraceContext::start($random, 'trace-1');
|
|
TraceContext::clear();
|
|
$trace2 = TraceContext::start($random, 'trace-2');
|
|
|
|
$user1 = new UserContext('user-1', 'user1@example.com');
|
|
$user2 = new UserContext('user-2', 'user2@example.com');
|
|
|
|
$context1 = LogContext::empty()
|
|
->withTrace($trace1)
|
|
->withUser($user1);
|
|
|
|
$context2 = LogContext::empty()
|
|
->withTrace($trace2)
|
|
->withUser($user2);
|
|
|
|
$merged = $context1->merge($context2);
|
|
|
|
expect($merged->trace)->toBe($trace2);
|
|
expect($merged->user)->toBe($user2);
|
|
|
|
// Cleanup
|
|
TraceContext::clear();
|
|
});
|
|
|
|
it('keeps original trace/user/request when other is null', function () {
|
|
$random = new SecureRandomGenerator();
|
|
$trace = TraceContext::start($random, 'trace-1');
|
|
$user = new UserContext('user-1', 'user1@example.com');
|
|
|
|
$context1 = LogContext::empty()
|
|
->withTrace($trace)
|
|
->withUser($user);
|
|
|
|
$context2 = LogContext::withData(['key' => 'value']);
|
|
|
|
$merged = $context1->merge($context2);
|
|
|
|
expect($merged->trace)->toBe($trace);
|
|
expect($merged->user)->toBe($user);
|
|
|
|
// Cleanup
|
|
TraceContext::clear();
|
|
});
|
|
});
|
|
|
|
describe('hasStructuredData()', function () {
|
|
it('returns true when structured data exists', function () {
|
|
$context = LogContext::withData(['key' => 'value']);
|
|
|
|
expect($context->hasStructuredData())->toBeTrue();
|
|
});
|
|
|
|
it('returns false when no structured data', function () {
|
|
$context = LogContext::empty();
|
|
|
|
expect($context->hasStructuredData())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('hasTags()', function () {
|
|
it('returns true when tags exist', function () {
|
|
$context = LogContext::withTags('tag1');
|
|
|
|
expect($context->hasTags())->toBeTrue();
|
|
});
|
|
|
|
it('returns false when no tags', function () {
|
|
$context = LogContext::empty();
|
|
|
|
expect($context->hasTags())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('hasTag()', function () {
|
|
it('returns true when specific tag exists', function () {
|
|
$context = LogContext::withTags('security', 'auth');
|
|
|
|
expect($context->hasTag('security'))->toBeTrue();
|
|
expect($context->hasTag('auth'))->toBeTrue();
|
|
});
|
|
|
|
it('returns false when tag does not exist', function () {
|
|
$context = LogContext::withTags('security');
|
|
|
|
expect($context->hasTag('performance'))->toBeFalse();
|
|
});
|
|
|
|
it('uses strict comparison', function () {
|
|
$context = LogContext::withTags('123');
|
|
|
|
expect($context->hasTag('123'))->toBeTrue();
|
|
// hasTag expects string, so we don't test with int
|
|
});
|
|
});
|
|
|
|
describe('toArray()', function () {
|
|
it('returns empty array for empty context', function () {
|
|
$context = LogContext::empty();
|
|
|
|
expect($context->toArray())->toBe([]);
|
|
});
|
|
|
|
it('includes structured data when present', function () {
|
|
$context = LogContext::withData(['key' => 'value']);
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKey('structured');
|
|
expect($array['structured'])->toBe(['key' => 'value']);
|
|
});
|
|
|
|
it('includes tags when present', function () {
|
|
$context = LogContext::withTags('tag1', 'tag2');
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKey('tags');
|
|
expect($array['tags'])->toBe(['tag1', 'tag2']);
|
|
});
|
|
|
|
it('includes trace when present', function () {
|
|
$random = new SecureRandomGenerator();
|
|
$trace = TraceContext::start($random, 'trace-123');
|
|
$context = LogContext::empty()->withTrace($trace);
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKey('trace');
|
|
expect($array['trace'])->toHaveKey('trace_id');
|
|
expect($array['trace']['trace_id'])->toBe('trace-123');
|
|
|
|
// Cleanup
|
|
TraceContext::clear();
|
|
});
|
|
|
|
it('includes user when present', function () {
|
|
$user = new UserContext('user-123', 'john@example.com');
|
|
$context = LogContext::empty()->withUser($user);
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKey('user');
|
|
expect($array['user'])->toBeArray();
|
|
});
|
|
|
|
it('includes request when present', function () {
|
|
$request = RequestContext::empty();
|
|
$context = LogContext::empty()->withRequest($request);
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKey('request');
|
|
expect($array['request'])->toBeArray();
|
|
});
|
|
|
|
it('includes metadata when present', function () {
|
|
$context = LogContext::empty()->addMetadata('key', 'value');
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKey('metadata');
|
|
expect($array['metadata'])->toBe(['key' => 'value']);
|
|
});
|
|
|
|
it('includes all sections when all are present', function () {
|
|
$random = new SecureRandomGenerator();
|
|
$trace = TraceContext::start($random, 'trace-123');
|
|
$user = new UserContext('user-123', 'john@example.com');
|
|
$request = RequestContext::empty();
|
|
|
|
$context = LogContext::withData(['key' => 'value'])
|
|
->addTags('security')
|
|
->withTrace($trace)
|
|
->withUser($user)
|
|
->withRequest($request)
|
|
->addMetadata('meta', 'data');
|
|
|
|
$array = $context->toArray();
|
|
|
|
expect($array)->toHaveKeys(['structured', 'tags', 'trace', 'user', 'request', 'metadata']);
|
|
|
|
// Cleanup
|
|
TraceContext::clear();
|
|
});
|
|
});
|
|
|
|
describe('immutability', function () {
|
|
it('is immutable through fluent API', function () {
|
|
$original = LogContext::empty();
|
|
|
|
$modified = $original
|
|
->addData('key', 'value')
|
|
->addTags('tag')
|
|
->addMetadata('meta', 'value');
|
|
|
|
// Original remains unchanged
|
|
expect($original->structured)->toBe([]);
|
|
expect($original->tags)->toBe([]);
|
|
expect($original->metadata)->toBe([]);
|
|
|
|
// Modified has all changes
|
|
expect($modified->structured)->toBe(['key' => 'value']);
|
|
expect($modified->tags)->toBe(['tag']);
|
|
expect($modified->metadata)->toBe(['meta' => 'value']);
|
|
});
|
|
});
|
|
});
|