- 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.
506 lines
17 KiB
PHP
506 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Logging\Handlers\JsonFileHandler;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
|
|
describe('JsonFileHandler', function () {
|
|
beforeEach(function () {
|
|
// Create test directory
|
|
$this->testDir = sys_get_temp_dir() . '/logger_json_test_' . uniqid();
|
|
mkdir($this->testDir, 0777, true);
|
|
|
|
$this->testLogFile = $this->testDir . '/test.json';
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Clean up test files
|
|
if (file_exists($this->testLogFile)) {
|
|
unlink($this->testLogFile);
|
|
}
|
|
if (is_dir($this->testDir)) {
|
|
rmdir($this->testDir);
|
|
}
|
|
});
|
|
|
|
describe('constructor', function () {
|
|
it('creates log file directory if it does not exist', function () {
|
|
$logFile = $this->testDir . '/nested/dir/test.json';
|
|
$handler = new JsonFileHandler($logFile);
|
|
|
|
expect(is_dir(dirname($logFile)))->toBeTrue();
|
|
});
|
|
|
|
it('accepts LogLevel enum as minLevel', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, LogLevel::ERROR);
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeFalse();
|
|
});
|
|
|
|
it('accepts int as minLevel', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, 400); // ERROR level
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeFalse();
|
|
});
|
|
|
|
it('defaults to INFO level when no minLevel specified', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$infoRecord = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$debugRecord = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::DEBUG,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($infoRecord))->toBeTrue();
|
|
expect($handler->isHandling($debugRecord))->toBeFalse();
|
|
});
|
|
|
|
it('uses default fields when no includedFields specified', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::withData(['key' => 'value']),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
// Neue einheitliche Feldnamen (konsistent mit JsonFormatter)
|
|
expect($json)->toHaveKeys(['timestamp', 'level', 'level_value', 'message', 'context', 'channel']);
|
|
});
|
|
|
|
it('uses custom includedFields when specified', function () {
|
|
$handler = new JsonFileHandler(
|
|
$this->testLogFile,
|
|
includedFields: ['level', 'message'] // Neue einheitliche Feldnamen
|
|
);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::withData(['key' => 'value']),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json)->toHaveKeys(['level', 'message']);
|
|
expect($json)->not->toHaveKey('context');
|
|
expect($json)->not->toHaveKey('channel');
|
|
});
|
|
});
|
|
|
|
describe('isHandling()', function () {
|
|
it('returns true when record level is above minLevel', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, LogLevel::WARNING);
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeTrue();
|
|
});
|
|
|
|
it('returns true when record level equals minLevel', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, LogLevel::WARNING);
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeTrue();
|
|
});
|
|
|
|
it('returns false when record level is below minLevel', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, LogLevel::ERROR);
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('handle()', function () {
|
|
it('writes valid JSON to file', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test log message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
expect(file_exists($this->testLogFile))->toBeTrue();
|
|
$content = file_get_contents($this->testLogFile);
|
|
|
|
// Check valid JSON
|
|
$json = json_decode(trim($content), true);
|
|
expect($json)->not->toBeNull();
|
|
expect(json_last_error())->toBe(JSON_ERROR_NONE);
|
|
});
|
|
|
|
it('includes message in JSON output', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Important message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json['message'])->toBe('Important message');
|
|
});
|
|
|
|
it('includes level in JSON output', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Error message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json['level'])->toBe('ERROR');
|
|
});
|
|
|
|
it('includes channel in JSON output', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'security'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json['channel'])->toBe('security');
|
|
});
|
|
|
|
it('includes context data in JSON output', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::withData(['user_id' => 123, 'action' => 'login']),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
// Context wird geflattened (nur structured data, ohne 'structured' wrapper)
|
|
expect($json['context'])->toMatchArray(['user_id' => 123, 'action' => 'login']);
|
|
});
|
|
|
|
it('includes extra data in JSON output', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = (new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
))->addExtra('request_id', 'req-123');
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json['extra'])->toBe(['request_id' => 'req-123']);
|
|
});
|
|
|
|
it('appends each log entry on new line', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record1 = new LogRecord(
|
|
message: 'First message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$record2 = new LogRecord(
|
|
message: 'Second message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record1);
|
|
$handler->handle($record2);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$lines = explode(PHP_EOL, trim($content));
|
|
|
|
expect(count($lines))->toBe(2);
|
|
|
|
$json1 = json_decode($lines[0], true);
|
|
$json2 = json_decode($lines[1], true);
|
|
|
|
expect($json1['message'])->toBe('First message');
|
|
expect($json2['message'])->toBe('Second message');
|
|
});
|
|
|
|
it('handles unicode characters correctly', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Übung mit Ümläüten und 日本語',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json['message'])->toBe('Übung mit Ümläüten und 日本語');
|
|
});
|
|
|
|
it('only includes specified fields when includedFields is set', function () {
|
|
$handler = new JsonFileHandler(
|
|
$this->testLogFile,
|
|
includedFields: ['level', 'message'] // Neue einheitliche Feldnamen
|
|
);
|
|
|
|
$record = (new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::withData(['key' => 'value']),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
))->withExtra('extra_key', 'extra_value'); // Immutable API
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect(array_keys($json))->toBe(['level', 'message']);
|
|
});
|
|
});
|
|
|
|
describe('setMinLevel()', function () {
|
|
it('updates minimum level with LogLevel', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, LogLevel::DEBUG);
|
|
$handler->setMinLevel(LogLevel::ERROR);
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeFalse();
|
|
});
|
|
|
|
it('updates minimum level with int', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile, LogLevel::DEBUG);
|
|
$handler->setMinLevel(400); // ERROR
|
|
|
|
$record = new LogRecord(
|
|
message: 'test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeFalse();
|
|
});
|
|
|
|
it('returns self for fluent API', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$result = $handler->setMinLevel(LogLevel::ERROR);
|
|
|
|
expect($result)->toBe($handler);
|
|
});
|
|
});
|
|
|
|
describe('setIncludedFields()', function () {
|
|
it('updates included fields', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
$handler->setIncludedFields(['message']);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Only message',
|
|
context: LogContext::withData(['should_not_appear' => 'value']),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect(array_keys($json))->toBe(['message']);
|
|
});
|
|
|
|
it('returns self for fluent API', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$result = $handler->setIncludedFields(['message']);
|
|
|
|
expect($result)->toBe($handler);
|
|
});
|
|
});
|
|
|
|
describe('setLogFile()', function () {
|
|
it('updates log file path', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$newLogFile = $this->testDir . '/new-test.json';
|
|
$handler->setLogFile($newLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'New file test',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
expect(file_exists($newLogFile))->toBeTrue();
|
|
|
|
// Cleanup
|
|
if (file_exists($newLogFile)) {
|
|
unlink($newLogFile);
|
|
}
|
|
});
|
|
|
|
it('returns self for fluent API', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$result = $handler->setLogFile($this->testDir . '/other.json');
|
|
|
|
expect($result)->toBe($handler);
|
|
});
|
|
});
|
|
|
|
describe('fluent interface', function () {
|
|
it('supports chaining methods', function () {
|
|
$handler = new JsonFileHandler($this->testLogFile);
|
|
|
|
$handler
|
|
->setMinLevel(LogLevel::WARNING)
|
|
->setIncludedFields(['level', 'message']) // Neue einheitliche Feldnamen
|
|
->setLogFile($this->testLogFile);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Chained config',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: new DateTimeImmutable(),
|
|
channel: 'test'
|
|
);
|
|
|
|
expect($handler->isHandling($record))->toBeTrue();
|
|
$handler->handle($record);
|
|
|
|
$content = file_get_contents($this->testLogFile);
|
|
$json = json_decode(trim($content), true);
|
|
|
|
expect($json)->toBe([
|
|
'level' => 'ERROR',
|
|
'message' => 'Chained config',
|
|
]);
|
|
});
|
|
});
|
|
});
|