Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Logger #10

Merged
merged 2 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"giggsey/libphonenumber-for-php": "^8.13",
"egulias/email-validator": "^4.0",
"tempest/highlight": "^2.0",
"symfony/var-exporter": "^7.0"
"symfony/var-exporter": "^7.0",
"psr/log": "^3.0",
"monolog/monolog": "^3.6"
},
"require-dev": {
"phpunit/phpunit": "^10.2",
Expand Down
9 changes: 9 additions & 0 deletions src/Config/logs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Tempest\Config;

use Tempest\Log\LogConfig;

return new LogConfig();
54 changes: 54 additions & 0 deletions src/Discovery/LogHandlerDiscovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Tempest\Discovery;

use ReflectionClass;
use Tempest\Container\Container;
use Tempest\Log\Channels\LogChannel;
use Tempest\Log\LogConfig;

final readonly class LogHandlerDiscovery implements Discovery
{
private const string CACHE_PATH = __DIR__ . '/log-handler-discovery.cache.php';

public function __construct(
private LogConfig $logConfig,
) {
}

public function discover(ReflectionClass $class): void
{
if (
! $class->isInstantiable()
|| ! $class->implementsInterface(LogChannel::class)
) {
return;
}

$this->logConfig->channels[$class->getName()] = $class->getName();
}

public function hasCache(): bool
{
return file_exists(self::CACHE_PATH);
}

public function storeCache(): void
{
file_put_contents(self::CACHE_PATH, serialize($this->logConfig->channels));
}

public function restoreCache(Container $container): void
{
$channels = unserialize(file_get_contents(self::CACHE_PATH));

$this->logConfig->channels = $channels;
}

public function destroyCache(): void
{
@unlink(self::CACHE_PATH);
}
}
31 changes: 31 additions & 0 deletions src/Log/Channels/AppendLogChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tempest\Log\Channels;

use Monolog\Handler\HandlerInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Processor\PsrLogMessageProcessor;

final class AppendLogChannel implements LogChannel
{
public function handler(Level $level, array $config): HandlerInterface
{
return new StreamHandler(
stream: $config['path'] ?? 'logs/tempest.log',
level: $level,
bubble: $config['bubble'] ?? true,
filePermission: $config['file_permission'] ?? null,
useLocking: $config['use_locking'] ?? false
);
}

public function processor(array $config): array
{
return [
new PsrLogMessageProcessor(),
];
}
}
32 changes: 32 additions & 0 deletions src/Log/Channels/DailyLogChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Tempest\Log\Channels;

use Monolog\Handler\HandlerInterface;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Level;
use Monolog\Processor\PsrLogMessageProcessor;

final class DailyLogChannel implements LogChannel
{
public function handler(Level $level, array $config): HandlerInterface
{
return new RotatingFileHandler(
filename: $config['path'] ?? 'logs/tempest.log',
maxFiles: $config['rotation'] ?? 30,
level: $level,
bubble: $config['bubble'] ?? true,
filePermission: $config['file_permission'] ?? null,
useLocking: $config['use_locking'] ?? false
);
}

public function processor(array $config): array
{
return [
new PsrLogMessageProcessor(),
];
}
}
25 changes: 25 additions & 0 deletions src/Log/Channels/LogChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Tempest\Log\Channels;

use Monolog\Handler\HandlerInterface;
use Monolog\Level;
use Monolog\Processor\ProcessorInterface;

interface LogChannel
brendt marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* @param Level $level
* @param array $config
*
* @return array<int, HandlerInterface>|HandlerInterface
*/
public function handler(Level $level, array $config): array|HandlerInterface;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better to call this method getHandlers and always have it return an array, for simplicity. Same for getProcessors


/**
* @return ProcessorInterface|array<int, ProcessorInterface>
*/
public function processor(array $config): array|ProcessorInterface;
}
95 changes: 95 additions & 0 deletions src/Log/GenericLogger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Tempest\Log;

use Monolog\Level;
use Monolog\Logger as Monolog;
use Psr\Log\LoggerInterface;
use Stringable;
use Tempest\Container\Container;
use Tempest\Log\Channels\LogChannel;
use Tempest\Support\ArrayHelper;

final class GenericLogger implements LoggerInterface
{
public function __construct(
private LogConfig $logConfig,
private Container $container,
/** @var array<class-string, Monolog> $drivers */
private array $drivers = [],
) {

}

public function emergency(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Emergency, $message, $context);
}

public function alert(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Alert, $message, $context);
}

public function critical(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Critical, $message, $context);
}

public function error(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Error, $message, $context);
}

public function warning(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Warning, $message, $context);
}

public function notice(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Notice, $message, $context);
}

public function info(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Info, $message, $context);
}

public function debug(Stringable|string $message, array $context = []): void
{
$this->writeLog(Level::Debug, $message, $context);
}

public function log($level, Stringable|string $message, array $context = []): void
{
$level = Level::tryFrom($level) ?? Level::Info;

$this->writeLog($level, $message, $context);
}

private function writeLog(Level $level, string $message, array $context): void
{
$this->resolveDriver($this->logConfig->channel, $level)->log($level, $message, $context);
}

private function resolveDriver(string $channelName, Level $level): Monolog
{
if (isset($this->drivers[$channelName])) {
return $this->drivers[$channelName];
}

/** @var LogChannel $channel */
$channel = $this->container->get($channelName);

$config = $this->logConfig->channelsConfig[$channelName] ?? [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config should be an object with typed properties, not an array


return $this->drivers[$channelName] = new Monolog(
name: $this->logConfig->prefix,
handlers: ArrayHelper::wrap($channel->handler($level, $config)),
processors: ArrayHelper::wrap($channel->processor($config)),
);
}
}
22 changes: 22 additions & 0 deletions src/Log/LogConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Tempest\Log;

use Tempest\Log\Channels\AppendLogChannel;
use Tempest\Log\Channels\LogChannel;

final class LogConfig
{
public function __construct(
/** @var LogChannel[] */
public array $channels = [],
/** @var array<class-string<LogChannel>, array|string> */
public array $channelsConfig = [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we need to improve channel config

/** @var class-string<LogChannel> */
public string $channel = AppendLogChannel::class,
public string $prefix = 'tempest',
) {
}
}
11 changes: 11 additions & 0 deletions src/Log/Logger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Tempest\Log;

use Psr\Log\LoggerInterface;

interface Logger extends LoggerInterface
{
}
22 changes: 22 additions & 0 deletions src/Log/LoggerInitializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Tempest\Log;

use Psr\Log\LoggerInterface;
use Tempest\Container\Container;
use Tempest\Container\Initializer;
use Tempest\Container\Singleton;

#[Singleton]
final readonly class LoggerInitializer implements Initializer
{
public function initialize(Container $container): LoggerInterface
{
return new GenericLogger(
$container->get(LogConfig::class),
$container,
);
}
}
6 changes: 4 additions & 2 deletions tests/Unit/KernelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Tempest\Discovery\DiscoveryLocation;
use Tempest\Discovery\EventBusDiscovery;
use Tempest\Discovery\InitializerDiscovery;
use Tempest\Discovery\LogHandlerDiscovery;
use Tempest\Discovery\MapperDiscovery;
use Tempest\Kernel;

Expand All @@ -38,11 +39,12 @@ public function test_discovery_boot(): void

$appConfig = $container->get(AppConfig::class);

$this->assertCount(4, $appConfig->discoveryClasses);
$this->assertCount(5, $appConfig->discoveryClasses);
$this->assertSame(DiscoveryDiscovery::class, $appConfig->discoveryClasses[0]);
$this->assertSame(EventBusDiscovery::class, $appConfig->discoveryClasses[1]);
$this->assertSame(MapperDiscovery::class, $appConfig->discoveryClasses[2]);
$this->assertSame(InitializerDiscovery::class, $appConfig->discoveryClasses[3]);
$this->assertSame(LogHandlerDiscovery::class, $appConfig->discoveryClasses[3]);
$this->assertSame(InitializerDiscovery::class, $appConfig->discoveryClasses[4]);

$this->assertCount(2, $appConfig->discoveryLocations);
$this->assertSame('Tempest\\', $appConfig->discoveryLocations[0]->namespace);
Expand Down
1 change: 1 addition & 0 deletions tests/Unit/Log/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
logs/
Loading