diff --git a/.env.example b/.env.example index 6e2ca12..db45496 100644 --- a/.env.example +++ b/.env.example @@ -3,20 +3,22 @@ KARIRICODE_PHP_VERSION=8.3 KARIRICODE_PHP_PORT=9303 # Canal de log padrão -LOG_CHANNEL=stack +LOG_CHANNEL=file # Nível de log padrão LOG_LEVEL=debug +LOG_ENCRYPTION_KEY=83302e6472acda6a8aeadf78409ceda3959994991393cdafbe23d2a46a148ba4 -# URL do Webhook do Slack para logs -LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX +# Slack para logs +SLACK_BOT_TOKEN=xoxb-your-bot-token-here +SLACK_CHANNEL=#your-channel-name # URL e Porta do Papertrail PAPERTRAIL_URL=logs.papertrailapp.com PAPERTRAIL_PORT=12345 # Formatter para logs stderr -LOG_STDERR_FORMATTER=\KaririCode\Logging\Formatter\JsonFormatter +LOG_STDERR_FORMATTER=json # Índice do Elasticsearch para logs ELASTIC_LOG_INDEX=logging-logs @@ -25,19 +27,29 @@ ELASTIC_LOG_INDEX=logging-logs ASYNC_LOG_ENABLED=true # Habilitar logs de consulta -QUERY_LOG_ENABLED=false -QUERY_LOG_CHANNEL=daily +QUERY_LOG_ENABLED=true +QUERY_LOG_CHANNEL=file QUERY_LOG_THRESHOLD=100 # Habilitar logs de desempenho -PERFORMANCE_LOG_ENABLED=false -PERFORMANCE_LOG_CHANNEL=daily +PERFORMANCE_LOG_ENABLED=true +PERFORMANCE_LOG_CHANNEL=file PERFORMANCE_LOG_THRESHOLD=1000 # Habilitar logs de erro ERROR_LOG_ENABLED=true -ERROR_LOG_CHANNEL=daily +ERROR_LOG_CHANNEL=file # Configurações do limpador de logs LOG_CLEANER_ENABLED=true LOG_CLEANER_KEEP_DAYS=30 + +# Configurações do CircuitBreaker +CIRCUIT_BREAKER_FAILURE_THRESHOLD=3 +CIRCUIT_BREAKER_RESET_TIMEOUT=60 + +# Configurações do Retry +RETRY_MAX_ATTEMPTS=3 +RETRY_DELAY=1000 +RETRY_MULTIPLIER=2 +RETRY_JITTER=100 diff --git a/.gitignore b/.gitignore index 2abd83c..69f72a6 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,7 @@ temp/ tmp/ .vscode/launch.json .vscode/extensions.json -tests/lista_de_arquivos.php \ No newline at end of file +tests/lista_de_arquivos.php +tests/lista_de_arquivos_test.php +lista_de_arquivos.txt +lista_de_arquivos_tests.txt diff --git a/Makefile b/Makefile index 23afa68..770b700 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Initial configurations -PHP_SERVICE := kariricode-contract +PHP_SERVICE := kariricode-logging DC := docker-compose # Command to execute commands inside the PHP container diff --git a/composer.json b/composer.json index 25f1e09..13ba835 100644 --- a/composer.json +++ b/composer.json @@ -50,11 +50,7 @@ "mockery/mockery": "^1.6", "enlightn/security-checker": "^2.0" }, - "scripts": { - "post-package-install": [ - "KaririCode\\Logging\\Util\\ComposerScripts::postPackageInstall" - ] - }, + "scripts": {}, "extra": { "branch-alias": { "dev-main": "1.0.x-dev" diff --git a/composer.lock b/composer.lock index ba0a320..5e37e75 100644 --- a/composer.lock +++ b/composer.lock @@ -3983,16 +3983,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.27", + "version": "10.5.28", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2425f713b2a5350568ccb1a2d3984841a23e83c5" + "reference": "ff7fb85cdf88131b83e721fb2a327b664dbed275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2425f713b2a5350568ccb1a2d3984841a23e83c5", - "reference": "2425f713b2a5350568ccb1a2d3984841a23e83c5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ff7fb85cdf88131b83e721fb2a327b664dbed275", + "reference": "ff7fb85cdf88131b83e721fb2a327b664dbed275", "shasum": "" }, "require": { @@ -4064,7 +4064,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.27" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.28" }, "funding": [ { @@ -4080,7 +4080,7 @@ "type": "tidelift" } ], - "time": "2024-07-10T11:48:06+00:00" + "time": "2024-07-18T14:54:16+00:00" }, { "name": "psr/cache", diff --git a/config/logging.php b/config/logging.php index c0d5bfb..0461d8c 100644 --- a/config/logging.php +++ b/config/logging.php @@ -1,161 +1,218 @@ ConfigHelper::env('LOG_CHANNEL', 'single'), - + 'default' => Config::env('LOG_CHANNEL', 'file'), + 'timezone' => Config::env('LOG_TIMEZONE', 'UTC'), 'channels' => [ - 'stack' => [ - 'driver' => 'stack', - 'channels' => ['daily', 'slack'], - 'ignore_exceptions' => false, + 'file' => [ + 'minLevel' => Config::env('LOG_LEVEL', 'debug'), + 'handlers' => [ + 'file' => [ + 'with' => ['filePath' => Config::storagePath('logs/file2.log')], + ], + ], + 'processors' => [ + 'introspection_processor', + 'anonymizer_processor' + ], + 'formatter' => [ + 'line' => [ + 'with' => [ + 'dateFormat' => 'Y-m-d H:i:s', + ] + ], + ], ], + 'console' => [ + 'minLevel' => Config::env('LOG_LEVEL', 'debug'), + 'handlers' => ['console'], + 'formatter' => [ + 'json' => [ + 'with' => [ + 'includeStacktraces' => false, + ] + ] + ], - 'single' => [ - 'driver' => 'single', - 'path' => ConfigHelper::storagePath('logs/logging.log'), - 'level' => ConfigHelper::env('LOG_LEVEL', 'debug'), - 'bubble' => true, - 'permission' => 0664, - 'locking' => false, + 'processors' => [ + 'introspection_processor', + 'anonymizer_processor' + ], ], - - 'daily' => [ - 'driver' => 'daily', - 'path' => ConfigHelper::storagePath('logs/logging.log'), - 'level' => ConfigHelper::env('LOG_LEVEL', 'debug'), - 'days' => 14, - 'bubble' => true, - 'permission' => 0664, - 'locking' => false, + 'syslog' => [ + 'minLevel' => Config::env('LOG_LEVEL', 'debug'), + 'handlers' => ['syslog'], ], - 'slack' => [ - 'driver' => 'slack', - 'url' => ConfigHelper::env('LOG_SLACK_WEBHOOK_URL'), - 'username' => 'KaririCode Log', - 'emoji' => ':boom:', - 'level' => ConfigHelper::env('LOG_LEVEL', 'critical'), - 'bubble' => true, - 'context' => ['from' => 'KaririCode'], - 'channels' => ['alerts'], + 'minLevel' => Config::env('LOG_LEVEL', 'critical'), + 'handlers' => ['slack'], + 'formatter' => [ + 'json' => [ + 'with' => [ + 'includeStacktraces' => true, + ] + ] + ], ], - - 'papertrail' => [ - 'driver' => 'monolog', - 'level' => ConfigHelper::env('LOG_LEVEL', 'debug'), - 'handler' => \KaririCode\Logging\Handler\SyslogUdpHandler::class, - 'handler_with' => [ - 'host' => ConfigHelper::env('PAPERTRAIL_URL'), - 'port' => ConfigHelper::env('PAPERTRAIL_PORT') + 'custom' => [ + 'handlers' => ['custom'], + ], + ], + 'async' => [ + 'enabled' => Config::env('ASYNC_LOG_ENABLED', true), + 'batch_size' => Config::env('ASYNC_LOG_BATCH_SIZE', 10), + 'channel' => Config::env('ASYNC_LOG_CHANNEL', 'file'), + ], + 'emergency' => [ + 'minLevel' => LogLevel::EMERGENCY, + 'path' => Config::storagePath('logs/emergency.log'), + ], + 'query' => [ + 'enabled' => Config::env('QUERY_LOG_ENABLED', false), + 'channel' => Config::env('QUERY_LOG_CHANNEL', 'file'), + 'threshold' => Config::env('QUERY_LOG_THRESHOLD', 100), // in milliseconds + 'handlers' => [ + 'console' => [ + 'with' => ['useColors' => true], + ], + 'file' => [ + 'with' => ['filePath' => Config::storagePath('logs/query.log')], ], ], - - 'stderr' => [ - 'driver' => 'monolog', - 'level' => ConfigHelper::env('LOG_LEVEL', 'debug'), - 'handler' => \KaririCode\Logging\Handler\ConsoleHandler::class, - 'formatter' => ConfigHelper::env('LOG_STDERR_FORMATTER'), + ], + 'performance' => [ + 'enabled' => Config::env('PERFORMANCE_LOG_ENABLED', false), + 'channel' => Config::env('PERFORMANCE_LOG_CHANNEL', 'file'), + 'threshold' => Config::env('PERFORMANCE_LOG_THRESHOLD', 1000), // in milliseconds + 'handlers' => [ + 'console' => [ + 'with' => ['useColors' => true], + ], + 'file' => [ + 'with' => ['filePath' => Config::storagePath('logs/performance.log')], + ], + ], + 'processors' => [ + 'memory_usage_processor', + 'cpu_usage_processor', + 'execution_time_processor', + ], + ], + 'error' => [ + 'enabled' => Config::env('ERROR_LOG_ENABLED', true), + 'channel' => Config::env('ERROR_LOG_CHANNEL', 'file'), + 'levels' => [ + LogLevel::ERROR, + LogLevel::CRITICAL, + LogLevel::ALERT, + LogLevel::EMERGENCY + ], + ], + 'log_cleaner' => [ + 'enabled' => Config::env('LOG_CLEANER_ENABLED', true), + 'keep_days' => Config::env('LOG_CLEANER_KEEP_DAYS', 30), + 'channels' => ['single', 'file'], + ], + 'handlers' => [ + 'file' => [ + 'class' => \KaririCode\Logging\Handler\FileHandler::class, 'with' => [ - 'stream' => 'php://stderr', + 'filePath' => Config::storagePath('logs/file.log'), ], ], - - 'syslog' => [ - 'driver' => 'syslog', - 'level' => ConfigHelper::env('LOG_LEVEL', 'debug'), - 'facility' => LOG_USER, - 'bubble' => true, + 'console' => [ + 'class' => \KaririCode\Logging\Handler\ConsoleHandler::class, + 'with' => [ + 'minLevel' => LogLevel::DEBUG, + 'useColors' => true, + ], ], - - 'errorlog' => [ - 'driver' => 'errorlog', - 'level' => ConfigHelper::env('LOG_LEVEL', 'debug'), - 'message_type' => 0, + 'syslog' => [ + 'class' => \KaririCode\Logging\Handler\SyslogUdpHandler::class, + 'with' => [ + 'host' => '', + 'port' => 0, + ], ], - - 'null' => [ - 'driver' => 'monolog', - 'handler' => \KaririCode\Logging\Handler\NullHandler::class, + 'slack' => [ + 'class' => \KaririCode\Logging\Handler\SlackHandler::class, + 'with' => [ + 'slackClient' => \KaririCode\Logging\Util\SlackClient::create( + Config::env('SLACK_BOT_TOKEN'), + Config::env('SLACK_CHANNEL', '#logs'), + new \KaririCode\Logging\Resilience\CircuitBreaker( + Config::env('CIRCUIT_BREAKER_FAILURE_THRESHOLD', 3), + Config::env('CIRCUIT_BREAKER_RESET_TIMEOUT', 60) + ), + new \KaririCode\Logging\Resilience\Retry( + Config::env('RETRY_MAX_ATTEMPTS', 3), + Config::env('RETRY_DELAY', 1000), + Config::env('RETRY_MULTIPLIER', 2), + Config::env('RETRY_JITTER', 100) + ), + new \KaririCode\Logging\Resilience\Fallback(), + new \KaririCode\Logging\Util\CurlClient() + ), + 'minLevel' => LogLevel::CRITICAL, + ], ], - - 'emergency' => [ - 'path' => ConfigHelper::storagePath('logs/emergency.log'), - 'level' => LogLevel::EMERGENCY, + 'custom' => [ + 'class' => \KaririCode\Logging\Handler\NullHandler::class, ], ], - 'processors' => [ - 'introspection' => [ + 'introspection_processor' => [ 'class' => \KaririCode\Logging\Processor\IntrospectionProcessor::class, - 'level' => LogLevel::DEBUG, + 'with' => [ + 'stackDepth' => 7 + ] + ], + 'memory_usage_processor' => [ + 'class' => \KaririCode\Logging\Processor\Metric\MemoryUsageProcessor::class, ], - 'git' => [ - 'class' => \KaririCode\Logging\Processor\GitProcessor::class, - 'level' => LogLevel::INFO, + 'execution_time_processor' => [ + 'class' => \KaririCode\Logging\Processor\Metric\ExecutionTimeProcessor::class, ], - 'memory_usage' => [ - 'class' => \KaririCode\Logging\Processor\MemoryUsageProcessor::class, - 'level' => LogLevel::DEBUG, + 'cpu_usage_processor' => [ + 'class' => \KaririCode\Logging\Processor\Metric\CpuUsageProcessor::class, + ], + 'metrics_processor' => [ + 'class' => \KaririCode\Logging\Processor\MetricsProcessor::class, ], 'web_processor' => [ 'class' => \KaririCode\Logging\Processor\WebProcessor::class, - 'level' => LogLevel::INFO, + ], + 'anonymizer_processor' => [ + 'class' => \KaririCode\Logging\Processor\AnonymizerProcessor::class, + 'with' => [ + 'anonymizer' => new \KaririCode\Logging\Security\Anonymizer([ + 'phone' => new \KaririCode\Logging\Security\Anonymizer\PhoneAnonymizer(), + 'ip' => new \KaririCode\Logging\Security\Anonymizer\IpAnonymizer(), + ]), + ], + ], + 'encryption_processor' => [ + 'class' => \KaririCode\Logging\Processor\EncryptionProcessor::class, + 'with' => [ + 'encryptor' => new \KaririCode\Logging\Security\Encryptor(Config::env('LOG_ENCRYPTION_KEY')), + ], ], ], - 'formatters' => [ - 'default' => [ + 'line' => [ 'class' => \KaririCode\Logging\Formatter\LineFormatter::class, - 'format' => "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n", - 'date_format' => 'Y-m-d H:i:s', - 'colors' => true, - 'multiline' => true, + 'with' => [ + 'dateFormat' => 'Y-m-d H:i:s', + ] ], 'json' => [ 'class' => \KaririCode\Logging\Formatter\JsonFormatter::class, - 'include_stacktraces' => true, - ], - 'elastic' => [ - 'class' => \KaririCode\Logging\Formatter\ElasticFormatter::class, - 'index' => ConfigHelper::env('ELASTIC_LOG_INDEX', 'logging-logs'), + 'with' => [ + 'includeStacktraces' => true, + ] ], ], - - 'async' => [ - 'enabled' => ConfigHelper::env('ASYNC_LOG_ENABLED', true), - 'driver' => \KaririCode\Logging\Decorator\AsyncLogger::class, - 'batch_size' => 10, // Process logs in batches of 10 - ], - - 'emergency_logger' => [ - 'path' => ConfigHelper::storagePath('logs/emergency.log'), - 'level' => LogLevel::EMERGENCY, - ], - - 'query_logger' => [ - 'enabled' => ConfigHelper::env('QUERY_LOG_ENABLED', false), - 'channel' => ConfigHelper::env('QUERY_LOG_CHANNEL', 'daily'), - 'threshold' => ConfigHelper::env('QUERY_LOG_THRESHOLD', 100), // in milliseconds - ], - - 'performance_logger' => [ - 'enabled' => ConfigHelper::env('PERFORMANCE_LOG_ENABLED', false), - 'channel' => ConfigHelper::env('PERFORMANCE_LOG_CHANNEL', 'daily'), - 'threshold' => ConfigHelper::env('PERFORMANCE_LOG_THRESHOLD', 1000), // in milliseconds - ], - - 'error_logger' => [ - 'enabled' => ConfigHelper::env('ERROR_LOG_ENABLED', true), - 'channel' => ConfigHelper::env('ERROR_LOG_CHANNEL', 'daily'), - 'levels' => [LogLevel::ERROR, LogLevel::CRITICAL, LogLevel::ALERT, LogLevel::EMERGENCY], - ], - - 'log_cleaner' => [ - 'enabled' => ConfigHelper::env('LOG_CLEANER_ENABLED', true), - 'keep_days' => ConfigHelper::env('LOG_CLEANER_KEEP_DAYS', 30), - 'channels' => ['single', 'daily'], - ], ]; diff --git a/docker-compose.yml b/docker-compose.yml index 0c1b8de..f345335 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ -version: "3.8" +name: kariricode-logging services: php: - container_name: kariricode-contract + container_name: kariricode-logging build: context: . dockerfile: .docker/php/Dockerfile diff --git a/src/Contract/AnonymizerStrategy.php b/src/Contract/AnonymizerStrategy.php new file mode 100644 index 0000000..04ca348 --- /dev/null +++ b/src/Contract/AnonymizerStrategy.php @@ -0,0 +1,14 @@ +processor = new AsyncLogProcessor($logger, $batchSize); + + // Register shutdown function to ensure logs are processed + register_shutdown_function([$this, 'shutdown']); } public function log(LogLevel $level, \Stringable|string $message, array $context = []): void @@ -25,9 +28,13 @@ public function log(LogLevel $level, \Stringable|string $message, array $context $this->processor->enqueue($record); } + public function shutdown(): void + { + $this->processor->processRemaining(); + } + public function __destruct() { - // Explicitly call the destructor to ensure all logs are processed before object is destroyed - $this->processor->__destruct(); + $this->shutdown(); } } diff --git a/src/Exception/InvalidConfigurationException.php b/src/Exception/InvalidConfigurationException.php new file mode 100644 index 0000000..476eb59 --- /dev/null +++ b/src/Exception/InvalidConfigurationException.php @@ -0,0 +1,9 @@ +formatter; } - // Implement the toArray method required by ImmutableValue interface public function toArray(): array { return [ 'dateFormat' => $this->dateFormat, - 'formatter' => $this->formatter instanceof ImmutableValue ? $this->formatter->toArray() : (string) $this->formatter, + 'formatter' => $this->formatter->toArray() ?? null, ]; } } diff --git a/src/Formatter/ConsoleColorFormatter.php b/src/Formatter/ConsoleColorFormatter.php index 1f1a46e..f41a039 100644 --- a/src/Formatter/ConsoleColorFormatter.php +++ b/src/Formatter/ConsoleColorFormatter.php @@ -4,25 +4,14 @@ namespace KaririCode\Logging\Formatter; -use KaririCode\Contract\Logging\LogLevel as LoggingLogLevel; -use KaririCode\Logging\LogLevel; +use KaririCode\Contract\Logging\LogLevel; class ConsoleColorFormatter { - private array $colors = [ - LogLevel::DEBUG->value => "\033[0;37m", // Light gray - LogLevel::INFO->value => "\033[0;32m", // Green - LogLevel::NOTICE->value => "\033[1;34m", // Light blue - LogLevel::WARNING->value => "\033[1;33m", // Yellow - LogLevel::ERROR->value => "\033[0;31m", // Red - LogLevel::CRITICAL->value => "\033[1;35m", // Magenta - LogLevel::ALERT->value => "\033[1;31m", // Light red - LogLevel::EMERGENCY->value => "\033[1;37m\033[41m", // White on red background - ]; private string $resetColor = "\033[0m"; - public function format(LoggingLogLevel $level, string $message): string + public function format(LogLevel $level, string $message): string { - return $this->colors[$level->value] . $message . $this->resetColor; + return $level->getColor() . $message . $this->resetColor; } } diff --git a/src/Formatter/JsonFormatter.php b/src/Formatter/JsonFormatter.php index 473ad69..c7ffc50 100644 --- a/src/Formatter/JsonFormatter.php +++ b/src/Formatter/JsonFormatter.php @@ -8,7 +8,23 @@ class JsonFormatter extends AbstractFormatter { + private const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; + public function format(ImmutableValue $record): string + { + $data = $this->prepareData($record); + + return $this->encodeJson($data); + } + + public function formatBatch(array $records): string + { + $formattedRecords = array_map([$this, 'prepareData'], $records); + + return $this->encodeJson($formattedRecords); + } + + private function prepareData(ImmutableValue $record): array { $data = [ 'datetime' => $record->datetime->format($this->dateFormat), @@ -20,16 +36,11 @@ public function format(ImmutableValue $record): string $data['context'] = $record->context; } - return json_encode( - $data, - JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - ); + return $data; } - public function formatBatch(array $records): string + private function encodeJson($data): string { - return json_encode(array_map(function ($record) { - return json_decode($this->format($record), true); - }, $records), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + return json_encode($data, self::JSON_OPTIONS | JSON_THROW_ON_ERROR); } } diff --git a/src/Formatter/LoggerFormatterFactory.php b/src/Formatter/LoggerFormatterFactory.php new file mode 100644 index 0000000..ffe39a4 --- /dev/null +++ b/src/Formatter/LoggerFormatterFactory.php @@ -0,0 +1,51 @@ +formatterMap = $config->get('formatters', [ + 'line' => LineFormatter::class, + 'json' => JsonFormatter::class, + ]); + $this->channelConfigs = $config->get('channels', []); + } + + public function createFormatter(string $channelName): LogFormatter + { + $channelConfig = $this->channelConfigs[$channelName] ?? []; + $formatterConfig = $channelConfig['formatter'] ?? 'line'; + + [$formatterType, $formatterOptions] = $this->extractMergedConfig($formatterConfig); + + $formatterClass = $this->getClassFromMap($this->formatterMap, $formatterType); + + return $this->createInstance($formatterClass, $formatterOptions); + } + + private function extractMergedConfig($config): array + { + if (is_string($config)) { + return [$config, []]; + } + + $type = key($config); + $options = $config[$type]['with'] ?? []; + + return [$type, $options]; + } +} diff --git a/src/Handler/AbstractFileHandler.php b/src/Handler/AbstractFileHandler.php index 58bc228..5cb8dd2 100644 --- a/src/Handler/AbstractFileHandler.php +++ b/src/Handler/AbstractFileHandler.php @@ -24,11 +24,25 @@ public function __construct( protected function ensureDirectoryExists(): void { $directory = dirname($this->filePath); - if (!is_dir($directory) && !mkdir($directory, 0755, true)) { - throw new LoggingException("Unable to create log directory: {$directory}"); + if (!is_dir($directory)) { + if (!$this->createDirectory($directory)) { + throw new LoggingException("Unable to create log directory: $directory"); + } + } elseif (!$this->isDirectoryWritable($directory)) { + throw new LoggingException("Log directory is not writable: $directory"); } } + protected function createDirectory($path) + { + return mkdir($path, 0777, true); + } + + protected function isDirectoryWritable($path) + { + return is_writable($path); + } + protected function openFile(): void { $this->fileHandle = fopen($this->filePath, 'a'); diff --git a/src/Handler/ConsoleHandler.php b/src/Handler/ConsoleHandler.php index 3e82c28..021fb4f 100644 --- a/src/Handler/ConsoleHandler.php +++ b/src/Handler/ConsoleHandler.php @@ -23,11 +23,16 @@ public function __construct( ) { parent::__construct($minLevel, $formatter); $this->output = fopen('php://stdout', 'w'); + $this->setFormatter($formatter); $this->colorFormatter = new ConsoleColorFormatter(); } public function handle(ImmutableValue $record): void { + if (!$this->isHandling($record)) { + return; + } + $message = $this->formatter->format($record); if ($this->useColors) { $message = $this->colorFormatter->format($record->level, $message); diff --git a/src/Handler/ExceptionHandler.php b/src/Handler/ExceptionHandler.php deleted file mode 100644 index ea7af56..0000000 --- a/src/Handler/ExceptionHandler.php +++ /dev/null @@ -1,27 +0,0 @@ -logger->error( - $exception->getMessage(), - [ - 'exception' => get_class($exception), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => $exception->getTraceAsString(), - ] - ); - } -} diff --git a/src/Handler/FileHandler.php b/src/Handler/FileHandler.php index 48e978e..00583a0 100644 --- a/src/Handler/FileHandler.php +++ b/src/Handler/FileHandler.php @@ -10,7 +10,7 @@ class FileHandler extends AbstractFileHandler { public function handle(ImmutableValue $record): void { - if ($record->level->value < $this->minLevel->value) { + if (!$this->isHandling($record)) { return; } diff --git a/src/Handler/LoggerHandlerFactory.php b/src/Handler/LoggerHandlerFactory.php new file mode 100644 index 0000000..8b9ed79 --- /dev/null +++ b/src/Handler/LoggerHandlerFactory.php @@ -0,0 +1,90 @@ +handlerMap = $config->get('handlers', []); + + $this->config = $config; + } + + public function createHandlers(string $handlerName): array + { + $handlersConfig = $this->getHandlersConfig($handlerName); + + $handlers = []; + foreach ($handlersConfig as $key => $value) { + [$handlerName, $handlerOptions] = $this->extractMergedConfig($key, $value); + $handlers[] = $this->createHandler($handlerName, $handlerOptions); + } + + return $handlers; + } + + private function getHandlersConfig(string $handlerName): array + { + $channelHandlerConfig = $this->getChannelHandlersConfig($handlerName); + $optionalHandlerConfig = $this->getOptionalHandlersConfig($handlerName); + + return $channelHandlerConfig ?? $optionalHandlerConfig ?? []; + } + + private function getChannelHandlersConfig(string $handlerName): ?array + { + $channelConfigs = $this->config->get('channels', []); + + return $channelConfigs[$handlerName]['handlers'] ?? null; + } + + private function getOptionalHandlersConfig(string $handlerName): ?array + { + $optionalHandlerConfigs = $this->config->get($handlerName, []); + + if (!self::isOptionalHandlerEnabled($optionalHandlerConfigs)) { + return []; + } + + return $optionalHandlerConfigs['handlers'] ?? $this->getChannelHandlersConfig( + $optionalHandlerConfigs['channel'] ?? 'file' + ); + } + + private static function isOptionalHandlerEnabled(array $optionalHandlerConfigs): bool + { + return isset($optionalHandlerConfigs['enabled']) && $optionalHandlerConfigs['enabled']; + } + + private function createHandler(string $handlerName, array $handlerOptions): LogHandler + { + $handlerClass = $this->getClassFromMap($this->handlerMap, $handlerName); + $handlerConfig = $this->getHandlerConfig($handlerName, $handlerOptions); + + $channelConfig = $this->config->get("channels.$handlerName", []); + $handlerConfig['minLevel'] = LogLevel::from($channelConfig['minLevel'] ?? LogLevel::DEBUG->value); + + return $this->createInstance($handlerClass, $handlerConfig); + } + + private function getHandlerConfig(string $handlerName, array $handlerOptions): array + { + $defaultConfig = $this->handlerMap[$handlerName]['with'] ?? []; + + return array_merge($defaultConfig, $handlerOptions); + } +} diff --git a/src/Handler/RotatingFileHandler.php b/src/Handler/RotatingFileHandler.php index ba695cf..e042d1d 100644 --- a/src/Handler/RotatingFileHandler.php +++ b/src/Handler/RotatingFileHandler.php @@ -35,7 +35,7 @@ public function handle(ImmutableValue $record): void $this->rotateIfNecessary(); $this->writeToFile($record); } catch (\Exception $e) { - throw new LoggingException("Error handling log record: " . $e->getMessage(), 0, $e); + throw new LoggingException('Error handling log record: ' . $e->getMessage(), 0, $e); } } diff --git a/src/Handler/SlackHandler.php b/src/Handler/SlackHandler.php index c42f808..9a55db1 100644 --- a/src/Handler/SlackHandler.php +++ b/src/Handler/SlackHandler.php @@ -7,20 +7,18 @@ use KaririCode\Contract\ImmutableValue; use KaririCode\Contract\Logging\LogFormatter; use KaririCode\Contract\Logging\LogLevel as LoggingLogLevel; +use KaririCode\Logging\Formatter\LineFormatter; use KaririCode\Logging\LogLevel; use KaririCode\Logging\Util\SlackClient; class SlackHandler extends AbstractHandler { - private SlackClient $slackClient; - public function __construct( - SlackClient $slackClient, - LoggingLogLevel $minLevel = LogLevel::CRITICAL, - ?LogFormatter $formatter = null + private SlackClient $slackClient, + protected LoggingLogLevel $minLevel = LogLevel::CRITICAL, + protected LogFormatter $formatter = new LineFormatter(), ) { parent::__construct($minLevel, $formatter); - $this->slackClient = $slackClient; } public function handle(ImmutableValue $record): void diff --git a/src/Handler/SyslogUdpHandler.php b/src/Handler/SyslogUdpHandler.php index a459787..8fd8757 100644 --- a/src/Handler/SyslogUdpHandler.php +++ b/src/Handler/SyslogUdpHandler.php @@ -40,6 +40,10 @@ protected function sendToSocket(string $packet): bool public function handle(ImmutableValue $record): void { + if (!$this->isHandling($record)) { + return; + } + $message = $this->formatter->format($record); $packet = '<' . $this->getSyslogPriority($record->level) . '>' . $message; diff --git a/src/LogLevel.php b/src/LogLevel.php index b957242..0398936 100644 --- a/src/LogLevel.php +++ b/src/LogLevel.php @@ -24,7 +24,7 @@ public function getLevel(): string public function getValue(): int { - return match($this) { + return match ($this) { self::EMERGENCY => 800, self::ALERT => 700, self::CRITICAL => 600, @@ -35,4 +35,18 @@ public function getValue(): int self::DEBUG => 100, }; } + + public function getColor(): string + { + return match ($this) { + self::DEBUG => "\033[0;37m", // Light gray + self::INFO => "\033[0;32m", // Green + self::NOTICE => "\033[1;34m", // Light blue + self::WARNING => "\033[1;33m", // Yellow + self::ERROR => "\033[0;31m", // Red + self::CRITICAL => "\033[1;35m", // Magenta + self::ALERT => "\033[1;31m", // Light red + self::EMERGENCY => "\033[1;37m\033[41m", // White on red background + }; + } } diff --git a/src/LoggerConfiguration.php b/src/LoggerConfiguration.php index 982b37a..0e9c208 100644 --- a/src/LoggerConfiguration.php +++ b/src/LoggerConfiguration.php @@ -5,19 +5,25 @@ namespace KaririCode\Logging; use KaririCode\Logging\Exception\LoggingException; +use KaririCode\Logging\Validation\ConfigurationValidator; class LoggerConfiguration { private array $config = []; + public function __construct( + private ConfigurationValidator $validator = new ConfigurationValidator() + ) { + } + public function set(string $key, mixed $value): void { - $this->config[$key] = $value; + $this->setNestedValue($this->config, $this->parseKey($key), $value); } public function get(string $key, mixed $default = null): mixed { - return $this->config[$key] ?? $default; + return $this->getNestedValue($this->config, $this->parseKey($key)) ?? $default; } public function load(string $path): void @@ -25,6 +31,54 @@ public function load(string $path): void if (!file_exists($path)) { throw new LoggingException("Configuration file not found: {$path}"); } - $this->config = require $path; + + $loadedConfig = require $path; + + if (!is_array($loadedConfig)) { + throw new LoggingException("Invalid configuration file: {$path}. Expected an array."); + } + + $this->config = $loadedConfig; + $this->validate(); + } + + public function getConfig(): array + { + return $this->config; + } + + public function validate(): void + { + $this->validator->validate($this->config); + } + + private function setNestedValue(array &$array, array $keys, mixed $value): void + { + $key = array_shift($keys); + if (empty($keys)) { + $array[$key] = $value; + } else { + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = []; + } + $this->setNestedValue($array[$key], $keys, $value); + } + } + + private function getNestedValue(array $array, array $keys): mixed + { + foreach ($keys as $key) { + if (!is_array($array) || !array_key_exists($key, $array)) { + return null; + } + $array = $array[$key]; + } + + return $array; + } + + private function parseKey(string $key): array + { + return preg_split('/(?value ?? 'debug'), - $formatter - ); - } - - if (isset($config['url'])) { - $handlers[] = new SlackHandler($config['url'], LogLevel::from($config['level']->value ?? 'critical'), $formatter); - } - - if (isset($config['handler'])) { - $handlerClass = $config['handler']; - if (SyslogUdpHandler::class === $handlerClass) { - $handlers[] = new $handlerClass( - $config['handler_with']['host'] ?? null, - (int) $config['handler_with']['port'] ?? 0 - ); - } else { - $handlerParams = $config['handler_with'] ?? []; - $handlers[] = new $handlerClass(...$handlerParams); - } - } + public function __construct( + private LoggerConfiguration $config, + private LoggerHandlerFactory $handlerFactory = new LoggerHandlerFactory(), + private LoggerProcessorFactory $processorFactory = new LoggerProcessorFactory(), + private LoggerFormatterFactory $formatterFactory = new LoggerFormatterFactory() + ) { + $this->handlerFactory->initializeFromConfiguration($config); + $this->processorFactory->initializeFromConfiguration($config); + $this->formatterFactory->initializeFromConfiguration($config); + } - $processorsConfig = $config['processors'] ?? []; - foreach ($processorsConfig as $processorConfig) { - if (class_exists($processorConfig['class'])) { - $processors[] = new $processorConfig['class'](); - } - } + public function createLogger(string $name): Logger + { + $handlers = $this->handlerFactory->createHandlers($name); + $processors = $this->processorFactory->createProcessors($name); + $formatter = $this->formatterFactory->createFormatter($name); return new LoggerManager($name, $handlers, $processors, $formatter); } - public static function createQueryLogger(string $channel, int $threshold): Logger + public function createPerformanceLogger(): Logger { - $config = [ - 'channel' => $channel, - 'threshold' => $threshold, - 'formatter' => ['class' => JsonFormatter::class], - ]; + /** @var LoggerManager $logger */ + $logger = $this->createLogger('performance'); + $threshold = $this->config->get('performance.threshold', 1000); + $logger->setThreshold('execution_time', $threshold); - return self::createLogger('query', $config); + return $logger; } - public static function createPerformanceLogger(string $channel, int $threshold): Logger + public function createQueryLogger(): Logger { - $config = [ - 'channel' => $channel, - 'threshold' => $threshold, - 'formatter' => ['class' => JsonFormatter::class], - ]; + /** @var LoggerManager $logger */ + $logger = $this->createLogger('query'); + $threshold = $this->config->get('query.threshold', 100); + $logger->setThreshold('time', $threshold); - return self::createLogger('performance', $config); + return $logger; } - public static function createErrorLogger(string $channel, array $levels): Logger + public function createErrorLogger(): Logger { - $config = [ - 'channel' => $channel, - 'levels' => $levels, - 'formatter' => ['class' => LineFormatter::class], - ]; - - return self::createLogger('error', $config); + return $this->createLogger('error'); } - public static function createAsyncLogger(string $driver, int $batchSize): Logger + public function createAsyncLogger(Logger $logger, int $batchSize): Logger { - $config = [ - 'driver' => $driver, - 'batch_size' => $batchSize, - 'formatter' => ['class' => JsonFormatter::class], - ]; - - return self::createLogger('async', $config); + return new AsyncLogger($logger, $batchSize); } } diff --git a/src/LoggerManager.php b/src/LoggerManager.php index 1f56a1f..c4ba62e 100644 --- a/src/LoggerManager.php +++ b/src/LoggerManager.php @@ -16,6 +16,8 @@ class LoggerManager implements Logger { use LoggerTrait; + private array $thresholds = []; + public function __construct( private readonly string $name, private array $handlers = [], @@ -24,8 +26,17 @@ public function __construct( ) { } + public function setThreshold(string $key, int $value): void + { + $this->thresholds[$key] = $value; + } + public function log(LogLevel $level, string|\Stringable $message, array $context = []): void { + if (!$this->passesThreshold($context)) { + return; + } + $record = new LogRecord($level, $message, $context); foreach ($this->processors as $processor) { @@ -37,6 +48,17 @@ public function log(LogLevel $level, string|\Stringable $message, array $context } } + private function passesThreshold(array $context): bool + { + foreach ($this->thresholds as $key => $threshold) { + if (isset($context[$key]) && $context[$key] < $threshold) { + return false; + } + } + + return true; + } + public function addHandler(HandlerAware $handler): self { $this->handlers[] = $handler; diff --git a/src/LoggerRegistry.php b/src/LoggerRegistry.php index 666e2dc..e341ba2 100644 --- a/src/LoggerRegistry.php +++ b/src/LoggerRegistry.php @@ -5,23 +5,37 @@ namespace KaririCode\Logging; use KaririCode\Contract\Logging\Logger; +use KaririCode\Logging\Exception\LoggerNotFoundException; class LoggerRegistry { - private static array $loggers = []; + /** @var array */ + private array $loggers = []; - public static function addLogger(string $name, Logger $logger): void + public function addLogger(string $name, Logger $logger): void { - self::$loggers[$name] = $logger; + if (isset($this->loggers[$name])) { + throw new \InvalidArgumentException('Logger with name "' . $name . '" already exists.'); + } + + $this->loggers[$name] = $logger; } - public static function getLogger(string $name): ?Logger + public function getLogger(string $name): ?Logger { - return self::$loggers[$name] ?? null; + if (!isset($this->loggers[$name])) { + throw new LoggerNotFoundException('Logger with name "' . $name . '" not found.'); + } + + return $this->loggers[$name]; } - public static function removeLogger(string $name): void + public function removeLogger(string $name): void { - unset(self::$loggers[$name]); + if (!isset($this->loggers[$name])) { + throw new LoggerNotFoundException('Logger with name "' . $name . '" not found.'); + } + + unset($this->loggers[$name]); } } diff --git a/src/Processor/AnonymizerProcessor.php b/src/Processor/AnonymizerProcessor.php new file mode 100644 index 0000000..4b57ff7 --- /dev/null +++ b/src/Processor/AnonymizerProcessor.php @@ -0,0 +1,26 @@ +anonymizer->anonymize($record->message); + + return new LogRecord( + $record->level, + $anonymizedMessage, + $record->context, + $record->datetime + ); + } +} diff --git a/src/Processor/AsyncLogProcessor.php b/src/Processor/AsyncLogProcessor.php index bcae9a2..5f3c0ba 100644 --- a/src/Processor/AsyncLogProcessor.php +++ b/src/Processor/AsyncLogProcessor.php @@ -25,25 +25,41 @@ public function enqueue(LogRecord $record): void private function ensureProcessingStarted(): void { if (null === $this->processingFiber || $this->processingFiber->isTerminated()) { - $this->processingFiber = new \Fiber(function (): void { - while (!empty($this->queue)) { - $batch = array_splice($this->queue, 0, $this->batchSize); - foreach ($batch as $record) { - $this->logger->log($record->level, $record->message, $record->context); - \Fiber::suspend(); // Cooperatively yield control - } - } - }); - $this->processingFiber->start(); + $this->startFiber(); } elseif ($this->processingFiber->isSuspended()) { $this->processingFiber->resume(); } } - public function __destruct() + private function startFiber(): void + { + $this->processingFiber = new \Fiber(function (): void { + while (!empty($this->queue)) { + $batch = array_splice($this->queue, 0, $this->batchSize); + foreach ($batch as $record) { + $this->processRecord($record); + \Fiber::suspend(); // Cooperatively yield control + } + } + }); + + $this->processingFiber->start(); + } + + private function processRecord(LogRecord $record): void + { + $this->logger->log($record->level, $record->message, $record->context); + } + + public function processRemaining(): void { while (!empty($this->queue)) { $this->ensureProcessingStarted(); } } + + public function __destruct() + { + $this->processRemaining(); + } } diff --git a/src/Processor/EncryptionProcessor.php b/src/Processor/EncryptionProcessor.php new file mode 100644 index 0000000..6719023 --- /dev/null +++ b/src/Processor/EncryptionProcessor.php @@ -0,0 +1,29 @@ +encryptor = $encryptor; + } + + public function process(ImmutableValue $record): ImmutableValue + { + $encryptedMessage = $this->encryptor->encrypt($record->message); + + return new LogRecord( + $record->level, + $encryptedMessage, + $record->context, + $record->datetime + ); + } +} diff --git a/src/Processor/GitProcessor.php b/src/Processor/GitProcessor.php deleted file mode 100644 index 662c31b..0000000 --- a/src/Processor/GitProcessor.php +++ /dev/null @@ -1,53 +0,0 @@ -detectGitInfo(); - } - - private function detectGitInfo(): void - { - if (file_exists('.git/HEAD')) { - $headContent = file_get_contents('.git/HEAD'); - if (preg_match('#ref: refs/heads/(.+)#', $headContent, $matches)) { - $this->branch = trim($matches[1]); - } - } - - if (file_exists('.git/refs/heads/' . $this->branch)) { - $this->commit = trim(file_get_contents('.git/refs/heads/' . $this->branch)); - } - } - - public function process(ImmutableValue $record): ImmutableValue - { - $context = $record->context; - - if ($this->branch) { - $context['git']['branch'] = $this->branch; - } - if ($this->commit) { - $context['git']['commit'] = $this->commit; - } - - return new LogRecord( - $record->level, - $record->message, - $context, - $record->datetime, - $record->extra - ); - } -} diff --git a/src/Processor/IntrospectionProcessor.php b/src/Processor/IntrospectionProcessor.php index 4557078..7158a9b 100644 --- a/src/Processor/IntrospectionProcessor.php +++ b/src/Processor/IntrospectionProcessor.php @@ -5,39 +5,73 @@ namespace KaririCode\Logging\Processor; use KaririCode\Contract\ImmutableValue; +use KaririCode\Logging\LogLevel; use KaririCode\Logging\LogRecord; class IntrospectionProcessor extends AbstractProcessor { - public function __construct(private int $stackDepth = 6) + public function __construct(private readonly int $stackDepth = 6) { } public function process(ImmutableValue $record): ImmutableValue { - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7); - - $context = []; - - if (!empty($trace) && isset($trace[$this->stackDepth])) { - $frame = $trace[$this->stackDepth]; - if ($this->hasValidContext($frame)) { - $context = array_merge( - $record->context, - $context = [ - 'file' => $frame['file'], - 'line' => $frame['line'], - 'class' => $frame['class'], - 'function' => $frame['function'], - ] - ); - } + if ($this->shouldTrack($record->level)) { + $trace = $this->getDebugBacktrace(); + $maxDepth = $this->getMaxDepth($trace); + + $context = $this->isValidTraceDepth($trace, $maxDepth) + ? $this->createContext($trace[$maxDepth], $record->context) + : $record->context; + + return new LogRecord( + $record->level, + $record->message, + $context, + $record->datetime + ); } - return new LogRecord( - $record->level, - $record->message, - $context + return $record; + } + + private function getDebugBacktrace(): array + { + return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + + private function getMaxDepth(array $trace): int + { + return min($this->stackDepth, count($trace) - 1); + } + + private function isValidTraceDepth(array $trace, int $depth): bool + { + return isset($trace[$depth]); + } + + private function createContext(array $frame, array $originalContext): array + { + return array_merge( + $originalContext, + [ + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + 'class' => $frame['class'] ?? null, + 'function' => $frame['function'] ?? null, + ] ); } + + private function shouldTrack(LogLevel $level): bool + { + $levelsToTrack = [ + LogLevel::ERROR, + LogLevel::CRITICAL, + LogLevel::ALERT, + LogLevel::EMERGENCY, + ]; + + return in_array($level, $levelsToTrack, true); + } } diff --git a/src/Processor/LoggerProcessorFactory.php b/src/Processor/LoggerProcessorFactory.php new file mode 100644 index 0000000..af74f13 --- /dev/null +++ b/src/Processor/LoggerProcessorFactory.php @@ -0,0 +1,90 @@ +processorMap = $config->get('processors', []); + + $this->config = $config; + } + + public function createProcessors(string $channelName): array + { + $processorsConfig = $this->getProcessorsConfig($channelName); + $processors = []; + foreach ($processorsConfig as $key => $value) { + [$processorName, $processorOptions] = $this->extractMergedConfig($key, $value); + $processors[] = $this->createProcessor($processorName, $processorOptions); + } + + return $processors; + } + + private function getProcessorsConfig(string $channelName): array + { + $channelProcessorConfig = $this->getChannelProcessorsConfig($channelName); + $optionalProcessorConfig = $this->getOptionalProcessorsConfig($channelName); + + return array_merge($channelProcessorConfig, $optionalProcessorConfig); + } + + private function getChannelProcessorsConfig(string $channelName): array + { + $channelConfigs = $this->config->get('channels', []); + + return $channelConfigs[$channelName]['processors'] ?? []; + } + + private function getOptionalProcessorsConfig(string $channelName): ?array + { + $optionalProcessorConfigs = $this->config->get($channelName, []); + + if (!self::isOptionalProcessorEnabled($optionalProcessorConfigs)) { + return []; + } + + return $optionalProcessorConfigs['processors'] ?? $this->getChannelProcessorsConfig( + $optionalProcessorConfigs['channel'] ?? 'file' + ); + } + + private static function isOptionalProcessorEnabled(array $optionalProcessorConfigs): bool + { + return isset($optionalProcessorConfigs['enabled']) && $optionalProcessorConfigs['enabled']; + } + + private function createProcessor(string $processorName, array $processorOptions): ProcessorAware + { + $processorClass = $this->getClassFromMap($this->processorMap, $processorName); + $processorConfig = $this->getProcessorConfig($processorName, $processorOptions); + + return $this->createInstance($processorClass, $processorConfig); + } + + private function getProcessorConfig(string $processorName, array $channelConfig): array + { + $defaultConfig = $this->processorMap[$processorName]['with'] ?? []; + + return array_merge($defaultConfig, $channelConfig); + } + + public function getProcessorMap(): array + { + return $this->processorMap; + } +} diff --git a/src/Processor/Metric/CpuUsageProcessor.php b/src/Processor/Metric/CpuUsageProcessor.php new file mode 100644 index 0000000..e396cb0 --- /dev/null +++ b/src/Processor/Metric/CpuUsageProcessor.php @@ -0,0 +1,25 @@ +context, ['cpu_usage' => $cpuUsage]); + + return new LogRecord( + $record->level, + $record->message, + $context, + $record->datetime + ); + } +} diff --git a/src/Processor/Metric/ExecutionTimeProcessor.php b/src/Processor/Metric/ExecutionTimeProcessor.php new file mode 100644 index 0000000..c97fac8 --- /dev/null +++ b/src/Processor/Metric/ExecutionTimeProcessor.php @@ -0,0 +1,35 @@ + $this->threshold) { + $context = array_merge($record->context, ['execution_time' => $executionTime]); + + return new LogRecord( + $record->level, + $record->message, + $context, + $record->datetime, + $record->extra + ); + } + + return $record; + } +} diff --git a/src/Processor/MemoryUsageProcessor.php b/src/Processor/Metric/MemoryUsageProcessor.php similarity index 88% rename from src/Processor/MemoryUsageProcessor.php rename to src/Processor/Metric/MemoryUsageProcessor.php index 749d4b0..605a8d5 100644 --- a/src/Processor/MemoryUsageProcessor.php +++ b/src/Processor/Metric/MemoryUsageProcessor.php @@ -1,9 +1,10 @@ level, $record->message, $context, - $record->datetime, - $record->extra + $record->datetime ); } diff --git a/src/Processor/MetricsProcessor.php b/src/Processor/MetricsProcessor.php new file mode 100644 index 0000000..c1e28e2 --- /dev/null +++ b/src/Processor/MetricsProcessor.php @@ -0,0 +1,27 @@ +processors = $processors; + } + + public function process(ImmutableValue $record): ImmutableValue + { + foreach ($this->processors as $processor) { + $record = $processor->process($record); + } + + return $record; + } +} diff --git a/src/Processor/WebProcessor.php b/src/Processor/WebProcessor.php index 487e62c..1554852 100644 --- a/src/Processor/WebProcessor.php +++ b/src/Processor/WebProcessor.php @@ -4,21 +4,19 @@ use KaririCode\Contract\ImmutableValue; use KaririCode\Logging\LogRecord; +use KaririCode\Logging\Util\Http\Contract\HttpRequest; +use KaririCode\Logging\Util\Http\ServerHttpRequest; class WebProcessor extends AbstractProcessor { + public function __construct( + private HttpRequest $request = new ServerHttpRequest() + ) { + } + public function process(ImmutableValue $record): ImmutableValue { - $server = $_SERVER; - $context = array_merge($record->context, [ - 'url' => ($server['HTTPS'] ?? 'off') === 'on' ? 'https://' : 'http://' . - ($server['HTTP_HOST'] ?? 'localhost') . - ($server['REQUEST_URI'] ?? '/'), - 'ip' => $server['REMOTE_ADDR'] ?? null, - 'http_method' => $server['REQUEST_METHOD'] ?? null, - 'server' => $server['SERVER_NAME'] ?? null, - 'referrer' => $server['HTTP_REFERER'] ?? null, - ]); + $context = $this->buildContext($record->context); return new LogRecord( $record->level, @@ -28,4 +26,15 @@ public function process(ImmutableValue $record): ImmutableValue $record->extra ); } + + private function buildContext(array $existingContext): array + { + return array_merge($existingContext, [ + 'url' => $this->request->getUrl(), + 'ip' => $this->request->getIp(), + 'http_method' => $this->request->getMethod(), + 'server' => $this->request->getServerName(), + 'referrer' => $this->request->getReferrer(), + ]); + } } diff --git a/src/Resilience/CircuitBreaker.php b/src/Resilience/CircuitBreaker.php index 5e81957..56a7300 100644 --- a/src/Resilience/CircuitBreaker.php +++ b/src/Resilience/CircuitBreaker.php @@ -25,13 +25,14 @@ public function isOpen(): bool } if ($this->failures >= $this->failureThreshold) { - if ($this->lastFailureTime === null) { + if (null === $this->lastFailureTime) { return true; } $elapsedTime = time() - $this->lastFailureTime->getTimestamp(); if ($elapsedTime > $this->resetTimeout) { $this->resetFailures(); + return false; } diff --git a/src/Rotator/SizeBasedRotator.php b/src/Rotator/SizeBasedRotator.php index cd0230d..78c536d 100644 --- a/src/Rotator/SizeBasedRotator.php +++ b/src/Rotator/SizeBasedRotator.php @@ -10,9 +10,8 @@ class SizeBasedRotator implements LogRotator { public function __construct( private int $maxFiles = 5, - private int $maxFileSize = 5 * 1024 * 1024 + private int $maxFileSize = 5 * 1024 * 1024 ) { - } public function shouldRotate(string $filePath): bool diff --git a/src/Security/Anonymizer.php b/src/Security/Anonymizer.php index 0747bad..7b7cb6b 100644 --- a/src/Security/Anonymizer.php +++ b/src/Security/Anonymizer.php @@ -4,59 +4,51 @@ namespace KaririCode\Logging\Security; +use KaririCode\Logging\Contract\AnonymizerStrategy; +use KaririCode\Logging\Security\Anonymizer\CreditCardAnonymizer; +use KaririCode\Logging\Security\Anonymizer\EmailAnonymizer; + class Anonymizer { - private array $patterns = [ - 'email' => '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', - 'ip' => '/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', - 'credit_card' => '/\b(?:\d{4}[-\s]?){3}\d{4}\b/', - ]; + /** @var AnonymizerStrategy[] */ + private array $anonymizers; - public function anonymize(string $message): string + public function __construct(array $anonymizers = []) { - foreach ($this->patterns as $type => $pattern) { - $message = preg_replace_callback($pattern, function ($match) use ($type) { - return $this->mask($match[0], $type); - }, $message); - } - - return $message; + $this->anonymizers = array_merge($this->getDefaultAnonymizers(), $anonymizers); } - private function mask(string $value, string $type): string + private function getDefaultAnonymizers(): array { - switch ($type) { - case 'email': - [$username, $domain] = explode('@', $value); - $maskedUsername = substr($username, 0, 2) . str_repeat('*', strlen($username) - 2); - - return $maskedUsername . '@' . $domain; - case 'ip': - return preg_replace('/\d/', '*', $value); - case 'credit_card': - $cleanNumber = preg_replace('/[^0-9]/', '', $value); - $masked = str_repeat('*', strlen($cleanNumber) - 4) . substr($cleanNumber, -4); - - return preg_replace('/(.{4})/', '$1-', substr($masked, 0, -4)) . substr($masked, -4); + return [ + 'email' => new EmailAnonymizer(), + 'credit_card' => new CreditCardAnonymizer(), + ]; + } - default: - return preg_replace('/\S/', '*', $value); + public function anonymize(string $message): string + { + foreach ($this->anonymizers as $anonymizer) { + if ($anonymizer instanceof AnonymizerStrategy) { + $message = $anonymizer->anonymize($message); + } } + + return $message; } - public function addPattern(string $name, string $pattern): void + public function addAnonymizer(string $name, AnonymizerStrategy $anonymizer): void { - // Validate the pattern before adding it - if ($this->isInvalidRegex($pattern)) { - throw new \InvalidArgumentException('Invalid regex pattern for type: invalid'); + if ($this->isInvalidRegex($anonymizer->getPattern())) { + throw new \InvalidArgumentException('Invalid regex pattern for type: ' . $name); } - $this->patterns[$name] = $pattern; + $this->anonymizers[$name] = $anonymizer; } - public function removePattern(string $name): void + public function removeAnonymizer(string $name): void { - unset($this->patterns[$name]); + unset($this->anonymizers[$name]); } private function isInvalidRegex(string $pattern): bool diff --git a/src/Security/Anonymizer/CreditCardAnonymizer.php b/src/Security/Anonymizer/CreditCardAnonymizer.php new file mode 100644 index 0000000..ea2eace --- /dev/null +++ b/src/Security/Anonymizer/CreditCardAnonymizer.php @@ -0,0 +1,32 @@ +mask($matches[0]); + }, $value); + } + + public function mask(string $creditCard): string + { + $cleanNumber = preg_replace('/[^0-9]/', '', $creditCard); + $masked = str_repeat('*', strlen($cleanNumber) - 4) . substr($cleanNumber, -4); + + return preg_replace('/(.{4})/', '$1-', substr($masked, 0, -4)) . substr($masked, -4); + } + + public function getPattern(): string + { + return self::CREDIT_CARD_PATTERN; + } +} diff --git a/src/Security/Anonymizer/EmailAnonymizer.php b/src/Security/Anonymizer/EmailAnonymizer.php new file mode 100644 index 0000000..92fd613 --- /dev/null +++ b/src/Security/Anonymizer/EmailAnonymizer.php @@ -0,0 +1,32 @@ +mask($matches[0]); + }, $value); + } + + public function mask(string $email): string + { + [$username, $domain] = explode('@', $email); + $maskedUsername = substr($username, 0, 2) . str_repeat('*', strlen($username) - 2); + + return $maskedUsername . '@' . $domain; + } + + public function getPattern(): string + { + return self::EMAIL_PATTERN; + } +} diff --git a/src/Security/Anonymizer/IpAnonymizer.php b/src/Security/Anonymizer/IpAnonymizer.php new file mode 100644 index 0000000..3a857c8 --- /dev/null +++ b/src/Security/Anonymizer/IpAnonymizer.php @@ -0,0 +1,29 @@ +mask($matches[0]); + }, $value); + } + + public function mask(string $ip): string + { + return '***.***.***.***'; + } + + public function getPattern(): string + { + return self::IP_PATTERN; + } +} diff --git a/src/Security/Anonymizer/PhoneAnonymizer.php b/src/Security/Anonymizer/PhoneAnonymizer.php new file mode 100644 index 0000000..416eff2 --- /dev/null +++ b/src/Security/Anonymizer/PhoneAnonymizer.php @@ -0,0 +1,29 @@ +mask($matches[0]); + }, $value); + } + + public function mask(string $phone): string + { + return preg_replace('/(\d{4})-?(\d{2})/', '****-**', $phone); + } + + public function getPattern(): string + { + return self::PHONE_PATTERN; + } +} diff --git a/src/Security/Encryptor.php b/src/Security/Encryptor.php index 0ef8950..cd69c52 100644 --- a/src/Security/Encryptor.php +++ b/src/Security/Encryptor.php @@ -8,8 +8,8 @@ class Encryptor { public function __construct(private readonly string $key) { - if (32 !== strlen($key)) { - throw new \InvalidArgumentException('Key must be exactly 32 bytes long'); + if (32 > strlen($key)) { + throw new \InvalidArgumentException('Key must be at least 32 bytes long'); } } diff --git a/src/Service/LoggerServiceProvider.php b/src/Service/LoggerServiceProvider.php index dbf978a..6494f9d 100644 --- a/src/Service/LoggerServiceProvider.php +++ b/src/Service/LoggerServiceProvider.php @@ -4,72 +4,79 @@ namespace KaririCode\Logging\Service; -use KaririCode\Logging\Decorator\AsyncLogger; +use KaririCode\Logging\Exception\InvalidConfigurationException; use KaririCode\Logging\LoggerConfiguration; use KaririCode\Logging\LoggerFactory; use KaririCode\Logging\LoggerRegistry; -use KaririCode\Logging\LogLevel; class LoggerServiceProvider { - public function register(LoggerConfiguration $config): void + public function __construct( + private LoggerConfiguration $config, + private LoggerFactory $loggerFactory, + private LoggerRegistry $loggerRegistry + ) { + } + + public function register(): void + { + $this->registerDefaultLoggers(); + $this->registerEmergencyLogger(); + $this->registerOptionalLoggers(); + } + + private function registerDefaultLoggers(): void { - $defaultChannel = $config->get('default'); - $channelsConfig = $config->get('channels', []); + $defaultChannel = $this->config->get('default'); + $channelsConfig = $this->config->get('channels'); + + if (null === $defaultChannel || null === $channelsConfig) { + throw new InvalidConfigurationException("The 'default' and 'channels' configurations are required."); + } foreach ($channelsConfig as $channelName => $channelConfig) { - $logger = LoggerFactory::createLogger($channelName, $channelConfig); - LoggerRegistry::addLogger($channelName, $logger); + $logger = $this->loggerFactory->createLogger($channelName, $channelConfig); + $this->loggerRegistry->addLogger($channelName, $logger); if ($channelName === $defaultChannel) { - LoggerRegistry::addLogger('default', $logger); + $this->loggerRegistry->addLogger('default', $logger); } } + } - // Register emergency logger - $emergencyLoggerConfig = $config->get('emergency_logger', []); - $emergencyLogger = LoggerFactory::createLogger('emergency', $emergencyLoggerConfig); - LoggerRegistry::addLogger('emergency', $emergencyLogger); - - // Register query logger - if ($config->get('query_logger.enabled', false)) { - $queryLogger = LoggerFactory::createQueryLogger( - 'query', - $config->get('query_logger.threshold', 100) - ); - LoggerRegistry::addLogger('query', $queryLogger); - } + private function registerEmergencyLogger(): void + { + $emergencyLoggerConfig = $this->config->get('emergency_logger', []); + $emergencyLogger = $this->loggerFactory->createLogger( + 'emergency', + $emergencyLoggerConfig + ); + $this->loggerRegistry->addLogger('emergency', $emergencyLogger); + } - // Register performance logger - if ($config->get('performance_logger.enabled', false)) { - $performanceLogger = LoggerFactory::createPerformanceLogger( - 'performance', - $config->get('performance_logger.threshold', 1000) - ); - LoggerRegistry::addLogger('performance', $performanceLogger); - } + private function registerOptionalLoggers(): void + { + $this->registerLogger('query', 'createQueryLogger'); + $this->registerLogger('performance', 'createPerformanceLogger'); + $this->registerLogger('error', 'createErrorLogger'); + $this->registerAsyncLoggerIfEnabled(); + } - // Register error logger - if ($config->get('error_logger.enabled', true)) { - $errorLogger = LoggerFactory::createErrorLogger( - 'error', - $config->get('error_logger.levels', [ - LogLevel::ERROR, - LogLevel::CRITICAL, - LogLevel::ALERT, - LogLevel::EMERGENCY, - ]) - ); - LoggerRegistry::addLogger('error', $errorLogger); - } + private function registerLogger( + string $configKey, + string $factoryMethod, + ): void { + $loggerConfig = $this->config->get($configKey, []); + $logger = $this->loggerFactory->$factoryMethod($loggerConfig); + $this->loggerRegistry->addLogger($configKey, $logger); + } - // Register async logger if enabled - if ($config->get('async.enabled', true)) { - $asyncLogger = LoggerFactory::createAsyncLogger( - $config->get('async.driver', AsyncLogger::class), - $config->get('async.batch_size', 10) - ); - LoggerRegistry::addLogger('async', $asyncLogger); - } + private function registerAsyncLoggerIfEnabled(): void + { + $asyncLogger = $this->loggerFactory->createAsyncLogger( + $this->loggerRegistry->getLogger('default'), + (int) $this->config->get('async.batch_size', 10) + ); + $this->loggerRegistry->addLogger('async', $asyncLogger); } } diff --git a/src/Util/AssetPublisher.php b/src/Util/AssetPublisher.php index 0191fdd..6b949a7 100644 --- a/src/Util/AssetPublisher.php +++ b/src/Util/AssetPublisher.php @@ -8,6 +8,8 @@ class AssetPublisher { + private const DIRECTORY_PERMISSIONS = 0755; + public static function publishAssets(Event $event): void { $vendorDir = $event->getComposer()->getConfig()->get('vendor-dir'); @@ -15,36 +17,44 @@ public static function publishAssets(Event $event): void $targetDir = dirname($vendorDir) . '/resources/logging'; if (!is_dir($sourceDir)) { - $event->getIO()->write('Source directory not found: ' . $sourceDir . ''); + $event->getIO()->writeError(sprintf('Source directory not found: %s', $sourceDir)); return; } - if (!is_dir($targetDir) && !@mkdir($targetDir, 0755, true) && !is_dir($targetDir)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', $targetDir)); - } + self::createDirectory($targetDir); - /** @var RecursiveDirectoryIterator $iterator */ $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST ); + /** @var \RecursiveDirectoryIterator $iterator */ foreach ($iterator as $item) { $subPathName = $iterator->getSubPathName(); + $targetPath = $targetDir . DIRECTORY_SEPARATOR . $subPathName; + if ($item->isDir()) { - $targetPath = $targetDir . DIRECTORY_SEPARATOR . $subPathName; - if (!is_dir($targetPath) && !@mkdir($targetPath, 0755, true) && !is_dir($targetPath)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', $targetPath)); - } + self::createDirectory($targetPath); } else { - $targetPath = $targetDir . DIRECTORY_SEPARATOR . $subPathName; - if (!copy($item->getPathname(), $targetPath)) { - throw new \RuntimeException(sprintf('Failed to copy "%s" to "%s"', $item->getPathname(), $targetPath)); - } + self::copyFile($item->getPathname(), $targetPath); } } - $event->getIO()->write('Published assets to: ' . $targetDir . ''); + $event->getIO()->write(sprintf('Published assets to: %s', $targetDir)); + } + + private static function createDirectory(string $path): void + { + if (!is_dir($path) && !mkdir($path, self::DIRECTORY_PERMISSIONS, true)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $path)); + } + } + + private static function copyFile(string $source, string $destination): void + { + if (!copy($source, $destination)) { + throw new \RuntimeException(sprintf('Failed to copy "%s" to "%s"', $source, $destination)); + } } } diff --git a/src/Util/Config.php b/src/Util/Config.php new file mode 100644 index 0000000..3707bad --- /dev/null +++ b/src/Util/Config.php @@ -0,0 +1,74 @@ +load(); + } + + public static function env(string $key, mixed $default = null): mixed + { + $value = getenv($key); + if (false === $value) { + return $default; + } + + return self::getEnvParser()->parse($value); + } + + public static function storagePath(string $path = ''): string + { + $rootPath = self::getEnvLoader()->findRootPath(); + + return $rootPath . DIRECTORY_SEPARATOR . 'storage' . ($path ? DIRECTORY_SEPARATOR . $path : $path); + } + + public static function parseIntValue(string $value): int + { + return self::getEnvParser()->parseIntValue($value); + } + + public static function parseFloatValue(string $value): float + { + return self::getEnvParser()->parseFloatValue($value); + } + + public static function parseBooleanValue(string $value): bool + { + return self::getEnvParser()->parseBooleanValue($value); + } + + public static function parseStringValue(string $value): string + { + return self::getEnvParser()->parseStringValue($value); + } + + private static function getEnvLoader(): EnvLoader + { + if (null === self::$envLoader) { + self::$envLoader = new EnvLoader(); + } + + return self::$envLoader; + } + + private static function getEnvParser(): EnvParser + { + if (null === self::$envParser) { + self::$envParser = new EnvParser(); + } + + return self::$envParser; + } +} diff --git a/src/Util/ConfigGenerator.php b/src/Util/ConfigGenerator.php index 3f167cd..2780924 100644 --- a/src/Util/ConfigGenerator.php +++ b/src/Util/ConfigGenerator.php @@ -8,6 +8,9 @@ class ConfigGenerator { + private const DIRECTORY_PERMISSION = 0755; + private const FILE_PERMISSION = 0664; + public static function generateConfig(Event $event): void { $vendorDir = $event->getComposer()->getConfig()->get('vendor-dir'); @@ -15,21 +18,33 @@ public static function generateConfig(Event $event): void $configDir = $projectRoot . '/config'; $configFile = $configDir . '/logging.php'; - if (!is_dir($configDir) && !mkdir($configDir, 0755, true) && !is_dir($configDir)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', $configDir)); - } + self::ensureConfigDirectoryExists($configDir); if (!file_exists($configFile)) { - $configContent = self::getConfigContent(); - if (false === file_put_contents($configFile, $configContent)) { - throw new \RuntimeException(sprintf('Failed to write config file: %s', $configFile)); - } - $event->getIO()->write('Created config file: ' . $configFile . ''); + self::createConfigFile($configFile, $event); } else { $event->getIO()->write('Config file already exists: ' . $configFile . ''); } } + private static function ensureConfigDirectoryExists(string $configDir): void + { + if (!is_dir($configDir) && !mkdir($configDir, self::DIRECTORY_PERMISSION, true) && !is_dir($configDir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $configDir)); + } + } + + private static function createConfigFile(string $configFile, Event $event): void + { + $configContent = self::getConfigContent(); + if (false === file_put_contents($configFile, $configContent)) { + throw new \RuntimeException(sprintf('Failed to write config file: %s', $configFile)); + } + + chmod($configFile, self::FILE_PERMISSION); // Aplicando a permissão ao arquivo + $event->getIO()->write('Created config file: ' . $configFile . ''); + } + private static function getConfigContent(): string { return <<<'EOT' @@ -127,10 +142,6 @@ private static function getConfigContent(): string 'class' => \KaririCode\Logging\Processor\IntrospectionProcessor::class, 'level' => LogLevel::DEBUG, ], - 'git' => [ - 'class' => \KaririCode\Logging\Processor\GitProcessor::class, - 'level' => LogLevel::INFO, - ], 'memory_usage' => [ 'class' => \KaririCode\Logging\Processor\MemoryUsageProcessor::class, 'level' => LogLevel::DEBUG, @@ -194,6 +205,7 @@ private static function getConfigContent(): string 'channels' => ['single', 'daily'], ], ]; + EOT; } } diff --git a/src/Util/ConfigHelper.php b/src/Util/ConfigHelper.php deleted file mode 100644 index e262da4..0000000 --- a/src/Util/ConfigHelper.php +++ /dev/null @@ -1,81 +0,0 @@ -findRootPath() . DIRECTORY_SEPARATOR . self::ENV_FILE; + if (!file_exists($envPath)) { + return; + } + + $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + if ($this->isCommentLine($line)) { + continue; + } + [$name, $value] = $this->parseEnvLine($line); + $sanitizedValue = $this->sanitizeValue($value); + putenv(sprintf('%s=%s', $name, $sanitizedValue)); + } + } + + public function findRootPath(): string + { + $dir = __DIR__; + while (!file_exists($dir . DIRECTORY_SEPARATOR . self::ENV_FILE) && '/' !== $dir) { + $dir = dirname($dir); + } + + if (file_exists($dir . DIRECTORY_SEPARATOR . self::ENV_FILE)) { + return $dir; + } + + throw new \RuntimeException('Root path with .env file not found.'); + } + + private function isCommentLine(string $line): bool + { + return 0 === strpos(trim($line), '#'); + } + + private function parseEnvLine(string $line): array + { + [$name, $value] = explode('=', $line, 2); + + return [trim($name), trim($value)]; + } + + private function sanitizeValue(string $value): string + { + // Remove any potentially harmful characters + $value = preg_replace('/[^a-zA-Z0-9_\-\.,@\/\\\\:;]/', '', $value); + + // Ensure the value doesn't start with a dash (which could be interpreted as a command line option) + $value = ltrim($value, '-'); + + // Limit the length of the value to prevent buffer overflow attacks + $maxLength = 1000; // Adjust this value based on your requirements + if (strlen($value) > $maxLength) { + $value = substr($value, 0, $maxLength); + } + + return $value; + } +} diff --git a/src/Util/ConfigLoader/EnvParser.php b/src/Util/ConfigLoader/EnvParser.php new file mode 100644 index 0000000..a9623e9 --- /dev/null +++ b/src/Util/ConfigLoader/EnvParser.php @@ -0,0 +1,80 @@ + true, + 'false', '(false)' => false, + 'empty', '(empty)' => '', + 'null', '(null)' => null, + default => $this->parseNumericOrString($value), + }; + } + + private function parseNumericOrString(string $value): int|float|string + { + if ($this->canParseAsInt($value)) { + return $this->parseIntValue($value); + } + if ($this->canParseAsFloat($value)) { + return $this->parseFloatValue($value); + } + + return $this->parseStringValue($value); + } + + public function parseIntValue(string $value): int + { + $result = filter_var($value, FILTER_VALIDATE_INT); + if (false === $result) { + throw new \InvalidArgumentException("Value '$value' cannot be parsed as integer."); + } + + return $result; + } + + public function parseFloatValue(string $value): float + { + $result = filter_var($value, FILTER_VALIDATE_FLOAT); + if (false === $result) { + throw new \InvalidArgumentException("Value '$value' cannot be parsed as float."); + } + + return $result; + } + + public function parseBooleanValue(string $value): bool + { + $lowercaseValue = strtolower($value); + if (in_array($lowercaseValue, ['true', '(true)', '1', 'yes', 'on'], true)) { + return true; + } + if (in_array($lowercaseValue, ['false', '(false)', '0', 'no', 'off'], true)) { + return false; + } + throw new \InvalidArgumentException("Value '$value' cannot be parsed as boolean."); + } + + public function parseStringValue(string $value): string + { + return $value; + } + + private function canParseAsInt(string $value): bool + { + return false !== filter_var($value, FILTER_VALIDATE_INT); + } + + private function canParseAsFloat(string $value): bool + { + return false !== filter_var($value, FILTER_VALIDATE_FLOAT); + } +} diff --git a/src/Util/CurlClient.php b/src/Util/CurlClient.php index 75327a9..636a483 100644 --- a/src/Util/CurlClient.php +++ b/src/Util/CurlClient.php @@ -7,7 +7,6 @@ class CurlClient { private const TIMEOUT = 30; - private const DEFAULT_HEADERS = ['Content-Type: application/json']; public function post(string $url, array $data, array $headers = []): array { @@ -25,25 +24,6 @@ public function post(string $url, array $data, array $headers = []): array ]; } - /** - * Initialize a new cURL session. - * - * @param string $url the URL to initialize the cURL session with - * - * @throws \RuntimeException if cURL initialization fails - * - * @return \CurlHandle the cURL handle - */ - private function initializeCurl(string $url): \CurlHandle - { - $ch = curl_init($url); - if (false === $ch) { - throw new \RuntimeException('Failed to initialize cURL'); - } - - return $ch; - } - /** * Set POST options for the cURL session. * @@ -55,21 +35,39 @@ private function initializeCurl(string $url): \CurlHandle */ private function setPostOptions(\CurlHandle $ch, array $data, array $headers): void { - try { - $payload = json_encode($data, JSON_THROW_ON_ERROR); - } catch (\JsonException $e) { - throw new \JsonException('Failed to encode data: ' . $e->getMessage(), $e->getCode(), $e); - } + $payload = json_encode($data, JSON_THROW_ON_ERROR); + + $defaultHeaders = ['Content-Type: application/json']; + $headers = array_merge($defaultHeaders, $headers); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, - CURLOPT_HTTPHEADER => array_merge(self::DEFAULT_HEADERS, $headers), + CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => self::TIMEOUT, ]); } + /** + * Initialize a new cURL session. + * + * @param string $url the URL to initialize the cURL session with + * + * @throws \RuntimeException if cURL initialization fails + * + * @return \CurlHandle the cURL handle + */ + private function initializeCurl(string $url): \CurlHandle + { + $ch = curl_init($url); + if (false === $ch) { + throw new \RuntimeException('Failed to initialize cURL'); + } + + return $ch; + } + /** * Execute the cURL request. * diff --git a/src/Util/Http/Contract/HttpRequest.php b/src/Util/Http/Contract/HttpRequest.php new file mode 100644 index 0000000..e40e687 --- /dev/null +++ b/src/Util/Http/Contract/HttpRequest.php @@ -0,0 +1,18 @@ +serverParams = array_merge($_SERVER, $serverParams); + } + + public function getUrl(): string + { + $scheme = ($this->serverParams['HTTPS'] ?? 'off') === 'on' ? 'https://' : 'http://'; + $host = $this->serverParams['HTTP_HOST'] ?? 'localhost'; + $uri = $this->serverParams['REQUEST_URI'] ?? '/'; + + return $scheme . $host . $uri; + } + + public function getIp(): ?string + { + return $this->serverParams['REMOTE_ADDR'] ?? null; + } + + public function getMethod(): string + { + return $this->serverParams['REQUEST_METHOD'] ?? 'GET'; + } + + public function getServerName(): ?string + { + return $this->serverParams['SERVER_NAME'] ?? null; + } + + public function getReferrer(): ?string + { + return $this->serverParams['HTTP_REFERER'] ?? null; + } +} diff --git a/src/Util/ReflectionFactoryTrait.php b/src/Util/ReflectionFactoryTrait.php new file mode 100644 index 0000000..bb05cda --- /dev/null +++ b/src/Util/ReflectionFactoryTrait.php @@ -0,0 +1,206 @@ +getReflectionClass($class); + $filteredParameters = $this->filterConstructorParameters($reflectionClass, $parameters); + + return $reflectionClass->newInstanceArgs($filteredParameters); + } + + /** + * Gets a ReflectionClass instance after validating the class. + * + * @param string $class the fully qualified class name + * + * @throws InvalidConfigurationException if the class doesn't exist or is not instantiable + */ + protected function getReflectionClass(string $class): \ReflectionClass + { + if (!class_exists($class)) { + throw new InvalidConfigurationException("Class does not exist: $class"); + } + + $reflectionClass = new \ReflectionClass($class); + + if (!$reflectionClass->isInstantiable()) { + throw new InvalidConfigurationException("Class is not instantiable: $class"); + } + + return $reflectionClass; + } + + /** + * Filters the parameters to match the constructor's expected parameters. + * + * @param \ReflectionClass $reflectionClass the reflection class + * @param array $parameters the parameters to filter + * + * @throws InvalidConfigurationException if a required parameter is missing + * + * @return array the filtered parameters + */ + private function filterConstructorParameters(\ReflectionClass $reflectionClass, array $parameters): array + { + $constructor = $reflectionClass->getConstructor(); + if (!$constructor) { + return []; + } + + $constructorParameters = $constructor->getParameters(); + $filteredParameters = []; + + foreach ($constructorParameters as $param) { + $paramName = $param->getName(); + if (isset($parameters[$paramName])) { + $filteredParameters[] = $parameters[$paramName]; + } elseif ($param->isDefaultValueAvailable()) { + $filteredParameters[] = $param->getDefaultValue(); + } elseif ($param->allowsNull()) { + $filteredParameters[] = null; + } else { + throw new InvalidConfigurationException("Missing required parameter: $paramName"); + } + } + + return $filteredParameters; + } + + /** + * Gets the class from a configuration map. + * + * @param array $map the configuration map + * @param string $key the key to look up in the map + * + * @throws InvalidConfigurationException if the class configuration is invalid or the class doesn't exist + * + * @return string the fully qualified class name + */ + protected function getClassFromMap(array $map, string $key): string + { + if (!isset($map[$key])) { + throw new InvalidConfigurationException("Configuration not found for key: $key"); + } + + return self::validateAndExtractClass($map[$key], $key); + } + + /** + * Validates and extracts the class from a configuration value. + * + * @param mixed $config the configuration value + * @param string $key the configuration key (for error reporting) + * + * @throws InvalidConfigurationException if the class configuration is invalid or the class doesn't exist + * + * @return string the validated class name + */ + protected static function validateAndExtractClass(mixed $config, string $key): string + { + $class = is_string($config) ? $config : ($config['class'] ?? null); + + if (!is_string($class) || !class_exists($class)) { + throw new InvalidConfigurationException("Invalid class configuration for key: $key"); + } + + return $class; + } + + /** + * Gets configuration from a map for a specific key. + * + * @param array $map the configuration map + * @param string $key the key to look up in the map + * @param string $configKey the configuration key to retrieve (default: 'with') + * @param array $default the default value if the configuration is not found + * + * @return array the configuration array + */ + protected function getConfigFromMap(array $map, string $key, string $configKey = 'with', $default = []): array + { + return $map[$key][$configKey] ?? $default; + } + + /** + * Merges multiple configurations. + * + * @param array ...$configs The configurations to merge. + * + * @return array the merged configuration + */ + protected function mergeConfigurations(array ...$configs): array + { + return array_merge(...$configs); + } + + /** + * Gets the component configuration by merging default and channel-specific configs. + * + * @param string $componentType the type of the component + * @param string $componentName the name of the component + * @param array $channelConfig the channel-specific configuration + * @param array $defaultConfig the default configuration + * + * @return array the merged component configuration + */ + protected function getComponentConfig(string $componentType, string $componentName, array $channelConfig, array $defaultConfig): array + { + $channelComponentConfig = $channelConfig[$componentType][$componentName] ?? []; + + return $this->mergeConfigurations($defaultConfig, $channelComponentConfig); + } + + /** + * Extracts the merged configuration from a key-value pair. + * + * @param mixed $key the configuration key + * @param mixed $value the configuration value + * + * @return array an array containing the extracted class and configuration + */ + protected function extractMergedConfig($key, $value): array + { + if ($this->isSimpleHandlerConfig($key)) { + return [$value, []]; + } + + return [$key, $value['with'] ?? []]; + } + + /** + * Checks if the given key represents a simple handler configuration. + * + * @param mixed $key the configuration key to check + * + * @return bool true if it's a simple handler configuration, false otherwise + */ + protected function isSimpleHandlerConfig($key): bool + { + return is_int($key); + } +} diff --git a/src/Util/SlackClient.php b/src/Util/SlackClient.php index 8560e73..1ed3027 100644 --- a/src/Util/SlackClient.php +++ b/src/Util/SlackClient.php @@ -11,39 +11,25 @@ class SlackClient { - private readonly string $webhookUrl; - private readonly CircuitBreaker $circuitBreaker; - private readonly Retry $retry; - private readonly Fallback $fallback; - private readonly CurlClient $curlClient; + protected const SLACK_API_URL = 'https://slack.com/api/chat.postMessage'; public function __construct( - string $webhookUrl, - CircuitBreaker $circuitBreaker, - Retry $retry, - Fallback $fallback, - CurlClient $curlClient + private string $botToken, + private string $channel, + private CircuitBreaker $circuitBreaker = new CircuitBreaker(3, 60), + private Retry $retry = new Retry(3, 1000, 2, 100), + private Fallback $fallback = new Fallback(), + private CurlClient $curlClient = new CurlClient() ) { - $this->setWebhookUrl($webhookUrl); - $this->circuitBreaker = $circuitBreaker; - $this->retry = $retry; - $this->fallback = $fallback; - $this->curlClient = $curlClient; } public static function create( - string $webhookUrl, - ?CircuitBreaker $circuitBreaker = null, - ?Retry $retry = null, - ?Fallback $fallback = null, - ?CurlClient $curlClient = null + string $botToken, + string $channel, ): self { return new self( - $webhookUrl, - $circuitBreaker ?? new CircuitBreaker(3, 60), - $retry ?? new Retry(3, 1000, 2, 100), - $fallback ?? new Fallback(), - $curlClient ?? new CurlClient() + $botToken, + $channel ); } @@ -75,13 +61,21 @@ private function doSendMessage(string $message): void private function createPayload(string $message): array { - return ['text' => $message]; + return [ + 'channel' => $this->channel, + 'text' => $message, + ]; } private function sendRequest(array $payload): array { + $headers = [ + 'Content-Type: application/json; charset=utf-8', + 'Authorization: Bearer ' . $this->botToken, + ]; + try { - return $this->curlClient->post($this->webhookUrl, $payload); + return $this->curlClient->post(self::SLACK_API_URL, $payload, $headers); } catch (\JsonException $e) { $this->circuitBreaker->recordFailure(); throw new LoggingException('Failed to encode message for Slack: ' . $e->getMessage(), 0, $e); @@ -94,26 +88,14 @@ private function sendRequest(array $payload): array private function handleResponse(array $response): void { $httpCode = $response['status']; - $responseBody = $response['body']; + $responseBody = json_decode($response['body'], true); - if ($httpCode < 200 || $httpCode >= 300) { + if ($httpCode < 200 || $httpCode >= 300 || !$responseBody['ok']) { $this->circuitBreaker->recordFailure(); - throw new LoggingException('Slack API responded with HTTP code ' . $httpCode . ': ' . $responseBody); + $errorMessage = $responseBody['error'] ?? 'Unknown error'; + throw new LoggingException('Slack API responded with error: ' . $errorMessage); } $this->circuitBreaker->recordSuccess(); } - - private function setWebhookUrl(string $webhookUrl): void - { - if (false === filter_var($webhookUrl, FILTER_VALIDATE_URL)) { - throw new \InvalidArgumentException('Invalid webhook URL'); - } - $this->webhookUrl = $webhookUrl; - } - - public function getWebhookUrl(): string - { - return $this->webhookUrl; - } } diff --git a/src/Validation/ConfigurationValidator.php b/src/Validation/ConfigurationValidator.php new file mode 100644 index 0000000..a579fcd --- /dev/null +++ b/src/Validation/ConfigurationValidator.php @@ -0,0 +1,119 @@ +validateRequiredKeys($config, self::REQUIRED_KEYS); + $this->validateChannels($config['channels']); + $this->validateHandlers($config['handlers']); + $this->validateProcessors($config['processors']); + $this->validateFormatters($config['formatters']); + $this->validateOptionalLogs($config); + } + + private function validateRequiredKeys(array $config, array $requiredKeys, string $context = ''): void + { + foreach ($requiredKeys as $key) { + if (!isset($config[$key])) { + throw new InvalidConfigurationException("Missing required key '{$key}' in configuration" . ($context ? " for {$context}" : '')); + } + } + } + + private function validateChannels(array $channels): void + { + foreach ($channels as $channelName => $channelConfig) { + $this->validateRequiredKeys($channelConfig, self::CHANNEL_REQUIRED_KEYS, "channel '{$channelName}'"); + + if (!is_array($channelConfig['handlers'])) { + throw new InvalidConfigurationException("Handlers for channel '{$channelName}' must be an array"); + } + } + } + + private function validateHandlers(array $handlers): void + { + foreach ($handlers as $handlerName => $handlerConfig) { + $this->validateRequiredKeys($handlerConfig, self::HANDLER_REQUIRED_KEYS, "handler '{$handlerName}'"); + + if (!class_exists($handlerConfig['class'])) { + throw new InvalidConfigurationException("Handler class '{$handlerConfig['class']}' for '{$handlerName}' does not exist"); + } + } + } + + private function validateProcessors(array $processors): void + { + foreach ($processors as $processorName => $processorConfig) { + $this->validateRequiredKeys($processorConfig, self::PROCESSOR_REQUIRED_KEYS, "processor '{$processorName}'"); + + if (!class_exists($processorConfig['class'])) { + throw new InvalidConfigurationException("Processor class '{$processorConfig['class']}' for '{$processorName}' does not exist"); + } + } + } + + private function validateFormatters(array $formatters): void + { + foreach ($formatters as $formatterName => $formatterConfig) { + $this->validateRequiredKeys($formatterConfig, self::FORMATTER_REQUIRED_KEYS, "formatter '{$formatterName}'"); + + if (!class_exists($formatterConfig['class'])) { + throw new InvalidConfigurationException("Formatter class '{$formatterConfig['class']}' for '{$formatterName}' does not exist"); + } + } + } + + private function validateOptionalLogs(array $config): void + { + foreach (self::OPTIONAL_LOGS as $log) { + if (isset($config[$log])) { + $this->validateRequiredKeys($config[$log], self::OPTIONAL_LOG_KEYS, "optional log '{$log}'"); + + if (isset($config[$log]['handlers']) && !is_array($config[$log]['handlers'])) { + throw new InvalidConfigurationException("Handlers for optional log '{$log}' must be an array"); + } + + if (isset($config[$log]['processors']) && !is_array($config[$log]['processors'])) { + throw new InvalidConfigurationException("Processors for optional log '{$log}' must be an array"); + } + } + } + } +} diff --git a/tests/Decorator/AsyncLoggerTest.php b/tests/Decorator/AsyncLoggerTest.php index e2386cd..71e3be1 100644 --- a/tests/Decorator/AsyncLoggerTest.php +++ b/tests/Decorator/AsyncLoggerTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class AsyncLoggerTest extends TestCase +final class AsyncLoggerTest extends TestCase { private AsyncLogger $asyncLogger; private Logger|MockObject $logger; @@ -49,7 +49,7 @@ public function testDestructorProcessesAllLogs(): void $this->assertSame($logs[$callCount][0], $level); $this->assertSame($logs[$callCount][1], $message); $this->assertSame($logs[$callCount][2], $context); - $callCount++; + ++$callCount; }); foreach ($logs as $log) { diff --git a/tests/Decorator/BaseLoggerDecoratorTest.php b/tests/Decorator/BaseLoggerDecoratorTest.php index 3af0061..5a66b2e 100644 --- a/tests/Decorator/BaseLoggerDecoratorTest.php +++ b/tests/Decorator/BaseLoggerDecoratorTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class BaseLoggerDecoratorTest extends TestCase +final class BaseLoggerDecoratorTest extends TestCase { private BaseLoggerDecorator $baseLoggerDecorator; private Logger|MockObject $logger; diff --git a/tests/Decorator/ContextualLoggerTest.php b/tests/Decorator/ContextualLoggerTest.php index 39960cc..0e40ac8 100644 --- a/tests/Decorator/ContextualLoggerTest.php +++ b/tests/Decorator/ContextualLoggerTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class ContextualLoggerTest extends TestCase +final class ContextualLoggerTest extends TestCase { private ContextualLogger $contextualLogger; private Logger|MockObject $logger; diff --git a/tests/Exception/LoggingExceptionTest.php b/tests/Exception/LoggingExceptionTest.php index e3d0975..cb3ea5d 100644 --- a/tests/Exception/LoggingExceptionTest.php +++ b/tests/Exception/LoggingExceptionTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Exception\LoggingException; use PHPUnit\Framework\TestCase; -class LoggingExceptionTest extends TestCase +final class LoggingExceptionTest extends TestCase { public function testExceptionMessage(): void { diff --git a/tests/Formatter/AbstractFormatterTest.php b/tests/Formatter/AbstractFormatterTest.php index 9427f1a..252d005 100644 --- a/tests/Formatter/AbstractFormatterTest.php +++ b/tests/Formatter/AbstractFormatterTest.php @@ -8,7 +8,7 @@ use KaririCode\Logging\Formatter\AbstractFormatter; use PHPUnit\Framework\TestCase; -class AbstractFormatterTest extends TestCase +final class AbstractFormatterTest extends TestCase { public function testGetFormatter(): void { diff --git a/tests/Formatter/ConsoleColorFormatterTest.php b/tests/Formatter/ConsoleColorFormatterTest.php index 5d101df..4799de4 100644 --- a/tests/Formatter/ConsoleColorFormatterTest.php +++ b/tests/Formatter/ConsoleColorFormatterTest.php @@ -8,7 +8,7 @@ use KaririCode\Logging\LogLevel; use PHPUnit\Framework\TestCase; -class ConsoleColorFormatterTest extends TestCase +final class ConsoleColorFormatterTest extends TestCase { private ConsoleColorFormatter $formatter; diff --git a/tests/Formatter/ElasticFormatterTest.php b/tests/Formatter/ElasticFormatterTest.php index 87c8fb4..9478d8e 100644 --- a/tests/Formatter/ElasticFormatterTest.php +++ b/tests/Formatter/ElasticFormatterTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\LogRecord; use PHPUnit\Framework\TestCase; -class ElasticFormatterTest extends TestCase +final class ElasticFormatterTest extends TestCase { private ElasticFormatter $formatter; diff --git a/tests/Formatter/JsonFormatterTest.php b/tests/Formatter/JsonFormatterTest.php index 646af84..fe81a12 100644 --- a/tests/Formatter/JsonFormatterTest.php +++ b/tests/Formatter/JsonFormatterTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\LogRecord; use PHPUnit\Framework\TestCase; -class JsonFormatterTest extends TestCase +final class JsonFormatterTest extends TestCase { private JsonFormatter $jsonFormatter; diff --git a/tests/Formatter/LineFormatterTest.php b/tests/Formatter/LineFormatterTest.php index e4db970..4e0cfbb 100644 --- a/tests/Formatter/LineFormatterTest.php +++ b/tests/Formatter/LineFormatterTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\LogRecord; use PHPUnit\Framework\TestCase; -class LineFormatterTest extends TestCase +final class LineFormatterTest extends TestCase { private LineFormatter $formatter; diff --git a/tests/Formatter/LoggerFormatterFactoryTest.php b/tests/Formatter/LoggerFormatterFactoryTest.php new file mode 100644 index 0000000..2d694c3 --- /dev/null +++ b/tests/Formatter/LoggerFormatterFactoryTest.php @@ -0,0 +1,148 @@ +configMock = $this->createMock(LoggerConfiguration::class); + $this->factory = new LoggerFormatterFactory(); + } + + public function testInitializeFromConfiguration(): void + { + $config = [ + 'formatters' => [ + 'line' => LineFormatter::class, + 'json' => JsonFormatter::class, + 'custom' => 'CustomFormatter', + ], + 'channels' => [ + 'default' => ['formatter' => 'line'], + 'api' => ['formatter' => ['json' => ['with' => ['prettyPrint' => true]]]], + ], + ]; + + $this->configMock->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['formatters', ['line' => LineFormatter::class, 'json' => JsonFormatter::class], $config['formatters']], + ['channels', [], $config['channels']], + ]); + + $this->factory->initializeFromConfiguration($this->configMock); + + $this->assertInstanceOf(LoggerFormatterFactory::class, $this->factory); + } + + public function testCreateFormatterWithDefaultFormatter(): void + { + $config = [ + 'formatters' => [ + 'line' => LineFormatter::class, + ], + 'channels' => [ + 'default' => ['formatter' => 'line'], + ], + ]; + + $this->configMock->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['formatters', ['line' => LineFormatter::class, 'json' => JsonFormatter::class], $config['formatters']], + ['channels', [], $config['channels']], + ]); + + $this->factory->initializeFromConfiguration($this->configMock); + + $formatter = $this->factory->createFormatter('default'); + $this->assertInstanceOf(LineFormatter::class, $formatter); + } + + public function testCreateFormatterWithCustomFormatter(): void + { + $config = [ + 'formatters' => [ + 'json' => JsonFormatter::class, + ], + 'channels' => [ + 'api' => ['formatter' => ['json' => ['with' => ['prettyPrint' => true]]]], + ], + ]; + + $this->configMock->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['formatters', ['line' => LineFormatter::class, 'json' => JsonFormatter::class], $config['formatters']], + ['channels', [], $config['channels']], + ]); + + $this->factory->initializeFromConfiguration($this->configMock); + + $formatter = $this->factory->createFormatter('api'); + $this->assertInstanceOf(JsonFormatter::class, $formatter); + } + + public function testCreateFormatterWithNonExistentFormatter(): void + { + $config = [ + 'formatters' => [ + 'line' => LineFormatter::class, + 'json' => JsonFormatter::class, + ], + 'channels' => [ + 'unknown' => ['formatter' => 'non_existent'], + ], + ]; + + $this->configMock->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['formatters', ['line' => LineFormatter::class, 'json' => JsonFormatter::class], $config['formatters']], + ['channels', [], $config['channels']], + ]); + + $this->factory->initializeFromConfiguration($this->configMock); + + $this->expectException(\KaririCode\Logging\Exception\InvalidConfigurationException::class); + $this->expectExceptionMessage('Configuration not found for key: non_existent'); + + $this->factory->createFormatter('unknown'); + } + + public function testCreateFormatterWithFallbackToDefaultFormatter(): void + { + $config = [ + 'formatters' => [ + 'line' => LineFormatter::class, + ], + 'channels' => [ + 'fallback' => [], + ], + ]; + + $this->configMock->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['formatters', ['line' => LineFormatter::class, 'json' => JsonFormatter::class], $config['formatters']], + ['channels', [], $config['channels']], + ]); + + $this->factory->initializeFromConfiguration($this->configMock); + + $formatter = $this->factory->createFormatter('fallback'); + $this->assertInstanceOf(LineFormatter::class, $formatter); + } +} diff --git a/tests/Handler/AbstractHandlerTest.php b/tests/Handler/AbstractHandlerTest.php index d804891..1df59b3 100644 --- a/tests/Handler/AbstractHandlerTest.php +++ b/tests/Handler/AbstractHandlerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Handler; +namespace KaririCode\Logging\KaririCode\Logging\Tests\Logging\Handler; use KaririCode\Contract\ImmutableValue; use KaririCode\Contract\Logging\LogFormatter; @@ -12,7 +12,7 @@ use KaririCode\Logging\LogRecord; use PHPUnit\Framework\TestCase; -class ConcreteHandler extends AbstractHandler +final class ConcreteHandler extends AbstractHandler { public function handle(ImmutableValue $record): void { @@ -20,7 +20,7 @@ public function handle(ImmutableValue $record): void } } -class AbstractHandlerTest extends TestCase +final class AbstractHandlerTest extends TestCase { private ConcreteHandler $handler; diff --git a/tests/Handler/ConsoleHandlerTest.php b/tests/Handler/ConsoleHandlerTest.php index 190a4b7..38cbdd9 100644 --- a/tests/Handler/ConsoleHandlerTest.php +++ b/tests/Handler/ConsoleHandlerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Handler; +namespace KaririCode\Logging\KaririCode\Logging\Tests\Logging\Handler; use KaririCode\Contract\Logging\LogFormatter; use KaririCode\Logging\Formatter\ConsoleColorFormatter; @@ -13,7 +13,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class ConsoleHandlerTest extends TestCase +final class ConsoleHandlerTest extends TestCase { private mixed $outputMock; private LogFormatter|MockObject $formatterMock; diff --git a/tests/Handler/ExceptionHandlerTest.php b/tests/Handler/ExceptionHandlerTest.php deleted file mode 100644 index 757e197..0000000 --- a/tests/Handler/ExceptionHandlerTest.php +++ /dev/null @@ -1,80 +0,0 @@ -logger = $this->createMock(Logger::class); - $this->exceptionHandler = new ExceptionHandler($this->logger); - } - - public function testHandle(): void - { - $exception = new \Exception('Test exception message'); - - $this->logger->expects($this->once()) - ->method('error') - ->with( - $exception->getMessage(), - [ - 'exception' => get_class($exception), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => $exception->getTraceAsString(), - ] - ); - - $this->exceptionHandler->handle($exception); - } - - public function testHandleWithDifferentException(): void - { - $exception = new \RuntimeException('Runtime exception message'); - - $this->logger->expects($this->once()) - ->method('error') - ->with( - $exception->getMessage(), - [ - 'exception' => get_class($exception), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => $exception->getTraceAsString(), - ] - ); - - $this->exceptionHandler->handle($exception); - } - - public function testHandleWithComplexException(): void - { - $previousException = new \Exception('Previous exception message'); - $exception = new \Exception('Complex exception message', 0, $previousException); - - $this->logger->expects($this->once()) - ->method('error') - ->with( - $exception->getMessage(), - [ - 'exception' => get_class($exception), - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => $exception->getTraceAsString(), - ] - ); - - $this->exceptionHandler->handle($exception); - } -} diff --git a/tests/Handler/FileHandlerTest.php b/tests/Handler/FileHandlerTest.php index 57cfd66..376cab4 100644 --- a/tests/Handler/FileHandlerTest.php +++ b/tests/Handler/FileHandlerTest.php @@ -2,23 +2,25 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Handler; +namespace KaririCode\Logging\Tests\Handler; -use KaririCode\Contract\ImmutableValue; use KaririCode\Logging\Exception\LoggingException; use KaririCode\Logging\Formatter\LineFormatter; use KaririCode\Logging\Handler\FileHandler; use KaririCode\Logging\LogLevel; +use KaririCode\Logging\LogRecord; use PHPUnit\Framework\TestCase; -class FileHandlerTest extends TestCase +final class FileHandlerTest extends TestCase { private string $testLogDir; private string $testLogFile; + private array $mockFunctions = []; protected function setUp(): void { - $this->testLogDir = sys_get_temp_dir() . '/test_logs'; + $this->testLogDir = sys_get_temp_dir() . '/test_logs_' . uniqid(); + mkdir($this->testLogDir); $this->testLogFile = $this->testLogDir . '/test.log'; if (file_exists($this->testLogFile)) { @@ -31,12 +33,19 @@ protected function setUp(): void protected function tearDown(): void { - if (file_exists($this->testLogFile)) { - unlink($this->testLogFile); + $this->removeDirectory($this->testLogDir); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; } - if (is_dir($this->testLogDir)) { - rmdir($this->testLogDir); + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? $this->removeDirectory("$dir/$file") : unlink("$dir/$file"); } + rmdir($dir); } public function testConstructorCreatesDirectory(): void @@ -48,25 +57,67 @@ public function testConstructorCreatesDirectory(): void public function testConstructorThrowsExceptionOnInvalidPath(): void { $this->expectException(LoggingException::class); + $this->expectExceptionMessage('Unable to create log directory'); + + $invalidPath = '/path/that/cant/be/created/' . uniqid(); + + $mockFileHandler = $this->getMockBuilder(FileHandler::class) + ->setConstructorArgs([$invalidPath . '/test.log']) + ->onlyMethods(['createDirectory']) + ->getMock(); + + $mockFileHandler->expects($this->once()) + ->method('createDirectory') + ->willReturn(false); + + $mockFileHandler->__construct($invalidPath . '/test.log'); + } + + public function testConstructorThrowsExceptionOnNonWritableDirectory(): void + { + $this->expectException(LoggingException::class); + $this->expectExceptionMessage('Log directory is not writable'); - // Suppress warnings for this test - set_error_handler(function ($errno, $errstr, $errfile, $errline) { - // Don't throw E_WARNING, E_NOTICE, or E_USER_WARNING errors - return true; - }); - - try { - new FileHandler('/invalid/path/test.log'); - } finally { - // Restore the original error handler - restore_error_handler(); + $nonWritableDir = sys_get_temp_dir() . '/non_writable_dir_' . uniqid(); + mkdir($nonWritableDir); + + $mockFileHandler = $this->getMockBuilder(FileHandler::class) + ->setConstructorArgs([$nonWritableDir . '/test.log']) + ->onlyMethods(['isDirectoryWritable']) + ->getMock(); + + $mockFileHandler->expects($this->once()) + ->method('isDirectoryWritable') + ->willReturn(false); + /** @var AbstractFileHandlerc $mockFileHandler */ + $mockFileHandler->__construct($nonWritableDir . '/test.log'); + + $this->removeDirectory($nonWritableDir); + } + + // E adicionar este método à sua classe FileHandler: + protected function createDirectory($path) + { + if (isset($this->mockFunctions['mkdir'])) { + return call_user_func($this->mockFunctions['mkdir'], $path); + } + + return mkdir($path, 0777, true); + } + + protected function isDirectoryWritable($path) + { + if (isset($this->mockFunctions['is_writable'])) { + return call_user_func($this->mockFunctions['is_writable'], $path); } + + return is_writable($path); } public function testHandleWritesToFile(): void { $handler = new FileHandler($this->testLogFile); - $record = $this->createMockRecord('Test message', LogLevel::INFO); + $record = new LogRecord(LogLevel::INFO, 'Test message'); $handler->handle($record); @@ -78,8 +129,8 @@ public function testHandleWritesToFile(): void public function testHandleRespectsMinimumLogLevel(): void { $handler = new FileHandler($this->testLogFile, LogLevel::WARNING); - $debugRecord = $this->createMockRecord('Debug message', LogLevel::DEBUG); - $warningRecord = $this->createMockRecord('Warning message', LogLevel::WARNING); + $debugRecord = new LogRecord(LogLevel::DEBUG, 'Debug message'); + $warningRecord = new LogRecord(LogLevel::WARNING, 'Warning message'); $handler->handle($debugRecord); $handler->handle($warningRecord); @@ -99,7 +150,7 @@ public function testHandleUsesFormatter(): void $handler = new FileHandler($this->testLogFile); $handler->setFormatter($mockFormatter); - $record = $this->createMockRecord('Test message', LogLevel::INFO); + $record = new LogRecord(LogLevel::INFO, 'Test message'); $handler->handle($record); $content = file_get_contents($this->testLogFile); @@ -120,28 +171,10 @@ public function testDestructorClosesFileHandle(): void $this->assertFalse(is_resource($fileHandleProperty->getValue($handler))); } - private function createMockRecord(string $message, LogLevel $level): ImmutableValue + public function testLogFileCreatedWithCorrectPermissions(): void { - return new class($message, $level) implements ImmutableValue { - public function __construct( - public readonly string $message, - public readonly LogLevel $level, - public readonly array $context = [], - public readonly array $extra = [], - public readonly \DateTimeImmutable $datetime = new \DateTimeImmutable() - ) { - } - - public function toArray(): array - { - return [ - 'message' => $this->message, - 'level' => $this->level, - 'context' => $this->context, - 'extra' => $this->extra, - 'datetime' => $this->datetime, - ]; - } - }; + new FileHandler($this->testLogFile); + $this->assertFileExists($this->testLogFile); + $this->assertEquals('0644', substr(sprintf('%o', fileperms($this->testLogFile)), -4)); } } diff --git a/tests/Handler/LoggerHandlerFactoryTest.php b/tests/Handler/LoggerHandlerFactoryTest.php new file mode 100644 index 0000000..3962d6e --- /dev/null +++ b/tests/Handler/LoggerHandlerFactoryTest.php @@ -0,0 +1,113 @@ +config = new LoggerConfiguration(); + $this->config->set('handlers', [ + 'file' => [ + 'class' => FileHandler::class, + 'with' => [ + 'filePath' => $logFilePath, + ], + ], + 'console' => ConsoleHandler::class, + 'slack' => SlackHandler::class, + ]); + + $this->config->set('channels', [ + 'default' => [ + 'handlers' => ['file', 'console'], + ], + 'slack' => [ + 'handlers' => ['slack'], + ], + ]); + + $this->loggerHandlerFactory = new LoggerHandlerFactory(); + $this->loggerHandlerFactory->initializeFromConfiguration($this->config); + } + + public function testInitializeFromConfiguration(): void + { + // Valida se a configuração foi corretamente inicializada + $reflection = new \ReflectionClass($this->loggerHandlerFactory); + $handlerMap = $reflection->getProperty('handlerMap'); + $handlerMap->setAccessible(true); + + $this->assertIsArray($handlerMap->getValue($this->loggerHandlerFactory)); + $this->assertArrayHasKey('file', $handlerMap->getValue($this->loggerHandlerFactory)); + $this->assertArrayHasKey('console', $handlerMap->getValue($this->loggerHandlerFactory)); + $this->assertArrayHasKey('slack', $handlerMap->getValue($this->loggerHandlerFactory)); + } + + public function testCreateHandlersForDefaultChannel(): void + { + // Criando handlers para o canal 'default' + $handlers = $this->loggerHandlerFactory->createHandlers('default'); + + // Verifica se os handlers são instâncias das classes corretas + $this->assertIsArray($handlers); + $this->assertCount(2, $handlers); + $this->assertInstanceOf(FileHandler::class, $handlers[0]); + $this->assertInstanceOf(ConsoleHandler::class, $handlers[1]); + } + + public function testCreateHandlersForSlackChannel(): void + { + // Mockando o SlackClient + $mockSlackClient = $this->createMock(SlackClient::class); + + // Configurando o LoggerConfiguration + $config = new LoggerConfiguration(); + $config->set('handlers', [ + 'slack' => [ + 'class' => SlackHandler::class, + 'with' => [ + 'slackClient' => $mockSlackClient, // Passando o mock de SlackClient + ], + ], + ]); + + $config->set('channels', [ + 'slack' => [ + 'handlers' => ['slack'], + ], + ]); + + $loggerHandlerFactory = new LoggerHandlerFactory(); + $loggerHandlerFactory->initializeFromConfiguration($config); + + $handlers = $loggerHandlerFactory->createHandlers('slack'); + + $this->assertNotEmpty($handlers); + $this->assertInstanceOf(SlackHandler::class, $handlers[0]); + } + + public function testCreateHandlersForNonExistingChannel(): void + { + $handlers = $this->loggerHandlerFactory->createHandlers('non_existing'); + $this->assertIsArray($handlers); + $this->assertEmpty($handlers); // Verifica se o array está vazio + } +} diff --git a/tests/Handler/NullHandlerTest.php b/tests/Handler/NullHandlerTest.php index a42dce3..ccaad7f 100644 --- a/tests/Handler/NullHandlerTest.php +++ b/tests/Handler/NullHandlerTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\LogRecord; use PHPUnit\Framework\TestCase; -class NullHandlerTest extends TestCase +final class NullHandlerTest extends TestCase { private NullHandler $nullHandler; diff --git a/tests/Handler/RotatingFileHandlerTest.php b/tests/Handler/RotatingFileHandlerTest.php index e4d4bf3..598d1f2 100644 --- a/tests/Handler/RotatingFileHandlerTest.php +++ b/tests/Handler/RotatingFileHandlerTest.php @@ -4,15 +4,15 @@ namespace KaririCode\Logging\Tests\Handler; -use KaririCode\Contract\ImmutableValue; use KaririCode\Contract\Logging\LogRotator; use KaririCode\Logging\Exception\LoggingException; use KaririCode\Logging\Handler\RotatingFileHandler; use KaririCode\Logging\LogLevel; +use KaririCode\Logging\LogRecord; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class RotatingFileHandlerTest extends TestCase +final class RotatingFileHandlerTest extends TestCase { private string $tempDir; private string $logFile; @@ -43,15 +43,15 @@ private function removeDirectory(string $dir): void rmdir($dir); } + private function createLogRecord(LogLevel $level, string $message, array $context = []): LogRecord + { + return new LogRecord($level, $message, $context); + } + public function testHandleWritesLogWhenLevelIsHighEnough(): void { $handler = new RotatingFileHandler($this->logFile, $this->mockRotator, LogLevel::INFO); - $record = $this->createMock(ImmutableValue::class); - $record->method('get')->willReturn([ - 'level' => LogLevel::ERROR, - 'message' => 'Test error message', - 'context' => [], - ]); + $record = $this->createLogRecord(LogLevel::ERROR, 'Test error message'); $handler->handle($record); @@ -62,16 +62,15 @@ public function testHandleWritesLogWhenLevelIsHighEnough(): void public function testHandleDoesNotWriteLogWhenLevelIsTooLow(): void { $handler = new RotatingFileHandler($this->logFile, $this->mockRotator, LogLevel::ERROR); - $record = $this->createMock(ImmutableValue::class); - $record->method('get')->willReturn([ - 'level' => LogLevel::INFO, - 'message' => 'Test info message', - 'context' => [], - ]); + $record = $this->createLogRecord(LogLevel::INFO, 'Test info message'); $handler->handle($record); - $this->assertFileDoesNotExist($this->logFile); + if (file_exists($this->logFile)) { + $this->assertEmpty(file_get_contents($this->logFile)); + } else { + $this->assertTrue(true); + } } public function testHandleRotatesFileWhenNecessary(): void @@ -80,12 +79,7 @@ public function testHandleRotatesFileWhenNecessary(): void $this->mockRotator->expects($this->once())->method('rotate'); $handler = new RotatingFileHandler($this->logFile, $this->mockRotator); - $record = $this->createMock(ImmutableValue::class); - $record->method('get')->willReturn([ - 'level' => LogLevel::INFO, - 'message' => 'Test rotation message', - 'context' => [], - ]); + $record = $this->createLogRecord(LogLevel::INFO, 'Test rotation message'); $handler->handle($record); @@ -98,12 +92,7 @@ public function testHandleThrowsLoggingExceptionOnError(): void $this->mockRotator->method('shouldRotate')->willThrowException(new \RuntimeException('Rotation error')); $handler = new RotatingFileHandler($this->logFile, $this->mockRotator); - $record = $this->createMock(ImmutableValue::class); - $record->method('get')->willReturn([ - 'level' => LogLevel::INFO, - 'message' => 'Test error handling', - 'context' => [], - ]); + $record = $this->createLogRecord(LogLevel::INFO, 'Test error handling'); $this->expectException(LoggingException::class); $this->expectExceptionMessage('Error handling log record: Rotation error'); @@ -117,12 +106,7 @@ public function testHandleReopensFileAfterRotation(): void $this->mockRotator->expects($this->once())->method('rotate'); $handler = new RotatingFileHandler($this->logFile, $this->mockRotator); - $record = $this->createMock(ImmutableValue::class); - $record->method('get')->willReturn([ - 'level' => LogLevel::INFO, - 'message' => 'Test message', - 'context' => [], - ]); + $record = $this->createLogRecord(LogLevel::INFO, 'Test message'); // First call - should rotate and reopen $handler->handle($record); diff --git a/tests/Handler/SlackHandlerTest.php b/tests/Handler/SlackHandlerTest.php index 138aace..7f5cd9d 100644 --- a/tests/Handler/SlackHandlerTest.php +++ b/tests/Handler/SlackHandlerTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace Tests\KaririCode\Logging\Handler; +namespace KaririCode\Logging\Tests\Logging\Handler; use KaririCode\Contract\Logging\LogFormatter; use KaririCode\Logging\Handler\SlackHandler; use KaririCode\Logging\LogLevel; use KaririCode\Logging\LogRecord; use KaririCode\Logging\Util\SlackClient; -use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; -class SlackHandlerTest extends TestCase +final class SlackHandlerTest extends TestCase { private SlackClient|MockObject $slackClient; private LogFormatter|MockObject $formatter; diff --git a/tests/Handler/SyslogUdpHandlerTest.php b/tests/Handler/SyslogUdpHandlerTest.php index 1dfd757..d803f7e 100644 --- a/tests/Handler/SyslogUdpHandlerTest.php +++ b/tests/Handler/SyslogUdpHandlerTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Handler; +namespace KaririCode\Logging\KaririCode\Logging\Tests\Logging\Handler; use KaririCode\Contract\Logging\LogFormatter; use KaririCode\Logging\Handler\SyslogUdpHandler; @@ -13,7 +13,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class SyslogUdpHandlerTest extends TestCase +final class SyslogUdpHandlerTest extends TestCase { private string $host = '127.0.0.1'; private int $port = 514; diff --git a/tests/Logger/LoggerBuilderTest.php b/tests/Logger/LoggerBuilderTest.php index d6bd89a..1e37153 100644 --- a/tests/Logger/LoggerBuilderTest.php +++ b/tests/Logger/LoggerBuilderTest.php @@ -6,11 +6,12 @@ use KaririCode\Contract\Logging\Logger; use KaririCode\Contract\Logging\Structural\HandlerAware; +use KaririCode\Contract\Logging\Structural\ProcessorAware; use KaririCode\Logging\Formatter\LineFormatter; use KaririCode\Logging\LoggerBuilder; use PHPUnit\Framework\TestCase; -class LoggerBuilderTest extends TestCase +final class LoggerBuilderTest extends TestCase { public function testBuildLogger(): void { @@ -26,7 +27,7 @@ public function testWithHandler(): void $handler = $this->createMock(HandlerAware::class); $builder = new LoggerBuilder('test'); $builder->withHandler($handler); - + /** @var LoggerManager */ $logger = $builder->build(); $this->assertContains($handler, $logger->getHandlers()); @@ -34,10 +35,11 @@ public function testWithHandler(): void public function testWithProcessor(): void { - $processor = $this->createMock(\KaririCode\Contract\Logging\ProcessorAware::class); + $processor = $this->createMock(ProcessorAware::class); $builder = new LoggerBuilder('test'); $builder->withProcessor($processor); + /** @var LoggerManager */ $logger = $builder->build(); $this->assertContains($processor, $logger->getProcessors()); @@ -49,6 +51,7 @@ public function testWithFormatter(): void $builder = new LoggerBuilder('test'); $builder->withFormatter($formatter); + /** @var LoggerManager */ $logger = $builder->build(); $this->assertSame($formatter, $logger->getFormatter()); diff --git a/tests/Logger/LoggerConfigurationTest.php b/tests/Logger/LoggerConfigurationTest.php index 3887863..2348f02 100644 --- a/tests/Logger/LoggerConfigurationTest.php +++ b/tests/Logger/LoggerConfigurationTest.php @@ -8,7 +8,7 @@ use KaririCode\Logging\LoggerConfiguration; use PHPUnit\Framework\TestCase; -class LoggerConfigurationTest extends TestCase +final class LoggerConfigurationTest extends TestCase { private LoggerConfiguration $config; @@ -31,10 +31,43 @@ public function testGetWithDefault(): void public function testLoad(): void { $configFile = __DIR__ . '/test_config.php'; - file_put_contents($configFile, " 'value'];"); + $configContent = << 'file', + 'channels' => [ + 'file' => [ + 'handlers' => ['file'], + ], + ], + 'handlers' => [ + 'file' => [ + 'class' => \KaririCode\Logging\Handler\FileHandler::class, + 'with' => [ + 'filePath' => '/path/to/logs/file.log', + ], + ], + ], + 'processors' => [], + 'formatters' => [ + 'line' => [ + 'class' => \KaririCode\Logging\Formatter\LineFormatter::class, + 'with' => [ + 'dateFormat' => 'Y-m-d H:i:s', + ], + ], + ], + ]; + PHP; + file_put_contents($configFile, $configContent); $this->config->load($configFile); - $this->assertEquals('value', $this->config->get('key')); + + $this->assertEquals('file', $this->config->get('default')); + $this->assertIsArray($this->config->get('channels')); + $this->assertIsArray($this->config->get('handlers')); + $this->assertIsArray($this->config->get('processors')); + $this->assertIsArray($this->config->get('formatters')); unlink($configFile); } diff --git a/tests/Logger/LoggerFactoryTest.php b/tests/Logger/LoggerFactoryTest.php index e592a01..66051fc 100644 --- a/tests/Logger/LoggerFactoryTest.php +++ b/tests/Logger/LoggerFactoryTest.php @@ -2,49 +2,138 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\Logger; +namespace KaririCode\Logging\Tests\Logging; use KaririCode\Contract\Logging\Logger; -use KaririCode\Logging\Formatter\JsonFormatter; +use KaririCode\Logging\Decorator\AsyncLogger; +use KaririCode\Logging\Formatter\LoggerFormatterFactory; +use KaririCode\Logging\Handler\LoggerHandlerFactory; +use KaririCode\Logging\LoggerConfiguration; use KaririCode\Logging\LoggerFactory; +use KaririCode\Logging\LoggerManager; +use KaririCode\Logging\Processor\LoggerProcessorFactory; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class LoggerFactoryTest extends TestCase +final class LoggerFactoryTest extends TestCase { + private LoggerConfiguration|MockObject $config; + private LoggerHandlerFactory|MockObject $handlerFactory; + private LoggerProcessorFactory|MockObject $processorFactory; + private LoggerFormatterFactory|MockObject $formatterFactory; + private LoggerFactory $loggerFactory; + + protected function setUp(): void + { + /** @var LoggerConfiguration */ + $this->config = $this->createMock(LoggerConfiguration::class); + /** @var LoggerHandlerFactory */ + $this->handlerFactory = $this->createMock(LoggerHandlerFactory::class); + /** @var LoggerProcessorFactory */ + $this->processorFactory = $this->createMock(LoggerProcessorFactory::class); + /** @var LoggerFormatterFactory */ + $this->formatterFactory = $this->createMock(LoggerFormatterFactory::class); + + $this->loggerFactory = new LoggerFactory( + $this->config, + $this->handlerFactory, + $this->processorFactory, + $this->formatterFactory + ); + } + public function testCreateLogger(): void { - $config = [ - 'formatter' => ['class' => JsonFormatter::class], - 'path' => '/tmp/test_log.log', - 'level' => 'debug', - ]; + $channelName = 'test_channel'; + + $this->handlerFactory->expects($this->once()) + ->method('createHandlers') + ->with($channelName) + ->willReturn([]); + + $this->processorFactory->expects($this->once()) + ->method('createProcessors') + ->with($channelName) + ->willReturn([]); - $logger = LoggerFactory::createLogger('test', $config); + $this->formatterFactory->expects($this->once()) + ->method('createFormatter') + ->with($channelName) + ->willReturn($this->createMock(\KaririCode\Contract\Logging\LogFormatter::class)); + + $logger = $this->loggerFactory->createLogger($channelName); $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerManager::class, $logger); } - public function testCreateQueryLogger(): void + public function testCreatePerformanceLogger(): void { - $logger = LoggerFactory::createQueryLogger('test_channel', 100); + $this->config->expects($this->once()) + ->method('get') + ->with('performance.threshold', 1000) + ->willReturn(500); + + $this->handlerFactory->method('createHandlers')->willReturn([]); + $this->processorFactory->method('createProcessors')->willReturn([]); + $this->formatterFactory->method('createFormatter')->willReturn($this->createMock(\KaririCode\Contract\Logging\LogFormatter::class)); + + $logger = $this->loggerFactory->createPerformanceLogger(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerManager::class, $logger); } - public function testCreatePerformanceLogger(): void + public function testCreateQueryLogger(): void { - $logger = LoggerFactory::createPerformanceLogger('test_channel', 1000); + $this->config->expects($this->once()) + ->method('get') + ->with('query.threshold', 100) + ->willReturn(50); + + $this->handlerFactory->method('createHandlers')->willReturn([]); + $this->processorFactory->method('createProcessors')->willReturn([]); + $this->formatterFactory->method('createFormatter')->willReturn($this->createMock(\KaririCode\Contract\Logging\LogFormatter::class)); + + $logger = $this->loggerFactory->createQueryLogger(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerManager::class, $logger); } public function testCreateErrorLogger(): void { - $logger = LoggerFactory::createErrorLogger('test_channel', ['error', 'critical']); + $this->handlerFactory->method('createHandlers')->willReturn([]); + $this->processorFactory->method('createProcessors')->willReturn([]); + $this->formatterFactory->method('createFormatter')->willReturn($this->createMock(\KaririCode\Contract\Logging\LogFormatter::class)); + + $logger = $this->loggerFactory->createErrorLogger(); + $this->assertInstanceOf(Logger::class, $logger); + $this->assertInstanceOf(LoggerManager::class, $logger); } public function testCreateAsyncLogger(): void { - $logger = LoggerFactory::createAsyncLogger('async_driver', 10); - $this->assertInstanceOf(Logger::class, $logger); + /** @var Logger */ + $baseLogger = $this->createMock(Logger::class); + $batchSize = 10; + + $asyncLogger = $this->loggerFactory->createAsyncLogger( + $baseLogger, + $batchSize + ); + + $this->assertInstanceOf(AsyncLogger::class, $asyncLogger); + } + + public function testCreateLoggerWithInvalidChannel(): void + { + $this->handlerFactory->method('createHandlers')->willThrowException(new \InvalidArgumentException('Invalid channel')); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid channel'); + + $this->loggerFactory->createLogger('invalid_channel'); } } diff --git a/tests/Logger/LoggerManagerTest.php b/tests/Logger/LoggerManagerTest.php index 45efc1a..ba2b6d3 100644 --- a/tests/Logger/LoggerManagerTest.php +++ b/tests/Logger/LoggerManagerTest.php @@ -6,21 +6,17 @@ use KaririCode\Contract\ImmutableValue; use KaririCode\Contract\Logging\LogFormatter; -use KaririCode\Contract\Logging\LogHandler; -use KaririCode\Contract\Logging\Structural\FormatterAware; use KaririCode\Contract\Logging\Structural\HandlerAware; use KaririCode\Contract\Logging\Structural\ProcessorAware; use KaririCode\Logging\Formatter\LineFormatter; use KaririCode\Logging\Handler\AbstractHandler; -use KaririCode\Logging\Handler\ConsoleHandler; -use KaririCode\Logging\Handler\NullHandler; use KaririCode\Logging\LoggerManager; use KaririCode\Logging\LogLevel; use KaririCode\Logging\LogRecord; use KaririCode\Logging\Processor\AbstractProcessor; use PHPUnit\Framework\TestCase; -class LoggerManagerTest extends TestCase +final class LoggerManagerTest extends TestCase { private LoggerManager $loggerManager; @@ -68,7 +64,6 @@ public function testLog(): void ->method('handle') ->with($this->isInstanceOf(ImmutableValue::class)); - $processor = $this->createMock(AbstractProcessor::class); $processor->expects($this->once()) ->method('process') diff --git a/tests/Logger/LoggerRegistryTest.php b/tests/Logger/LoggerRegistryTest.php index 7ac199f..e0e91d1 100644 --- a/tests/Logger/LoggerRegistryTest.php +++ b/tests/Logger/LoggerRegistryTest.php @@ -5,27 +5,62 @@ namespace KaririCode\Logging\Tests\Logger; use KaririCode\Contract\Logging\Logger; +use KaririCode\Logging\Exception\LoggerNotFoundException; use KaririCode\Logging\LoggerRegistry; use PHPUnit\Framework\TestCase; -class LoggerRegistryTest extends TestCase +final class LoggerRegistryTest extends TestCase { - public function testAddAndRetrieveLogger(): void + private LoggerRegistry $registry; + protected Logger $mockLogger; + + protected function setUp(): void { - $logger = $this->createMock(Logger::class); - LoggerRegistry::addLogger('test', $logger); + $this->registry = new LoggerRegistry(); + $this->mockLogger = $this->createMock(Logger::class); + } - $retrievedLogger = LoggerRegistry::getLogger('test'); - $this->assertSame($logger, $retrievedLogger); + public function testAddAndGetLogger(): void + { + $this->registry->addLogger('test', $this->mockLogger); + + $this->assertSame($this->mockLogger, $this->registry->getLogger('test')); + } + + public function testGetNonexistentLogger(): void + { + $this->expectException(LoggerNotFoundException::class); + $this->expectExceptionMessage('Logger with name "nonexistent" not found.'); + + $this->registry->getLogger('nonexistent'); } public function testRemoveLogger(): void { - $logger = $this->createMock(Logger::class); - LoggerRegistry::addLogger('test', $logger); - LoggerRegistry::removeLogger('test'); + $this->registry->addLogger('test', $this->mockLogger); + $this->registry->removeLogger('test'); + + $this->expectException(LoggerNotFoundException::class); + $this->expectExceptionMessage('Logger with name "test" not found.'); + + $this->registry->getLogger('test'); + } + + public function testCannotAddLoggerWithSameNameTwice(): void + { + $this->registry->addLogger('test', $this->mockLogger); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Logger with name "test" already exists.'); + + $this->registry->addLogger('test', $this->mockLogger); + } + + public function testRemoveNonexistentLogger(): void + { + $this->expectException(LoggerNotFoundException::class); + $this->expectExceptionMessage('Logger with name "nonexistent" not found.'); - $retrievedLogger = LoggerRegistry::getLogger('test'); - $this->assertNull($retrievedLogger); + $this->registry->removeLogger('nonexistent'); } } diff --git a/tests/Logger/QueryLoggerTest.php b/tests/Logger/QueryLoggerTest.php new file mode 100644 index 0000000..99ea239 --- /dev/null +++ b/tests/Logger/QueryLoggerTest.php @@ -0,0 +1,61 @@ +logger = $this->createMock(Logger::class); + $this->queryLogger = new QueryLogger($this->logger, 100); + } + + public function testLogSlowQuery(): void + { + $query = 'SELECT * FROM users WHERE id = ?'; + $bindings = [1]; + $executionTime = 150.0; + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Slow query detected', + [ + 'query' => $query, + 'bindings' => $bindings, + 'time' => $executionTime, + ] + ); + + $this->queryLogger->log($query, $bindings, $executionTime); + } + + public function testLogFastQuery(): void + { + $query = 'SELECT * FROM users WHERE id = ?'; + $bindings = [1]; + $executionTime = 50.0; + + $this->logger->expects($this->once()) + ->method('debug') + ->with( + 'Query executed', + [ + 'query' => $query, + 'bindings' => $bindings, + 'time' => $executionTime, + ] + ); + + $this->queryLogger->log($query, $bindings, $executionTime); + } +} diff --git a/tests/Metric/MetricsCollectorTest.php b/tests/Metric/MetricsCollectorTest.php index 10b1ea1..8a0ee61 100644 --- a/tests/Metric/MetricsCollectorTest.php +++ b/tests/Metric/MetricsCollectorTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Metric\MetricsCollector; use PHPUnit\Framework\TestCase; -class MetricsCollectorTest extends TestCase +final class MetricsCollectorTest extends TestCase { private MetricsCollector $metricsCollector; diff --git a/tests/Metric/MonitorTest.php b/tests/Metric/MonitorTest.php index c72b708..7f10559 100644 --- a/tests/Metric/MonitorTest.php +++ b/tests/Metric/MonitorTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Metric\Monitor; use PHPUnit\Framework\TestCase; -class MonitorTest extends TestCase +final class MonitorTest extends TestCase { private Monitor $monitor; diff --git a/tests/Processor/AbstractProcessorTest.php b/tests/Processor/AbstractProcessorTest.php index 3497504..00175b7 100644 --- a/tests/Processor/AbstractProcessorTest.php +++ b/tests/Processor/AbstractProcessorTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Processor; +namespace KaririCode\Logging\KaririCode\Logging\Tests\Logging\Processor; use KaririCode\Contract\ImmutableValue; use KaririCode\Contract\Logging\LogProcessor; @@ -10,23 +10,7 @@ use KaririCode\Logging\Processor\AbstractProcessor; use PHPUnit\Framework\TestCase; -// Classe concreta para testar AbstractProcessor -class ConcreteTestProcessor extends AbstractProcessor -{ - public function process(ImmutableValue $record): ImmutableValue - { - // Implementação simples para teste - return $record; - } - - // Expõe o método protegido para teste - public function testHasValidContext(array $context): bool - { - return $this->hasValidContext($context); - } -} - -class AbstractProcessorTest extends TestCase +final class AbstractProcessorTest extends TestCase { private ConcreteTestProcessor $processor; @@ -96,3 +80,18 @@ public function testProcessMethod(): void $this->assertSame($mockRecord, $result); } } + +final class ConcreteTestProcessor extends AbstractProcessor +{ + public function process(ImmutableValue $record): ImmutableValue + { + // Implementação simples para teste + return $record; + } + + // Expõe o método protegido para teste + public function testHasValidContext(array $context): bool + { + return $this->hasValidContext($context); + } +} diff --git a/tests/Processor/AnonymizerProcessorTest.php b/tests/Processor/AnonymizerProcessorTest.php new file mode 100644 index 0000000..e2ca57f --- /dev/null +++ b/tests/Processor/AnonymizerProcessorTest.php @@ -0,0 +1,116 @@ +anonymizer = $this->createMock(Anonymizer::class); + $this->processor = new AnonymizerProcessor($this->anonymizer); + } + + public function testProcessAnonymizesEmail(): void + { + // Setup + $message = 'Sensitive information: user@example.com'; + $anonymizedMessage = 'Sensitive information: us****@example.com'; + + $this->anonymizer->expects($this->once()) + ->method('anonymize') + ->with($message) + ->willReturn($anonymizedMessage); + + $record = new LogRecord( + LogLevel::INFO, + $message, + [], + new DateTimeImmutable() + ); + + $result = $this->processor->process($record); + + $this->assertInstanceOf(LogRecord::class, $result); + $this->assertEquals($anonymizedMessage, $result->message); + } + + public function testProcessAnonymizesIp(): void + { + $message = 'Sensitive information: 192.168.0.1'; + $anonymizedMessage = 'Sensitive information: ***.***.***.***'; + + $this->anonymizer->expects($this->once()) + ->method('anonymize') + ->with($message) + ->willReturn($anonymizedMessage); + + $record = new LogRecord( + LogLevel::INFO, + $message, + [], + new DateTimeImmutable() + ); + + $result = $this->processor->process($record); + + $this->assertEquals($anonymizedMessage, $result->message); + } + + public function testProcessAnonymizesCreditCard(): void + { + $message = 'Sensitive information: 4111 1111 1111 1111'; + $anonymizedMessage = 'Sensitive information: **** **** **** 1111'; + + $this->anonymizer->expects($this->once()) + ->method('anonymize') + ->with($message) + ->willReturn($anonymizedMessage); + + $record = new LogRecord( + LogLevel::INFO, + $message, + [], + new DateTimeImmutable() + ); + + $result = $this->processor->process($record); + + $this->assertEquals($anonymizedMessage, $result->message); + } + + public function testProcessDoesNotChangeContextOrDateTime(): void + { + $message = 'Sensitive information'; + $anonymizedMessage = 'Anonymized information'; + + $this->anonymizer->expects($this->once()) + ->method('anonymize') + ->with($message) + ->willReturn($anonymizedMessage); + + $context = ['some_key' => 'some_value']; + $datetime = new DateTimeImmutable(); + $record = new LogRecord( + LogLevel::INFO, + $message, + $context, + $datetime + ); + + $result = $this->processor->process($record); + + $this->assertEquals($context, $result->context); + $this->assertEquals($datetime, $result->datetime); + } +} diff --git a/tests/Processor/AsyncLogProcessorTest.php b/tests/Processor/AsyncLogProcessorTest.php index 18ca7bd..7e819b3 100644 --- a/tests/Processor/AsyncLogProcessorTest.php +++ b/tests/Processor/AsyncLogProcessorTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class AsyncLogProcessorTest extends TestCase +final class AsyncLogProcessorTest extends TestCase { private AsyncLogProcessor $asyncLogProcessor; private Logger|MockObject $logger; diff --git a/tests/Processor/BulkProcessorTest.php b/tests/Processor/BulkProcessorTest.php index 8db7152..4068106 100644 --- a/tests/Processor/BulkProcessorTest.php +++ b/tests/Processor/BulkProcessorTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\Processor\BulkProcessor; use PHPUnit\Framework\TestCase; -class BulkProcessorTest extends TestCase +final class BulkProcessorTest extends TestCase { private BulkProcessor $bulkProcessor; private array $flushedRecords; diff --git a/tests/Processor/EncryptionProcessorTest.php b/tests/Processor/EncryptionProcessorTest.php new file mode 100644 index 0000000..03b3c55 --- /dev/null +++ b/tests/Processor/EncryptionProcessorTest.php @@ -0,0 +1,70 @@ +encryptor = $this->createMock(Encryptor::class); + $this->processor = new EncryptionProcessor($this->encryptor); + } + + public function testProcessEncryptsMessage(): void + { + $message = 'Sensitive information'; + $encryptedMessage = 'EncryptedSensitiveInformation'; + + $this->encryptor->expects($this->once()) + ->method('encrypt') + ->with($message) + ->willReturn($encryptedMessage); + + $record = new LogRecord( + LogLevel::INFO, + $message, + [], + new DateTimeImmutable() + ); + + $result = $this->processor->process($record); + + $this->assertInstanceOf(LogRecord::class, $result); + $this->assertEquals($encryptedMessage, $result->message); + $this->assertEquals([], $result->context); + $this->assertEquals($record->datetime, $result->datetime); + } + + public function testProcessThrowsExceptionOnEncryptionFailure(): void + { + $message = 'Sensitive information'; + + $this->encryptor->expects($this->once()) + ->method('encrypt') + ->with($message) + ->willThrowException(new RuntimeException('Encryption failed')); + + $record = new LogRecord( + LogLevel::INFO, + $message, + [], + new DateTimeImmutable() + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Encryption failed'); + + $this->processor->process($record); + } +} diff --git a/tests/Processor/GitProcessorTest.php b/tests/Processor/GitProcessorTest.php deleted file mode 100644 index c54aff2..0000000 --- a/tests/Processor/GitProcessorTest.php +++ /dev/null @@ -1,42 +0,0 @@ -gitProcessor = new GitProcessor(); - } - - public function testProcessHappyPath(): void - { - $record = new LogRecord(LogLevel::INFO, 'Test message'); - - $processedRecord = $this->gitProcessor->process($record); - - $this->assertArrayHasKey('git', $processedRecord->context); - } - - public function testProcessWithGitInfo(): void - { - // Simulate Git info - file_put_contents('.git/HEAD', 'ref: refs/heads/main'); - file_put_contents('.git/refs/heads/main', 'commit_hash'); - - $record = new LogRecord(LogLevel::INFO, 'Test message'); - $processedRecord = $this->gitProcessor->process($record); - - $this->assertEquals('main', $processedRecord->context['git']['branch']); - $this->assertEquals('commit_hash', $processedRecord->context['git']['commit']); - } -} diff --git a/tests/Processor/IntrospectionProcessorTest.php b/tests/Processor/IntrospectionProcessorTest.php index dc62240..e10bfb9 100644 --- a/tests/Processor/IntrospectionProcessorTest.php +++ b/tests/Processor/IntrospectionProcessorTest.php @@ -2,30 +2,111 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\Processor; +namespace KaririCode\Logging\Tests\Logging\Processor; +use KaririCode\Contract\ImmutableValue; use KaririCode\Logging\LogLevel; use KaririCode\Logging\LogRecord; use KaririCode\Logging\Processor\IntrospectionProcessor; use PHPUnit\Framework\TestCase; -class IntrospectionProcessorTest extends TestCase +final class IntrospectionProcessorTest extends TestCase { - private IntrospectionProcessor $introspectionProcessor; + private IntrospectionProcessor $processor; protected function setUp(): void { - $this->introspectionProcessor = new IntrospectionProcessor(); + $this->processor = new IntrospectionProcessor(); } - public function testProcessHappyPath(): void + /** + * @dataProvider provideNonTrackableLevels + */ + public function testProcessDoesNotModifyRecordForNonTrackableLevels(LogLevel $level): void { - $record = new LogRecord(LogLevel::INFO, 'Test message'); - $processedRecord = $this->introspectionProcessor->process($record); + $record = $this->createMockRecord($level); + $processedRecord = $this->processor->process($record); + $this->assertSame($record, $processedRecord); + } + /** + * @dataProvider provideTrackableLevels + */ + public function testProcessAddsIntrospectionDataForTrackableLevels(LogLevel $level): void + { + $record = $this->createMockRecord($level); + $processedRecord = $this->processor->process($record); + $this->assertInstanceOf(LogRecord::class, $processedRecord); $this->assertArrayHasKey('file', $processedRecord->context); $this->assertArrayHasKey('line', $processedRecord->context); $this->assertArrayHasKey('class', $processedRecord->context); $this->assertArrayHasKey('function', $processedRecord->context); } + + public function testProcessRespectsStackDepth(): void + { + $customDepthProcessor = new IntrospectionProcessor(2); + $record = $this->createMockRecord(LogLevel::ERROR); + $processedRecord = $customDepthProcessor->process($record); + $this->assertInstanceOf(LogRecord::class, $processedRecord); + $this->assertArrayHasKey('file', $processedRecord->context); + } + + public function testProcessPreservesOriginalContext(): void + { + $originalContext = ['key' => 'value']; + $record = $this->createMockRecord(LogLevel::ERROR, $originalContext); + $processedRecord = $this->processor->process($record); + $this->assertInstanceOf(LogRecord::class, $processedRecord); + $this->assertArrayHasKey('key', $processedRecord->context); + $this->assertEquals('value', $processedRecord->context['key']); + } + + public function testGetMaxDepthHandlesInvalidTraceDepth(): void + { + $reflection = new \ReflectionClass(IntrospectionProcessor::class); + $getMaxDepthMethod = $reflection->getMethod('getMaxDepth'); + $getMaxDepthMethod->setAccessible(true); + $deepProcessor = new IntrospectionProcessor(1000); + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $maxDepth = $getMaxDepthMethod->invoke($deepProcessor, $trace); + $this->assertLessThanOrEqual(count($trace) - 1, $maxDepth); + $this->assertGreaterThan(0, $maxDepth); + } + + /** + * @return array + */ + public static function provideNonTrackableLevels(): array + { + return [ + [LogLevel::DEBUG], + [LogLevel::INFO], + [LogLevel::NOTICE], + [LogLevel::WARNING], + ]; + } + + /** + * @return array + */ + public static function provideTrackableLevels(): array + { + return [ + [LogLevel::ERROR], + [LogLevel::CRITICAL], + [LogLevel::ALERT], + [LogLevel::EMERGENCY], + ]; + } + + private function createMockRecord(LogLevel $level, array $context = []): ImmutableValue + { + return new LogRecord( + $level, + 'Test message', + $context, + new \DateTimeImmutable() + ); + } } diff --git a/tests/Processor/LoggerProcessorFactoryTest.php b/tests/Processor/LoggerProcessorFactoryTest.php new file mode 100644 index 0000000..946f4af --- /dev/null +++ b/tests/Processor/LoggerProcessorFactoryTest.php @@ -0,0 +1,115 @@ +config = $this->createMock(LoggerConfiguration::class); + $this->factory = new LoggerProcessorFactory(); + } + + public function testInitializeFromConfiguration(): void + { + $processorMap = [ + 'introspection' => IntrospectionProcessor::class, + 'memory_usage_processor' => MemoryUsageProcessor::class, + 'execution_time_processor' => ExecutionTimeProcessor::class, + 'cpu_usage_processor' => CpuUsageProcessor::class, + 'metrics_processor' => MetricsProcessor::class, + 'web_processor' => WebProcessor::class, + ]; + + $this->config->method('get') + ->willReturnMap([ + ['processors', [], $processorMap], + ]); + + $this->factory->initializeFromConfiguration($this->config); + + $reflection = new \ReflectionClass($this->factory); + $property = $reflection->getProperty('processorMap'); + $property->setAccessible(true); + + $actualProcessorMap = $property->getValue($this->factory); + $this->assertSame($processorMap, $actualProcessorMap); + } + + public function testCreateProcessors(): void + { + $channelName = 'test_channel'; + $processorConfig = [ + 'introspection' => [], + 'memory_usage_processor' => ['with' => ['threshold' => 1000]], + ]; + + $this->config->method('get') + ->willReturnMap([ + ['processors', [], [ + 'introspection' => IntrospectionProcessor::class, + 'memory_usage_processor' => MemoryUsageProcessor::class, + ]], + ['channels', [], [$channelName => ['processors' => $processorConfig]]], + [$channelName, [], []], + ]); + + $this->factory->initializeFromConfiguration($this->config); + $processors = $this->factory->createProcessors($channelName); + + $this->assertCount(2, $processors); + $this->assertInstanceOf(IntrospectionProcessor::class, $processors[0]); + $this->assertInstanceOf(MemoryUsageProcessor::class, $processors[1]); + } + + public function testCreateProcessorsWithOptionalConfig(): void + { + $channelName = 'optional_channel'; + $processorConfig = [ + 'web_processor' => [], + ]; + + $this->config->method('get') + ->willReturnMap([ + ['processors', [], ['web_processor' => WebProcessor::class]], + ['channels', [], []], + [$channelName, [], ['enabled' => true, 'processors' => $processorConfig]], + ]); + + $this->factory->initializeFromConfiguration($this->config); + $processors = $this->factory->createProcessors($channelName); + + $this->assertCount(1, $processors); + $this->assertInstanceOf(WebProcessor::class, $processors[0]); + } + + public function testGetProcessorMap(): void + { + $processorMap = [ + 'introspection' => IntrospectionProcessor::class, + 'memory_usage_processor' => MemoryUsageProcessor::class, + ]; + + $this->config->method('get') + ->willReturnMap([ + ['processors', [], $processorMap], + ]); + + $this->factory->initializeFromConfiguration($this->config); + + $this->assertSame($processorMap, $this->factory->getProcessorMap()); + } +} diff --git a/tests/Processor/MemoryUsageProcessorTest.php b/tests/Processor/MemoryUsageProcessorTest.php index 7a849a1..da3a774 100644 --- a/tests/Processor/MemoryUsageProcessorTest.php +++ b/tests/Processor/MemoryUsageProcessorTest.php @@ -6,10 +6,10 @@ use KaririCode\Logging\LogLevel; use KaririCode\Logging\LogRecord; -use KaririCode\Logging\Processor\MemoryUsageProcessor; +use KaririCode\Logging\Processor\Metric\MemoryUsageProcessor; use PHPUnit\Framework\TestCase; -class MemoryUsageProcessorTest extends TestCase +final class MemoryUsageProcessorTest extends TestCase { private MemoryUsageProcessor $memoryUsageProcessor; diff --git a/tests/Processor/MetricsProcessorTest.php b/tests/Processor/MetricsProcessorTest.php new file mode 100644 index 0000000..a2ec545 --- /dev/null +++ b/tests/Processor/MetricsProcessorTest.php @@ -0,0 +1,85 @@ +createMock(LogProcessor::class); + $processor1->expects($this->once()) + ->method('process') + ->with($record) + ->willReturn($record); + + /** @var MockObject&LogProcessor $processor2 */ + $processor2 = $this->createMock(LogProcessor::class); + $processor2->expects($this->once()) + ->method('process') + ->with($record) + ->willReturn($record); + + $processors = [$processor1, $processor2]; + $metricsProcessor = new MetricsProcessor($processors); + + $result = $metricsProcessor->process($record); + + $this->assertSame($record, $result); + } + + public function testProcessWithoutProcessors(): void + { + $record = new LogRecord( + LogLevel::INFO, + 'Initial message', + [], + new DateTimeImmutable() + ); + + $metricsProcessor = new MetricsProcessor([]); + + $result = $metricsProcessor->process($record); + + $this->assertSame($record, $result); + } + + public function testProcessWithProcessorThrowingException(): void + { + $record = new LogRecord( + LogLevel::INFO, + 'Initial message', + [], + new DateTimeImmutable() + ); + + /** @var MockObject&LogProcessor $processor */ + $processor = $this->createMock(LogProcessor::class); + $processor->expects($this->once()) + ->method('process') + ->with($record) + ->willThrowException(new RuntimeException('Processor failed')); + + $processors = [$processor]; + $metricsProcessor = new MetricsProcessor($processors); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Processor failed'); + + $metricsProcessor->process($record); + } +} diff --git a/tests/Processor/WebProcessorTest.php b/tests/Processor/WebProcessorTest.php index 44b999a..ddb6584 100644 --- a/tests/Processor/WebProcessorTest.php +++ b/tests/Processor/WebProcessorTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\Processor\WebProcessor; use PHPUnit\Framework\TestCase; -class WebProcessorTest extends TestCase +final class WebProcessorTest extends TestCase { private WebProcessor $webProcessor; diff --git a/tests/Resilience/CircuitBreakerTest.php b/tests/Resilience/CircuitBreakerTest.php index 4f711e7..33a26b0 100644 --- a/tests/Resilience/CircuitBreakerTest.php +++ b/tests/Resilience/CircuitBreakerTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Resilience\CircuitBreaker; use PHPUnit\Framework\TestCase; -class CircuitBreakerTest extends TestCase +final class CircuitBreakerTest extends TestCase { private CircuitBreaker $circuitBreaker; diff --git a/tests/Resilience/FallbackTest.php b/tests/Resilience/FallbackTest.php index 7bfb9cf..382d2e6 100644 --- a/tests/Resilience/FallbackTest.php +++ b/tests/Resilience/FallbackTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Resilience\Fallback; use PHPUnit\Framework\TestCase; -class FallbackTest extends TestCase +final class FallbackTest extends TestCase { public function testPrimaryOperationSucceeds(): void { diff --git a/tests/Resilience/RateLimiterTest.php b/tests/Resilience/RateLimiterTest.php index 333f39e..c759edd 100644 --- a/tests/Resilience/RateLimiterTest.php +++ b/tests/Resilience/RateLimiterTest.php @@ -9,7 +9,7 @@ use KaririCode\Logging\Resilience\RateLimiter; use PHPUnit\Framework\TestCase; -class RateLimiterTest extends TestCase +final class RateLimiterTest extends TestCase { private RateLimiter $rateLimiter; diff --git a/tests/Resilience/RetryTest.php b/tests/Resilience/RetryTest.php index d3100a1..53905ce 100644 --- a/tests/Resilience/RetryTest.php +++ b/tests/Resilience/RetryTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Resilience\Retry; use PHPUnit\Framework\TestCase; -class RetryTest extends TestCase +final class RetryTest extends TestCase { public function testExecuteHappyPath(): void { diff --git a/tests/Rotetor/SizeBasedRotatorTest.php b/tests/Rotetor/SizeBasedRotatorTest.php index a23f315..32ec5e8 100644 --- a/tests/Rotetor/SizeBasedRotatorTest.php +++ b/tests/Rotetor/SizeBasedRotatorTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tests\KaririCode\Logging\Rotator; +namespace KaririCode\Logging\Tests\Logging\Rotator; use KaririCode\Logging\Rotator\SizeBasedRotator; use PHPUnit\Framework\TestCase; -class SizeBasedRotatorTest extends TestCase +final class SizeBasedRotatorTest extends TestCase { private string $tempDir; private string $logFile; diff --git a/tests/Security/AnonymizerTest.php b/tests/Security/AnonymizerTest.php index 877ad45..0560e9e 100644 --- a/tests/Security/AnonymizerTest.php +++ b/tests/Security/AnonymizerTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Security; +namespace KaririCode\Logging\Tests\Security; +use KaririCode\Logging\Contract\AnonymizerStrategy; use KaririCode\Logging\Security\Anonymizer; use PHPUnit\Framework\TestCase; -class AnonymizerTest extends TestCase +final class AnonymizerTest extends TestCase { private Anonymizer $anonymizer; @@ -35,18 +36,10 @@ public static function provideAnonymizeData(): array 'Contact us at info@example.com for more information.', 'Contact us at in**@example.com for more information.', ], - 'ip' => [ - 'Server IP: 192.168.1.1', - 'Server IP: ***.***.*.*', - ], 'credit_card' => [ 'Payment with card: 1234-5678-9012-3456', 'Payment with card: ****-****-****-3456', ], - 'multiple_patterns' => [ - 'Email: user@domain.com, IP: 10.0.0.1, Card: 9876-5432-1098-7654', - 'Email: us**@domain.com, IP: **.*.*.*, Card: ****-****-****-7654', - ], 'no_sensitive_data' => [ 'This is a regular message without sensitive data.', 'This is a regular message without sensitive data.', @@ -54,37 +47,53 @@ public static function provideAnonymizeData(): array ]; } - public function testAddPattern(): void + public function testAddAnonymizer(): void { - $this->anonymizer->addPattern('phone', '/\+\d{1,3}\s?\d{1,14}/'); - - $input = 'Call me at +1 1234567890'; - $expected = 'Call me at ** **********'; - + // Mocking a new anonymizer strategy + $ipAnonymizer = $this->createMock(AnonymizerStrategy::class); + $ipAnonymizer->expects($this->once()) + ->method('anonymize') + ->with('Server IP: 192.168.1.1') + ->willReturn('Server IP: ***.***.*.*'); + $ipAnonymizer->expects($this->once()) + ->method('getPattern') + ->willReturn('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/'); + + $this->anonymizer->addAnonymizer('ip', $ipAnonymizer); + + $input = 'Server IP: 192.168.1.1'; + $expected = 'Server IP: ***.***.*.*'; $result = $this->anonymizer->anonymize($input); + $this->assertEquals($expected, $result); } - public function testRemovePattern(): void + public function testRemoveAnonymizer(): void { - $input = 'Email: test@example.com'; - $expectedBefore = 'Email: te**@example.com'; - $expectedAfter = 'Email: test@example.com'; + $input = 'Email: info@example.com'; + $expectedBefore = 'Email: in**@example.com'; + $expectedAfter = 'Email: info@example.com'; + // Anonymize with default email anonymizer $resultBefore = $this->anonymizer->anonymize($input); $this->assertEquals($expectedBefore, $resultBefore); - $this->anonymizer->removePattern('email'); - + // Remove email anonymizer and ensure it's not applied anymore + $this->anonymizer->removeAnonymizer('email'); $resultAfter = $this->anonymizer->anonymize($input); $this->assertEquals($expectedAfter, $resultAfter); } - public function testAnonymizeWithInvalidRegex(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid regex pattern for type: invalid'); + // public function testAnonymizeWithInvalidRegex(): void + // { + // $invalidAnonymizer = $this->createMock(AnonymizerStrategy::class); + // $invalidAnonymizer->expects($this->once()) + // ->method('getPattern') + // ->willReturn('*'); // Invalid regex pattern - $this->anonymizer->addPattern('invalid', '*'); - } + // $this->expectException(\InvalidArgumentException::class); + // $this->expectExceptionMessage('Invalid regex pattern for type: invalid'); + + // $this->anonymizer->addAnonymizer('invalid', $invalidAnonymizer); + // } } diff --git a/tests/Security/EncryptorTest.php b/tests/Security/EncryptorTest.php index 7d83ed0..482fbd5 100644 --- a/tests/Security/EncryptorTest.php +++ b/tests/Security/EncryptorTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Security\Encryptor; use PHPUnit\Framework\TestCase; -class EncryptorTest extends TestCase +final class EncryptorTest extends TestCase { private Encryptor $encryptor; diff --git a/tests/Security/MutexTest.php b/tests/Security/MutexTest.php index 4dbda90..46c5965 100644 --- a/tests/Security/MutexTest.php +++ b/tests/Security/MutexTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class MutexTest extends TestCase +final class MutexTest extends TestCase { private Mutex|MockObject $mutex; diff --git a/tests/Security/SimpleMutexTest.php b/tests/Security/SimpleMutexTest.php index b135ea8..9d80935 100644 --- a/tests/Security/SimpleMutexTest.php +++ b/tests/Security/SimpleMutexTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Security\SimpleMutex; use PHPUnit\Framework\TestCase; -class SimpleMutexTest extends TestCase +final class SimpleMutexTest extends TestCase { private SimpleMutex $simpleMutex; diff --git a/tests/Security/ThreadSafeTest.php b/tests/Security/ThreadSafeTest.php index 4c41a7a..def132b 100644 --- a/tests/Security/ThreadSafeTest.php +++ b/tests/Security/ThreadSafeTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class ThreadSafeTest extends TestCase +final class ThreadSafeTest extends TestCase { private ThreadSafe $threadSafe; private Mutex|MockObject $mutex; diff --git a/tests/Service/LoggerServiceProviderTest.php b/tests/Service/LoggerServiceProviderTest.php index 343229a..472d8e6 100644 --- a/tests/Service/LoggerServiceProviderTest.php +++ b/tests/Service/LoggerServiceProviderTest.php @@ -2,51 +2,210 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\Service; +namespace KaririCode\Logging\Tests\Logging\Service; +use KaririCode\Contract\Logging\Logger; +use KaririCode\Logging\Exception\InvalidConfigurationException; use KaririCode\Logging\LoggerConfiguration; +use KaririCode\Logging\LoggerFactory; use KaririCode\Logging\LoggerRegistry; use KaririCode\Logging\Service\LoggerServiceProvider; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class LoggerServiceProviderTest extends TestCase +final class LoggerServiceProviderTest extends TestCase { - private LoggerServiceProvider $loggerServiceProvider; - private LoggerConfiguration $config; + private LoggerConfiguration|MockObject $config; + private LoggerFactory|MockObject $loggerFactory; + private LoggerRegistry|MockObject $loggerRegistry; + private LoggerServiceProvider $serviceProvider; protected function setUp(): void { - $this->loggerServiceProvider = new LoggerServiceProvider(); - $this->config = new LoggerConfiguration(); + $this->config = $this->createMock(LoggerConfiguration::class); + $this->loggerFactory = $this->createMock(LoggerFactory::class); + $this->loggerRegistry = $this->createMock(LoggerRegistry::class); + + $this->serviceProvider = new LoggerServiceProvider( + $this->config, + $this->loggerFactory, + $this->loggerRegistry + ); } - public function testRegisterLogger(): void + public function testRegister(): void + { + $this->config->method('get') + ->willReturnMap([ + ['default', null, 'default_channel'], + ['channels', null, ['channel1' => [], 'channel2' => []]], + ['emergency_logger', [], []], + ['query', [], []], + ['performance', [], []], + ['error', [], []], + ['async.batch_size', 10, 10], + ]); + + $mockLogger = $this->createMock(Logger::class); + $this->loggerFactory->method('createLogger')->willReturn($mockLogger); + $this->loggerFactory->method('createQueryLogger')->willReturn($mockLogger); + $this->loggerFactory->method('createPerformanceLogger')->willReturn($mockLogger); + $this->loggerFactory->method('createErrorLogger')->willReturn($mockLogger); + $this->loggerFactory->method('createAsyncLogger')->willReturn($mockLogger); + + $this->loggerRegistry->method('getLogger')->willReturn($mockLogger); + + $this->loggerRegistry->expects($this->atLeastOnce()) + ->method('addLogger'); + + $this->serviceProvider->register(); + } + + public function testRegisterThrowsExceptionWhenDefaultChannelIsMissing(): void + { + $this->config->method('get') + ->willReturnMap([ + ['default', null, null], + ['channels', null, ['channel1' => []]], + ]); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage("The 'default' and 'channels' configurations are required."); + + $this->serviceProvider->register(); + } + + public function testRegisterThrowsExceptionWhenChannelsConfigIsMissing(): void { - $this->config->set('default', 'test'); - $this->config->set('channels', [ - 'test' => [ - 'driver' => 'single', - 'path' => '/tmp/test_log.log', - 'level' => 'debug', - ], - ]); + $this->config->method('get') + ->willReturnMap([ + ['default', null, 'default_channel'], + ['channels', null, null], + ]); - $this->loggerServiceProvider->register($this->config); - $logger = LoggerRegistry::getLogger('test'); + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage("The 'default' and 'channels' configurations are required."); - $this->assertNotNull($logger); + $this->serviceProvider->register(); + } + + public function testRegisterDefaultLoggers(): void + { + $this->config->method('get') + ->willReturnMap([ + ['default', null, 'default_channel'], + ['channels', null, ['channel1' => [], 'default_channel' => []]], + ]); + + $logger = $this->createMock(Logger::class); + $this->loggerFactory->method('createLogger')->willReturn($logger); + + $this->loggerRegistry->expects($this->exactly(3)) + ->method('addLogger'); + + $method = new \ReflectionMethod(LoggerServiceProvider::class, 'registerDefaultLoggers'); + $method->setAccessible(true); + $method->invoke($this->serviceProvider); } public function testRegisterEmergencyLogger(): void { - $this->config->set('emergency_logger', [ - 'path' => '/tmp/emergency_log.log', - 'level' => 'emergency', - ]); + $this->config->method('get') + ->willReturnMap([ + ['emergency_logger', [], ['config' => 'value']], + ]); + + $emergencyLogger = $this->createMock(Logger::class); + $this->loggerFactory->method('createLogger') + ->with('emergency', ['config' => 'value']) + ->willReturn($emergencyLogger); + + $this->loggerRegistry->expects($this->once()) + ->method('addLogger') + ->with('emergency', $emergencyLogger); + + $method = new \ReflectionMethod(LoggerServiceProvider::class, 'registerEmergencyLogger'); + $method->setAccessible(true); + $method->invoke($this->serviceProvider); + } + + public function testRegisterOptionalLoggers(): void + { + $this->config->method('get') + ->willReturnMap([ + ['query', [], ['query_config' => 'value']], + ['performance', [], ['performance_config' => 'value']], + ['error', [], ['error_config' => 'value']], + ['async.batch_size', 10, 20], + ]); + + $mockLogger = $this->createMock(Logger::class); + $this->loggerFactory->method('createQueryLogger')->willReturn($mockLogger); + $this->loggerFactory->method('createPerformanceLogger')->willReturn($mockLogger); + $this->loggerFactory->method('createErrorLogger')->willReturn($mockLogger); + + // Mock the getLogger method to return a Logger instance + $this->loggerRegistry->method('getLogger') + ->with('default') + ->willReturn($mockLogger); + + // Expect createAsyncLogger to be called with a Logger instance and batch size + $this->loggerFactory->expects($this->once()) + ->method('createAsyncLogger') + ->with($this->isInstanceOf(Logger::class), 20) + ->willReturn($mockLogger); + + $this->loggerRegistry->expects($this->exactly(4)) + ->method('addLogger'); + + $method = new \ReflectionMethod(LoggerServiceProvider::class, 'registerOptionalLoggers'); + $method->setAccessible(true); + $method->invoke($this->serviceProvider); + } + + public function testRegisterLogger(): void + { + $this->config->method('get') + ->willReturnMap([ + ['test_logger', [], ['config' => 'value']], + ]); + + $logger = $this->createMock(Logger::class); + $this->loggerFactory->method('createQueryLogger') + ->with(['config' => 'value']) + ->willReturn($logger); + + $this->loggerRegistry->expects($this->once()) + ->method('addLogger') + ->with('test_logger', $logger); + + $method = new \ReflectionMethod(LoggerServiceProvider::class, 'registerLogger'); + $method->setAccessible(true); + $method->invoke($this->serviceProvider, 'test_logger', 'createQueryLogger'); + } + + public function testRegisterAsyncLoggerIfEnabled(): void + { + $defaultLogger = $this->createMock(Logger::class); + $this->loggerRegistry->method('getLogger') + ->with('default') + ->willReturn($defaultLogger); + + $this->config->method('get') + ->with('async.batch_size', 10) + ->willReturn(20); + + $asyncLogger = $this->createMock(Logger::class); + $this->loggerFactory->method('createAsyncLogger') + ->with($defaultLogger, 20) + ->willReturn($asyncLogger); - $this->loggerServiceProvider->register($this->config); - $logger = LoggerRegistry::getLogger('emergency'); + $this->loggerRegistry->expects($this->once()) + ->method('addLogger') + ->with('async', $asyncLogger); - $this->assertNotNull($logger); + $method = new \ReflectionMethod(LoggerServiceProvider::class, 'registerAsyncLoggerIfEnabled'); + $method->setAccessible(true); + $method->invoke($this->serviceProvider); } } diff --git a/tests/Tracing/DistributedTracingTest.php b/tests/Tracing/DistributedTracingTest.php index 97c2516..5acff78 100644 --- a/tests/Tracing/DistributedTracingTest.php +++ b/tests/Tracing/DistributedTracingTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Tracing\DistributedTracing; use PHPUnit\Framework\TestCase; -class DistributedTracingTest extends TestCase +final class DistributedTracingTest extends TestCase { private DistributedTracing $distributedTracing; diff --git a/tests/Trait/LoggerAwareTraitTest.php b/tests/Trait/LoggerAwareTraitTest.php new file mode 100644 index 0000000..63fa6d7 --- /dev/null +++ b/tests/Trait/LoggerAwareTraitTest.php @@ -0,0 +1,87 @@ +testObject = new TestLoggerAware(); + $this->loggerMock = $this->createMock(Logger::class); + } + + public function testSetLogger(): void + { + $this->testObject->setLogger($this->loggerMock); + + $this->assertSame($this->loggerMock, $this->testObject->getLogger()); + } + + public function testLoggerInitiallyNull(): void + { + $this->assertNull($this->testObject->getLogger()); + } + + public function testOverwriteLogger(): void + { + /** @var Logger */ + $firstLogger = $this->createMock(Logger::class); + /** @var Logger */ + $secondLogger = $this->createMock(Logger::class); + + $this->testObject->setLogger($firstLogger); + $this->assertSame($firstLogger, $this->testObject->getLogger()); + + $this->testObject->setLogger($secondLogger); + $this->assertSame($secondLogger, $this->testObject->getLogger()); + } + + public function testUseLogger(): void + { + $message = 'Test message'; + $context = ['key' => 'value']; + + $this->loggerMock->expects($this->once()) + ->method('info') + ->with($message, $context); + + $this->testObject->setLogger($this->loggerMock); + $this->testObject->doSomethingWithLogging($message, $context); + } + + public function testUseLoggerWhenNotSet(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Logger has not been set'); + + $this->testObject->doSomethingWithLogging('Test', []); + } +} + +class TestLoggerAware +{ + use LoggerAwareTrait; + + public function getLogger(): ?Logger + { + return $this->logger ?? null; + } + + public function doSomethingWithLogging(string $message, array $context = []): void + { + if (!isset($this->logger)) { + throw new \RuntimeException('Logger has not been set'); + } + $this->logger->info($message, $context); + } +} diff --git a/tests/Trait/LoggerTraitTest.php b/tests/Trait/LoggerTraitTest.php new file mode 100644 index 0000000..dbd1646 --- /dev/null +++ b/tests/Trait/LoggerTraitTest.php @@ -0,0 +1,101 @@ +logger = new TestLogger(); + } + + /** + * @dataProvider logLevelProvider + */ + public function testLogMethods(string $method, LoggingLogLevel $expectedLevel): void + { + $message = 'Test message'; + $context = ['key' => 'value']; + + $this->logger->$method($message, $context); + + $this->assertEquals([ + 'level' => $expectedLevel, + 'message' => $message, + 'context' => $context, + ], $this->logger->getLastLogEntry()); + } + + public static function logLevelProvider(): array + { + return [ + ['emergency', LogLevel::EMERGENCY], + ['alert', LogLevel::ALERT], + ['critical', LogLevel::CRITICAL], + ['error', LogLevel::ERROR], + ['warning', LogLevel::WARNING], + ['notice', LogLevel::NOTICE], + ['info', LogLevel::INFO], + ['debug', LogLevel::DEBUG], + ]; + } + + public function testLogWithStringableMessage(): void + { + $stringableMessage = new class() implements \Stringable { + public function __toString(): string + { + return 'Stringable message'; + } + }; + + $this->logger->info($stringableMessage); + + $this->assertEquals([ + 'level' => LogLevel::INFO, + 'message' => 'Stringable message', + 'context' => [], + ], $this->logger->getLastLogEntry()); + } + + public function testLogWithEmptyContext(): void + { + $this->logger->debug('Debug message'); + + $this->assertEquals([ + 'level' => LogLevel::DEBUG, + 'message' => 'Debug message', + 'context' => [], + ], $this->logger->getLastLogEntry()); + } +} + +class TestLogger +{ + use LoggerTrait; + + private array $logs = []; + + public function log(LoggingLogLevel $level, \Stringable|string $message, array $context = []): void + { + $this->logs[] = [ + 'level' => $level, + 'message' => (string) $message, + 'context' => $context, + ]; + } + + public function getLastLogEntry(): ?array + { + return end($this->logs) ?: null; + } +} diff --git a/tests/Util/AssetPublisherTest.php b/tests/Util/AssetPublisherTest.php index c8ea985..917eb67 100644 --- a/tests/Util/AssetPublisherTest.php +++ b/tests/Util/AssetPublisherTest.php @@ -2,44 +2,36 @@ declare(strict_types=1); -namespace KaririCode\Logging\Tests\KaririCode\Logging\Util; +namespace KaririCode\Logging\Tests\Util; use Composer\Composer; use Composer\Config; use Composer\IO\IOInterface; use Composer\Script\Event; use KaririCode\Logging\Util\AssetPublisher; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class AssetPublisherTest extends TestCase +final class AssetPublisherTest extends TestCase { private string $tempDir; - private string $vendorDir; - private string $sourceDir; - private string $targetDir; - private Event|MockObject $event; - private IOInterface|MockObject $io; + private Event|\PHPUnit\Framework\MockObject\MockObject $eventMock; + private Composer|\PHPUnit\Framework\MockObject\MockObject $composerMock; + private Config|\PHPUnit\Framework\MockObject\MockObject $configMock; + private IOInterface|\PHPUnit\Framework\MockObject\MockObject $ioMock; protected function setUp(): void { $this->tempDir = sys_get_temp_dir() . '/asset_publisher_test_' . uniqid(); mkdir($this->tempDir); - $this->vendorDir = $this->tempDir . '/vendor'; - $this->sourceDir = $this->vendorDir . '/kariricode/logging/resources'; - $this->targetDir = $this->tempDir . '/resources/logging'; - - // Create mock objects - $composer = $this->createMock(Composer::class); - $config = $this->createMock(Config::class); - $this->io = $this->createMock(IOInterface::class); - $this->event = $this->createMock(Event::class); - - // Set up expectations - $composer->method('getConfig')->willReturn($config); - $config->method('get')->with('vendor-dir')->willReturn($this->vendorDir); - $this->event->method('getComposer')->willReturn($composer); - $this->event->method('getIO')->willReturn($this->io); + + $this->eventMock = $this->createMock(Event::class); + $this->composerMock = $this->createMock(Composer::class); + $this->configMock = $this->createMock(Config::class); + $this->ioMock = $this->createMock(IOInterface::class); + + $this->eventMock->method('getComposer')->willReturn($this->composerMock); + $this->eventMock->method('getIO')->willReturn($this->ioMock); + $this->composerMock->method('getConfig')->willReturn($this->configMock); } protected function tearDown(): void @@ -47,105 +39,84 @@ protected function tearDown(): void $this->removeDirectory($this->tempDir); } - private function removeDirectory(string $dir): void + public function testPublishAssetsCreatesTargetDirectoryAndCopiesFiles(): void { - if (!file_exists($dir)) { - return; - } + $vendorDir = $this->tempDir . '/vendor'; + $sourceDir = $vendorDir . '/kariricode/logging/resources'; + $targetDir = $this->tempDir . '/resources/logging'; - $files = array_diff(scandir($dir), ['.', '..']); - foreach ($files as $file) { - $path = $dir . '/' . $file; - is_dir($path) ? $this->removeDirectory($path) : unlink($path); - } - rmdir($dir); + mkdir($sourceDir, 0777, true); + file_put_contents($sourceDir . '/test.php', 'configMock->method('get')->with('vendor-dir')->willReturn($vendorDir); + + $this->ioMock->expects($this->once()) + ->method('write') + ->with($this->stringContains('Published assets to:')); + + AssetPublisher::publishAssets($this->eventMock); + + $this->assertDirectoryExists($targetDir); + $this->assertFileExists($targetDir . '/test.php'); + $this->assertEquals('sourceDir . '/css', 0755, true); - file_put_contents($this->sourceDir . '/css/style.css', 'body { color: black; }'); - - // Act - AssetPublisher::publishAssets($this->event); - - // Assert - $this->assertDirectoryExists($this->targetDir); - $this->assertDirectoryExists($this->targetDir . '/css'); - $this->assertFileExists($this->targetDir . '/css/style.css'); - $this->assertEquals('body { color: black; }', file_get_contents($this->targetDir . '/css/style.css')); + $vendorDir = $this->tempDir . '/non_existent_vendor'; + $this->configMock->method('get')->with('vendor-dir')->willReturn($vendorDir); + + $this->ioMock->expects($this->once()) + ->method('writeError') + ->with($this->stringContains('Source directory not found:')); + + AssetPublisher::publishAssets($this->eventMock); + + $this->assertDirectoryDoesNotExist($this->tempDir . '/resources/logging'); } - public function testPublishAssetsWhenSourceDirectoryDoesNotExist(): void + public function testPublishAssetsHandlesExistingTargetDirectory(): void { - // Arrange - $this->io->expects($this->once()) + $vendorDir = $this->tempDir . '/vendor'; + $sourceDir = $vendorDir . '/kariricode/logging/resources'; + $targetDir = $this->tempDir . '/resources/logging'; + + mkdir($sourceDir, 0777, true); + mkdir($targetDir, 0777, true); + file_put_contents($sourceDir . '/test.php', 'configMock->method('get')->with('vendor-dir')->willReturn($vendorDir); + + $this->ioMock->expects($this->once()) ->method('write') - ->with($this->stringContains('Source directory not found')); + ->with($this->stringContains('Published assets to:')); - // Act - AssetPublisher::publishAssets($this->event); + AssetPublisher::publishAssets($this->eventMock); - // Assert - $this->assertDirectoryDoesNotExist($this->targetDir); + $this->assertDirectoryExists($targetDir); + $this->assertFileExists($targetDir . '/test.php'); + $this->assertEquals('sourceDir, 0755, true); - mkdir(dirname($this->targetDir), 0000, true); // Make parent directory inaccessible - - // Assert - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage(sprintf('Directory "%s" was not created', $this->targetDir)); - - // Act - try { - AssetPublisher::publishAssets($this->event); - } finally { - chmod(dirname($this->targetDir), 0755); // Restore permissions to allow cleanup + if (!is_dir($dir)) { + return; } - } - public function testPublishAssetsWhenCopyFails(): void - { - // Arrange - mkdir($this->sourceDir, 0755, true); - file_put_contents($this->sourceDir . '/test.txt', 'Test content'); - - // Cria o diretório de destino, mas com permissões que impedem a escrita - mkdir(dirname($this->targetDir), 0555, true); - - // Assert - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage(sprintf('Directory "%s" was not created', $this->targetDir)); - - // Act - try { - AssetPublisher::publishAssets($this->event); - } finally { - // Restaura as permissões para permitir a limpeza - chmod(dirname($this->targetDir), 0755); + $objects = scandir($dir); + foreach ($objects as $object) { + if ('.' === $object || '..' === $object) { + continue; + } + + $path = $dir . '/' . $object; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } } - } - - public function testPublishAssetsWithSubdirectories(): void - { - // Arrange - mkdir($this->sourceDir . '/css/nested', 0755, true); - file_put_contents($this->sourceDir . '/css/style.css', 'body { color: black; }'); - file_put_contents($this->sourceDir . '/css/nested/nested.css', '.nested { display: none; }'); - - // Act - AssetPublisher::publishAssets($this->event); - - // Assert - $this->assertDirectoryExists($this->targetDir . '/css/nested'); - $this->assertFileExists($this->targetDir . '/css/style.css'); - $this->assertFileExists($this->targetDir . '/css/nested/nested.css'); - $this->assertEquals('body { color: black; }', file_get_contents($this->targetDir . '/css/style.css')); - $this->assertEquals('.nested { display: none; }', file_get_contents($this->targetDir . '/css/nested/nested.css')); + rmdir($dir); } } diff --git a/tests/Util/ComposerScriptsTest.php b/tests/Util/ComposerScriptsTest.php index e0272fe..a247dd5 100644 --- a/tests/Util/ComposerScriptsTest.php +++ b/tests/Util/ComposerScriptsTest.php @@ -4,23 +4,82 @@ namespace KaririCode\Logging\Tests\Util; +use Composer\DependencyResolver\Operation\InstallOperation; use Composer\Installer\PackageEvent; +use Composer\Package\PackageInterface; +use Composer\Script\Event; +use KaririCode\Logging\Util\AssetPublisher; use KaririCode\Logging\Util\ComposerScripts; +use KaririCode\Logging\Util\ConfigGenerator; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class ComposerScriptsTest extends TestCase +class TestConfigGenerator extends ConfigGenerator { - public function testPostPackageInstall(): void + public static bool $generateConfigCalled = false; + + public static function generateConfig(Event $event): void + { + self::$generateConfigCalled = true; + } +} + +class TestAssetPublisher extends AssetPublisher +{ + public static bool $publishAssetsCalled = false; + + public static function publishAssets(Event $event): void + { + self::$publishAssetsCalled = true; + } +} + +final class ComposerScriptsTest extends TestCase +{ + private PackageEvent|MockObject $packageEventMock; + private InstallOperation|MockObject $operationMock; + private PackageInterface|MockObject $packageMock; + + protected function setUp(): void { - $event = $this->createMock(PackageEvent::class); + $this->packageEventMock = $this->createMock(PackageEvent::class); + $this->operationMock = $this->createMock(InstallOperation::class); + $this->packageMock = $this->createMock(PackageInterface::class); - $operation = $this->createMock(\Composer\DependencyResolver\Operation\InstallOperation::class); - $package = $this->createMock(\Composer\Package\Package::class); - $package->method('getName')->willReturn('kariricode/logging'); - $operation->method('getPackage')->willReturn($package); - $event->method('getOperation')->willReturn($operation); + $this->packageEventMock->method('getOperation')->willReturn($this->operationMock); + $this->operationMock->method('getPackage')->willReturn($this->packageMock); - ComposerScripts::postPackageInstall($event); - $this->assertTrue(true); // If no exception occurs, test passes + TestConfigGenerator::$generateConfigCalled = false; + TestAssetPublisher::$publishAssetsCalled = false; + + $this->mockClassInNamespace(ComposerScripts::class, 'ConfigGenerator', TestConfigGenerator::class); + $this->mockClassInNamespace(ComposerScripts::class, 'AssetPublisher', TestAssetPublisher::class); + } + + public function testPostPackageInstallForOtherPackages(): void + { + $this->packageMock->method('getName')->willReturn('some/other-package'); + + ComposerScripts::postPackageInstall($this->packageEventMock); + + $this->assertFalse(TestConfigGenerator::$generateConfigCalled, 'ConfigGenerator::generateConfig should not have been called'); + $this->assertFalse(TestAssetPublisher::$publishAssetsCalled, 'AssetPublisher::publishAssets should not have been called'); + } + + private function mockClassInNamespace(string $namespace, string $className, string $mockClass): void + { + $reflector = new \ReflectionClass($namespace); + $namespaceName = $reflector->getNamespaceName(); + $alias = "{$namespaceName}\\{$className}"; + if (!class_exists($alias)) { + class_alias($mockClass, $alias); + } + } + + protected function tearDown(): void + { + // Reset the mocked classes + $this->mockClassInNamespace(ComposerScripts::class, 'ConfigGenerator', ConfigGenerator::class); + $this->mockClassInNamespace(ComposerScripts::class, 'AssetPublisher', AssetPublisher::class); } } diff --git a/tests/Util/ConfigGeneratorTest.php b/tests/Util/ConfigGeneratorTest.php deleted file mode 100644 index 3afa9a7..0000000 --- a/tests/Util/ConfigGeneratorTest.php +++ /dev/null @@ -1,26 +0,0 @@ -createMock(Event::class); - $config = $this->createMock(\Composer\Config::class); - - $event->method('getComposer')->willReturnSelf(); - $event->method('getConfig')->willReturn($config); - $config->method('get')->with('vendor-dir')->willReturn(__DIR__); - - ConfigGenerator::generateConfig($event); - - $this->assertFileExists(__DIR__ . '/config/logging.php'); - } -} diff --git a/tests/Util/ConfigHelperTest.php b/tests/Util/ConfigHelperTest.php deleted file mode 100644 index abfbb3a..0000000 --- a/tests/Util/ConfigHelperTest.php +++ /dev/null @@ -1,30 +0,0 @@ -assertTrue(ConfigHelper::env('TEST_ENV')); - putenv('TEST_ENV=false'); - $this->assertFalse(ConfigHelper::env('TEST_ENV')); - putenv('TEST_ENV'); - } - - public function testStoragePath(): void - { - $this->assertStringContainsString('storage', ConfigHelper::storagePath()); - } - - public function testFindRootPath(): void - { - $this->assertStringContainsString('tests', ConfigHelper::findRootPath()); - } -} diff --git a/tests/Util/ContextPropagatorTest.php b/tests/Util/ContextPropagatorTest.php index 321ebf1..691a945 100644 --- a/tests/Util/ContextPropagatorTest.php +++ b/tests/Util/ContextPropagatorTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Util\ContextPropagator; use PHPUnit\Framework\TestCase; -class ContextPropagatorTest extends TestCase +final class ContextPropagatorTest extends TestCase { public function testSetGetContext(): void { diff --git a/tests/Util/CurlClientTest.php b/tests/Util/CurlClientTest.php index d84518b..a7f780d 100644 --- a/tests/Util/CurlClientTest.php +++ b/tests/Util/CurlClientTest.php @@ -2,92 +2,98 @@ declare(strict_types=1); -namespace Tests\KaririCode\Logging\Util; +namespace KaririCode\Logging\Tests\Logging\Util; use KaririCode\Logging\Util\CurlClient; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class CurlClientTest extends TestCase +final class CurlClientTest extends TestCase { - private CurlClient $curlClient; + private CurlClient|MockObject $curlClientMock; protected function setUp(): void { - $this->curlClient = new CurlClient(); + parent::setUp(); + $this->curlClientMock = $this->createCurlClientMock(); + } + + private function createCurlClientMock(): CurlClient|MockObject + { + return $this->createMock(CurlClient::class); } public function testPostSuccessful(): void { - // Arrange $url = 'https://api.example.com/endpoint'; $data = ['key' => 'value']; $headers = ['Authorization: Bearer token']; $expectedResponse = ['status' => 200, 'body' => '{"success": true}']; - // Mock the CurlClient methods - $curlClientMock = $this->createMock(CurlClient::class); - $curlClientMock->method('post')->willReturn($expectedResponse); + $this->curlClientMock->method('post')->willReturn($expectedResponse); - // Act - $response = $curlClientMock->post($url, $data, $headers); + $response = $this->curlClientMock->post($url, $data, $headers); - // Assert $this->assertSame($expectedResponse, $response); } - public function testPostWithJsonEncodingError(): void - { - // Arrange - $url = 'https://api.example.com/endpoint'; - $data = ['key' => INF]; // INF cannot be JSON encoded - $headers = []; - - // Assert - $this->expectException(\JsonException::class); - $this->expectExceptionMessage('Failed to encode data: Inf and NaN cannot be JSON encoded'); - - // Act - $this->curlClient->post($url, $data, $headers); - } - public function testPostWithCurlInitializationError(): void { - // Arrange $url = 'https://api.example.com/endpoint'; $data = ['key' => 'value']; $headers = []; - // Mock the CurlClient methods - $curlClientMock = $this->createMock(CurlClient::class); - $curlClientMock->method('post') + $this->curlClientMock->method('post') ->will($this->throwException(new \RuntimeException('Failed to initialize cURL'))); - // Assert $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Failed to initialize cURL'); - // Act - $curlClientMock->post($url, $data, $headers); + $this->curlClientMock->post($url, $data, $headers); } public function testPostWithCurlExecutionError(): void { - // Arrange $url = 'https://api.example.com/endpoint'; $data = ['key' => 'value']; $headers = []; $errorMessage = 'Connection timed out'; - // Mock the CurlClient methods - $curlClientMock = $this->createMock(CurlClient::class); - $curlClientMock->method('post') + $this->curlClientMock->method('post') ->will($this->throwException(new \RuntimeException('Failed to send request: ' . $errorMessage))); - // Assert $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Failed to send request: ' . $errorMessage); - // Act - $curlClientMock->post($url, $data, $headers); + $this->curlClientMock->post($url, $data, $headers); + } + + public function testPostWithCurlError(): void + { + $url = 'https://api.example.com/endpoint'; + $data = ['key' => 'value']; + $headers = []; + + $this->curlClientMock->method('post') + ->willReturn(['status' => 500, 'body' => 'Internal Server Error']); + + $response = $this->curlClientMock->post($url, $data, $headers); + + $this->assertEquals(500, $response['status']); + $this->assertEquals('Internal Server Error', $response['body']); + } + + public function testPostWithSuccessAndHeaders(): void + { + $url = 'https://api.example.com/endpoint'; + $data = ['key' => 'value']; + $headers = ['Authorization: Bearer token']; + $expectedResponse = ['status' => 200, 'body' => '{"success": true}']; + + $this->curlClientMock->method('post')->willReturn($expectedResponse); + + $response = $this->curlClientMock->post($url, $data, $headers); + + $this->assertSame($expectedResponse, $response); } } diff --git a/tests/Util/SamplerTest.php b/tests/Util/SamplerTest.php index bf725ec..7cba795 100644 --- a/tests/Util/SamplerTest.php +++ b/tests/Util/SamplerTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tests\KaririCode\Logging\Util; +namespace KaririCode\Logging\Tests\Logging\Util; use KaririCode\Logging\Util\Sampler; use PHPUnit\Framework\TestCase; -class SamplerTest extends TestCase +final class SamplerTest extends TestCase { public function testConstructorWithValidSampleRate(): void { @@ -24,7 +24,6 @@ public function testConstructorWithInvalidSampleRate(): void public function testShouldSampleWithZeroRate(): void { - $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Sample rate must be between 0 and 1'); $sampler = new Sampler(0); @@ -42,9 +41,9 @@ public function testShouldSampleDistribution(): void $samples = 10000; $trueCount = 0; - for ($i = 0; $i < $samples; $i++) { + for ($i = 0; $i < $samples; ++$i) { if ($sampler->shouldSample()) { - $trueCount++; + ++$trueCount; } } diff --git a/tests/Util/SerializerTest.php b/tests/Util/SerializerTest.php index 2f65d8b..021f432 100644 --- a/tests/Util/SerializerTest.php +++ b/tests/Util/SerializerTest.php @@ -7,7 +7,7 @@ use KaririCode\Logging\Util\Serializer; use PHPUnit\Framework\TestCase; -class SerializerTest extends TestCase +final class SerializerTest extends TestCase { private Serializer $serializer; diff --git a/tests/Util/SlackClientTest.php b/tests/Util/SlackClientTest.php index 92cd5b9..0d3eb8a 100644 --- a/tests/Util/SlackClientTest.php +++ b/tests/Util/SlackClientTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\KaririCode\Logging\Util; +namespace KaririCode\Logging\Tests\Logging\Util; use KaririCode\Logging\Exception\LoggingException; use KaririCode\Logging\Resilience\CircuitBreaker; @@ -23,13 +23,18 @@ final class SlackClientTest extends TestCase protected function setUp(): void { + /** @var CircuitBreaker */ $this->circuitBreaker = $this->createMock(CircuitBreaker::class); + /** @var Retry */ $this->retry = $this->createMock(Retry::class); + /** @var Fallback */ $this->fallback = $this->createMock(Fallback::class); + /** @var CurlClient */ $this->curlClient = $this->createMock(CurlClient::class); $this->slackClient = new SlackClient( - 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX', + 'fake_bot_token', + '#general', $this->circuitBreaker, $this->retry, $this->fallback, @@ -40,15 +45,21 @@ protected function setUp(): void public function testSendMessageSuccess(): void { $message = 'Test message'; - $payload = ['text' => $message]; + $payload = ['channel' => '#general', 'text' => $message]; $response = ['status' => 200, 'body' => '{"ok": true}']; $this->circuitBreaker->expects($this->once())->method('isOpen')->willReturn(false); $this->circuitBreaker->expects($this->once())->method('recordSuccess'); + $this->curlClient->expects($this->once())->method('post')->with( - $this->slackClient->getWebhookUrl(), - $payload + 'https://slack.com/api/chat.postMessage', // Hardcoded URL + $payload, + $this->callback(function ($headers) { + return in_array('Content-Type: application/json; charset=utf-8', $headers, true) + && in_array('Authorization: Bearer fake_bot_token', $headers, true); + }) )->willReturn($response); + $this->retry->expects($this->once())->method('execute')->willReturnCallback(function ($callback) { return $callback(); }); @@ -78,13 +89,14 @@ public function testSendMessageCircuitOpen(): void public function testSendMessageCurlClientThrowsJsonException(): void { $message = 'Test message'; - $payload = ['text' => $message]; + $payload = ['channel' => '#general', 'text' => $message]; $this->circuitBreaker->expects($this->once())->method('isOpen')->willReturn(false); $this->circuitBreaker->expects($this->once())->method('recordFailure'); $this->curlClient->expects($this->once())->method('post')->with( - $this->slackClient->getWebhookUrl(), - $payload + 'https://slack.com/api/chat.postMessage', + $payload, + $this->anything() )->willThrowException(new \JsonException('JSON encoding error')); $this->retry->expects($this->once())->method('execute')->willReturnCallback(function ($callback) { @@ -103,13 +115,14 @@ public function testSendMessageCurlClientThrowsJsonException(): void public function testSendMessageCurlClientThrowsRuntimeException(): void { $message = 'Test message'; - $payload = ['text' => $message]; + $payload = ['channel' => '#general', 'text' => $message]; $this->circuitBreaker->expects($this->once())->method('isOpen')->willReturn(false); $this->circuitBreaker->expects($this->once())->method('recordFailure'); $this->curlClient->expects($this->once())->method('post')->with( - $this->slackClient->getWebhookUrl(), - $payload + 'https://slack.com/api/chat.postMessage', + $payload, + $this->anything() )->willThrowException(new \RuntimeException('Curl error')); $this->retry->expects($this->once())->method('execute')->willReturnCallback(function ($callback) { @@ -139,7 +152,7 @@ public function testSendMessageFallbackOperation(): void $fallbackCalled = false; $this->fallback->expects($this->once()) ->method('execute') - ->willReturnCallback(function ($primary, $fallback) use (&$fallbackCalled, $exception) { + ->willReturnCallback(function ($primary, $fallback) use (&$fallbackCalled) { try { $primary(); } catch (\Throwable $e) { diff --git a/tests/application.php b/tests/application.php index ac536d5..3056c42 100644 --- a/tests/application.php +++ b/tests/application.php @@ -1,81 +1,64 @@ load($configPath); -$serviceProvider = new LoggerServiceProvider(); -$serviceProvider->register($loggerConfig); - -// / Obtém o logger padrão -$defaultLogger = LoggerRegistry::getLogger('default'); - -// Testa o logger padrão -$defaultLogger->info('This is an info message.'); -$defaultLogger->error('This is an error message.'); - -// Testa o logger assíncrono -if (LoggerRegistry::getLogger('async') instanceof AsyncLogger) { - $asyncLogger = LoggerRegistry::getLogger('async'); - $asyncLogger->info('This is an async info message.'); - $asyncLogger->error('This is an async error message.'); -} - -// Testa o query logger -if (LoggerRegistry::getLogger('query')) { - $queryLogger = LoggerRegistry::getLogger('query'); - $queryLogger->debug('Executing query...', ['query' => 'SELECT * FROM users', 'bindings' => []]); +$loggerFactory = new LoggerFactory($loggerConfig); +$loggerRegistry = new LoggerRegistry(); +$serviceProvider = new LoggerServiceProvider( + $loggerConfig, + $loggerFactory, + $loggerRegistry +); + +$serviceProvider->register(); + +$defaultLogger = $loggerRegistry->getLogger('console'); + +$defaultLogger->debug('User email is john.doe@example.com'); +$defaultLogger->info('User IP is 192.168.1.1'); +$defaultLogger->notice('User credit card number is 1234-5678-1234-5678', ['context' => 'credit card']); +$defaultLogger->warning('User phone number is (11) 91234-7890', ['context' => 'phone']); +$defaultLogger->error('This is an error message with email john.doe@example.com', ['context' => 'error']); +$defaultLogger->critical('This is a critical message with IP 192.168.1.1', ['context' => 'critical']); +$defaultLogger->alert('This is an alert message with credit card 1234-5678-1234-5678', ['context' => 'alert']); +$defaultLogger->emergency('This is an emergency message with phone number 123-456-7890', ['context' => 'emergency']); + +$asyncLogger = $loggerRegistry->getLogger('async'); +if ($asyncLogger) { + for ($i = 0; $i < 3; ++$i) { + $asyncLogger->info("Async log message {$i}", ['context' => "batch {$i}"]); + } } -// Testa o performance logger -if (LoggerRegistry::getLogger('performance')) { - $performanceLogger = LoggerRegistry::getLogger('performance'); - $performanceLogger->debug('Performance logging', ['execution_time' => 1500]); -} - -// Testa o error logger -if (LoggerRegistry::getLogger('error')) { - $errorLogger = LoggerRegistry::getLogger('error'); - $errorLogger->error('This is a critical error.', ['context' => 'Testing error logger']); -} - -// Exemplo de registro de um log de emergência -$emergencyLogger = LoggerRegistry::getLogger('emergency'); -$emergencyLogger->emergency('This is an emergency message.'); - -// Exemplo de registro de um log com processador de introspecção -$defaultLogger->info('Testing introspection processor.'); +$queryLogger = $loggerRegistry->getLogger('query'); +$queryLogger->info('Executing a query', ['time' => 90, 'query' => 'SELECT * FROM users', 'bindings' => []]); -// Exemplo de registro de um log com processador de memória -$defaultLogger->debug('Testing memory usage processor.', ['memory_usage' => memory_get_usage(true)]); +$queryLogger = $loggerRegistry->getLogger('query'); +$queryLogger->info('Executing a query', ['query' => 'SELECT * FROM users', 'bindings' => []]); -// Exemplo de registro de um log com processador de Git -$defaultLogger->info('Testing Git processor.', ['branch' => 'main', 'commit' => '1234567890abcdef']); +$performanceLogger = $loggerRegistry->getLogger('performance'); +$performanceLogger->debug('Performance logging', ['execution_time' => 1000, 'additional_context' => 'example']); -// Exemplo de registro de um log com processador Web -$_SERVER['REQUEST_URI'] = '/test-uri'; -$_SERVER['REMOTE_ADDR'] = '127.0.0.1'; -$_SERVER['REQUEST_METHOD'] = 'GET'; -$_SERVER['SERVER_NAME'] = 'localhost'; -$_SERVER['HTTP_REFERER'] = 'http://localhost/referrer'; +$performanceLogger = $loggerRegistry->getLogger('performance'); +$performanceLogger->debug('Performance logging'); -$defaultLogger->info('Testing web processor.', [ - 'url' => ($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https://' : 'http://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . ($_SERVER['REQUEST_URI'] ?? '/'), - 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, - 'http_method' => $_SERVER['REQUEST_METHOD'] ?? null, - 'server' => $_SERVER['SERVER_NAME'] ?? null, - 'referrer' => $_SERVER['HTTP_REFERER'] ?? null, -]); +$errorLogger = $loggerRegistry->getLogger('error'); +$errorLogger->error('This is a critical error.', ['context' => 'Testing error logger']); -echo "All loggers tested successfully.\n"; +$slackLogger = $loggerRegistry->getLogger('slack'); +$slackLogger->critical('Este é um teste de mensagem crítica enviada para o Slack');