diff --git a/docs/2-features/09-logging.md b/docs/2-features/09-logging.md index 0b98496ca..26be2f9d6 100644 --- a/docs/2-features/09-logging.md +++ b/docs/2-features/09-logging.md @@ -1,96 +1,196 @@ --- title: Logging +description: "Learn how to use Tempest's logging features to monitor and debug your application." --- -Logging is an essential part of any developer's job. Whether it's for debugging or for production monitoring. Tempest has a powerful set of tools to help you access the relevant information you need. +## Overview -## Debug log +Tempest provides a logging implementation built on top of [Monolog](https://github.com/Seldaek/monolog) that follows PSR-3 and the [RFC 5424 specification](https://datatracker.ietf.org/doc/html/rfc5424). This gives you access to eight standard log levels and the ability to send log messages to multiple destinations simultaneously. -First up are Tempest's debug functions: `ld()` (log, die), `lw()` (log, write), and `ll()` (log, log). These three functions are similar to Symfony's var dumper and Laravel's `dd()`, although there's an important difference. +The system supports file logging, Slack integration, system logs, and custom channels. You can configure different loggers for different parts of your application using Tempest's [tagged singletons](../1-essentials/05-container.md#tagged-singletons) feature. -You can think of `ld()` or `lw()` as Laravel's `dd()` and `dump()` variants. In fact, Tempest uses Symfony's var-dumper under the hood, just like Laravel. Furthermore, if you haven't installed Tempest in a project that already includes Laravel, Tempest will also provide `dd()` and `dump()` as aliases to `ld()` and `lw()`. +## Writing logs -The main difference is that Tempest's debug functions will **also write to the debug log**, which can be tailed with tempest's built-in `tail` command. This is its default output: +To start logging messsages, you may inject the {b`Tempest\Log\Logger`} interface in any class. By default, log messages will be written to a daily rotating log file stored in `.tempest/logs`. This may be customized by providing a different [logging configuration](#configuration). -```console -./tempest tail +```php app/Services/UserService.php +use Tempest\Log\Logger; -

Project

Listening at /Users/brent/Dev/tempest-docs/log/tempest.log -

Server

No server log configured in LogConfig -

Debug

Listening at /Users/brent/Dev/tempest-docs/log/debug.log +final readonly class UserService +{ + public function __construct( + private Logger $logger, + ) {} +} ``` -Wherever you call `ld()` or `lw()` from, the output will also be written to the debug log, and tailed automatically with the `./tempest tail` command. On top of that, `tail` also monitors two other logs: +Tempest supports all eight levels described in the [RFC 5424](https://tools.ietf.org/html/rfc5424) specification. It is possible to configure channels to only log messages at or above a certain level. -- The **project log**, which contains everything the default logger writes to -- The **server log**, which should be manually configured in `LogConfig`: +```php +$logger->emergency('System is unusable'); +$logger->alert('Action required immediately'); +$logger->critical('Important, unexpected error'); +$logger->error('Runtime error that should be monitored'); +$logger->warning('Exceptional occurrence that is not an error'); +$logger->notice('Uncommon event'); +$logger->info('Miscellaneous event'); +$logger->debug('Detailed debug information'); +``` + +### Providing context + +All log methods accept an optional context array for additional information. This data is formatted as JSON and included with your log message: ```php -// app/Config/log.config.php +$logger->error('Order processing failed', context: [ + 'user_id' => $order->userId, + 'order_id' => $order->id, + 'total_amount' => $order->total, + 'payment_method' => $order->paymentMethod, + 'error_code' => $exception->getCode(), + 'error_message' => $exception->getMessage(), +]); +``` -use Tempest\Log\LogConfig; +## Configuration -return new LogConfig( - serverLogPath: '/path/to/nginx.log' +By default, Tempest uses a daily rotating log configuration that creates a new log file each day and retains up to 31 files: - // … +```php config/logging.config.php +use Tempest\Log\Config\DailyLogConfig; +use Tempest; + +return new DailyLogConfig( + path: Tempest\internal_storage_path('logs', 'tempest.log'), + maxFiles: Tempest\env('LOG_MAX_FILES', default: 31) ); ``` -If you're only interested in tailing one or more specific logs, you can filter the `tail` output like so: +To configure a different logging channel, you may create a `logging.config.php` file anywhere and return one of the [available configuration classes](#available-configurations-and-channels). + +### Specifying a minimum log level + +Every configuration class and log channel accept a `minimumLogLevel` property, which defines the lowest severity level that will be logged. Messages below this level will be ignored. -```console -./tempest tail --debug +```php config/logging.config.php +use Tempest\Log\Config\MultipleChannelsLogConfig; +use Tempest\Log\Channels\DailyLogChannel; +use Tempest\Log\Channels\SlackLogChannel; +use Tempest; -

Debug

Listening at /Users/brent/Dev/tempest-docs/log/debug.log +return new MultipleChannelsLogConfig( + channels: [ + new DailyLogChannel( + path: Tempest\internal_storage_path('logs', 'tempest.log'), + maxFiles: Tempest\env('LOG_MAX_FILES', default: 31), + minimumLogLevel: LogLevel::DEBUG, + ), + new SlackLogChannel( + webhookUrl: Tempest\env('SLACK_LOGGING_WEBHOOK_URL'), + channelId: '#alerts', + minimumLogLevel: LogLevel::CRITICAL, + ), + ], +); ``` -Finally, the `ll()` function will do exactly the same as `lw()`, but **only write to the debug log, and not output anything in the browser or terminal**. +### Using multiple loggers -## Logging channels +In situations where you would like to log different types of information to different places, you may create multiple tagged configurations to create separate loggers for different purposes. -On top of debug logging, Tempest includes a monolog implementation which allows you to log to one or more channels. Writing to the logger is as simple as injecting `\Tempest\Log\Logger` wherever you'd like: +For instance, you could have a logger dedicated to critical alerts, while each of your application's module have its own logger: -```php -// app/Rss.php +```php src/Monitoring/logging.config.php +use Tempest\Log\Config\DailyLogConfig; +use Modules\Monitoring\Logging; +use Tempest; + +return new SlackLogConfig( + webhookUrl: Tempest\env('SLACK_LOGGING_WEBHOOK_URL'), + channelId: '#alerts', + minimumLogLevel: LogLevel::CRITICAL, + tag: Logging::SLACK, +); +``` + +```php src/Orders/logging.config.php +use Tempest\Log\Config\DailyLogConfig; +use Modules\Monitoring\Logging; +use Tempest; -use Tempest\Console\Console; -use Tempest\Console\ConsoleCommand; +return new DailyLogConfig( + path: Tempest\internal_storage_path('logs', 'orders.log'), + tag: Logging::ORDERS, +); +``` + +Using this approach, you can inject the appropriate logger using [tagged singletons](../1-essentials/05-container.md#tagged-singletons). This gives you the flexibility to customize logging behavior in different parts of your application. + +```php src/Orders/ProcessOrder.php use Tempest\Log\Logger; -final readonly class Rss +final readonly class ProcessOrder { public function __construct( - private Console $console, + #[Tag(Logging::ORDERS)] private Logger $logger, ) {} - #[ConsoleCommand] - public function sync() + public function __invoke(Order $order): void { - $this->logger->info('Starting RSS sync'); - - // … + $this->logger->info('Processing new order', ['order' => $order]); + + // ... } } ``` -If you're familiar with [monolog](https://seldaek.github.io/monolog/), you know how it supports multiple handlers to handle a log message. Tempest adds a small layer on top of these handlers called channels, they can be configured within `LogConfig`: +### Available configurations and channels -```php -// app/Config/log.config.php +Tempest provides a few log channels that correspond to common logging needs: -use Tempest\Log\LogConfig; -use Tempest\Log\Channels\AppendLogChannel; +- {b`Tempest\Log\Channel\AppendLogChannel`} — append all messages to a single file without rotation, +- {b`Tempest\Log\Channel\DailyLogChannel`} — create a new file each day and remove old files automatically, +- {b`Tempest\Log\Channel\WeeklyLogChannel`} — create a new file each week and remove old files automatically, +- {b`Tempest\Log\Channel\SlackLogChannel`} — send messages to a Slack channel via webhook, +- {b`Tempest\Log\Channel\SysLogChannel`} — write messages to the system log. -return new LogConfig( - channels: [ - new AppendLogChannel(path: __DIR__ . '/../log/project.log'), - ] -); -``` +As a convenient abstraction, a configuration class for each channel is provided: + +- {b`Tempest\Log\Config\SimpleLogConfig`} +- {b`Tempest\Log\Config\DailyLogConfig`} +- {b`Tempest\Log\Config\WeeklyLogConfig`} +- {b`Tempest\Log\Config\SlackLogConfig`} +- {b`Tempest\Log\Config\SysLogConfig`} + +These configuration classes also accept a `channels` property, which allows for providing multiple channels for a single logger. Alternatively, you may use the {b`Tempest\Log\Config\MultipleChannelsLogConfig`} configuration class to achieve the same result more explicitly. -**Please note:** +## Debugging -- Currently, Tempest only supports the `AppendLogChannel` and `DailyLogChannel`, but we're adding more channels in the future. You can always add your own channels by implementing `\Tempest\Log\LogChannel`. -- Also, it's currently not possible to configure environment-specific logging channels, this we'll also support in the future. Again, you're free to make your own channels that take the current environment into account. +Tempest includes several global functions for debugging. Typically, these functions are for quick debugging and should not be committed to production. + +- `ll()` — writes values to the debug log without displaying them, +- `lw()` (also `dump()`) — logs values and displays them, +- `ld()` (also `dd()`) — logs values, displays them, and stops execution, +- `le()` — logs values and emits an {b`Tempest\Debug\ItemsDebugged`} event. + +### Tailing debug logs + +Debug logs are written with console formatting, so they can be tailed with syntax highlighting. You may use `./tempest tail:debug` to monitor the debug log in real time. + +:::warning +By default, debug logs are cleared every time the `tail:debug` command is run. If you want to keep previous log entries, you may pass the `--no-clear` flag. +::: + +### Configuring the debug log + +By default, the debug log is written to `.tempest/debug.log`. This is configurable by creating a `debug.config.php` file that returns a {b`Tempest\Debug\DebugConfig`} with a different `path`: + +```php config/debug.config.php +use Tempest\Debug\DebugConfig; +use Tempest; + +return new DebugConfig( + logPath: Tempest\internal_storage_path('logs', 'debug.log') +); +``` diff --git a/packages/console/src/Commands/TailCommand.php b/packages/console/src/Commands/TailCommand.php deleted file mode 100644 index be8edd600..000000000 --- a/packages/console/src/Commands/TailCommand.php +++ /dev/null @@ -1,61 +0,0 @@ - $loggers */ - $loggers = array_filter([ - $shouldFilter === false || $project ? $this->tailProjectLogCommand : null, - $shouldFilter === false || $server ? $this->tailServerLogCommand : null, - $shouldFilter === false || $debug ? $this->tailDebugLogCommand : null, - ]); - - /** @var Fiber[] $fibers */ - $fibers = []; - - foreach ($loggers as $key => $logger) { - $fiber = new Fiber(fn () => $logger()); - $fibers[$key] = $fiber; - $fiber->start(); - } - - while ($fibers !== []) { - foreach ($fibers as $key => $fiber) { - if ($fiber->isSuspended()) { - $fiber->resume(); - } - - if ($fiber->isTerminated()) { - unset($fibers[$key]); - } - } - } - } -} diff --git a/packages/console/src/Commands/TailDebugLogCommand.php b/packages/console/src/Commands/TailDebugLogCommand.php deleted file mode 100644 index 4dd60e9e5..000000000 --- a/packages/console/src/Commands/TailDebugLogCommand.php +++ /dev/null @@ -1,55 +0,0 @@ -logConfig->debugLogPath; - - if (! $debugLogPath) { - $this->console->error('No debug log configured in LogConfig.'); - - return; - } - - $dir = pathinfo($debugLogPath, PATHINFO_DIRNAME); - - if (! is_dir($dir)) { - mkdir($dir); - } - - if (! file_exists($debugLogPath)) { - touch($debugLogPath); - } - - $this->console->header('Tailing debug logs', "Reading …"); - - new TailReader()->tail( - path: $debugLogPath, - format: fn (string $text) => $this->highlighter->parse( - $text, - new VarExportLanguage(), - ), - ); - } -} diff --git a/packages/console/src/Commands/TailServerLogCommand.php b/packages/console/src/Commands/TailServerLogCommand.php deleted file mode 100644 index 42d5cebe0..000000000 --- a/packages/console/src/Commands/TailServerLogCommand.php +++ /dev/null @@ -1,51 +0,0 @@ -logConfig->serverLogPath; - - if (! $serverLogPath) { - $this->console->error('No server log configured in LogConfig.'); - - return; - } - - if (! file_exists($serverLogPath)) { - $this->console->error("No valid server log at "); - - return; - } - - $this->console->header('Tailing server logs', "Reading …"); - - new TailReader()->tail( - path: $serverLogPath, - format: fn (string $text) => $this->highlighter->parse( - $text, - new LogLanguage(), - ), - ); - } -} diff --git a/packages/core/src/LogExceptionProcessor.php b/packages/core/src/LogExceptionProcessor.php index f74569f67..f851866de 100644 --- a/packages/core/src/LogExceptionProcessor.php +++ b/packages/core/src/LogExceptionProcessor.php @@ -3,6 +3,7 @@ namespace Tempest\Core; use Tempest\Debug\Debug; +use Tempest\Log\Logger; use Throwable; /** @@ -10,6 +11,10 @@ */ final class LogExceptionProcessor implements ExceptionProcessor { + public function __construct( + private readonly Logger $logger, + ) {} + public function process(Throwable $throwable): void { $items = [ @@ -21,6 +26,8 @@ public function process(Throwable $throwable): void : [], ]; + $this->logger->error($throwable->getMessage(), $items); + Debug::resolve()->log($items, writeToOut: false); } } diff --git a/packages/debug/composer.json b/packages/debug/composer.json index d5eae9289..bdf25a63d 100644 --- a/packages/debug/composer.json +++ b/packages/debug/composer.json @@ -5,6 +5,7 @@ "minimum-stability": "dev", "require": { "php": "^8.4", + "tempest/console": "dev-main", "tempest/highlight": "^2.11.4", "symfony/var-dumper": "^7.1" }, diff --git a/packages/debug/src/Debug.php b/packages/debug/src/Debug.php index 3022dabf8..549872156 100644 --- a/packages/debug/src/Debug.php +++ b/packages/debug/src/Debug.php @@ -11,29 +11,33 @@ use Tempest\Container\GenericContainer; use Tempest\EventBus\EventBus; use Tempest\Highlight\Themes\TerminalStyle; -use Tempest\Log\LogConfig; +use Tempest\Support\Filesystem; final readonly class Debug { private function __construct( - private ?LogConfig $logConfig = null, + private ?DebugConfig $config = null, private ?EventBus $eventBus = null, ) {} public static function resolve(): self { try { - $container = GenericContainer::instance(); - return new self( - logConfig: $container?->get(LogConfig::class), - eventBus: $container?->get(EventBus::class), + config: GenericContainer::instance()->get(DebugConfig::class), + eventBus: GenericContainer::instance()->get(EventBus::class), ); } catch (Exception) { return new self(); } } + /** + * Logs and/or dumps the given items. + * + * @param bool $writeToLog Whether to write the items to the log file. + * @param bool $writeToOut Whether to dump the items to the standard output. + */ public function log(array $items, bool $writeToLog = true, bool $writeToOut = true): void { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); @@ -52,30 +56,23 @@ public function log(array $items, bool $writeToLog = true, bool $writeToOut = tr private function writeToLog(array $items, string $callPath): void { - if ($this->logConfig === null) { + if ($this->config === null) { return; } - if (! $this->logConfig->debugLogPath) { + if (! $this->config->logPath) { return; } - $directory = dirname($this->logConfig->debugLogPath); - - if (! is_dir($directory)) { - mkdir(directory: $directory, recursive: true); - } + Filesystem\create_directory_for_file($this->config->logPath); - $handle = @fopen($this->logConfig->debugLogPath, 'a'); - - if (! $handle) { + if (! ($handle = @fopen($this->config->logPath, 'a'))) { return; } foreach ($items as $key => $item) { - $output = $this->createDump($item) . $callPath; - - fwrite($handle, "{$key} " . $output . PHP_EOL); + fwrite($handle, TerminalStyle::BG_BLUE(" {$key} ") . TerminalStyle::FG_GRAY(' → ' . TerminalStyle::ITALIC($callPath))); + fwrite($handle, $this->createCliDump($item) . PHP_EOL); } fclose($handle); @@ -86,27 +83,27 @@ private function writeToOut(array $items, string $callPath): void foreach ($items as $key => $item) { if (defined('STDOUT')) { fwrite(STDOUT, TerminalStyle::BG_BLUE(" {$key} ") . ' '); - - $output = $this->createDump($item); - - fwrite(STDOUT, $output); - - fwrite(STDOUT, $callPath . PHP_EOL); + fwrite(STDOUT, $this->createCliDump($item)); + fwrite(STDOUT, TerminalStyle::DIM('→ ' . TerminalStyle::ITALIC($callPath)) . PHP_EOL . PHP_EOL); } else { echo - sprintf( - '%s (%s)', - 'Source Code Pro, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace', - $key, - $callPath, + vsprintf( + <<%s (%s) + HTML, + [ + 'Source Code Pro, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace', + $key, + $callPath, + ], ) ; @@ -115,10 +112,9 @@ private function writeToOut(array $items, string $callPath): void } } - private function createDump(mixed $input): string + private function createCliDump(mixed $input): string { $cloner = new VarCloner(); - $output = ''; $dumper = new CliDumper(function ($line, $depth) use (&$output): void { @@ -130,7 +126,6 @@ private function createDump(mixed $input): string }); $dumper->setColors(true); - $dumper->dump($cloner->cloneVar($input)); return preg_replace( diff --git a/packages/debug/src/DebugConfig.php b/packages/debug/src/DebugConfig.php new file mode 100644 index 000000000..11cc9fad5 --- /dev/null +++ b/packages/debug/src/DebugConfig.php @@ -0,0 +1,13 @@ +debugConfig->logPath; + + if (! $debugLogPath) { + $this->console->error('No debug log configured in DebugConfig.'); + + return; + } + + if ($clear && Filesystem\is_file($debugLogPath)) { + Filesystem\delete_file($debugLogPath); + } + + Filesystem\create_file($debugLogPath); + + $this->console->header('Tailing debug logs', "Reading …"); + + new TailReader()->tail($debugLogPath); + } +} diff --git a/packages/debug/src/debug.config.php b/packages/debug/src/debug.config.php new file mode 100644 index 000000000..30e24ba3f --- /dev/null +++ b/packages/debug/src/debug.config.php @@ -0,0 +1,7 @@ +minimumLogLevel->includes(LogLevel::fromMonolog($level))) { + return []; + } + return [ new StreamHandler( stream: $this->path, @@ -37,9 +50,4 @@ public function getProcessors(): array new PsrLogMessageProcessor(), ]; } - - public function getPath(): string - { - return $this->path; - } } diff --git a/packages/log/src/Channels/DailyLogChannel.php b/packages/log/src/Channels/DailyLogChannel.php index 18534524d..938c16576 100644 --- a/packages/log/src/Channels/DailyLogChannel.php +++ b/packages/log/src/Channels/DailyLogChannel.php @@ -8,27 +8,42 @@ use Monolog\Processor\PsrLogMessageProcessor; use Tempest\Log\FileHandlers\RotatingFileHandler; use Tempest\Log\LogChannel; +use Tempest\Log\LogLevel; final readonly class DailyLogChannel implements LogChannel { + /** + * This channel writes logs to a file that is rotated daily. + * + * @param string $path The base log file name. + * @param int $maxFiles The maximal amount of files to keep (0 means unlimited) + * @param bool $lockFilesDuringWrites Whether to try to lock log file before doing any writes. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param null|int $filePermission Optional file permissions (default (0644) are only for owner read/write) + */ public function __construct( private string $path, private int $maxFiles = 31, + private LogLevel $minimumLogLevel = LogLevel::DEBUG, + private bool $lockFilesDuringWrites = false, private bool $bubble = true, private ?int $filePermission = null, - private bool $useLocking = false, ) {} public function getHandlers(Level $level): array { + if (! $this->minimumLogLevel->includes(LogLevel::fromMonolog($level))) { + return []; + } + return [ new RotatingFileHandler( filename: $this->path, - maxFiles: $this->maxFiles, + maxFiles: $this->maxFiles ?? 0, level: $level, bubble: $this->bubble, filePermission: $this->filePermission, - useLocking: $this->useLocking, + useLocking: $this->lockFilesDuringWrites, dateFormat: RotatingFileHandler::FILE_PER_DAY, ), ]; diff --git a/packages/log/src/Channels/Slack/PresentationMode.php b/packages/log/src/Channels/Slack/PresentationMode.php new file mode 100644 index 000000000..76b25ce96 --- /dev/null +++ b/packages/log/src/Channels/Slack/PresentationMode.php @@ -0,0 +1,21 @@ +minimumLogLevel->includes(LogLevel::fromMonolog($level))) { + return []; + } + + return [ + new SlackWebhookHandler( + webhookUrl: $this->webhookUrl, + channel: $this->channelId, + username: $this->username, + level: $level, + useAttachment: $this->mode === PresentationMode::BLOCKS || $this->mode === PresentationMode::BLOCKS_WITH_CONTEXT, + includeContextAndExtra: $this->mode === PresentationMode::BLOCKS_WITH_CONTEXT, + ), + ]; + } + + public function getProcessors(): array + { + return [ + new PsrLogMessageProcessor(), + ]; + } +} diff --git a/packages/log/src/Channels/SysLogChannel.php b/packages/log/src/Channels/SysLogChannel.php new file mode 100644 index 000000000..5e5caa78d --- /dev/null +++ b/packages/log/src/Channels/SysLogChannel.php @@ -0,0 +1,53 @@ +minimumLogLevel->includes(LogLevel::fromMonolog($level))) { + return []; + } + + return [ + new SyslogHandler( + ident: $this->identity, + facility: $this->facility, + level: $level, + bubble: $this->bubble, + logopts: $this->flags, + ), + ]; + } + + public function getProcessors(): array + { + return [ + new PsrLogMessageProcessor(), + ]; + } +} diff --git a/packages/log/src/Channels/WeeklyLogChannel.php b/packages/log/src/Channels/WeeklyLogChannel.php index af52ab689..007d582ff 100644 --- a/packages/log/src/Channels/WeeklyLogChannel.php +++ b/packages/log/src/Channels/WeeklyLogChannel.php @@ -8,19 +8,33 @@ use Monolog\Processor\PsrLogMessageProcessor; use Tempest\Log\FileHandlers\RotatingFileHandler; use Tempest\Log\LogChannel; +use Tempest\Log\LogLevel; final readonly class WeeklyLogChannel implements LogChannel { + /** + * @param string $path The base log file name. + * @param int $maxFiles The maximal amount of files to keep. + * @param bool $lockFilesDuringWrites Whether to try to lock log file before doing any writes. + * @param LogLevel $minimumLogLevel The minimum log level to record. + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not. + * @param null|int $filePermission Optional file permissions (default (0644) are only for owner read/write). + */ public function __construct( private string $path, private int $maxFiles = 5, + private bool $lockFilesDuringWrites = false, + private LogLevel $minimumLogLevel = LogLevel::DEBUG, private bool $bubble = true, private ?int $filePermission = null, - private bool $useLocking = false, ) {} public function getHandlers(Level $level): array { + if (! $this->minimumLogLevel->includes(LogLevel::fromMonolog($level))) { + return []; + } + return [ new RotatingFileHandler( filename: $this->path, @@ -28,7 +42,7 @@ public function getHandlers(Level $level): array level: $level, bubble: $this->bubble, filePermission: $this->filePermission, - useLocking: $this->useLocking, + useLocking: $this->lockFilesDuringWrites, dateFormat: RotatingFileHandler::FILE_PER_WEEK, ), ]; diff --git a/packages/log/src/Config/DailyLogConfig.php b/packages/log/src/Config/DailyLogConfig.php new file mode 100644 index 000000000..281c7ef38 --- /dev/null +++ b/packages/log/src/Config/DailyLogConfig.php @@ -0,0 +1,47 @@ + [ + new DailyLogChannel( + path: $this->path, + maxFiles: $this->maxFiles, + minimumLogLevel: $this->minimumLogLevel, + lockFilesDuringWrites: $this->lockFilesDuringWrites, + filePermission: $this->filePermission, + ), + ...$this->channels, + ]; + } + + /** + * A logging configuration that creates a new log file each day and retains a maximum number of files. + * + * @param string $path The base log file name. + * @param int $maxFiles The maximal amount of files to keep (0 means unlimited) + * @param LogLevel $minimumLogLevel The minimum log level to record. + * @param array $channels Additional channels to include in the configuration. + * @param bool $lockFilesDuringWrites Whether to try to lock log file before doing any writes. + * @param null|int $filePermission Optional file permissions (default (0644) are only for owner read/write) + * @param null|string $prefix An optional prefix displayed in all log messages. By default, the current environment is used. + * @param null|UnitEnum|string $tag An optional tag to identify the logger instance associated to this configuration. + */ + public function __construct( + private(set) string $path, + private(set) int $maxFiles = 31, + private(set) LogLevel $minimumLogLevel = LogLevel::DEBUG, + private(set) array $channels = [], + private(set) bool $lockFilesDuringWrites = false, + private(set) ?int $filePermission = null, + private(set) ?string $prefix = null, + private(set) null|UnitEnum|string $tag = null, + ) {} +} diff --git a/packages/log/src/Config/MultipleChannelsLogConfig.php b/packages/log/src/Config/MultipleChannelsLogConfig.php new file mode 100644 index 000000000..7ffef9125 --- /dev/null +++ b/packages/log/src/Config/MultipleChannelsLogConfig.php @@ -0,0 +1,27 @@ + $this->channels; + } + + /** + * A logging configuration that uses multiple log channels. + * + * @param LogChannel[] $channels The log channels to which log messages will be sent. + * @param null|string $prefix An optional prefix displayed in all log messages. By default, the current environment is used. + * @param null|UnitEnum|string $tag An optional tag to identify the logger instance associated to this configuration. + */ + public function __construct( + private(set) array $channels, + private(set) ?string $prefix, + private(set) null|UnitEnum|string $tag = null, + ) {} +} diff --git a/packages/log/src/Config/NullLogConfig.php b/packages/log/src/Config/NullLogConfig.php new file mode 100644 index 000000000..d72a4b711 --- /dev/null +++ b/packages/log/src/Config/NullLogConfig.php @@ -0,0 +1,21 @@ + []; + } + + /** + * A logging configuration that does not log anything. + */ + public function __construct( + private(set) ?string $prefix = null, + private(set) null|UnitEnum|string $tag = null, + ) {} +} diff --git a/packages/log/src/Config/SimpleLogConfig.php b/packages/log/src/Config/SimpleLogConfig.php new file mode 100644 index 000000000..3d12dfcba --- /dev/null +++ b/packages/log/src/Config/SimpleLogConfig.php @@ -0,0 +1,44 @@ + [ + new AppendLogChannel( + path: $this->path, + useLocking: $this->useLocking, + minimumLogLevel: $this->minimumLogLevel, + filePermission: $this->filePermission, + ), + ...$this->channels, + ]; + } + + /** + * A basic logging configuration that appends all logs to a single file. + * + * @param string $path The log file path. + * @param LogLevel $minimumLogLevel The minimum log level to record. + * @param array $channels Additional channels to include in the configuration. + * @param bool $useLocking Whether to try to lock log file before doing any writes. + * @param null|int $filePermission Optional file permissions (default (0644) are only for owner read/write). + * @param null|string $prefix An optional prefix displayed in all log messages. By default, the current environment is used. + * @param null|UnitEnum|string $tag An optional tag to identify the logger instance associated to this configuration. + */ + public function __construct( + private(set) string $path, + private(set) LogLevel $minimumLogLevel = LogLevel::DEBUG, + private(set) array $channels = [], + private(set) bool $useLocking = false, + private(set) ?int $filePermission = null, + private(set) ?string $prefix = null, + private(set) null|UnitEnum|string $tag = null, + ) {} +} diff --git a/packages/log/src/Config/SlackLogConfig.php b/packages/log/src/Config/SlackLogConfig.php new file mode 100644 index 000000000..b4f471d6f --- /dev/null +++ b/packages/log/src/Config/SlackLogConfig.php @@ -0,0 +1,46 @@ + [ + new SlackLogChannel( + webhookUrl: $this->webhookUrl, + channelId: $this->channelId, + username: $this->username, + mode: $this->mode, + minimumLogLevel: $this->minimumLogLevel, + ), + ...$this->channels, + ]; + } + + /** + * A logging configuration for sending log messages to a Slack channel using an Incoming Webhook. + * + * @param string $webhookUrl The Slack Incoming Webhook URL. + * @param string|null $channelId The Slack channel ID to send messages to. If null, the default channel configured in the webhook will be used. + * @param string|null $username The username to display as the sender of the message. + * @param PresentationMode $mode The display mode for the Slack messages. + * @param LogLevel $minimumLogLevel The minimum log level to record. + * @param null|string $prefix An optional prefix displayed in all log messages. By default, the current environment is used. + * @param null|UnitEnum|string $tag An optional tag to identify the logger instance associated to this configuration. + */ + public function __construct( + private(set) string $webhookUrl, + private(set) ?string $channelId = null, + private(set) ?string $username = null, + private(set) PresentationMode $mode = PresentationMode::INLINE, + private LogLevel $minimumLogLevel = LogLevel::DEBUG, + private(set) ?string $prefix = null, + private(set) null|UnitEnum|string $tag = null, + ) {} +} diff --git a/packages/log/src/Config/WeeklyLogConfig.php b/packages/log/src/Config/WeeklyLogConfig.php new file mode 100644 index 000000000..3b57e3e07 --- /dev/null +++ b/packages/log/src/Config/WeeklyLogConfig.php @@ -0,0 +1,47 @@ + [ + new WeeklyLogChannel( + path: $this->path, + maxFiles: $this->maxFiles, + lockFilesDuringWrites: $this->lockFilesDuringWrites, + minimumLogLevel: $this->minimumLogLevel, + filePermission: $this->filePermission, + ), + ...$this->channels, + ]; + } + + /** + * A logging configuration that creates a new log file each week and retains a maximum number of files. + * + * @param string $path The base log file name. + * @param int $maxFiles The maximal amount of files to keep. + * @param LogLevel $minimumLogLevel The minimum log level to record. + * @param null|string $prefix An optional prefix displayed in all log messages. By default, the current environment is used. + * @param bool $lockFilesDuringWrites Whether to try to lock log file before doing any writes. + * @param null|int $filePermission Optional file permissions (default (0644) are only for owner read/write). + * @param array $channels Additional channels to include in the configuration. + * @param null|UnitEnum|string $tag An optional tag to identify the logger instance associated to this configuration. + */ + public function __construct( + private(set) string $path, + private(set) int $maxFiles = 5, + private(set) LogLevel $minimumLogLevel = LogLevel::DEBUG, + private(set) array $channels = [], + private(set) ?string $prefix = null, + private(set) bool $lockFilesDuringWrites = false, + private(set) ?int $filePermission = null, + private(set) null|UnitEnum|string $tag = null, + ) {} +} diff --git a/packages/log/src/Config/logs.config.php b/packages/log/src/Config/logs.config.php deleted file mode 100644 index 1b16a3c76..000000000 --- a/packages/log/src/Config/logs.config.php +++ /dev/null @@ -1,14 +0,0 @@ -logConfig->channels as $channel) { + foreach ($this->logConfig->logChannels as $channel) { $this->resolveDriver($channel, $level)->log($level, $message, $context); } } @@ -95,7 +97,7 @@ private function resolveDriver(LogChannel $channel, MonologLogLevel $level): Mon if (! isset($this->drivers[$key])) { $this->drivers[$key] = new Monolog( - name: $this->logConfig->prefix, + name: $this->logConfig->prefix ?? $this->appConfig->environment->value, handlers: $channel->getHandlers($level), processors: $channel->getProcessors(), ); diff --git a/packages/log/src/LogConfig.php b/packages/log/src/LogConfig.php index e9293d98e..4e3502073 100644 --- a/packages/log/src/LogConfig.php +++ b/packages/log/src/LogConfig.php @@ -4,23 +4,23 @@ namespace Tempest\Log; -use Tempest\Log\Channels\AppendLogChannel; +use Tempest\Container\HasTag; -use function Tempest\root_path; - -final class LogConfig +interface LogConfig extends HasTag { - public function __construct( - /** @var LogChannel[] */ - public array $channels = [], - public string $prefix = 'tempest', - public ?string $debugLogPath = null, - public ?string $serverLogPath = null, - ) { - $this->debugLogPath ??= root_path('/log/debug.log'); + /** + * An optional prefix displayed in all log messages. By default, the current environment is used. + */ + public ?string $prefix { + get; + } - if ($this->channels === []) { - $this->channels[] = new AppendLogChannel(root_path('/log/tempest.log')); - } + /** + * The log channels to which log messages will be sent. + * + * @var LogChannel[] + */ + public array $logChannels { + get; } } diff --git a/packages/log/src/LogLevel.php b/packages/log/src/LogLevel.php index 002ccb8a3..f96950464 100644 --- a/packages/log/src/LogLevel.php +++ b/packages/log/src/LogLevel.php @@ -61,4 +61,26 @@ public static function fromMonolog(Level $level): self Level::Debug => self::DEBUG, }; } + + public function toMonolog(): Level + { + return match ($this) { + self::EMERGENCY => Level::Emergency, + self::ALERT => Level::Alert, + self::CRITICAL => Level::Critical, + self::ERROR => Level::Error, + self::WARNING => Level::Warning, + self::NOTICE => Level::Notice, + self::INFO => Level::Info, + self::DEBUG => Level::Debug, + }; + } + + /** + * Determines if this log level is higher than or equal to the given level. + */ + public function includes(self $level): bool + { + return $this->toMonolog()->includes($level->toMonolog()); + } } diff --git a/packages/log/src/LoggerInitializer.php b/packages/log/src/LoggerInitializer.php index b2bd6d19f..b3fc0bd5b 100644 --- a/packages/log/src/LoggerInitializer.php +++ b/packages/log/src/LoggerInitializer.php @@ -6,18 +6,27 @@ use Psr\Log\LoggerInterface; use Tempest\Container\Container; -use Tempest\Container\Initializer; +use Tempest\Container\DynamicInitializer; use Tempest\Container\Singleton; +use Tempest\Core\AppConfig; use Tempest\EventBus\EventBus; +use Tempest\Reflection\ClassReflector; +use UnitEnum; -final readonly class LoggerInitializer implements Initializer +final readonly class LoggerInitializer implements DynamicInitializer { + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool + { + return $class->getType()->matches(Logger::class) || $class->getType()->matches(LoggerInterface::class); + } + #[Singleton] - public function initialize(Container $container): LoggerInterface|Logger + public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): LoggerInterface|Logger { return new GenericLogger( - $container->get(LogConfig::class), - $container->get(EventBus::class), + logConfig: $container->get(LogConfig::class, $tag), + appConfig: $container->get(AppConfig::class), + eventBus: $container->get(EventBus::class), ); } } diff --git a/packages/console/src/Commands/TailProjectLogCommand.php b/packages/log/src/TailLogsCommand.php similarity index 55% rename from packages/console/src/Commands/TailProjectLogCommand.php rename to packages/log/src/TailLogsCommand.php index 0777335f7..b4cd35543 100644 --- a/packages/console/src/Commands/TailProjectLogCommand.php +++ b/packages/log/src/TailLogsCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Console\Commands; +namespace Tempest\Log; use Tempest\Console\Console; use Tempest\Console\ConsoleCommand; @@ -12,53 +12,41 @@ use Tempest\Highlight\Highlighter; use Tempest\Log\Channels\AppendLogChannel; use Tempest\Log\LogConfig; +use Tempest\Support\Filesystem; -final readonly class TailProjectLogCommand +final readonly class TailLogsCommand { public function __construct( private Console $console, - private LogConfig $logConfig, + private LogConfig $config, #[Tag('console')] private Highlighter $highlighter, ) {} - #[ConsoleCommand('tail:project', description: 'Tails the project log')] + #[ConsoleCommand('tail:logs', description: 'Tails the project logs', aliases: ['log:tail', 'logs:tail'])] public function __invoke(): void { $appendLogChannel = null; - foreach ($this->logConfig->channels as $channel) { + foreach ($this->config->logChannels as $channel) { if ($channel instanceof AppendLogChannel) { $appendLogChannel = $channel; - break; } } if ($appendLogChannel === null) { - $this->console->error('No AppendLogChannel registered'); - + $this->console->error('Tailing logs is only supported when a AppendLogChannel is configured.'); return; } - $dir = pathinfo($appendLogChannel->getPath(), PATHINFO_DIRNAME); - - if (! is_dir($dir)) { - mkdir($dir); - } - - if (! file_exists($appendLogChannel->getPath())) { - touch($appendLogChannel->getPath()); - } + Filesystem\create_file($appendLogChannel->path); - $this->console->header('Tailing project logs', "Reading getPath()}'/>…"); + $this->console->header('Tailing project logs', "Reading path}'/>…"); new TailReader()->tail( - path: $appendLogChannel->getPath(), - format: fn (string $text) => $this->highlighter->parse( - $text, - new LogLanguage(), - ), + path: $appendLogChannel->path, + format: fn (string $text) => $this->highlighter->parse($text, new LogLanguage()), ); } } diff --git a/packages/log/src/logging.config.php b/packages/log/src/logging.config.php new file mode 100644 index 000000000..227806df9 --- /dev/null +++ b/packages/log/src/logging.config.php @@ -0,0 +1,14 @@ +assertSame($expected, LogLevel::fromMonolog($level)); } diff --git a/packages/router/src/HttpApplication.php b/packages/router/src/HttpApplication.php index 78b6e6961..faee9bc69 100644 --- a/packages/router/src/HttpApplication.php +++ b/packages/router/src/HttpApplication.php @@ -11,11 +11,6 @@ use Tempest\Core\Tempest; use Tempest\Http\RequestFactory; use Tempest\Http\Session\SessionManager; -use Tempest\Log\Channels\AppendLogChannel; -use Tempest\Log\LogConfig; - -use function Tempest\env; -use function Tempest\Support\path; #[Singleton] final readonly class HttpApplication implements Application @@ -25,24 +20,9 @@ public function __construct( ) {} /** @param \Tempest\Discovery\DiscoveryLocation[] $discoveryLocations */ - public static function boot( - string $root, - array $discoveryLocations = [], - ): self { - $container = Tempest::boot($root, $discoveryLocations); - - $application = $container->get(HttpApplication::class); - - // Application-specific setup - $logConfig = $container->get(LogConfig::class); - - if ($logConfig->debugLogPath === null && $logConfig->serverLogPath === null && $logConfig->channels === []) { - $logConfig->debugLogPath = path($container->get(Kernel::class)->root, '/log/debug.log')->toString(); - $logConfig->serverLogPath = env('SERVER_LOG'); - $logConfig->channels[] = new AppendLogChannel(path($root, '/log/tempest.log')->toString()); - } - - return $application; + public static function boot(string $root, array $discoveryLocations = []): self + { + return Tempest::boot($root, $discoveryLocations)->get(HttpApplication::class); } public function run(): void diff --git a/src/Tempest/Framework/Testing/IntegrationTest.php b/src/Tempest/Framework/Testing/IntegrationTest.php index da9225197..1b089f30f 100644 --- a/src/Tempest/Framework/Testing/IntegrationTest.php +++ b/src/Tempest/Framework/Testing/IntegrationTest.php @@ -16,7 +16,6 @@ use Tempest\Console\OutputBuffer; use Tempest\Console\Testing\ConsoleTester; use Tempest\Container\GenericContainer; -use Tempest\Core\AppConfig; use Tempest\Core\ExceptionTester; use Tempest\Core\FrameworkKernel; use Tempest\Core\Kernel; @@ -50,8 +49,6 @@ abstract class IntegrationTest extends TestCase /** @var \Tempest\Discovery\DiscoveryLocation[] */ protected array $discoveryLocations = []; - protected AppConfig $appConfig; - protected Kernel $kernel; protected GenericContainer $container; @@ -224,8 +221,6 @@ protected function tearDown(): void /** @phpstan-ignore-next-line */ unset($this->discoveryLocations); /** @phpstan-ignore-next-line */ - unset($this->appConfig); - /** @phpstan-ignore-next-line */ unset($this->kernel); /** @phpstan-ignore-next-line */ unset($this->container); diff --git a/tests/Integration/Console/Commands/CompleteCommandTest.php b/tests/Integration/Console/Commands/CompleteCommandTest.php index 12462a380..a61125e25 100644 --- a/tests/Integration/Console/Commands/CompleteCommandTest.php +++ b/tests/Integration/Console/Commands/CompleteCommandTest.php @@ -4,6 +4,7 @@ namespace Tests\Tempest\Integration\Console\Commands; +use PHPUnit\Framework\Attributes\Test; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** @@ -11,20 +12,22 @@ */ final class CompleteCommandTest extends FrameworkIntegrationTestCase { - public function test_complete_commands(): void + #[Test] + public function complete_commands(): void { $this->console ->complete() - ->assertSee('tail:server' . PHP_EOL) + ->assertSee('migrate:up' . PHP_EOL) ->assertSee('schedule:run' . PHP_EOL); } - public function test_complete_arguments(): void + #[Test] + public function complete_arguments(): void { $this->console - ->complete('tail:') - ->assertSee('tail:server' . PHP_EOL) - ->assertSee('tail:project' . PHP_EOL) - ->assertSee('tail:debug' . PHP_EOL); + ->complete('migrate:') + ->assertSee('migrate:down' . PHP_EOL) + ->assertSee('migrate:up' . PHP_EOL) + ->assertSee('migrate:rehash' . PHP_EOL); } } diff --git a/tests/Integration/Log/GenericLoggerTest.php b/tests/Integration/Log/GenericLoggerTest.php index 8120b8707..ffcab0b46 100644 --- a/tests/Integration/Log/GenericLoggerTest.php +++ b/tests/Integration/Log/GenericLoggerTest.php @@ -6,16 +6,24 @@ use Monolog\Level; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\PostCondition; +use PHPUnit\Framework\Attributes\PreCondition; +use PHPUnit\Framework\Attributes\Test; use Psr\Log\LogLevel as PsrLogLevel; use ReflectionClass; +use Tempest\Core\AppConfig; +use Tempest\DateTime\Duration; use Tempest\EventBus\EventBus; use Tempest\Log\Channels\AppendLogChannel; -use Tempest\Log\Channels\DailyLogChannel; -use Tempest\Log\Channels\WeeklyLogChannel; +use Tempest\Log\Config\DailyLogConfig; +use Tempest\Log\Config\MultipleChannelsLogConfig; +use Tempest\Log\Config\NullLogConfig; +use Tempest\Log\Config\SimpleLogConfig; +use Tempest\Log\Config\WeeklyLogConfig; use Tempest\Log\GenericLogger; -use Tempest\Log\LogConfig; use Tempest\Log\LogLevel; use Tempest\Log\MessageLogged; +use Tempest\Support\Filesystem; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** @@ -23,118 +31,121 @@ */ final class GenericLoggerTest extends FrameworkIntegrationTestCase { - public function test_append_log_channel_works(): void - { - $filePath = __DIR__ . '/logs/tempest.log'; - - $config = new LogConfig( - channels: [ - new AppendLogChannel($filePath), - ], - ); - - $logger = new GenericLogger($config, $this->container->get(EventBus::class)); + private EventBus $bus { + get => $this->container->get(EventBus::class); + } - $logger->info('test'); + private AppConfig $appConfig { + get => $this->container->get(AppConfig::class); + } - $this->assertFileExists($filePath); + #[PreCondition] + protected function configure(): void + { + Filesystem\ensure_directory_empty(__DIR__ . '/logs'); + } - $this->assertStringContainsString('test', file_get_contents($filePath)); + #[PostCondition] + protected function cleanup(): void + { + Filesystem\delete_directory(__DIR__ . '/logs'); } - protected function tearDown(): void + #[Test] + public function simple_log_config(): void { - $files = glob(__DIR__ . '/logs/*.log'); + $filePath = __DIR__ . '/logs/tempest.log'; - foreach ($files as $file) { - if (is_file($file)) { - unlink($file); - } - } + $config = new SimpleLogConfig($filePath, prefix: 'tempest'); + + $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger->info('test'); + + $this->assertFileExists($filePath); + $this->assertStringContainsString('test', Filesystem\read_file($filePath)); } - public function test_daily_log_channel_works(): void + #[Test] + public function daily_log_config(): void { + $clock = $this->clock(); $filePath = __DIR__ . '/logs/tempest-' . date('Y-m-d') . '.log'; + $config = new DailyLogConfig(__DIR__ . '/logs/tempest.log', prefix: 'tempest'); - $config = new LogConfig( - channels: [ - new DailyLogChannel(__DIR__ . '/logs/tempest.log'), - ], - ); - - $logger = new GenericLogger($config, $this->container->get(EventBus::class)); - + $logger = new GenericLogger($config, $this->appConfig, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); + $this->assertStringContainsString('test', Filesystem\read_file($filePath)); + + $clock->plus(Duration::day()); + $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger->info('test'); - $this->assertStringContainsString('test', file_get_contents($filePath)); + $clock->plus(Duration::days(2)); + $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger->info('test'); } - public function test_weekly_log_channel_works(): void + #[Test] + public function weekly_log_config(): void { $filePath = __DIR__ . '/logs/tempest-' . date('Y-W') . '.log'; + $config = new WeeklyLogConfig(__DIR__ . '/logs/tempest.log', prefix: 'tempest'); - $config = new LogConfig( - channels: [ - new WeeklyLogChannel(__DIR__ . '/logs/tempest.log'), - ], - ); - - $logger = new GenericLogger($config, $this->container->get(EventBus::class)); - + $logger = new GenericLogger($config, $this->appConfig, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); - - $this->assertStringContainsString('test', file_get_contents($filePath)); + $this->assertStringContainsString('test', Filesystem\read_file($filePath)); } - public function test_multiple_same_log_channels_works(): void + #[Test] + public function multiple_same_log_channels(): void { $filePath = __DIR__ . '/logs/multiple-tempest1.log'; $secondFilePath = __DIR__ . '/logs/multiple-tempest2.log'; - $config = new LogConfig( + $config = new MultipleChannelsLogConfig( channels: [ new AppendLogChannel($filePath), new AppendLogChannel($secondFilePath), ], + prefix: 'tempest', ); - $logger = new GenericLogger($config, $this->container->get(EventBus::class)); + $logger = new GenericLogger($config, $this->appConfig, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); - $this->assertStringContainsString('test', file_get_contents($filePath)); + $this->assertStringContainsString('test', Filesystem\read_file($filePath)); $this->assertFileExists($secondFilePath); - $this->assertStringContainsString('test', file_get_contents($secondFilePath)); + $this->assertStringContainsString('test', Filesystem\read_file($secondFilePath)); } + #[Test] #[DataProvider('psrLogLevelProvider')] #[DataProvider('monologLevelProvider')] #[DataProvider('tempestLevelProvider')] - public function test_log_levels(mixed $level, string $expected): void + public function log_levels(mixed $level, string $expected): void { $filePath = __DIR__ . '/logs/tempest.log'; - $config = new LogConfig( + $config = new SimpleLogConfig( + path: $filePath, prefix: 'tempest', - channels: [ - new AppendLogChannel($filePath), - ], ); - $logger = new GenericLogger($config, $this->container->get(EventBus::class)); + $logger = new GenericLogger($config, $this->appConfig, $this->bus); $logger->log($level, 'test'); $this->assertFileExists($filePath); - $this->assertStringContainsString('tempest.' . $expected, file_get_contents($filePath)); + $this->assertStringContainsString('tempest.' . $expected, Filesystem\read_file($filePath)); } + #[Test] #[DataProvider('tempestLevelProvider')] - public function test_message_logged_emitted(LogLevel $level, string $_expected): void + public function message_logged_emitted(LogLevel $level, string $_expected): void { $eventBus = $this->container->get(EventBus::class); @@ -144,28 +155,26 @@ public function test_message_logged_emitted(LogLevel $level, string $_expected): $this->assertSame(['foo' => 'bar'], $event->context); }); - $logger = new GenericLogger(new LogConfig(), $eventBus); + $logger = new GenericLogger(new NullLogConfig(), $this->appConfig, $this->bus); $logger->log($level, 'This is a log message of level: ' . $level->value, context: ['foo' => 'bar']); } - public function test_different_log_levels_works(): void + #[Test] + public function different_log_levels(): void { $filePath = __DIR__ . '/logs/tempest.log'; - $config = new LogConfig( + $config = new SimpleLogConfig( + path: $filePath, prefix: 'tempest', - channels: [ - new AppendLogChannel($filePath), - ], ); - $logger = new GenericLogger($config, $this->container->get(EventBus::class)); + $logger = new GenericLogger($config, $this->appConfig, $this->bus); $logger->critical('critical'); $logger->debug('debug'); $this->assertFileExists($filePath); - $content = file_get_contents($filePath); - $this->assertStringContainsString('critical', $content); - $this->assertStringContainsString('debug', $content); + $this->assertStringContainsString('critical', Filesystem\read_file($filePath)); + $this->assertStringContainsString('debug', Filesystem\read_file($filePath)); } public static function tempestLevelProvider(): array diff --git a/tests/Integration/Log/LogConfigTest.php b/tests/Integration/Log/LogConfigTest.php deleted file mode 100644 index ada19f1a1..000000000 --- a/tests/Integration/Log/LogConfigTest.php +++ /dev/null @@ -1,23 +0,0 @@ -container->get(LogConfig::class); - - $this->assertSame(root_path('log/debug.log'), $logConfig->debugLogPath); - } -}