Skip to content

Commit

Permalink
L-2183 Ensure flushed logs in long running scripts (#23)
Browse files Browse the repository at this point in the history
- add options flushIntervalMs (default 5s) and throwExceptions (default false) to LogtailHandler
- change default of bufferLimit from 0 to 1000 and flushOnOverflow from false to true
- add LogtailHandlerBuilder
  • Loading branch information
PetrHeinz authored Jun 27, 2024
1 parent ba7adf5 commit 6436570
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 67 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
push:
branches:
- master
- 2.x

jobs:
build:
Expand Down
6 changes: 3 additions & 3 deletions example-project/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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("<source-token>"));
$logger->pushHandler(LogtailHandlerBuilder::withSourceToken("<source-token>")->build());
```

Don’t forget to change `<source-token>` to your actual token which you can find in the *Basic information* section when clicking on *Edit* on your select source.
Expand All @@ -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("<source-token>"));
$cart_logger->pushHandler(LogtailHandlerBuilder::withSourceToken("<source-token>")->build());

# Logger for payment component
$payment_logger = new Logger("payment");
$payment_logger->pushHandler(new LogtailHandler("<source-token>"));
$payment_logger->pushHandler(LogtailHandlerBuilder::withSourceToken("<source-token>")->build());
```

Then you can filter your logs using the following search formula:
Expand Down
9 changes: 7 additions & 2 deletions example-project/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Setting logger
use Monolog\Logger;
use Logtail\Monolog\LogtailHandler;
use Logtail\Monolog\LogtailHandlerBuilder;

# Check for arguments
if($argc != 2){
Expand All @@ -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.
Expand Down
34 changes: 6 additions & 28 deletions src/Monolog/LogtailClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand All @@ -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();
}

Expand All @@ -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();

Expand Down
88 changes: 67 additions & 21 deletions src/Monolog/LogtailHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
180 changes: 180 additions & 0 deletions src/Monolog/LogtailHandlerBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php declare(strict_types=1);

/*
* This file is part of the logtail/monolog-logtail package.
*
* (c) Better Stack
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Logtail\Monolog;

use Monolog\Level;

final class LogtailHandlerBuilder
{
private string $sourceToken;
private Level $level = Level::Debug;
private bool $bubble = LogtailHandler::DEFAULT_BUBBLE;
private string $endpoint = LogtailClient::URL;
private int $bufferLimit = LogtailHandler::DEFAULT_BUFFER_LIMIT;
private bool $flushOnOverflow = LogtailHandler::DEFAULT_FLUSH_ON_OVERFLOW;
private int $connectionTimeoutMs = LogtailClient::DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS;
private int $timeoutMs = LogtailClient::DEFAULT_TIMEOUT_MILLISECONDS;
private ?int $flushIntervalMs = LogtailHandler::DEFAULT_FLUSH_INTERVAL_MILLISECONDS;
private bool $throwExceptions = SynchronousLogtailHandler::DEFAULT_THROW_EXCEPTION;

/**
* @internal use {@see self::withSourceToken()} instead
*/
private function __construct($sourceToken)
{
$this->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
);
}
}
Loading

0 comments on commit 6436570

Please sign in to comment.