- 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.
355 lines
11 KiB
PHP
355 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Logging\Formatter\LineFormatter;
|
|
use App\Framework\Logging\LogChannel;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
|
|
describe('LineFormatter', function () {
|
|
describe('default format', function () {
|
|
it('formats basic log record with default format', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('app');
|
|
expect($formatted)->toContain('INFO');
|
|
expect($formatted)->toContain('Test message');
|
|
});
|
|
|
|
it('includes timestamp in formatted output', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
// Check for timestamp pattern (YYYY-MM-DD HH:MM:SS)
|
|
expect($formatted)->toMatch('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/');
|
|
});
|
|
|
|
it('includes channel in formatted output', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: new LogChannel('security')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('security');
|
|
});
|
|
|
|
it('includes level name in formatted output', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::ERROR,
|
|
message: 'Error message',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('ERROR');
|
|
});
|
|
|
|
it('includes message in formatted output', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Important information',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('Important information');
|
|
});
|
|
|
|
it('defaults to "app" channel when no channel specified', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: null
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('app');
|
|
});
|
|
});
|
|
|
|
describe('custom format', function () {
|
|
it('uses custom format string', function () {
|
|
$formatter = new LineFormatter(format: '{level}: {message}');
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::WARNING,
|
|
message: 'Custom format',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toBe('WARNING: Custom format');
|
|
});
|
|
|
|
it('allows custom timestamp format', function () {
|
|
$formatter = new LineFormatter(timestampFormat: 'Y-m-d');
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
// Should match YYYY-MM-DD format without time
|
|
expect($formatted)->toMatch('/\d{4}-\d{2}-\d{2}/');
|
|
expect($formatted)->not->toMatch('/\d{2}:\d{2}:\d{2}/');
|
|
});
|
|
|
|
it('supports minimal format', function () {
|
|
$formatter = new LineFormatter(format: '{message}');
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Just the message',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toBe('Just the message');
|
|
});
|
|
});
|
|
|
|
describe('context handling', function () {
|
|
it('includes context data as JSON when present', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'User action',
|
|
channel: new LogChannel('app'),
|
|
context: ['user_id' => 123, 'action' => 'login']
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('user_id');
|
|
expect($formatted)->toContain('123');
|
|
expect($formatted)->toContain('action');
|
|
expect($formatted)->toContain('login');
|
|
});
|
|
|
|
it('includes extra data as JSON when present', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: new LogChannel('app'),
|
|
extra: ['request_id' => 'req-123']
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('request_id');
|
|
expect($formatted)->toContain('req-123');
|
|
});
|
|
|
|
it('merges context and extra data in JSON', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Test message',
|
|
channel: new LogChannel('app'),
|
|
context: ['key1' => 'value1'],
|
|
extra: ['key2' => 'value2']
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('key1');
|
|
expect($formatted)->toContain('value1');
|
|
expect($formatted)->toContain('key2');
|
|
expect($formatted)->toContain('value2');
|
|
});
|
|
|
|
it('shows empty string when no context or extra data', function () {
|
|
$formatter = new LineFormatter(format: '{message} {context}');
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'No context',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toBe('No context ');
|
|
});
|
|
});
|
|
|
|
describe('different log levels', function () {
|
|
it('formats DEBUG level correctly', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::DEBUG,
|
|
message: 'Debug info',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('DEBUG');
|
|
});
|
|
|
|
it('formats NOTICE level correctly', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::NOTICE,
|
|
message: 'Notice',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('NOTICE');
|
|
});
|
|
|
|
it('formats CRITICAL level correctly', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::CRITICAL,
|
|
message: 'Critical error',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('CRITICAL');
|
|
});
|
|
|
|
it('formats EMERGENCY level correctly', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::EMERGENCY,
|
|
message: 'Emergency',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('EMERGENCY');
|
|
});
|
|
});
|
|
|
|
describe('special characters', function () {
|
|
it('handles unicode characters in message', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Übung mit Ümläüten und 日本語',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('Übung mit Ümläüten und 日本語');
|
|
});
|
|
|
|
it('handles newlines in message', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: "Line 1\nLine 2",
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain("Line 1\nLine 2");
|
|
});
|
|
|
|
it('handles quotes in message', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Message with "quotes" and \'apostrophes\'',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('Message with "quotes" and \'apostrophes\'');
|
|
});
|
|
});
|
|
|
|
describe('complex scenarios', function () {
|
|
it('formats record with all fields', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::ERROR,
|
|
message: 'Database error',
|
|
channel: new LogChannel('database'),
|
|
context: ['query' => 'SELECT * FROM users'],
|
|
extra: ['request_id' => 'req-456']
|
|
);
|
|
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toContain('ERROR');
|
|
expect($formatted)->toContain('Database error');
|
|
expect($formatted)->toContain('database');
|
|
expect($formatted)->toContain('query');
|
|
expect($formatted)->toContain('request_id');
|
|
});
|
|
|
|
it('is invokable', function () {
|
|
$formatter = new LineFormatter();
|
|
|
|
$record = new LogRecord(
|
|
level: LogLevel::INFO,
|
|
message: 'Invokable test',
|
|
channel: new LogChannel('app')
|
|
);
|
|
|
|
// Test that formatter is invokable
|
|
$formatted = $formatter($record);
|
|
|
|
expect($formatted)->toBeString();
|
|
expect($formatted)->toContain('Invokable test');
|
|
});
|
|
});
|
|
});
|