Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use craft\helpers\App;
use craft\log\MonologTarget;
use craft\queue\Queue as CraftQueue;
use Monolog\Handler\FormattableHandlerInterface;
use yii\caching\ArrayCache;
use yii\redis\Cache as RedisCache;

Expand Down Expand Up @@ -161,9 +162,17 @@ private function getDefinitions(): array
return [
Temp::class => TmpFs::class,
MonologTarget::class => function($container, $params, $config) {
return new MonologTarget([
$target = new MonologTarget([
'logContext' => false,
] + $config);

foreach ($target->getLogger()->getHandlers() as $handler) {
if ($handler instanceof FormattableHandlerInterface) {
$handler->setFormatter(new TruncatingFormatter($handler->getFormatter()));
}
}

return $target;
},

/**
Expand Down
49 changes: 49 additions & 0 deletions src/TruncatingFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace craft\cloud;

use Monolog\Formatter\FormatterInterface;

class TruncatingFormatter implements FormatterInterface
{
/**
* Caps records at 240 KiB, leaving 10 KiB below Bref's 256000-byte FPM limit.
*
* @see https://github.com/brefphp/aws-lambda-layers/blob/main/src/php-fpm.conf Bref's FPM configuration
* @see https://www.php.net/manual/en/install.fpm.configuration.php#log-limit PHP's log_limit
* @see https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html#runtimes-logs-api-buffering Lambda Logs API buffering
* @see https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_InputLogEvent.html CloudWatch InputLogEvent limits
*/
private const MAX_BYTES = 240 * 1024;
private const SUFFIX = '... [truncated]';

public function __construct(private readonly FormatterInterface $formatter)
{
}

public function format($record)
Comment thread
timkelty marked this conversation as resolved.
{
return $this->truncate($this->formatter->format($record));
}

public function formatBatch(array $records)
{
return $this->truncate($this->formatter->formatBatch($records));
}
Comment thread
timkelty marked this conversation as resolved.

private function truncate(mixed $formatted): mixed
{
if (is_array($formatted)) {
return array_map($this->truncate(...), $formatted);
}

if (!is_string($formatted) || strlen($formatted) <= self::MAX_BYTES) {
return $formatted;
}

$lineEnding = preg_match('/\R\z/', $formatted, $matches) ? $matches[0] : '';
$bytes = self::MAX_BYTES - strlen(self::SUFFIX . $lineEnding);

return mb_strcut($formatted, 0, $bytes, 'UTF-8') . self::SUFFIX . $lineEnding;
}
}
38 changes: 38 additions & 0 deletions tests/unit/TruncatingFormatterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace craft\cloud\tests\unit;

use Codeception\Test\Unit;
use craft\cloud\TruncatingFormatter;
use Monolog\Formatter\FormatterInterface;

class TruncatingFormatterTest extends Unit
{
public function testCapsFormattedStrings(): void
{
$formatter = new class implements FormatterInterface {
public string $formatted = '';

public function format($record): string
{
return $this->formatted;
}

public function formatBatch(array $records): string
{
return $this->formatted;
}
};
$truncatingFormatter = new TruncatingFormatter($formatter);

$formatter->formatted = "short\r\n";
$this->assertSame($formatter->formatted, $truncatingFormatter->format(null));

$formatter->formatted = str_repeat('é', 240 * 1024) . "\r\n";
$formatted = $truncatingFormatter->format(null);

$this->assertLessThanOrEqual(240 * 1024, strlen($formatted));
$this->assertTrue(mb_check_encoding($formatted, 'UTF-8'));
$this->assertStringEndsWith("... [truncated]\r\n", $formatted);
}
}
Loading