diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 0798957..c75fd1e 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 @@ -40,7 +40,7 @@ jobs: - name: Check syntax run: | php -l src/ - vendor/bin/phpcs --standard=psr2 --ignore=Tests src/ + vendor/bin/phpcs --standard=psr12 src/ tests/ - name: Run tests run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml diff --git a/README.md b/README.md index 9fb7740..e8cfe38 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 @@ -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/composer.json b/composer.json index a6f4ea8..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", - "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..e0b0885 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 | null $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 = Level::Debug, + bool $bubble = true, + bool $createGroup = true ) { if ($batchSize > 10000) { throw new \InvalidArgumentException('Batch size can not be greater than 10000'); @@ -155,13 +86,10 @@ public function __construct( parent::__construct($level, $bubble); - $this->savedTime = new \DateTime; + $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,11 +192,8 @@ 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 + private function formatRecords(LogRecord $entry): array { $entries = str_split($entry['formatted'], self::EVENT_SIZE_LIMIT); $timestamp = $entry['datetime']->format('U.u') * 1000; @@ -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(); diff --git a/tests/Handler/CloudWatchTest.php b/tests/Handler/CloudWatchTest.php index 985241f..59fa76e 100644 --- a/tests/Handler/CloudWatchTest.php +++ b/tests/Handler/CloudWatchTest.php @@ -2,38 +2,23 @@ namespace Maxbanton\Cwh\Test\Handler; - use Aws\CloudWatchLogs\CloudWatchLogsClient; use Aws\CloudWatchLogs\Exception\CloudWatchLogsException; use Aws\Result; 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 { @@ -85,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'); @@ -307,7 +302,7 @@ public function testSendsOnClose() $handler = $this->getCUT(1); - $handler->handle($this->getRecord(Logger::DEBUG)); + $handler->handle($this->getRecord(Level::Debug)); $handler->close(); } @@ -366,7 +361,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 +425,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 +478,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 +491,19 @@ 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 = []) - { - return [ - 'message' => $message, - 'context' => $context, - 'level' => $level, - 'level_name' => Logger::getLevelName($level), - 'channel' => 'test', - 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))), - 'extra' => [], - ]; + private function getRecord( + Level $level = Level::WARNING, + string $message = 'test', + $context = [], + \DateTimeImmutable $dt = new \DateTimeImmutable() + ): LogRecord { + return new LogRecord( + $dt, + 'test', + Level::Debug, + $message, + $context + ); } /** @@ -522,11 +512,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'), ]; } }