diff --git a/composer.json b/composer.json index a6f4ea8..f44a80d 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "maxbanton/cwh", + "name": "vnrag/px-cwh", "homepage": "https://github.com/maxbanton/cwh", "type": "library", "description": "AWS CloudWatch Handler for Monolog library", @@ -22,11 +22,11 @@ }, "require": { "php": "^7.2 || ^8", - "monolog/monolog": "^2.0", + "monolog/monolog": "^3.3.1", "aws/aws-sdk-php": "^3.18" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.4", + "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.5" }, "suggest": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 48b930d..3f899ec 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,16 @@ - + stopOnFailure="false" + failOnDeprecation="false"> + src - + ./tests/ diff --git a/src/Handler/CloudWatch.php b/src/Handler/CloudWatch.php index 26a1271..a23a98d 100755 --- a/src/Handler/CloudWatch.php +++ b/src/Handler/CloudWatch.php @@ -6,7 +6,8 @@ use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Monolog\Handler\AbstractProcessingHandler; -use Monolog\Logger; +use Monolog\Level; +use Monolog\LogRecord; class CloudWatch extends AbstractProcessingHandler { @@ -74,10 +75,6 @@ class CloudWatch extends AbstractProcessingHandler */ private $tags = []; - /** - * @var bool - */ - private $createGroup; /** * Data amount limit (http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html) @@ -124,22 +121,20 @@ class CloudWatch extends AbstractProcessingHandler * @param int $retention * @param int $batchSize * @param array $tags - * @param int $level + * @param Level $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 + Level $level = Level::Debug, + bool $bubble = true ) { if ($batchSize > 10000) { throw new \InvalidArgumentException('Batch size can not be greater than 10000'); @@ -151,7 +146,6 @@ public function __construct( $this->retention = $retention; $this->batchSize = $batchSize; $this->tags = $tags; - $this->createGroup = $createGroup; parent::__construct($level, $bubble); @@ -161,7 +155,7 @@ public function __construct( /** * {@inheritdoc} */ - protected function write(array $record): void + protected function write(LogRecord $record): void { $records = $this->formatRecords($record); @@ -277,13 +271,13 @@ 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 + * @param LogRecord $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; + $entries = str_split($entry->formatted, self::EVENT_SIZE_LIMIT); + $timestamp = $entry->datetime->format('U.u') * 1000; $records = []; foreach ($entries as $entry) { @@ -324,7 +318,6 @@ private function send(array $entries): void return 0; }); - $data = [ 'logGroupName' => $this->group, 'logStreamName' => $this->stream, @@ -342,54 +335,8 @@ private function send(array $entries): void $this->sequenceToken = $response->get('nextSequenceToken'); } - private function initializeGroup(): void - { - // fetch existing groups - $existingGroups = - $this - ->client - ->describeLogGroups(['logGroupNamePrefix' => $this->group]) - ->get('logGroups'); - - // extract existing groups names - $existingGroupsNames = array_map( - function ($group) { - return $group['logGroupName']; - }, - $existingGroups - ); - - // create group and set retention policy if not created yet - if (!in_array($this->group, $existingGroupsNames, true)) { - $createLogGroupArguments = ['logGroupName' => $this->group]; - - if (!empty($this->tags)) { - $createLogGroupArguments['tags'] = $this->tags; - } - - $this - ->client - ->createLogGroup($createLogGroupArguments); - - if ($this->retention !== null) { - $this - ->client - ->putRetentionPolicy( - [ - 'logGroupName' => $this->group, - 'retentionInDays' => $this->retention, - ] - ); - } - } - } - private function initialize(): void { - if ($this->createGroup) { - $this->initializeGroup(); - } - $this->refreshSequenceToken(); } diff --git a/tests/Handler/CloudWatchTest.php b/tests/Handler/CloudWatchTest.php index 985241f..de6f93f 100644 --- a/tests/Handler/CloudWatchTest.php +++ b/tests/Handler/CloudWatchTest.php @@ -8,7 +8,8 @@ use Aws\Result; use Maxbanton\Cwh\Handler\CloudWatch; use Monolog\Formatter\LineFormatter; -use Monolog\Logger; +use Monolog\Level; +use Monolog\LogRecord; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; @@ -42,30 +43,17 @@ protected function setUp(): void ->getMockBuilder(CloudWatchLogsClient::class) ->addMethods( [ - 'describeLogGroups', - 'CreateLogGroup', - 'PutRetentionPolicy', - 'DescribeLogStreams', - 'CreateLogStream', - 'PutLogEvents' + 'describeLogStreams', + 'createLogStream', + 'putLogEvents' ] ) ->disableOriginalConstructor() ->getMock(); } - public function testInitializeWithCreateGroupDisabled() + public function testInitializeWithExistingStream() { - $this - ->clientMock - ->expects($this->never()) - ->method('describeLogGroups'); - - $this - ->clientMock - ->expects($this->never()) - ->method('createLogGroup'); - $logStreamResult = new Result([ 'logStreams' => [ [ @@ -85,43 +73,10 @@ public function testInitializeWithCreateGroupDisabled() ]) ->willReturn($logStreamResult); - $handler = new CloudWatch($this->clientMock, $this->groupName, $this->streamName, 14, 10000, [], Logger::DEBUG, true, false); - - $reflection = new \ReflectionClass($handler); - $reflectionMethod = $reflection->getMethod('initialize'); - $reflectionMethod->setAccessible(true); - $reflectionMethod->invoke($handler); - } - - public function testInitializeWithExistingLogGroup() - { - $logGroupsResult = new Result(['logGroups' => [['logGroupName' => $this->groupName]]]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogGroups') - ->with(['logGroupNamePrefix' => $this->groupName]) - ->willReturn($logGroupsResult); - - $logStreamResult = new Result([ - 'logStreams' => [ - [ - 'logStreamName' => $this->streamName, - 'uploadSequenceToken' => '49559307804604887372466686181995921714853186581450198322' - ] - ] - ]); - $this ->clientMock - ->expects($this->once()) - ->method('describeLogStreams') - ->with([ - 'logGroupName' => $this->groupName, - 'logStreamNamePrefix' => $this->streamName, - ]) - ->willReturn($logStreamResult); + ->expects($this->never()) + ->method('createLogStream'); $handler = $this->getCUT(); @@ -131,128 +86,8 @@ public function testInitializeWithExistingLogGroup() $reflectionMethod->invoke($handler); } - public function testInitializeWithTags() - { - $tags = [ - 'applicationName' => 'dummyApplicationName', - 'applicationEnvironment' => 'dummyApplicationEnvironment' - ]; - - $logGroupsResult = new Result(['logGroups' => [['logGroupName' => $this->groupName . 'foo']]]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogGroups') - ->with(['logGroupNamePrefix' => $this->groupName]) - ->willReturn($logGroupsResult); - - $this - ->clientMock - ->expects($this->once()) - ->method('createLogGroup') - ->with([ - 'logGroupName' => $this->groupName, - 'tags' => $tags - ]); - - $logStreamResult = new Result([ - 'logStreams' => [ - [ - 'logStreamName' => $this->streamName, - 'uploadSequenceToken' => '49559307804604887372466686181995921714853186581450198322' - ] - ] - ]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogStreams') - ->with([ - 'logGroupName' => $this->groupName, - 'logStreamNamePrefix' => $this->streamName, - ]) - ->willReturn($logStreamResult); - - $handler = new CloudWatch($this->clientMock, $this->groupName, $this->streamName, 14, 10000, $tags); - - $reflection = new \ReflectionClass($handler); - $reflectionMethod = $reflection->getMethod('initialize'); - $reflectionMethod->setAccessible(true); - $reflectionMethod->invoke($handler); - } - - public function testInitializeWithEmptyTags() + public function testInitializeWithMissingStream() { - $logGroupsResult = new Result(['logGroups' => [['logGroupName' => $this->groupName . 'foo']]]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogGroups') - ->with(['logGroupNamePrefix' => $this->groupName]) - ->willReturn($logGroupsResult); - - $this - ->clientMock - ->expects($this->once()) - ->method('createLogGroup') - ->with(['logGroupName' => $this->groupName]); //The empty array of tags is not handed over - - $logStreamResult = new Result([ - 'logStreams' => [ - [ - 'logStreamName' => $this->streamName, - 'uploadSequenceToken' => '49559307804604887372466686181995921714853186581450198322' - ] - ] - ]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogStreams') - ->with([ - 'logGroupName' => $this->groupName, - 'logStreamNamePrefix' => $this->streamName, - ]) - ->willReturn($logStreamResult); - - $handler = new CloudWatch($this->clientMock, $this->groupName, $this->streamName); - - $reflection = new \ReflectionClass($handler); - $reflectionMethod = $reflection->getMethod('initialize'); - $reflectionMethod->setAccessible(true); - $reflectionMethod->invoke($handler); - } - - public function testInitializeWithMissingGroupAndStream() - { - $logGroupsResult = new Result(['logGroups' => [['logGroupName' => $this->groupName . 'foo']]]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogGroups') - ->with(['logGroupNamePrefix' => $this->groupName]) - ->willReturn($logGroupsResult); - - $this - ->clientMock - ->expects($this->once()) - ->method('createLogGroup') - ->with(['logGroupName' => $this->groupName]); - - $this - ->clientMock - ->expects($this->once()) - ->method('putRetentionPolicy') - ->with([ - 'logGroupName' => $this->groupName, - 'retentionInDays' => 14, - ]); - $logStreamResult = new Result([ 'logStreams' => [ [ @@ -302,12 +137,12 @@ public function testSendsOnClose() $this ->clientMock ->expects($this->once()) - ->method('PutLogEvents') + ->method('putLogEvents') ->willReturn($this->awsResultMock); $handler = $this->getCUT(1); - $handler->handle($this->getRecord(Logger::DEBUG)); + $handler->handle($this->getRecord(Level::Debug)); $handler->close(); } @@ -319,7 +154,7 @@ public function testSendsBatches() $this ->clientMock ->expects($this->exactly(2)) - ->method('PutLogEvents') + ->method('putLogEvents') ->willReturn($this->awsResultMock); $handler = $this->getCUT(3); @@ -342,44 +177,8 @@ public function testFormatter() $this->assertEquals($expected, $formatter); } - public function testExceptionFromDescribeLogGroups() - { - // e.g. 'User is not authorized to perform logs:DescribeLogGroups' - /** @var CloudWatchLogsException */ - $awsException = $this->getMockBuilder(CloudWatchLogsException::class) - ->disableOriginalConstructor() - ->getMock(); - - // if this fails ... - $this - ->clientMock - ->expects($this->atLeastOnce()) - ->method('describeLogGroups') - ->will($this->throwException($awsException)); - - // ... this should not be called: - $this - ->clientMock - ->expects($this->never()) - ->method('describeLogStreams'); - - $this->expectException(CloudWatchLogsException::class); - - $handler = $this->getCUT(0); - $handler->handle($this->getRecord(Logger::INFO)); - } - private function prepareMocks() { - $logGroupsResult = new Result(['logGroups' => [['logGroupName' => $this->groupName]]]); - - $this - ->clientMock - ->expects($this->once()) - ->method('describeLogGroups') - ->with(['logGroupNamePrefix' => $this->groupName]) - ->willReturn($logGroupsResult); - $logStreamResult = new Result([ 'logStreams' => [ [ @@ -414,7 +213,7 @@ public function testSortsEntriesChronologically() $this ->clientMock ->expects($this->once()) - ->method('PutLogEvents') + ->method('putLogEvents') ->willReturnCallback(function (array $data) { $this->assertStringContainsString('record1', $data['logEvents'][0]['message']); $this->assertStringContainsString('record2', $data['logEvents'][1]['message']); @@ -430,8 +229,15 @@ public function testSortsEntriesChronologically() $records = []; for ($i = 1; $i <= 4; ++$i) { - $record = $this->getRecord(Logger::INFO, 'record' . $i); - $record['datetime'] = \DateTime::createFromFormat('U', time() + $i); + $datetime = \DateTimeImmutable::createFromFormat('U', (string)(time() + $i)); + $record = new LogRecord( + $datetime, + 'test', + Level::Info, + 'record' . $i, + [], + [] + ); $records[] = $record; } @@ -451,7 +257,7 @@ public function testSendsBatchesSpanning24HoursOrLess() $this ->clientMock ->expects($this->exactly(3)) - ->method('PutLogEvents') + ->method('putLogEvents') ->willReturnCallback(function (array $data) { /** @var int|null */ $earliestTime = null; @@ -483,8 +289,15 @@ 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); + $datetime = \DateTimeImmutable::createFromFormat('U', (string)(time() + $i * 5 * 60 * 60)); + $record = new LogRecord( + $datetime, + 'test', + Level::Info, + 'record' . $i, + [], + [] + ); $handler->handle($record); } @@ -498,35 +311,34 @@ private function getCUT($batchSize = 1000) } /** - * @param int $level + * @param Level $level * @param string $message * @param array $context - * @return array + * @return LogRecord */ - private function getRecord($level = Logger::WARNING, $message = 'test', $context = []) + private function getRecord(Level $level = Level::Warning, string $message = 'test', array $context = []): 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( + \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true))), + 'test', + $level, + $message, + $context, + [] + ); } /** - * @return array + * @return LogRecord[] */ - private function getMultipleRecords() + private function getMultipleRecords(): array { 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'), ]; } }