diff --git a/src/BundledPlugin/Logger/Formatter/GelfFormatter.php b/src/BundledPlugin/Logger/Formatter/GelfFormatter.php index aa3a8c6..9d34d6a 100644 --- a/src/BundledPlugin/Logger/Formatter/GelfFormatter.php +++ b/src/BundledPlugin/Logger/Formatter/GelfFormatter.php @@ -21,17 +21,15 @@ private const VERSION = '1.1'; public const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_UNICODE - | JSON_PRESERVE_ZERO_FRACTION - | JSON_INVALID_UTF8_SUBSTITUTE - | JSON_PARTIAL_OUTPUT_ON_ERROR + | JSON_PRESERVE_ZERO_FRACTION + | JSON_INVALID_UTF8_SUBSTITUTE + | JSON_PARTIAL_OUTPUT_ON_ERROR ; private string $hostName; - public function __construct( - string $hostName = null, - private bool $includeStacktraces = false, - ) { + public function __construct(string $hostName = null, private bool $includeStacktraces = false) + { $this->hostName = $hostName ?? \gethostname(); } diff --git a/src/BundledPlugin/Logger/Handler/ConsoleHandler.php b/src/BundledPlugin/Logger/Handler/ConsoleHandler.php index 257a3de..77ac131 100644 --- a/src/BundledPlugin/Logger/Handler/ConsoleHandler.php +++ b/src/BundledPlugin/Logger/Handler/ConsoleHandler.php @@ -5,12 +5,14 @@ namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; use Amp\ByteStream\WritableResourceStream; +use Amp\Future; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\ConsoleFormatter; use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel; use Luzrain\PHPStreamServer\Internal\Console\Colorizer; +use function Amp\async; use function Luzrain\PHPStreamServer\Internal\getStderr; use function Luzrain\PHPStreamServer\Internal\getStdout; @@ -31,16 +33,18 @@ public function __construct( parent::__construct($level, $channels); } - public function start(): void + public function start(): Future { $this->stream = $this->output === self::OUTPUT_STDERR ? getStderr() : getStdout(); $this->colorSupport = Colorizer::hasColorSupport($this->stream->getResource()); + + return async(static fn() => null); } public function handle(LogEntry $record): void { $message = $this->formatter->format($record); $message = $this->colorSupport ? Colorizer::colorize($message) : Colorizer::stripTags($message); - $this->stream->write($message . PHP_EOL); + $this->stream->write($message . "\n"); } } diff --git a/src/BundledPlugin/Logger/Handler/FileHandler.php b/src/BundledPlugin/Logger/Handler/FileHandler.php index 4cd590f..a2675f0 100644 --- a/src/BundledPlugin/Logger/Handler/FileHandler.php +++ b/src/BundledPlugin/Logger/Handler/FileHandler.php @@ -6,12 +6,14 @@ use Amp\ByteStream\ReadableResourceStream; use Amp\ByteStream\WritableResourceStream; +use Amp\Future; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\StringFormatter; use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel; use Revolt\EventLoop; +use function Amp\async; use function Amp\delay; final class FileHandler extends Handler @@ -44,23 +46,25 @@ public function __construct( parent::__construct($level, $channels); } - public function start(): void + public function start(): Future { - $file = !\str_starts_with($this->filename, '/') ? \getcwd() . '/' . $this->filename : $this->filename; - $this->logFile = new \SplFileInfo($file); + return async(function () { + $file = !\str_starts_with($this->filename, '/') ? \getcwd() . '/' . $this->filename : $this->filename; + $this->logFile = new \SplFileInfo($file); - if(!\is_dir($this->logFile->getPath())) { - \mkdir(directory: $this->logFile->getPath(), recursive: true); - } + if(!\is_dir($this->logFile->getPath())) { + \mkdir(directory: $this->logFile->getPath(), recursive: true); + } - $this->stream = new WritableResourceStream(\fopen($this->logFile->getPathname(), 'a')); - \chmod($this->logFile->getPathname(), $this->permission); + $this->stream = new WritableResourceStream(\fopen($this->logFile->getPathname(), 'a')); + \chmod($this->logFile->getPathname(), $this->permission); - if ($this->rotate) { - $this->scheduleRotate(); - } + if ($this->rotate) { + $this->scheduleRotate(); + } - $this->pause = false; + $this->pause = false; + }); } private function scheduleRotate(): void @@ -157,6 +161,6 @@ public function handle(LogEntry $record): void delay(0.01); } - $this->stream->write($this->formatter->format($record) . PHP_EOL); + $this->stream->write($this->formatter->format($record) . "\n"); } } diff --git a/src/BundledPlugin/Logger/Handler/GelfHandler.php b/src/BundledPlugin/Logger/Handler/GelfHandler.php index 81f58a0..d9592e5 100644 --- a/src/BundledPlugin/Logger/Handler/GelfHandler.php +++ b/src/BundledPlugin/Logger/Handler/GelfHandler.php @@ -4,25 +4,85 @@ namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; +use Amp\Future; +use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\GelfFormatter; +use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; +use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfHttpTransport; +use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfTcpTransport; +use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfTransport; +use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfUdpTransport; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel; +use Revolt\EventLoop; +use function Amp\async; final class GelfHandler extends Handler { + private FormatterInterface $formatter; + private GelfTransport $transport; + + /** + * @param string $address gelf address. Can start with udp:// tcp://, http:// or https:// + */ public function __construct( - private readonly string $address, + string $address, + string|null $hostName = null, + bool $includeStacktraces = false, LogLevel $level = LogLevel::DEBUG, array $channels = [], ) { + [$scheme, $host, $port] = $this->parseAddress($address); + $this->formatter = new GelfFormatter($hostName, $includeStacktraces); + $this->transport = match ($scheme) { + 'udp' => new GelfUdpTransport($host, $port), + 'tcp' => new GelfTcpTransport($host, $port), + 'http', 'https' => new GelfHttpTransport($address), + }; parent::__construct($level, $channels); } - public function start(): void + /** + * @return array{0: string, 1: string, 2: int} + */ + private function parseAddress(string $address): array + { + if ( + !\str_starts_with($address, 'udp://') && + !\str_starts_with($address, 'tcp://') && + !\str_starts_with($address, 'http://') && + !\str_starts_with($address, 'https://') + ) { + throw new \InvalidArgumentException('Address should start with "udp://", "tcp://", "http://" or "https://"'); + } + + $parts = \parse_url($address); + if ($parts === false || !isset($parts['scheme'], $parts['host'])) { + throw new \InvalidArgumentException('Invalid address format'); + } + + $scheme = $parts['scheme']; + $host = $parts['host']; + $port = $parts['port'] ?? match ($scheme) { + 'http' => 80, + 'https' => 443, + default => throw new \InvalidArgumentException('Address should contain port'), + }; + + return [$scheme, $host, $port]; + } + + public function start(): Future { + return async(function () { + $this->transport->start(); + }); } public function handle(LogEntry $record): void { + EventLoop::queue(function () use ($record) { + $this->transport->write($this->formatter->format($record)); + }); } } diff --git a/src/BundledPlugin/Logger/Handler/SyslogHandler.php b/src/BundledPlugin/Logger/Handler/SyslogHandler.php index 5da13d3..bef08ef 100644 --- a/src/BundledPlugin/Logger/Handler/SyslogHandler.php +++ b/src/BundledPlugin/Logger/Handler/SyslogHandler.php @@ -4,12 +4,14 @@ namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; +use Amp\Future; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\StringFormatter; use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel; use Luzrain\PHPStreamServer\Server; +use function Amp\async; final class SyslogHandler extends Handler { @@ -20,7 +22,7 @@ final class SyslogHandler extends Handler */ public function __construct( private readonly string $prefix = Server::SHORTNAME, - private readonly int $flags = LOG_PID, + private readonly int $flags = 0, private readonly string|int $facility = LOG_USER, LogLevel $level = LogLevel::DEBUG, array $channels = [], @@ -28,10 +30,12 @@ public function __construct( parent::__construct($level, $channels); } - public function start(): void + public function start(): Future { $this->formatter = new StringFormatter(messageFormat: '{channel}.{level} {message} {context}'); \openlog($this->prefix, $this->flags, $this->facility); + + return async(static fn () => null); } public function handle(LogEntry $record): void diff --git a/src/BundledPlugin/Logger/HandlerInterface.php b/src/BundledPlugin/Logger/HandlerInterface.php index f9dc7e7..54b944b 100644 --- a/src/BundledPlugin/Logger/HandlerInterface.php +++ b/src/BundledPlugin/Logger/HandlerInterface.php @@ -4,11 +4,12 @@ namespace Luzrain\PHPStreamServer\BundledPlugin\Logger; +use Amp\Future; use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry; interface HandlerInterface { - public function start(): void; + public function start(): Future; public function isHandling(LogEntry $record): bool; diff --git a/src/BundledPlugin/Logger/Internal/GelfTransport/GelfHttpTransport.php b/src/BundledPlugin/Logger/Internal/GelfTransport/GelfHttpTransport.php new file mode 100644 index 0000000..a462414 --- /dev/null +++ b/src/BundledPlugin/Logger/Internal/GelfTransport/GelfHttpTransport.php @@ -0,0 +1,45 @@ +httpClient = (new HttpClientBuilder())->followRedirects(0)->build(); + } + + public function write(string $buffer): void + { + $request = new Request($this->url, 'POST', $buffer); + $request->setHeader('Content-Type', 'application/json'); + $request->setTransferTimeout(5); + + try { + $this->httpClient->request($request); + $this->inErrorState = false; + } catch (SocketException $e) { + if($this->inErrorState === false) { + \trigger_error($e->getMessage(), E_USER_WARNING); + $this->inErrorState = true; + } + } + } +} diff --git a/src/BundledPlugin/Logger/Internal/GelfTransport/GelfTcpTransport.php b/src/BundledPlugin/Logger/Internal/GelfTransport/GelfTcpTransport.php new file mode 100644 index 0000000..3439d8f --- /dev/null +++ b/src/BundledPlugin/Logger/Internal/GelfTransport/GelfTcpTransport.php @@ -0,0 +1,63 @@ +withConnectTimeout(self::CONNECT_TIMEOUT); + + try { + $this->socket = $connector->connect(\sprintf('tcp://%s:%d', $this->host, $this->port), $context); + $this->inErrorState = false; + } catch (ConnectException $e) { + $this->socket = new NullWritableStream(); + + if ($this->inErrorState === false) { + \trigger_error($e->getMessage(), E_USER_WARNING); + $this->inErrorState = true; + } + + EventLoop::delay(self::RECONNECT_TIMEOUT, function () { + $this->start(); + }); + } + } + + public function write(string $buffer): void + { + try { + $this->socket->write($buffer . "\0"); + } catch (StreamException) { + $this->start(); + // try to send second time after connect + try { + $this->socket->write($buffer. "\0"); + } catch (StreamException) { + // do nothing + } + } + } +} diff --git a/src/BundledPlugin/Logger/Internal/GelfTransport/GelfTransport.php b/src/BundledPlugin/Logger/Internal/GelfTransport/GelfTransport.php new file mode 100644 index 0000000..1b23344 --- /dev/null +++ b/src/BundledPlugin/Logger/Internal/GelfTransport/GelfTransport.php @@ -0,0 +1,12 @@ +socket = $connector->connect(\sprintf('udp://%s:%d', $this->host, $this->port)); + } + + public function write(string $buffer): void + { + if (\extension_loaded('zlib') && \strlen($buffer) > self::COMPRESS_FROM_SIZE) { + $buffer = \gzcompress($buffer, 1, ZLIB_ENCODING_DEFLATE); + } + + if (\strlen($buffer) <= self::CHUNK_SIZE) { + $this->socket->write($buffer); + return; + } + + $chunks = \str_split($buffer, self::CHUNK_SIZE - self::HEADER_SIZE); + $chunksCount = \count($chunks); + + if ($chunksCount > self::MAX_CHUNKS) { + \trigger_error('Message is too big', E_USER_WARNING); + return; + } + + $chunkId = \random_bytes(8); + foreach ($chunks as $chunkIndex => $chunkData) { + $chunkHeader = self::MAGIC_BYTES . $chunkId . \pack('CC', $chunkIndex, $chunksCount); + $this->socket->write($chunkHeader . $chunkData); + } + } +} diff --git a/src/BundledPlugin/Logger/Internal/MasterLogger.php b/src/BundledPlugin/Logger/Internal/MasterLogger.php index b89a613..8bddad0 100644 --- a/src/BundledPlugin/Logger/Internal/MasterLogger.php +++ b/src/BundledPlugin/Logger/Internal/MasterLogger.php @@ -19,12 +19,16 @@ final class MasterLogger implements LoggerInterface /** * @var array */ - private array $handlers; + private array $handlers = []; private string $channel = 'server'; - public function __construct(HandlerInterface ...$handlers) + public function __construct() { - $this->handlers = $handlers; + } + + public function addHandler(HandlerInterface $handler): void + { + $this->handlers[] = $handler; } public function withChannel(string $channel): self diff --git a/src/BundledPlugin/Logger/Internal/NullWritableStream.php b/src/BundledPlugin/Logger/Internal/NullWritableStream.php new file mode 100644 index 0000000..29e2c21 --- /dev/null +++ b/src/BundledPlugin/Logger/Internal/NullWritableStream.php @@ -0,0 +1,36 @@ +handlers); + $masterLoggerFactory = static function () { + return new MasterLogger(); }; $workerLoggerFactory = static function (Container $container) { @@ -43,14 +43,22 @@ public function start(): void /** @var MasterLogger $logger */ $logger = $this->masterContainer->get('logger'); - /** @var MessageHandlerInterface $handler */ - $handler = $this->masterContainer->get('handler'); + /** @var MessageHandlerInterface $messageBusHandler */ + $messageBusHandler = $this->masterContainer->get('handler'); foreach ($this->handlers as $loggerHandler) { - $loggerHandler->start(); + $loggerHandler + ->start() + ->map(function () use ($logger, $loggerHandler) { + $logger->addHandler($loggerHandler); + }) + ->catch(function (\Throwable $e) use ($logger) { + $logger->error($e->getMessage(), ['exception' => $e]); + }) + ; } - $handler->subscribe(LogEntry::class, static function (LogEntry $event) use ($logger): void { + $messageBusHandler->subscribe(LogEntry::class, static function (LogEntry $event) use ($logger): void { EventLoop::queue(static function () use ($event, $logger) { $logger->logEntry($event); }); diff --git a/src/Internal/Logger/ConsoleLogger.php b/src/Internal/Logger/ConsoleLogger.php index c863413..0148fb6 100644 --- a/src/Internal/Logger/ConsoleLogger.php +++ b/src/Internal/Logger/ConsoleLogger.php @@ -32,7 +32,7 @@ final class ConsoleLogger implements LoggerInterface 'error' => 'fg=red', 'critical' => 'fg=red', 'alert' => 'fg=red', - 'emergency' => 'bg=red', + 'emergency' => 'fg=red', ]; private ContextNormalizer $contextNormalizer; @@ -73,6 +73,6 @@ public function log(mixed $level, string|\Stringable $message, array $context = $message = $this->colorSupport ? Colorizer::colorize($message) : Colorizer::stripTags($message); - getStderr()->write($message . PHP_EOL); + getStderr()->write($message . "\n"); } }