diff --git a/.docs/README.md b/.docs/README.md index 513326b..8134cc1 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -1,145 +1,235 @@ # Contributte Monolog -[Monolog](https://github.com/Seldaek/monolog/) integration into [Nette/DI](https://github.com/nette/di) - -See also [Monolog documentation](https://github.com/Seldaek/monolog#documentation), this is only an integration. +Integration of [Monolog](https://github.com/Seldaek/monolog/) for Nette Framework. ## Content -- [Setup](#setup) +- [Installation](#installation) - [Configuration](#configuration) - - [Tracy](#tracy) -- [Logging](#logging) -- [Logger manager](#loggermanager) -- [Logger holder](#loggerholder) + - [Channels](#channels) + - [Handlers](#handlers) + - [Processors](#processors) + - [Tracy](#tracy) +- [Usage](#usage) + - [Logging](#logging) + - [LoggerManager](#loggermanager) + - [LoggerHolder](#loggerholder) +- [Examples](#examples) -## Setup +## Installation -Install package +Install package using composer. ```bash composer require contributte/monolog ``` -Register extension +Register prepared [compiler extension](https://doc.nette.org/en/dependency-injection/nette-container) in your `config.neon` file. ```neon extensions: - monolog: Contributte\Monolog\DI\MonologExtension + monolog: Contributte\Monolog\DI\MonologExtension ``` +> [!NOTE] +> This is just a Nette integration, see also [Monolog documentation](https://github.com/Seldaek/monolog#documentation) for more information about handlers, processors and formatters. + ## Configuration +### Channels + +You can configure multiple logging channels. The `default` channel is required and is the only one that is autowired. + +```neon +monolog: + channel: + default: + handlers: + - Monolog\Handler\StreamHandler(%appDir%/../log/app.log) + api: + handlers: + - Monolog\Handler\StreamHandler(%appDir%/../log/api.log) +``` + +### Handlers + +Handlers are responsible for writing log records to various destinations. You can use any [Monolog handler](https://github.com/Seldaek/monolog/blob/main/doc/02-handlers-formatters-processors.md#handlers). + ```neon monolog: - channel: - default: # default channel is required - handlers: - - Monolog\Handler\RotatingFileHandler(%appDir%/../log/syslog.log, 30, Monolog\Logger::WARNING) - # you can use same configuration as in services section (with setup, type, arguments, etc.) - - - type: Monolog\Handler\RotatingFileHandler - arguments: - - %appDir%/../log/syslog.log - - 30 - - Monolog\Logger::WARNING - - @serviceName # or reference an existing service - processors: - - Monolog\Processor\MemoryPeakUsageProcessor() + channel: + default: + handlers: + # Inline handler definition + - Monolog\Handler\StreamHandler(%appDir%/../log/app.log, Monolog\Logger::WARNING) + - Monolog\Handler\RotatingFileHandler(%appDir%/../log/app.log, 30, Monolog\Logger::DEBUG) + # Reference to existing service + - @myCustomHandler +``` + +### Processors + +Processors allow you to add extra data to log records. + +```neon +monolog: + channel: + default: + handlers: + - Monolog\Handler\StreamHandler(%appDir%/../log/app.log) + processors: + - Monolog\Processor\MemoryPeakUsageProcessor() + - Monolog\Processor\WebProcessor() + - Monolog\Processor\IntrospectionProcessor() ``` ### Tracy +Monolog integrates with Tracy debugger. By default, logs are bridged in both directions. + ```neon monolog: - hook: - fromTracy: true # enabled by default, log through Tracy into Monolog - toTracy: true # enabled by default, log through Monolog into Tracy + hook: + fromTracy: true # enabled by default, log Tracy messages to Monolog + toTracy: true # enabled by default, log Monolog messages to Tracy ``` -You may also want configure remote storage for Tracy bluescreens. In this case use [mangoweb-backend/monolog-tracy-handler](https://github.com/mangoweb-backend/monolog-tracy-handler) +> [!TIP] +> For remote storage of Tracy bluescreens, consider using [mangoweb-backend/monolog-tracy-handler](https://github.com/mangoweb-backend/monolog-tracy-handler). + +## Usage -## Logging +### Logging -Log message with injected logger (only `default` is autowired) +Inject the logger using constructor injection or `inject*` method. Only the `default` channel is autowired. ```php use Psr\Log\LoggerInterface; -class ExampleService +class OrderService { - /** @var LoggerInterface **/ - private $logger; + public function __construct( + private LoggerInterface $logger, + ) + { + } - public function injectLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - - public function doSomething(): void - { - $this->logger->info('Log that application did something'); - } + public function process(): void + { + $this->logger->info('Processing order'); + $this->logger->error('Order failed', ['orderId' => 123]); + } } ``` -## LoggerManager +### LoggerManager -You could also use logger manager in case you need to use multiple logger at once. +Use LoggerManager when you need to access multiple channels. ```neon monolog: - manager: - enabled: false # disabled by default + manager: + enabled: true # disabled by default ``` ```php use Contributte\Monolog\LoggerManager; -class ExampleService +class ReportService { - /** @var LoggerManager **/ - private $loggerManager; - - public function injectLoggerManager(LoggerManager $loggerManager): void - { - $this->loggerManager = $loggerManager; - } + public function __construct( + private LoggerManager $loggerManager, + ) + { + } - public function doSomething(): void - { - $this->loggerManager->get('default')->info('Log that application did something'); - $this->loggerManager->get('specialLogger')->info('Log something very special'); - } + public function generate(): void + { + $this->loggerManager->get('default')->info('Generating report'); + $this->loggerManager->get('api')->info('Fetching data from API'); + } } ``` -## LoggerHolder - -Allow you get default logger statically in case that DIC is not available. +### LoggerHolder -It add into message info about which class (or file) called LoggerHolder for easier debugging. +LoggerHolder provides static access to the default logger when DI container is not available. ```neon monolog: - holder: - enabled: false # disabled by default + holder: + enabled: true # disabled by default ``` ```php use Contributte\Monolog\LoggerHolder; -class VerySpecialClassWithoutDependencyInjectionContainerAvailable +class LegacyCode { - public function doSomething(): void - { - LoggerHolder::getInstance()->getLogger()->info('Log that application did something'); - } + public function process(): void + { + LoggerHolder::getInstance()->getLogger()->info('Processing'); + } } ``` + +> [!WARNING] +> LoggerHolder should only be used in legacy code or situations where dependency injection is not possible. + +## Examples + +### Full configuration + +```neon +monolog: + channel: + default: + handlers: + - Monolog\Handler\RotatingFileHandler(%appDir%/../log/app.log, 14, Monolog\Logger::DEBUG) + - Monolog\Handler\StreamHandler(php://stderr, Monolog\Logger::ERROR) + processors: + - Monolog\Processor\MemoryPeakUsageProcessor() + - Monolog\Processor\WebProcessor() + + email: + handlers: + - Monolog\Handler\StreamHandler(%appDir%/../log/email.log) + + hook: + fromTracy: true + toTracy: true + + manager: + enabled: true + + holder: + enabled: false +``` + +### Custom handler service + +```neon +services: + slackHandler: + factory: Monolog\Handler\SlackWebhookHandler( + %slack.webhookUrl%, + %slack.channel%, + %slack.username% + ) + +monolog: + channel: + default: + handlers: + - Monolog\Handler\StreamHandler(%appDir%/../log/app.log) + - @slackHandler +``` + +> [!TIP] +> Take a look at real **Contributte Monolog** configuration example at [contributte/webapp-skeleton](https://github.com/contributte/webapp-skeleton). diff --git a/composer.json b/composer.json index d597c88..88d8598 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,14 @@ ], "require": { "php": ">=8.2", - "contributte/di": "^0.5.3", "monolog/monolog": "^2.0.0 || ^3.0.0", + "nette/di": "^3.2", "nette/utils": "^3.0.0 || ^4.0.0" }, "require-dev": { "contributte/phpstan": "^0.1.0", "contributte/qa": "^0.4.0", - "contributte/tester": "^0.2.0", + "contributte/tester": "^0.4.0", "tracy/tracy": "^2.9.4" }, "autoload": { diff --git a/src/DI/Helpers/SmartStatement.php b/src/DI/Helpers/SmartStatement.php new file mode 100644 index 0000000..87af3e0 --- /dev/null +++ b/src/DI/Helpers/SmartStatement.php @@ -0,0 +1,28 @@ + Expect::arrayOf(Expect::structure([ 'handlers' => Expect::arrayOf( - Expect::anyOf(Expect::string(), Expect::array(), Expect::type(Statement::class)) + Expect::anyOf(Expect::string(), Expect::type(Statement::class)) )->required()->min(1), 'processors' => Expect::arrayOf( - Expect::anyOf(Expect::string(), Expect::array(), Expect::type(Statement::class)) + Expect::anyOf(Expect::string(), Expect::type(Statement::class)) ), ]))->required()->min(1), 'hook' => Expect::structure([ @@ -54,7 +53,6 @@ public function loadConfiguration(): void { $config = $this->config; $builder = $this->getContainerBuilder(); - $definitionsHelper = new ExtensionDefinitionsHelper($this->compiler); if (!isset($config->channel['default'])) { throw new InvalidStateException(sprintf('%s.channel.default is required.', $this->name)); @@ -81,39 +79,11 @@ public function loadConfiguration(): void $channel->handlers['tracy'] = $tracyHandler; } - // Register handlers - $handlerDefinitions = []; - - foreach ($channel->handlers as $handlerName => $handlerConfig) { - $handlerPrefix = $this->prefix('logger.' . $name . '.handler.' . $handlerName); - $handlerDefinition = $definitionsHelper->getDefinitionFromConfig($handlerConfig, $handlerPrefix); - - if ($handlerDefinition instanceof Definition) { - $handlerDefinition->setAutowired(false); - } - - $handlerDefinitions[] = $handlerDefinition; - } - - // Register processors - $processorDefinitions = []; - - foreach ($channel->processors as $processorName => $processorConfig) { - $processorPrefix = $this->prefix('logger.' . $name . '.processor.' . $processorName); - $processorDefinition = $definitionsHelper->getDefinitionFromConfig($processorConfig, $processorPrefix); - - if ($processorDefinition instanceof Definition) { - $processorDefinition->setAutowired(false); - } - - $processorDefinitions[] = $processorDefinition; - } - $logger = $builder->addDefinition($this->prefix('logger.' . $name)) ->setFactory(Logger::class, [ $name, - $handlerDefinitions, - $processorDefinitions, + array_map(static fn ($handler) => SmartStatement::from($handler), $channel->handlers), + array_map(static fn ($processor) => SmartStatement::from($processor), $channel->processors), ]); // Only default logger is autowired diff --git a/tests/Cases/DI/MonologExtensionTest.phpt b/tests/Cases/DI/MonologExtensionTest.phpt index 38ee66d..f12c841 100644 --- a/tests/Cases/DI/MonologExtensionTest.phpt +++ b/tests/Cases/DI/MonologExtensionTest.phpt @@ -48,23 +48,23 @@ Toolkit::test(static function (): void { Toolkit::test(static function (): void { Assert::exception(static function (): void { Helpers::createContainer(__DIR__ . '/../../fixtures/config_00.neon'); - }, InvalidConfigurationException::class, "The mandatory item 'monolog › channel' is missing."); + }, InvalidConfigurationException::class, '~mandatory item .+monolog.+channel.+ is missing~'); }); Toolkit::test(static function (): void { Assert::exception(static function (): void { Helpers::createContainer(__DIR__ . '/../../fixtures/config_01.neon'); - }, InvalidConfigurationException::class, "The length of item 'monolog › channel' expects to be in range 1.., 0 items given."); + }, InvalidConfigurationException::class, '~length of item .+monolog.+channel.+ expects to be in range 1~'); }); Toolkit::test(static function (): void { Assert::exception(static function (): void { Helpers::createContainer(__DIR__ . '/../../fixtures/config_02.neon'); - }, InvalidConfigurationException::class, "The mandatory item 'monolog › channel › default › handlers' is missing."); + }, InvalidConfigurationException::class, '~mandatory item .+monolog.+channel.+default.+handlers.+ is missing~'); }); Toolkit::test(static function (): void { Assert::exception(static function (): void { Helpers::createContainer(__DIR__ . '/../../fixtures/config_03.neon'); - }, InvalidConfigurationException::class, "The length of item 'monolog › channel › default › handlers' expects to be in range 1.., 0 items given."); + }, InvalidConfigurationException::class, '~length of item .+monolog.+channel.+default.+handlers.+ expects to be in range 1~'); });