Skip to content

Commit 37660ca

Browse files
committed
Added Gelf logger handler implementation
1 parent e9423f0 commit 37660ca

File tree

14 files changed

+332
-38
lines changed

14 files changed

+332
-38
lines changed

src/BundledPlugin/Logger/Formatter/GelfFormatter.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,15 @@
2121
private const VERSION = '1.1';
2222

2323
public const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_UNICODE
24-
| JSON_PRESERVE_ZERO_FRACTION
25-
| JSON_INVALID_UTF8_SUBSTITUTE
26-
| JSON_PARTIAL_OUTPUT_ON_ERROR
24+
| JSON_PRESERVE_ZERO_FRACTION
25+
| JSON_INVALID_UTF8_SUBSTITUTE
26+
| JSON_PARTIAL_OUTPUT_ON_ERROR
2727
;
2828

2929
private string $hostName;
3030

31-
public function __construct(
32-
string $hostName = null,
33-
private bool $includeStacktraces = false,
34-
) {
31+
public function __construct(string $hostName = null, private bool $includeStacktraces = false)
32+
{
3533
$this->hostName = $hostName ?? \gethostname();
3634
}
3735

src/BundledPlugin/Logger/Handler/ConsoleHandler.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
66

77
use Amp\ByteStream\WritableResourceStream;
8+
use Amp\Future;
89
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\ConsoleFormatter;
910
use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface;
1011
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
1112
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry;
1213
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel;
1314
use Luzrain\PHPStreamServer\Internal\Console\Colorizer;
15+
use function Amp\async;
1416
use function Luzrain\PHPStreamServer\Internal\getStderr;
1517
use function Luzrain\PHPStreamServer\Internal\getStdout;
1618

@@ -31,16 +33,18 @@ public function __construct(
3133
parent::__construct($level, $channels);
3234
}
3335

34-
public function start(): void
36+
public function start(): Future
3537
{
3638
$this->stream = $this->output === self::OUTPUT_STDERR ? getStderr() : getStdout();
3739
$this->colorSupport = Colorizer::hasColorSupport($this->stream->getResource());
40+
41+
return async(static fn() => null);
3842
}
3943

4044
public function handle(LogEntry $record): void
4145
{
4246
$message = $this->formatter->format($record);
4347
$message = $this->colorSupport ? Colorizer::colorize($message) : Colorizer::stripTags($message);
44-
$this->stream->write($message . PHP_EOL);
48+
$this->stream->write($message . "\n");
4549
}
4650
}

src/BundledPlugin/Logger/Handler/FileHandler.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
use Amp\ByteStream\ReadableResourceStream;
88
use Amp\ByteStream\WritableResourceStream;
9+
use Amp\Future;
910
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\StringFormatter;
1011
use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface;
1112
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
1213
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry;
1314
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel;
1415
use Revolt\EventLoop;
16+
use function Amp\async;
1517
use function Amp\delay;
1618

1719
final class FileHandler extends Handler
@@ -44,23 +46,25 @@ public function __construct(
4446
parent::__construct($level, $channels);
4547
}
4648

47-
public function start(): void
49+
public function start(): Future
4850
{
49-
$file = !\str_starts_with($this->filename, '/') ? \getcwd() . '/' . $this->filename : $this->filename;
50-
$this->logFile = new \SplFileInfo($file);
51+
return async(function () {
52+
$file = !\str_starts_with($this->filename, '/') ? \getcwd() . '/' . $this->filename : $this->filename;
53+
$this->logFile = new \SplFileInfo($file);
5154

52-
if(!\is_dir($this->logFile->getPath())) {
53-
\mkdir(directory: $this->logFile->getPath(), recursive: true);
54-
}
55+
if(!\is_dir($this->logFile->getPath())) {
56+
\mkdir(directory: $this->logFile->getPath(), recursive: true);
57+
}
5558

56-
$this->stream = new WritableResourceStream(\fopen($this->logFile->getPathname(), 'a'));
57-
\chmod($this->logFile->getPathname(), $this->permission);
59+
$this->stream = new WritableResourceStream(\fopen($this->logFile->getPathname(), 'a'));
60+
\chmod($this->logFile->getPathname(), $this->permission);
5861

59-
if ($this->rotate) {
60-
$this->scheduleRotate();
61-
}
62+
if ($this->rotate) {
63+
$this->scheduleRotate();
64+
}
6265

63-
$this->pause = false;
66+
$this->pause = false;
67+
});
6468
}
6569

6670
private function scheduleRotate(): void
@@ -157,6 +161,6 @@ public function handle(LogEntry $record): void
157161
delay(0.01);
158162
}
159163

160-
$this->stream->write($this->formatter->format($record) . PHP_EOL);
164+
$this->stream->write($this->formatter->format($record) . "\n");
161165
}
162166
}

src/BundledPlugin/Logger/Handler/GelfHandler.php

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,85 @@
44

55
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
66

7+
use Amp\Future;
8+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\GelfFormatter;
9+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface;
710
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
11+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfHttpTransport;
12+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfTcpTransport;
13+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfTransport;
14+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport\GelfUdpTransport;
815
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry;
916
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel;
17+
use Revolt\EventLoop;
18+
use function Amp\async;
1019

1120
final class GelfHandler extends Handler
1221
{
22+
private FormatterInterface $formatter;
23+
private GelfTransport $transport;
24+
25+
/**
26+
* @param string $address gelf address. Can start with udp:// tcp://, http:// or https://
27+
*/
1328
public function __construct(
14-
private readonly string $address,
29+
string $address,
30+
string|null $hostName = null,
31+
bool $includeStacktraces = false,
1532
LogLevel $level = LogLevel::DEBUG,
1633
array $channels = [],
1734
) {
35+
[$scheme, $host, $port] = $this->parseAddress($address);
36+
$this->formatter = new GelfFormatter($hostName, $includeStacktraces);
37+
$this->transport = match ($scheme) {
38+
'udp' => new GelfUdpTransport($host, $port),
39+
'tcp' => new GelfTcpTransport($host, $port),
40+
'http', 'https' => new GelfHttpTransport($address),
41+
};
1842
parent::__construct($level, $channels);
1943
}
2044

21-
public function start(): void
45+
/**
46+
* @return array{0: string, 1: string, 2: int}
47+
*/
48+
private function parseAddress(string $address): array
49+
{
50+
if (
51+
!\str_starts_with($address, 'udp://') &&
52+
!\str_starts_with($address, 'tcp://') &&
53+
!\str_starts_with($address, 'http://') &&
54+
!\str_starts_with($address, 'https://')
55+
) {
56+
throw new \InvalidArgumentException('Address should start with "udp://", "tcp://", "http://" or "https://"');
57+
}
58+
59+
$parts = \parse_url($address);
60+
if ($parts === false || !isset($parts['scheme'], $parts['host'])) {
61+
throw new \InvalidArgumentException('Invalid address format');
62+
}
63+
64+
$scheme = $parts['scheme'];
65+
$host = $parts['host'];
66+
$port = $parts['port'] ?? match ($scheme) {
67+
'http' => 80,
68+
'https' => 443,
69+
default => throw new \InvalidArgumentException('Address should contain port'),
70+
};
71+
72+
return [$scheme, $host, $port];
73+
}
74+
75+
public function start(): Future
2276
{
77+
return async(function () {
78+
$this->transport->start();
79+
});
2380
}
2481

2582
public function handle(LogEntry $record): void
2683
{
84+
EventLoop::queue(function () use ($record) {
85+
$this->transport->write($this->formatter->format($record));
86+
});
2787
}
2888
}

src/BundledPlugin/Logger/Handler/SyslogHandler.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
66

7+
use Amp\Future;
78
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Formatter\StringFormatter;
89
use Luzrain\PHPStreamServer\BundledPlugin\Logger\FormatterInterface;
910
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Handler;
1011
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry;
1112
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogLevel;
1213
use Luzrain\PHPStreamServer\Server;
14+
use function Amp\async;
1315

1416
final class SyslogHandler extends Handler
1517
{
@@ -20,18 +22,20 @@ final class SyslogHandler extends Handler
2022
*/
2123
public function __construct(
2224
private readonly string $prefix = Server::SHORTNAME,
23-
private readonly int $flags = LOG_PID,
25+
private readonly int $flags = 0,
2426
private readonly string|int $facility = LOG_USER,
2527
LogLevel $level = LogLevel::DEBUG,
2628
array $channels = [],
2729
) {
2830
parent::__construct($level, $channels);
2931
}
3032

31-
public function start(): void
33+
public function start(): Future
3234
{
3335
$this->formatter = new StringFormatter(messageFormat: '{channel}.{level} {message} {context}');
3436
\openlog($this->prefix, $this->flags, $this->facility);
37+
38+
return async(static fn () => null);
3539
}
3640

3741
public function handle(LogEntry $record): void

src/BundledPlugin/Logger/HandlerInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger;
66

7+
use Amp\Future;
78
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\LogEntry;
89

910
interface HandlerInterface
1011
{
11-
public function start(): void;
12+
public function start(): Future;
1213

1314
public function isHandling(LogEntry $record): bool;
1415

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport;
6+
7+
use Amp\Http\Client\HttpClient;
8+
use Amp\Http\Client\HttpClientBuilder;
9+
use Amp\Http\Client\Request;
10+
use Amp\Http\Client\SocketException;
11+
12+
final class GelfHttpTransport implements GelfTransport
13+
{
14+
private HttpClient $httpClient;
15+
private bool $inErrorState = false;
16+
17+
public function __construct(private readonly string $url)
18+
{
19+
if (!\class_exists(HttpClient::class)) {
20+
throw new \RuntimeException(\sprintf('You cannot use "%s" as the "http-client" package is not installed. Try running "composer require amphp/http-client".', __CLASS__));
21+
}
22+
}
23+
24+
public function start(): void
25+
{
26+
$this->httpClient = (new HttpClientBuilder())->followRedirects(0)->build();
27+
}
28+
29+
public function write(string $buffer): void
30+
{
31+
$request = new Request($this->url, 'POST', $buffer);
32+
$request->setHeader('Content-Type', 'application/json');
33+
$request->setTransferTimeout(5);
34+
35+
try {
36+
$this->httpClient->request($request);
37+
$this->inErrorState = false;
38+
} catch (SocketException $e) {
39+
if($this->inErrorState === false) {
40+
\trigger_error($e->getMessage(), E_USER_WARNING);
41+
$this->inErrorState = true;
42+
}
43+
}
44+
}
45+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport;
6+
7+
use Amp\ByteStream\StreamException;
8+
use Amp\ByteStream\WritableStream;
9+
use Amp\Socket\ConnectContext;
10+
use Amp\Socket\ConnectException;
11+
use Amp\Socket\DnsSocketConnector;
12+
use Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\NullWritableStream;
13+
use Revolt\EventLoop;
14+
15+
final class GelfTcpTransport implements GelfTransport
16+
{
17+
private const CONNECT_TIMEOUT = 4;
18+
private const RECONNECT_TIMEOUT = 10;
19+
20+
private WritableStream $socket;
21+
private bool $inErrorState = false;
22+
23+
public function __construct(private readonly string $host, private readonly int $port)
24+
{
25+
}
26+
27+
public function start(): void
28+
{
29+
$connector = new DnsSocketConnector();
30+
$context = (new ConnectContext())->withConnectTimeout(self::CONNECT_TIMEOUT);
31+
32+
try {
33+
$this->socket = $connector->connect(\sprintf('tcp://%s:%d', $this->host, $this->port), $context);
34+
$this->inErrorState = false;
35+
} catch (ConnectException $e) {
36+
$this->socket = new NullWritableStream();
37+
38+
if ($this->inErrorState === false) {
39+
\trigger_error($e->getMessage(), E_USER_WARNING);
40+
$this->inErrorState = true;
41+
}
42+
43+
EventLoop::delay(self::RECONNECT_TIMEOUT, function () {
44+
$this->start();
45+
});
46+
}
47+
}
48+
49+
public function write(string $buffer): void
50+
{
51+
try {
52+
$this->socket->write($buffer . "\0");
53+
} catch (StreamException) {
54+
$this->start();
55+
// try to send second time after connect
56+
try {
57+
$this->socket->write($buffer. "\0");
58+
} catch (StreamException) {
59+
// do nothing
60+
}
61+
}
62+
}
63+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Luzrain\PHPStreamServer\BundledPlugin\Logger\Internal\GelfTransport;
6+
7+
interface GelfTransport
8+
{
9+
public function start(): void;
10+
11+
public function write(string $buffer): void;
12+
}

0 commit comments

Comments
 (0)