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,531 @@
<?php
declare(strict_types=1);
use App\Framework\Logging\Formatter\StructuredFormatter;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
use App\Framework\Logging\ValueObjects\LogContext;
describe('StructuredFormatter', function () {
beforeEach(function () {
$this->timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin'));
});
describe('constructor', function () {
it('accepts default format', function () {
$formatter = new StructuredFormatter();
expect($formatter instanceof StructuredFormatter)->toBeTrue();
});
it('accepts logfmt format', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
expect($formatter instanceof StructuredFormatter)->toBeTrue();
});
it('accepts kv format', function () {
$formatter = new StructuredFormatter(format: 'kv');
expect($formatter instanceof StructuredFormatter)->toBeTrue();
});
it('accepts array format', function () {
$formatter = new StructuredFormatter(format: 'array');
expect($formatter instanceof StructuredFormatter)->toBeTrue();
});
});
describe('logfmt format', function () {
it('formats as logfmt string', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
$record = new LogRecord(
message: 'Test message',
context: LogContext::withData(['user_id' => 123]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBeString();
// Implementation uses abbreviated keys: ts, level, msg
expect(str_contains($result, 'level=INFO'))->toBeTrue();
expect(str_contains($result, 'msg="Test message"'))->toBeTrue();
expect(str_contains($result, 'user_id=123'))->toBeTrue();
});
it('quotes values with spaces', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
$record = new LogRecord(
message: 'Multi word message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(str_contains($result, 'msg="Multi word message"'))->toBeTrue();
});
it('quotes values with equals sign', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData(['equation' => '2+2=4']),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(str_contains($result, 'equation="2+2=4"'))->toBeTrue();
});
it('escapes quotes in values', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData(['quote' => 'He said "hello"']),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(str_contains($result, 'quote="He said \\"hello\\""'))->toBeTrue();
});
it('includes timestamp', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
// Implementation uses 'ts' key
expect(str_contains($result, 'ts='))->toBeTrue();
expect(str_contains($result, '2024-01-15'))->toBeTrue();
});
it('includes channel when provided', function () {
$formatter = new StructuredFormatter(format: 'logfmt');
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp,
channel: 'security'
);
$result = $formatter($record);
expect(str_contains($result, 'channel=security'))->toBeTrue();
});
});
describe('kv format', function () {
it('formats as key-value string', function () {
$formatter = new StructuredFormatter(format: 'kv');
$record = new LogRecord(
message: 'Test message',
context: LogContext::withData(['user_id' => 123]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBeString();
expect(str_contains($result, 'level: INFO'))->toBeTrue();
expect(str_contains($result, 'msg: Test message'))->toBeTrue();
expect(str_contains($result, 'user_id: 123'))->toBeTrue();
});
it('uses colon separator', function () {
$formatter = new StructuredFormatter(format: 'kv');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData(['key' => 'value']),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(str_contains($result, 'key: value'))->toBeTrue();
expect(str_contains($result, '='))->toBeFalse();
});
});
describe('array format', function () {
it('returns array', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBeArray();
});
it('includes all standard fields', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp,
channel: 'app'
);
$result = $formatter($record);
// Implementation uses abbreviated keys
expect(isset($result['ts']))->toBeTrue();
expect(isset($result['level']))->toBeTrue();
expect(isset($result['msg']))->toBeTrue();
expect(isset($result['channel']))->toBeTrue();
});
it('includes context data', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test message',
context: LogContext::withData([
'user_id' => 123,
'action' => 'login'
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['user_id'])->toBe('123'); // Sanitized to string
expect($result['action'])->toBe('login');
});
it('includes extras', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = (new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
))->addExtra('request_id', 'req-123');
$result = $formatter($record);
expect($result['request_id'])->toBe('req-123');
});
it('flattens context and extras', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = (new LogRecord(
message: 'Test message',
context: LogContext::withData(['ctx_key' => 'ctx_value']),
level: LogLevel::INFO,
timestamp: $this->timestamp
))->addExtra('extra_key', 'extra_value');
$result = $formatter($record);
expect($result['ctx_key'])->toBe('ctx_value');
expect($result['extra_key'])->toBe('extra_value');
});
});
describe('key sanitization', function () {
it('removes leading underscores', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData([
'_internal' => 'value'
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(isset($result['internal']))->toBeTrue();
});
it('makes keys aggregation friendly', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData([
'some.nested.key' => 'value1',
'another-key' => 'value2'
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(isset($result['some_nested_key']))->toBeTrue();
expect(isset($result['another_key']))->toBeTrue();
});
});
describe('value sanitization', function () {
it('handles string values', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData(['text' => 'simple string']),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['text'])->toBe('simple string');
});
it('converts numeric values to strings', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData([
'integer' => 42,
'float' => 3.14
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['integer'])->toBe('42');
expect($result['float'])->toBe('3.14');
});
it('converts boolean values to strings', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData([
'success' => true,
'failed' => false
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['success'])->toBe('true');
expect($result['failed'])->toBe('false');
});
it('converts null to string', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData(['nullable' => null]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['nullable'])->toBe('null');
});
it('converts arrays to JSON strings', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::withData([
'items' => ['item1', 'item2', 'item3']
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['items'])->toBeString();
expect(str_contains($result['items'], 'item1'))->toBeTrue();
expect(str_contains($result['items'], 'item2'))->toBeTrue();
});
it('converts objects to class name', function () {
$formatter = new StructuredFormatter(format: 'array');
$obj = new stdClass();
$obj->prop = 'value';
$record = new LogRecord(
message: 'Test',
context: LogContext::withData(['object' => $obj]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['object'])->toBe('stdClass');
});
});
describe('log levels', function () {
it('formats DEBUG level', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Debug',
context: LogContext::empty(),
level: LogLevel::DEBUG,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['level'])->toBe('DEBUG');
});
it('formats all log levels correctly', function () {
$formatter = new StructuredFormatter(format: 'array');
$levels = [
LogLevel::DEBUG,
LogLevel::INFO,
LogLevel::NOTICE,
LogLevel::WARNING,
LogLevel::ERROR,
LogLevel::CRITICAL,
LogLevel::ALERT,
LogLevel::EMERGENCY
];
foreach ($levels as $level) {
$record = new LogRecord(
message: 'Test',
context: LogContext::empty(),
level: $level,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['level'])->toBe($level->getName());
}
});
});
describe('readonly behavior', function () {
it('is a readonly class', function () {
$reflection = new ReflectionClass(StructuredFormatter::class);
expect($reflection->isReadOnly())->toBeTrue();
});
});
describe('edge cases', function () {
it('handles empty context and extras', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
// Should still include standard fields
expect(isset($result['ts']))->toBeTrue();
expect(isset($result['level']))->toBeTrue();
expect(isset($result['msg']))->toBeTrue();
});
it('handles very long values', function () {
$formatter = new StructuredFormatter(format: 'array');
$longValue = str_repeat('x', 10000);
$record = new LogRecord(
message: $longValue,
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result['msg'])->toHaveLength(10000);
});
it('handles unicode characters', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Unicode: 你好 🌍',
context: LogContext::withData(['key' => 'мир']),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect(str_contains($result['msg'], '你好'))->toBeTrue();
expect(str_contains($result['msg'], '🌍'))->toBeTrue();
expect($result['key'])->toBe('мир');
});
it('handles null channel', function () {
$formatter = new StructuredFormatter(format: 'array');
$record = new LogRecord(
message: 'Test',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp,
channel: null
);
$result = $formatter($record);
// Channel should not be in array if null
expect(isset($result['channel']))->toBeFalse();
});
});
});