diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 26292ef..da63cc5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,7 @@ on: push: branches: - master + - 2.x jobs: build: diff --git a/example-project/README.md b/example-project/README.md index 7d29e30..0bf3714 100644 --- a/example-project/README.md +++ b/example-project/README.md @@ -51,7 +51,7 @@ Now it’s time to create a `Logger` instance and push a `LogtailHandler` handle ```php $logger = new Logger("logtail-source"); -$logger->pushHandler(new LogtailHandler("")); +$logger->pushHandler(LogtailHandlerBuilder::withSourceToken("")->build()); ``` Don’t forget to change `` to your actual token which you can find in the *Basic information* section when clicking on *Edit* on your select source. @@ -69,11 +69,11 @@ Creating multiple loggers for different channels is fairly easy: ```php # Logger for shopping cart component $cart_logger = new Logger("shoping-cart"); -$cart_logger->pushHandler(new LogtailHandler("")); +$cart_logger->pushHandler(LogtailHandlerBuilder::withSourceToken("")->build()); # Logger for payment component $payment_logger = new Logger("payment"); -$payment_logger->pushHandler(new LogtailHandler("")); +$payment_logger->pushHandler(LogtailHandlerBuilder::withSourceToken("")->build()); ``` Then you can filter your logs using the following search formula: diff --git a/example-project/index.php b/example-project/index.php index 38f8564..6c235d8 100644 --- a/example-project/index.php +++ b/example-project/index.php @@ -7,7 +7,7 @@ # Setting logger use Monolog\Logger; -use Logtail\Monolog\LogtailHandler; +use Logtail\Monolog\LogtailHandlerBuilder; # Check for arguments if($argc != 2){ @@ -17,7 +17,12 @@ } $logger = new Logger("logtail-source"); -$logger->pushHandler(new LogtailHandler($argv[1])); +$handler = LogtailHandlerBuilder::withSourceToken($argv[1]) + ->withBufferLimit(100) + ->withFlushIntervalMilliseconds(500) + ->withExceptionThrowing(true) + ->build(); +$logger->pushHandler($handler); # Below you can see available methods that can be used to send logs to logtail. # Each method corresponds to Monologs log level. diff --git a/src/Monolog/LogtailClient.php b/src/Monolog/LogtailClient.php index 3c0d7a8..61c2f0d 100644 --- a/src/Monolog/LogtailClient.php +++ b/src/Monolog/LogtailClient.php @@ -21,29 +21,10 @@ class LogtailClient const DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS = 5000; const DEFAULT_TIMEOUT_MILLISECONDS = 5000; - /** - * @var string $sourceToken - */ - private $sourceToken; - - /** - * @var string $endpoint - */ - private $endpoint; - - /** - * @var \CurlHandle $handle - */ - private $handle = NULL; - - /** - * @var int $connectionTimeoutMs - */ + private string $sourceToken; + private string $endpoint; + private \CurlHandle $handle; private int $connectionTimeoutMs; - - /** - * @var int $timeoutMs - */ private int $timeoutMs; @@ -63,9 +44,9 @@ public function __construct( $this->timeoutMs = $timeoutMs; } - public function send($data) + public function send($data): void { - if (is_null($this->handle)) { + if (!isset($this->handle)) { $this->initCurlHandle(); } @@ -75,10 +56,7 @@ public function send($data) \Monolog\Handler\Curl\Util::execute($this->handle, 5, false); } - /** - * @return void - */ - private function initCurlHandle() + private function initCurlHandle(): void { $this->handle = \curl_init(); diff --git a/src/Monolog/LogtailHandler.php b/src/Monolog/LogtailHandler.php index a428e16..bc86f4d 100644 --- a/src/Monolog/LogtailHandler.php +++ b/src/Monolog/LogtailHandler.php @@ -13,38 +13,84 @@ use Monolog\Handler\BufferHandler; use Monolog\Level; +use Monolog\LogRecord; /** * Sends buffered logs to Logtail. */ class LogtailHandler extends BufferHandler { + const DEFAULT_BUBBLE = true; + const DEFAULT_BUFFER_LIMIT = 1000; + const DEFAULT_FLUSH_ON_OVERFLOW = true; + const DEFAULT_FLUSH_INTERVAL_MILLISECONDS = 5000; + + private ?int $flushIntervalMs; + private int|float|null $highResolutionTimeOfNextFlush; + /** - * @param string $sourceToken Logtail source token - * @param int|string $level The minimum logging level at which this handler will be triggered - * @param bool $bubble Whether the messages that are handled can bubble up the stack or not - * @param string $endpoint Logtail ingesting endpoint - * @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. - * @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded - * @param int $connectionTimeoutMs The maximum time in milliseconds that you allow the connection phase to the server to take - * @param int $timeoutMs The maximum time in milliseconds that you allow a transfer operation to take + * @param string $sourceToken Logtail source token + * @param int|string|Level $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $endpoint Logtail ingesting endpoint + * @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer + * @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded + * @param int $connectionTimeoutMs The maximum time in milliseconds that you allow the connection phase to the server to take + * @param int $timeoutMs The maximum time in milliseconds that you allow a transfer operation to take + * @param int|null $flushIntervalMs The time in milliseconds after which next log record will trigger flushing all logs. Null to disable + * @param bool $throwExceptions Whether to throw exceptions when sending logs fails */ public function __construct( - $sourceToken, - $level = Level::Debug, - $bubble = true, - $endpoint = LogtailClient::URL, - $bufferLimit = 0, - bool $flushOnOverflow = false, + string $sourceToken, + int|string|Level $level = Level::Debug, + bool $bubble = self::DEFAULT_BUBBLE, + string $endpoint = LogtailClient::URL, + int $bufferLimit = self::DEFAULT_BUFFER_LIMIT, + bool $flushOnOverflow = self::DEFAULT_FLUSH_ON_OVERFLOW, int $connectionTimeoutMs = LogtailClient::DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS, int $timeoutMs = LogtailClient::DEFAULT_TIMEOUT_MILLISECONDS, + ?int $flushIntervalMs = self::DEFAULT_FLUSH_INTERVAL_MILLISECONDS, + bool $throwExceptions = SynchronousLogtailHandler::DEFAULT_THROW_EXCEPTION ) { - parent::__construct( - new SynchronousLogtailHandler($sourceToken, $level, $bubble, $endpoint, $connectionTimeoutMs, $timeoutMs), - $bufferLimit, - $level, - $bubble, - $flushOnOverflow, - ); + parent::__construct(new SynchronousLogtailHandler($sourceToken, $level, $bubble, $endpoint, $connectionTimeoutMs, $timeoutMs, $throwExceptions), $bufferLimit, $level, $bubble, $flushOnOverflow); + $this->flushIntervalMs = $flushIntervalMs; + $this->setHighResolutionTimeOfLastFlush(); + } + + /** + * @inheritDoc + */ + public function handle(LogRecord $record): bool + { + $return = parent::handle($record); + + if ($this->highResolutionTimeOfNextFlush !== null && $this->highResolutionTimeOfNextFlush <= hrtime(true)) { + $this->flush(); + $this->setHighResolutionTimeOfLastFlush(); + } + + return $return; + } + + /** + * @inheritDoc + */ + public function flush(): void + { + parent::flush(); + $this->setHighResolutionTimeOfLastFlush(); + } + + private function setHighResolutionTimeOfLastFlush(): void + { + $currentHighResolutionTime = hrtime(true); + if ($this->flushIntervalMs === null || $currentHighResolutionTime === false) { + $this->highResolutionTimeOfNextFlush = null; + + return; + } + + // hrtime(true) returns nanoseconds, converting flushIntervalMs from milliseconds to nanoseconds + $this->highResolutionTimeOfNextFlush = $currentHighResolutionTime + $this->flushIntervalMs * 1e+6; } } diff --git a/src/Monolog/LogtailHandlerBuilder.php b/src/Monolog/LogtailHandlerBuilder.php new file mode 100644 index 0000000..7acf894 --- /dev/null +++ b/src/Monolog/LogtailHandlerBuilder.php @@ -0,0 +1,180 @@ +sourceToken = $sourceToken; + } + + /** + * Builder for comfortable creation of {@see LogtailHandler}. + * + * @var string $sourceToken Your Better Stack source token. + * @see https://logs.betterstack.com/team/0/sources + * @return self Always returns new immutable instance + */ + public static function withSourceToken(string $sourceToken): self + { + return new self($sourceToken); + } + + /** + * Sets the minimum logging level at which this handler will be triggered. + * + * @param Level $level + * @return self Always returns new immutable instance + */ + public function withLevel(Level $level): self + { + $clone = clone $this; + $clone->level = $level; + + return $clone; + } + + /** + * Sets whether the messages that are handled can bubble up the stack or not. + * + * @param bool $bubble + * @return self Always returns new immutable instance + */ + public function withLogBubbling(bool $bubble): self + { + $clone = clone $this; + $clone->bubble = $bubble; + + return $clone; + } + + /** + * Sets how many entries should be buffered at most, beyond that the oldest items are flushed or removed from the buffer. + * + * @param int $bufferLimit + * @return self Always returns new immutable instance + */ + public function withBufferLimit(int $bufferLimit): self + { + $clone = clone $this; + $clone->bufferLimit = $bufferLimit; + + return $clone; + } + + /** + * Sets whether the buffer is flushed (true) or discarded (false) when the max size has been reached. + * + * @param bool $flushOnOverflow + * @return self Always returns new immutable instance + */ + public function withFlushOnOverflow(bool $flushOnOverflow): self + { + $clone = clone $this; + $clone->flushOnOverflow = $flushOnOverflow; + + return $clone; + } + + /** + * Sets the maximum time in milliseconds that you allow the connection phase to the server to take. + * + * @param int $connectionTimeoutMs + * @return self Always returns new immutable instance + */ + public function withConnectionTimeoutMilliseconds(int $connectionTimeoutMs): self + { + $clone = clone $this; + $clone->connectionTimeoutMs = $connectionTimeoutMs; + + return $clone; + } + + /** + * Sets the maximum time in milliseconds that you allow a transfer operation to take. + * + * @param int $timeoutMs + * @return self Always returns new immutable instance + */ + public function withTimeoutMilliseconds(int $timeoutMs): self + { + $clone = clone $this; + $clone->timeoutMs = $timeoutMs; + + return $clone; + } + + /** + * Set the time in milliseconds after which next log record will trigger flushing all logs. Null to disable. + * + * @param int|null $flushIntervalMs + * @return self Always returns new immutable instance + */ + public function withFlushIntervalMilliseconds(?int $flushIntervalMs): self + { + $clone = clone $this; + $clone->flushIntervalMs = $flushIntervalMs; + + return $clone; + } + + /** + * Sets whether to throw exceptions when sending logs fails. + * + * @param bool $throwExceptions + * @return self Always returns new immutable instance + */ + public function withExceptionThrowing(bool $throwExceptions): self + { + $clone = clone $this; + $clone->throwExceptions = $throwExceptions; + + return $clone; + } + + /** + * Builds the {@see LogtailHandler} instance based on the setting. + * + * @return LogtailHandler + */ + public function build(): LogtailHandler + { + return new LogtailHandler( + $this->sourceToken, + $this->level, + $this->bubble, + $this->endpoint, + $this->bufferLimit, + $this->flushOnOverflow, + $this->connectionTimeoutMs, + $this->timeoutMs, + $this->flushIntervalMs + ); + } +} diff --git a/src/Monolog/SynchronousLogtailHandler.php b/src/Monolog/SynchronousLogtailHandler.php index b6d33e9..612fe03 100644 --- a/src/Monolog/SynchronousLogtailHandler.php +++ b/src/Monolog/SynchronousLogtailHandler.php @@ -23,31 +23,35 @@ /** * Sends log to Logtail. */ -class SynchronousLogtailHandler extends AbstractProcessingHandler { - /** - * @var LogtailClient $client - */ - private $client; +class SynchronousLogtailHandler extends AbstractProcessingHandler +{ + const DEFAULT_THROW_EXCEPTION = false; + + private LogtailClient $client; + private bool $throwExceptions; /** * @param string $sourceToken - * @param int $level + * @param int|string|Level $level * @param bool $bubble * @param string $endpoint * @param int $connectionTimeoutMs * @param int $timeoutMs + * @param bool throwExceptions */ public function __construct( - $sourceToken, - $level = Level::Debug, - bool $bubble = true, + string $sourceToken, + int|string|Level $level = Level::Debug, + bool $bubble = LogtailHandler::DEFAULT_BUBBLE, string $endpoint = LogtailClient::URL, int $connectionTimeoutMs = LogtailClient::DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS, int $timeoutMs = LogtailClient::DEFAULT_TIMEOUT_MILLISECONDS, + bool $throwExceptions = self::DEFAULT_THROW_EXCEPTION ) { parent::__construct($level, $bubble); $this->client = new LogtailClient($sourceToken, $endpoint, $connectionTimeoutMs, $timeoutMs); + $this->throwExceptions = $throwExceptions; $this->pushProcessor(new IntrospectionProcessor($level, ['Logtail\\'])); $this->pushProcessor(new WebProcessor); @@ -59,7 +63,15 @@ public function __construct( * @param LogRecord $record */ protected function write(LogRecord $record): void { - $this->client->send($record->formatted); + try { + $this->client->send($record->formatted); + } catch (Throwable $throwable) { + if ($this->throwExceptions) { + throw $throwable; + } else { + trigger_error("Failed to send a single log record to Better Stack because of " . $throwable, E_USER_WARNING); + } + } } /** @@ -69,7 +81,15 @@ protected function write(LogRecord $record): void { public function handleBatch(array $records): void { $formattedRecords = $this->getFormatter()->formatBatch($records); - $this->client->send($formattedRecords); + try { + $this->client->send($formattedRecords); + } catch (\Throwable $throwable) { + if ($this->throwExceptions) { + throw $throwable; + } else { + trigger_error("Failed to send " . count($records) . " log records to Better Stack because of " . $throwable, E_USER_WARNING); + } + } } /** diff --git a/tests/Monolog/LogtailHandlerTest.php b/tests/Monolog/LogtailHandlerTest.php index 55d8d2d..1b8f96c 100644 --- a/tests/Monolog/LogtailHandlerTest.php +++ b/tests/Monolog/LogtailHandlerTest.php @@ -5,10 +5,16 @@ use Monolog\Formatter\LineFormatter; use Monolog\Handler\BufferHandler; -class MockLogtailClient { +class MockLogtailClient extends LogtailClient { public $capturedData = NULL; - public function send($data) { + public function __construct() + { + parent::__construct("test-source-token"); + } + + public function send($data): void + { $this->capturedData = $data; } }