From 99c8a28a4975f68cbe752b0bd3c30c2750884491 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 14:56:43 +0000 Subject: [PATCH 1/9] WIP --- composer.json | 14 ++-- src/Handler/CloudWatch.php | 157 ++++++++----------------------------- 2 files changed, 39 insertions(+), 132 deletions(-) diff --git a/composer.json b/composer.json index 6f6f10a..842892a 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ "license": "MIT", "authors": [ { - "name": "Max Leonov", + "name": "Maksym Leonov", "email": "hi@maxleonov.pw", - "homepage": "https://maxleonov.pw" + "homepage": "https://github.com/maxbanton" } ], "support": { @@ -21,13 +21,13 @@ "source": "https://github.com/maxbanton/cwh" }, "require": { - "php": "^7.2 || ^8", - "monolog/monolog": "^2.0 || ^3.0", - "aws/aws-sdk-php": "^3.18" + "php": ">=8.1", + "monolog/monolog": "^3.0", + "aws/aws-sdk-php": "^3.2" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.4", - "squizlabs/php_codesniffer": "^3.5" + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.7" }, "suggest": { "maxbanton/dd": "Minimalistic dump-and-die function for easy debugging" diff --git a/src/Handler/CloudWatch.php b/src/Handler/CloudWatch.php index 26a1271..1aa3084 100755 --- a/src/Handler/CloudWatch.php +++ b/src/Handler/CloudWatch.php @@ -7,139 +7,70 @@ use Monolog\Formatter\LineFormatter; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; +use Monolog\LogRecord; +use Monolog\Level; class CloudWatch extends AbstractProcessingHandler { /** * Requests per second limit (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html) */ - const RPS_LIMIT = 5; + public const RPS_LIMIT = 5; /** * Event size limit (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html) - * - * @var int */ - const EVENT_SIZE_LIMIT = 262118; // 262144 - reserved 26 + public const EVENT_SIZE_LIMIT = 262118; // 262144 - reserved 26 /** * The batch of log events in a single PutLogEvents request cannot span more than 24 hours. - * - * @var int - */ - const TIMESPAN_LIMIT = 86400000; - - /** - * @var CloudWatchLogsClient - */ - private $client; - - /** - * @var string - */ - private $group; - - /** - * @var string - */ - private $stream; - - /** - * @var integer - */ - private $retention; - - /** - * @var bool - */ - private $initialized = false; - - /** - * @var string - */ - private $sequenceToken; - - /** - * @var int */ - private $batchSize; - - /** - * @var array - */ - private $buffer = []; - - /** - * @var array - */ - private $tags = []; - - /** - * @var bool - */ - private $createGroup; + public const TIMESPAN_LIMIT = 86400000; + + private CloudWatchLogsClient $client; + private string $group; + private string $stream; + private int $retention; + private bool $initialized = false; + private string $sequenceToken; + private int $batchSize; + /** @var LogRecord[] $buffer */ + private array $buffer = []; + private array $tags = []; + private bool $createGroup; /** * Data amount limit (http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html) - * - * @var int - */ - private $dataAmountLimit = 1048576; - - /** - * @var int - */ - private $currentDataAmount = 0; - - /** - * @var int - */ - private $remainingRequests = self::RPS_LIMIT; - - /** - * @var \DateTime - */ - private $savedTime; - - /** - * @var int|null */ - private $earliestTimestamp = null; + private int $dataAmountLimit = 1048576; + private int $currentDataAmount = 0; + private int $remainingRequests = self::RPS_LIMIT; + private \DateTime $savedTime; + private int|null $earliestTimestamp = null; /** * CloudWatchLogs constructor. - * @param CloudWatchLogsClient $client * * Log group names must be unique within a region for an AWS account. * Log group names can be between 1 and 512 characters long. * Log group names consist of the following characters: a-z, A-Z, 0-9, '_' (underscore), '-' (hyphen), * '/' (forward slash), and '.' (period). - * @param string $group * * Log stream names must be unique within the log group. * Log stream names can be between 1 and 512 characters long. * The ':' (colon) and '*' (asterisk) characters are not allowed. - * @param string $stream - * - * @param int $retention - * @param int $batchSize - * @param array $tags - * @param int $level - * @param bool $bubble - * @param bool $createGroup - * * @throws \Exception */ public function __construct( CloudWatchLogsClient $client, - $group, - $stream, - $retention = 14, - $batchSize = 10000, + string $group, + string $stream, + int $retention = 14, + int $batchSize = 10000, array $tags = [], - $level = Logger::DEBUG, - $bubble = true, - $createGroup = true + int | string | Level $level = Logger::DEBUG, + bool $bubble = true, + bool $createGroup = true ) { if ($batchSize > 10000) { throw new \InvalidArgumentException('Batch size can not be greater than 10000'); @@ -158,10 +89,7 @@ public function __construct( $this->savedTime = new \DateTime; } - /** - * {@inheritdoc} - */ - protected function write(array $record): void + protected function write(LogRecord $record): void { $records = $this->formatRecords($record); @@ -178,9 +106,6 @@ protected function write(array $record): void } } - /** - * @param array $record - */ private function addToBuffer(array $record): void { $this->currentDataAmount += $this->getMessageSize($record); @@ -240,11 +165,8 @@ private function checkThrottle(): void /** * http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html - * - * @param array $record - * @return int */ - private function getMessageSize($record): int + private function getMessageSize(array $record): int { return strlen($record['message']) + 26; } @@ -252,9 +174,6 @@ private function getMessageSize($record): int /** * Determine whether the specified record's message size in addition to the * size of the current queued messages will exceed AWS CloudWatch's limit. - * - * @param array $record - * @return bool */ protected function willMessageSizeExceedLimit(array $record): bool { @@ -264,9 +183,6 @@ protected function willMessageSizeExceedLimit(array $record): bool /** * Determine whether the specified record's timestamp exceeds the 24 hour timespan limit * for all batched messages written in a single call to PutLogEvents. - * - * @param array $record - * @return bool */ protected function willMessageTimestampExceedLimit(array $record): bool { @@ -276,9 +192,6 @@ protected function willMessageTimestampExceedLimit(array $record): bool /** * Event size in the batch can not be bigger than 256 KB * https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html - * - * @param array $entry - * @return array */ private function formatRecords(array $entry): array { @@ -307,7 +220,7 @@ private function formatRecords(array $entry): array * - The maximum number of log events in a batch is 10,000. * - A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails. * - * @param array $entries + * @param LogRecord[] $entries * * @throws \Aws\CloudWatchLogs\Exception\CloudWatchLogsException Thrown by putLogEvents for example in case of an * invalid sequence token @@ -435,17 +348,11 @@ function ($stream) { $this->initialized = true; } - /** - * {@inheritdoc} - */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter("%channel%: %level_name%: %message% %context% %extra%", null, false, true); } - /** - * {@inheritdoc} - */ public function close(): void { $this->flushBuffer(); From e89e81881dc478be9c7ede76d73de485fd235632 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 14:59:04 +0000 Subject: [PATCH 2/9] Allow build on 8.1 only --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 0798957..9fdab19 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['7.3', '7.4', '8.0'] + php-versions: ['8.1'] name: PHP ${{ matrix.php-versions }} steps: - name: Checkout From 69c4480fefc9870b9b89b17c4696db0fcab992e3 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 15:03:31 +0000 Subject: [PATCH 3/9] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fb7740..d87f953 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Please press **★ Star** button if you find this library useful. This library uses AWS API through AWS PHP SDK, which has limits on concurrent requests. It means that on high concurrent or high load applications it may not work on it's best way. Please consider using another solution such as logging to the stdout and redirecting logs with fluentd. ## Requirements -* PHP ^7.3 +* PHP >=8.1 * AWS account with proper permissions (see list of permissions below) ## Features From a26080f5fc57d6cf454a3cf624e03b74c9464a78 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 15:33:15 +0000 Subject: [PATCH 4/9] Test fixes --- src/Handler/CloudWatch.php | 4 +- tests/Handler/CloudWatchTest.php | 77 +++++++++++++------------------- 2 files changed, 32 insertions(+), 49 deletions(-) diff --git a/src/Handler/CloudWatch.php b/src/Handler/CloudWatch.php index 1aa3084..793a3a2 100755 --- a/src/Handler/CloudWatch.php +++ b/src/Handler/CloudWatch.php @@ -32,7 +32,7 @@ class CloudWatch extends AbstractProcessingHandler private string $stream; private int $retention; private bool $initialized = false; - private string $sequenceToken; + private string|null $sequenceToken; private int $batchSize; /** @var LogRecord[] $buffer */ private array $buffer = []; @@ -193,7 +193,7 @@ protected function willMessageTimestampExceedLimit(array $record): bool * Event size in the batch can not be bigger than 256 KB * https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html */ - private function formatRecords(array $entry): array + private function formatRecords(LogRecord $entry): array { $entries = str_split($entry['formatted'], self::EVENT_SIZE_LIMIT); $timestamp = $entry['datetime']->format('U.u') * 1000; diff --git a/tests/Handler/CloudWatchTest.php b/tests/Handler/CloudWatchTest.php index 985241f..dded928 100644 --- a/tests/Handler/CloudWatchTest.php +++ b/tests/Handler/CloudWatchTest.php @@ -9,31 +9,18 @@ use Maxbanton\Cwh\Handler\CloudWatch; use Monolog\Formatter\LineFormatter; use Monolog\Logger; +use Monolog\LogRecord; +use Monolog\Level; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; class CloudWatchTest extends TestCase { - /** - * @var MockObject | CloudWatchLogsClient - */ - private $clientMock; - - /** - * @var MockObject | Result - */ - private $awsResultMock; - - /** - * @var string - */ - private $groupName = 'group'; - - /** - * @var string - */ - private $streamName = 'stream'; + private MockObject | CloudWatchLogsClient $clientMock; + private MockObject | Result $awsResultMock; + private string $groupName = 'group'; + private string $streamName = 'stream'; protected function setUp(): void { @@ -307,7 +294,7 @@ public function testSendsOnClose() $handler = $this->getCUT(1); - $handler->handle($this->getRecord(Logger::DEBUG)); + $handler->handle($this->getRecord(Level::Debug)); $handler->close(); } @@ -366,7 +353,7 @@ public function testExceptionFromDescribeLogGroups() $this->expectException(CloudWatchLogsException::class); $handler = $this->getCUT(0); - $handler->handle($this->getRecord(Logger::INFO)); + $handler->handle($this->getRecord(Level::Info)); } private function prepareMocks() @@ -430,8 +417,8 @@ public function testSortsEntriesChronologically() $records = []; for ($i = 1; $i <= 4; ++$i) { - $record = $this->getRecord(Logger::INFO, 'record' . $i); - $record['datetime'] = \DateTime::createFromFormat('U', time() + $i); + $dt = \DateTimeImmutable::createFromFormat('U', time() + $i); + $record = $this->getRecord(Level::Info, 'record' . $i, [], $dt); $records[] = $record; } @@ -483,9 +470,8 @@ public function testSendsBatchesSpanning24HoursOrLess() // write 15 log entries spanning 3 days for ($i = 1; $i <= 15; ++$i) { - $record = $this->getRecord(Logger::INFO, 'record' . $i); - $record['datetime'] = \DateTime::createFromFormat('U', time() + $i * 5 * 60 * 60); - + $dt = \DateTimeImmutable::createFromFormat('U', time() + $i * 5 * 60 * 60); + $record = $this->getRecord(Level::Info, 'record' . $i, [], $dt); $handler->handle($record); } @@ -497,23 +483,20 @@ private function getCUT($batchSize = 1000) return new CloudWatch($this->clientMock, $this->groupName, $this->streamName, 14, $batchSize); } - /** - * @param int $level - * @param string $message - * @param array $context - * @return array - */ - private function getRecord($level = Logger::WARNING, $message = 'test', $context = []) + private function getRecord( + Level $level = Level::WARNING, + string $message = 'test', + $context = [], + \DateTimeImmutable $dt = new \DateTimeImmutable() + ): LogRecord { - return [ - 'message' => $message, - 'context' => $context, - 'level' => $level, - 'level_name' => Logger::getLevelName($level), - 'channel' => 'test', - 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))), - 'extra' => [], - ]; + return new LogRecord( + $dt, + 'test', + Level::Debug, + $message, + $context + ); } /** @@ -522,11 +505,11 @@ private function getRecord($level = Logger::WARNING, $message = 'test', $context private function getMultipleRecords() { return [ - $this->getRecord(Logger::DEBUG, 'debug message 1'), - $this->getRecord(Logger::DEBUG, 'debug message 2'), - $this->getRecord(Logger::INFO, 'information'), - $this->getRecord(Logger::WARNING, 'warning'), - $this->getRecord(Logger::ERROR, 'error'), + $this->getRecord(Level::Debug, 'debug message 1'), + $this->getRecord(Level::Debug, 'debug message 2'), + $this->getRecord(Level::Info, 'information'), + $this->getRecord(Level::Warning, 'warning'), + $this->getRecord(Level::Error, 'error'), ]; } } From dc3a2f353444b68b8d57bb9ce3e644b10b582226 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 15:36:59 +0000 Subject: [PATCH 5/9] Add Psalm --- .github/workflows/php.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 9fdab19..209f1a2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -41,6 +41,9 @@ jobs: run: | php -l src/ vendor/bin/phpcs --standard=psr2 --ignore=Tests src/ + + - name: Psalm + uses: docker://vimeo/psalm-github-actions - name: Run tests run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml From 389a5e9e959a7976869a51d1ecb800dbc7c15a8b Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 15:40:28 +0000 Subject: [PATCH 6/9] phpstan --- .github/workflows/php.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 209f1a2..b48fbb2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -42,8 +42,10 @@ jobs: php -l src/ vendor/bin/phpcs --standard=psr2 --ignore=Tests src/ - - name: Psalm - uses: docker://vimeo/psalm-github-actions + - name: phpstan + uses: php-actions/phpstan@v3 + with: + path: src/ - name: Run tests run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml From 8444633f02aaf510a75220383337d814f348b3fe Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 15:41:11 +0000 Subject: [PATCH 7/9] fix --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index b48fbb2..5c5763f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -44,7 +44,7 @@ jobs: - name: phpstan uses: php-actions/phpstan@v3 - with: + with: path: src/ - name: Run tests From 4e62b211820609cf9b9b65a1b1d5342e41560207 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Mon, 7 Nov 2022 15:52:55 +0000 Subject: [PATCH 8/9] Cosmetic --- README.md | 3 ++- src/Handler/CloudWatch.php | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d87f953..e8cfe38 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ $ composer require maxbanton/cwh:^2.0 use Aws\CloudWatchLogs\CloudWatchLogsClient; use Maxbanton\Cwh\Handler\CloudWatch; use Monolog\Logger; +use Monolog\Level; use Monolog\Formatter\JsonFormatter; $sdkParams = [ @@ -67,7 +68,7 @@ $streamName = 'ec2-instance-1'; $retentionDays = 30; // Instantiate handler (tags are optional) -$handler = new CloudWatch($client, $groupName, $streamName, $retentionDays, 10000, ['my-awesome-tag' => 'tag-value']); +$handler = new CloudWatch($client, $groupName, $streamName, $retentionDays, 10000, ['my-awesome-tag' => 'tag-value'], Level::Info); // Optionally set the JsonFormatter to be able to access your log messages in a structured way $handler->setFormatter(new JsonFormatter()); diff --git a/src/Handler/CloudWatch.php b/src/Handler/CloudWatch.php index 793a3a2..669c677 100755 --- a/src/Handler/CloudWatch.php +++ b/src/Handler/CloudWatch.php @@ -32,7 +32,7 @@ class CloudWatch extends AbstractProcessingHandler private string $stream; private int $retention; private bool $initialized = false; - private string|null $sequenceToken; + private string | null $sequenceToken; private int $batchSize; /** @var LogRecord[] $buffer */ private array $buffer = []; @@ -46,7 +46,7 @@ class CloudWatch extends AbstractProcessingHandler private int $currentDataAmount = 0; private int $remainingRequests = self::RPS_LIMIT; private \DateTime $savedTime; - private int|null $earliestTimestamp = null; + private int | null $earliestTimestamp = null; /** * CloudWatchLogs constructor. @@ -68,7 +68,7 @@ public function __construct( int $retention = 14, int $batchSize = 10000, array $tags = [], - int | string | Level $level = Logger::DEBUG, + int | string | Level $level = Level::Debug, bool $bubble = true, bool $createGroup = true ) { From daa70e31e74ea9a6debe3d5d6249ee4a4d53b220 Mon Sep 17 00:00:00 2001 From: Maksym Leonov Date: Wed, 9 Nov 2022 13:37:40 +0000 Subject: [PATCH 9/9] Fixes for compliance with PSR12 --- .github/workflows/php.yml | 7 +------ src/Handler/CloudWatch.php | 2 +- tests/Handler/CloudWatchTest.php | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 5c5763f..c75fd1e 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -40,12 +40,7 @@ jobs: - name: Check syntax run: | php -l src/ - vendor/bin/phpcs --standard=psr2 --ignore=Tests src/ - - - name: phpstan - uses: php-actions/phpstan@v3 - with: - path: src/ + vendor/bin/phpcs --standard=psr12 src/ tests/ - name: Run tests run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml diff --git a/src/Handler/CloudWatch.php b/src/Handler/CloudWatch.php index 669c677..e0b0885 100755 --- a/src/Handler/CloudWatch.php +++ b/src/Handler/CloudWatch.php @@ -86,7 +86,7 @@ public function __construct( parent::__construct($level, $bubble); - $this->savedTime = new \DateTime; + $this->savedTime = new \DateTime(); } protected function write(LogRecord $record): void diff --git a/tests/Handler/CloudWatchTest.php b/tests/Handler/CloudWatchTest.php index dded928..59fa76e 100644 --- a/tests/Handler/CloudWatchTest.php +++ b/tests/Handler/CloudWatchTest.php @@ -2,7 +2,6 @@ namespace Maxbanton\Cwh\Test\Handler; - use Aws\CloudWatchLogs\CloudWatchLogsClient; use Aws\CloudWatchLogs\Exception\CloudWatchLogsException; use Aws\Result; @@ -16,7 +15,6 @@ class CloudWatchTest extends TestCase { - private MockObject | CloudWatchLogsClient $clientMock; private MockObject | Result $awsResultMock; private string $groupName = 'group'; @@ -72,7 +70,17 @@ public function testInitializeWithCreateGroupDisabled() ]) ->willReturn($logStreamResult); - $handler = new CloudWatch($this->clientMock, $this->groupName, $this->streamName, 14, 10000, [], Logger::DEBUG, true, false); + $handler = new CloudWatch( + $this->clientMock, + $this->groupName, + $this->streamName, + 14, + 10000, + [], + Logger::DEBUG, + true, + false + ); $reflection = new \ReflectionClass($handler); $reflectionMethod = $reflection->getMethod('initialize'); @@ -488,8 +496,7 @@ private function getRecord( string $message = 'test', $context = [], \DateTimeImmutable $dt = new \DateTimeImmutable() - ): LogRecord - { + ): LogRecord { return new LogRecord( $dt, 'test',