- 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.
203 lines
7.5 KiB
PHP
203 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Framework\Logging\Formatter;
|
|
|
|
use App\Framework\Logging\Formatter\DevelopmentFormatter;
|
|
use App\Framework\Logging\Formatter\JsonFormatter;
|
|
use App\Framework\Logging\Formatter\LineFormatter;
|
|
use App\Framework\Logging\Formatter\StructuredFormatter;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
use DateTimeImmutable;
|
|
use DateTimeZone;
|
|
use Exception;
|
|
|
|
beforeEach(function () {
|
|
$this->timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin'));
|
|
$this->basicContext = LogContext::withData(['user_id' => 123, 'action' => 'login']);
|
|
});
|
|
|
|
describe('LineFormatter', function () {
|
|
it('formats basic log record', function () {
|
|
$formatter = new LineFormatter();
|
|
$record = new LogRecord('Test message', $this->basicContext, LogLevel::INFO, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toContain('2024-01-15 10:30:45');
|
|
expect($output)->toContain('INFO');
|
|
expect($output)->toContain('Test message');
|
|
expect($output)->toContain('user_id');
|
|
});
|
|
|
|
it('supports custom format template', function () {
|
|
$formatter = new LineFormatter('{level}: {message}');
|
|
$record = new LogRecord('Custom format', $this->basicContext, LogLevel::WARNING, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toBe('WARNING: Custom format');
|
|
});
|
|
|
|
it('includes channel in output', function () {
|
|
$formatter = new LineFormatter();
|
|
$record = new LogRecord('Channel test', $this->basicContext, LogLevel::ERROR, $this->timestamp, 'security');
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toContain('security.ERROR');
|
|
});
|
|
});
|
|
|
|
describe('JsonFormatter', function () {
|
|
it('formats record as valid JSON', function () {
|
|
$formatter = new JsonFormatter();
|
|
$record = new LogRecord('JSON test', $this->basicContext, LogLevel::DEBUG, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
$decoded = json_decode($output, true);
|
|
expect($decoded)->toBeArray();
|
|
expect($decoded['message'])->toBe('JSON test');
|
|
expect($decoded['level'])->toBe('DEBUG');
|
|
expect($decoded['context']['user_id'])->toBe(123);
|
|
});
|
|
|
|
it('formats with pretty print when enabled', function () {
|
|
$formatter = new JsonFormatter(prettyPrint: true);
|
|
$record = new LogRecord('Pretty JSON', $this->basicContext, LogLevel::INFO, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toContain("\n");
|
|
expect($output)->toContain(" ");
|
|
});
|
|
|
|
it('excludes extras when disabled', function () {
|
|
$formatter = new JsonFormatter(includeExtras: false);
|
|
$record = new LogRecord('No extras', $this->basicContext, LogLevel::INFO, $this->timestamp);
|
|
$record->addExtra('test_extra', 'value');
|
|
|
|
$output = $formatter($record);
|
|
$decoded = json_decode($output, true);
|
|
|
|
expect(isset($decoded['extra']))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('DevelopmentFormatter', function () {
|
|
it('formats with human-readable output', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
$record = new LogRecord('Dev message', $this->basicContext, LogLevel::ERROR, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toContain('10:30:45');
|
|
expect($output)->toContain('ERROR');
|
|
expect($output)->toContain('Context:');
|
|
expect($output)->toContain('user_id: 123');
|
|
});
|
|
|
|
it('formats exception details', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
$exception = new Exception('Test exception');
|
|
|
|
$context = LogContext::withData(['exception' => $exception]);
|
|
$record = new LogRecord('Exception occurred', $context, LogLevel::ERROR, $this->timestamp);
|
|
|
|
// Simulate exception processor enrichment
|
|
$record = $record->withExtras([
|
|
'exception_class' => Exception::class,
|
|
'exception_file' => __FILE__,
|
|
'exception_line' => __LINE__,
|
|
'exception_hash' => 'abc12345',
|
|
'exception_severity' => 'medium',
|
|
'stack_trace_short' => 'File.php:123 → Class::method()',
|
|
]);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toContain('Exception:');
|
|
expect($output)->toContain('Class: Exception');
|
|
expect($output)->toContain('Hash: abc12345');
|
|
expect($output)->toContain('Trace: File.php:123');
|
|
});
|
|
});
|
|
|
|
describe('StructuredFormatter', function () {
|
|
it('formats as logfmt by default', function () {
|
|
$formatter = new StructuredFormatter();
|
|
$record = new LogRecord('Structured test', $this->basicContext, LogLevel::INFO, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toBeString();
|
|
expect($output)->toContain('level=INFO');
|
|
expect($output)->toContain('msg="Structured test"');
|
|
expect($output)->toContain('user_id=123');
|
|
});
|
|
|
|
it('formats as key-value when specified', function () {
|
|
$formatter = new StructuredFormatter(format: 'kv');
|
|
$record = new LogRecord('KV test', $this->basicContext, LogLevel::WARNING, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toContain('level: WARNING');
|
|
expect($output)->toContain('msg: KV test');
|
|
expect($output)->toContain(', ');
|
|
});
|
|
|
|
it('returns array when format is array', function () {
|
|
$formatter = new StructuredFormatter(format: 'array');
|
|
$record = new LogRecord('Array test', $this->basicContext, LogLevel::INFO, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toBeArray();
|
|
expect($output['level'])->toBe('INFO');
|
|
expect($output['msg'])->toBe('Array test');
|
|
});
|
|
|
|
it('sanitizes keys properly', function () {
|
|
$context = LogContext::withData(['_internal_key' => 'value', 'key-with-dash' => 'value2']);
|
|
$formatter = new StructuredFormatter(format: 'array');
|
|
$record = new LogRecord('Sanitize test', $context, LogLevel::INFO, $this->timestamp);
|
|
|
|
$output = $formatter($record);
|
|
|
|
expect($output)->toHaveKey('internal_key'); // _ prefix removed
|
|
expect($output)->toHaveKey('key_with_dash'); // dash converted to underscore
|
|
});
|
|
});
|
|
|
|
describe('Formatter Integration', function () {
|
|
it('all formatters handle same record consistently', function () {
|
|
$context = LogContext::withData(['test' => 'value'])->addTags('integration');
|
|
$record = new LogRecord('Multi-formatter test', $context, LogLevel::INFO, $this->timestamp);
|
|
|
|
$lineFormatter = new LineFormatter();
|
|
$jsonFormatter = new JsonFormatter();
|
|
$devFormatter = new DevelopmentFormatter(colorOutput: false);
|
|
$structuredFormatter = new StructuredFormatter();
|
|
|
|
$lineOutput = $lineFormatter($record);
|
|
$jsonOutput = $jsonFormatter($record);
|
|
$devOutput = $devFormatter($record);
|
|
$structuredOutput = $structuredFormatter($record);
|
|
|
|
// All should contain the message
|
|
expect($lineOutput)->toContain('Multi-formatter test');
|
|
expect($jsonOutput)->toContain('Multi-formatter test');
|
|
expect($devOutput)->toContain('Multi-formatter test');
|
|
expect($structuredOutput)->toContain('Multi-formatter test');
|
|
|
|
// JSON should be valid
|
|
$jsonDecoded = json_decode($jsonOutput);
|
|
expect($jsonDecoded !== null)->toBeTrue();
|
|
});
|
|
});
|