Added a remote logging handler to allow user to forward logs to remote target.

This commit is contained in:
arabcoders
2025-04-13 22:39:16 +03:00
parent 7a2c5e01d5
commit 7951ad1659
5 changed files with 143 additions and 0 deletions

View File

@@ -194,6 +194,12 @@ return (function () {
'level' => env('WS_LOGGER_SYSLOG_LEVEL', Level::Error),
'name' => ag($config, 'name'),
],
'remote' => [
'type' => 'remote',
'enabled' => (bool)env('WS_LOGGER_REMOTE_ENABLE', false),
'level' => env('WS_LOGGER_REMOTE_LEVEL', Level::Error),
'url' => env('WS_LOGGER_REMOTE_URL', null),
],
];
$config['supported'] = [

View File

@@ -203,6 +203,32 @@ return (function () {
return $value;
},
],
[
'key' => 'WS_LOGGER_REMOTE_ENABLE',
'description' => 'Enable logging to remote logger.',
'type' => 'bool',
],
[
'key' => 'WS_LOGGER_REMOTE_LEVEL',
'description' => 'Set the log level for the remote logger. Default: ERROR.',
'type' => 'string',
],
[
'key' => 'WS_LOGGER_REMOTE_URL',
'description' => 'The URL to the remote logger.',
'type' => 'string',
'validate' => function (mixed $value): string {
if (!is_numeric($value) && empty($value)) {
throw new ValidationException('Invalid remote logger URL. Empty value.');
}
if (false === isValidURL($value)) {
throw new ValidationException('Invalid remote logger URL. Must be a valid URL.');
}
return $value;
},
'mask' => true,
],
];
$validateCronExpression = function (string $value): string {

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\API\System;
use App\Libs\Attributes\Route\Get;
use App\Libs\Exceptions\RuntimeException;
use Psr\Http\Message\ResponseInterface as iResponse;
final readonly class Explode
{
public const string URL = '%{api.prefix}/system/explode';
#[Get(self::URL . '[/]', name: 'system.explode')]
public function __invoke(): iResponse
{
throw new RuntimeException('Throwing an exception to test exception handling.');
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Libs\Extends;
use App\Libs\Enums\Http\Method;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Throwable;
final class RemoteHandler extends AbstractProcessingHandler
{
/**
* @var array<ResponseInterface>
*/
private array $requests = [];
public function __construct(
private readonly iHttp $client,
private readonly string $url,
$level = Level::Debug,
bool $bubble = true
) {
$this->bubble = $bubble;
parent::__construct($level, $bubble);
}
public function __destruct()
{
if (count($this->requests) > 0) {
foreach ($this->requests as $request) {
try {
$request->getStatusCode();
} catch (Throwable $e) {
syslog(LOG_DEBUG, self::class . ': ' . $e->getMessage());
}
}
}
parent::__destruct();
}
protected function write(LogRecord $record): void
{
$server = $_SERVER ?? [];
foreach ($server as $key => $value) {
if (is_string($key) && str_starts_with(strtoupper($key), 'WS_')) {
$server[$key] = '***';
}
}
try {
$this->requests[] = $this->client->request(Method::POST->value, $this->url, [
'timeout' => 6,
'json' => [
'id' => generateUUID(),
'message' => $record->message,
'trace' => ag($record->context, 'trace', []),
'structured' => ag($record->context, 'structured', []),
'server' => ag($_SERVER ?? [], ['HTTP_HOST', 'SERVER_NAME'], 'watchstate.cli'),
'context' => $server,
'raw' => $record->toArray(),
]
]);
} catch (Throwable $e) {
syslog(LOG_ERR, sprintf('%s: %s. (%s:%d)', $e::class, $e->getMessage(), $e->getFile(), $e->getLine()));
}
}
}

View File

@@ -11,6 +11,7 @@ use App\Libs\Exceptions\Backends\RuntimeException;
use App\Libs\Exceptions\HttpException;
use App\Libs\Extends\ConsoleHandler;
use App\Libs\Extends\ConsoleOutput;
use App\Libs\Extends\RemoteHandler;
use App\Libs\Extends\RouterStrategy;
use Closure;
use ErrorException;
@@ -29,6 +30,7 @@ use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
use Throwable;
/**
@@ -543,6 +545,20 @@ final class Initializer
)
);
break;
case 'remote':
if (null !== ($remoteUrl = ag($context, 'url'))) {
$logger->pushHandler(
$wrap->withHandler(
new RemoteHandler(
Container::get(iHttp::class),
$remoteUrl,
ag($context, 'level', Level::Warning),
(bool)ag($context, 'bubble', true),
)
)
);
}
break;
case 'console':
$logger->pushHandler($wrap->withHandler(new ConsoleHandler($this->cliOutput)));
break;