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);
- }
-}