Files
monolog-handlers/tests/Handler/CloudWatchTest.php
2024-09-30 20:56:51 -04:00

540 lines
16 KiB
PHP

<?php
namespace Siteworx\MonologHandlers\Test\Handler;
use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Aws\CloudWatchLogs\Exception\CloudWatchLogsException;
use Aws\Result;
use Monolog\Level;
use Monolog\LogRecord;
use Siteworx\MonologHandlers\Handler\CloudWatch;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
class CloudWatchTest extends TestCase
{
/**
* @var MockObject | CloudWatchLogsClient
*/
private $clientMock;
/**
* @var MockObject | Result
*/
private MockObject | Result $awsResultMock;
/**
* @var string
*/
private string $groupName = 'group';
/**
* @var string
*/
private string $streamName = 'stream';
protected function setUp(): void
{
$this->clientMock =
$this
->getMockBuilder(CloudWatchLogsClient::class)
->addMethods(
[
'describeLogGroups',
'CreateLogGroup',
'PutRetentionPolicy',
'DescribeLogStreams',
'CreateLogStream',
'PutLogEvents'
]
)
->disableOriginalConstructor()
->getMock();
}
/**
* @throws \ReflectionException
*/
public function testInitializeWithCreateGroupDisabled()
{
$this
->clientMock
->expects($this->never())
->method('describeLogGroups');
$this
->clientMock
->expects($this->never())
->method('createLogGroup');
$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, [], Level::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);
$handler = $this->getCUT();
$reflection = new \ReflectionClass($handler);
$reflectionMethod = $reflection->getMethod('initialize');
$reflectionMethod->setAccessible(true);
$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()
{
$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' => [
[
'logStreamName' => $this->streamName . 'bar',
'uploadSequenceToken' => '49559307804604887372466686181995921714853186581450198324'
]
]
]);
$this
->clientMock
->expects($this->once())
->method('describeLogStreams')
->with([
'logGroupName' => $this->groupName,
'logStreamNamePrefix' => $this->streamName,
])
->willReturn($logStreamResult);
$this
->clientMock
->expects($this->once())
->method('createLogStream')
->with([
'logGroupName' => $this->groupName,
'logStreamName' => $this->streamName
]);
$handler = $this->getCUT();
$reflection = new \ReflectionClass($handler);
$reflectionMethod = $reflection->getMethod('initialize');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invoke($handler);
}
public function testLimitExceeded()
{
$this->expectException(\InvalidArgumentException::class);
(new CloudWatch($this->clientMock, 'a', 'b', 14, 10001));
}
public function testSendsOnClose()
{
$this->prepareMocks();
$this
->clientMock
->expects($this->once())
->method('PutLogEvents')
->willReturn($this->awsResultMock);
$handler = $this->getCUT(1);
$handler->handle($this->getRecord(Level::Debug));
$handler->close();
}
public function testSendsBatches()
{
$this->prepareMocks();
$this
->clientMock
->expects($this->exactly(2))
->method('PutLogEvents')
->willReturn($this->awsResultMock);
$handler = $this->getCUT(3);
foreach ($this->getMultipleRecords() as $record) {
$handler->handle($record);
}
$handler->close();
}
public function testFormatter()
{
$handler = $this->getCUT();
$formatter = $handler->getFormatter();
$expected = new LineFormatter("%channel%: %level_name%: %message% %context% %extra%", null, false, true);
$this->assertEquals($expected, $formatter);
}
public function testExceptionFromDescribeLogGroups()
{
// e.g. 'User is not authorized to perform logs:DescribeLogGroups'
$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(Level::Info));
}
private function prepareMocks(): void
{
$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);
$this->awsResultMock =
$this
->getMockBuilder(Result::class)
->onlyMethods(['get'])
->disableOriginalConstructor()
->getMock();
}
public function testSortsEntriesChronologically()
{
$this->prepareMocks();
$this
->clientMock
->expects($this->once())
->method('PutLogEvents')
->willReturnCallback(function (array $data) {
$this->assertStringContainsString('record1', $data['logEvents'][0]['message']);
$this->assertStringContainsString('record2', $data['logEvents'][1]['message']);
$this->assertStringContainsString('record3', $data['logEvents'][2]['message']);
$this->assertStringContainsString('record4', $data['logEvents'][3]['message']);
return $this->awsResultMock;
});
$handler = $this->getCUT(4);
// created with chronological timestamps:
$records = [];
for ($i = 1; $i <= 4; ++$i) {
$dateTime = \DateTimeImmutable::createFromFormat('U', time() + $i);
if (!$dateTime) {
$dateTime = new \DateTimeImmutable();
}
$record = $this->getRecord(Level::Info, 'record' . $i, $dateTime);
$records[] = $record;
}
// but submitted in a different order:
$handler->handle($records[2]);
$handler->handle($records[0]);
$handler->handle($records[3]);
$handler->handle($records[1]);
$handler->close();
}
public function testSendsBatchesSpanning24HoursOrLess()
{
$this->prepareMocks();
$this
->clientMock
->expects($this->exactly(3))
->method('PutLogEvents')
->willReturnCallback(function (array $data) {
/** @var int|null */
$earliestTime = null;
/** @var int|null */
$latestTime = null;
/** @var LogRecord $logEvent */
foreach ($data['logEvents'] as $logEvent) {
$logTimestamp = $logEvent->datetime->getTimestamp();
if (!$earliestTime || $logTimestamp < $earliestTime) {
$earliestTime = $logTimestamp;
}
if (!$latestTime || $logTimestamp > $latestTime) {
$latestTime = $logTimestamp;
}
}
$this->assertNotNull($earliestTime);
$this->assertNotNull($latestTime);
$this->assertGreaterThanOrEqual($earliestTime, $latestTime);
$this->assertLessThanOrEqual(24 * 60 * 60 * 1000, $latestTime - $earliestTime);
return $this->awsResultMock;
});
$handler = $this->getCUT();
// write 15 log entries spanning 3 days
for ($i = 1; $i <= 15; ++$i) {
$dateTime = \DateTimeImmutable::createFromMutable(\DateTime::createFromFormat('U', time() + $i * 5 * 60 * 60));
$this->assertNotFalse($dateTime);
$record = $this->getRecord(Level::Info, 'record' . $i, $dateTime);
$handler->handle($record);
}
$handler->close();
}
/**
* @throws \Exception
*/
private function getCUT($batchSize = 1000): CloudWatch
{
return new CloudWatch($this->clientMock, $this->groupName, $this->streamName, 14, $batchSize);
}
/**
* @param \Monolog\Level $level
* @param string $message
* @param \DateTimeImmutable|null $dateTimeImmutable
* @return LogRecord
*/
private function getRecord(Level $level = Level::Warning, string $message = 'test', \DateTimeImmutable $dateTimeImmutable = null): LogRecord
{
if ($dateTimeImmutable === null) {
$dateTimeImmutable = new \DateTimeImmutable();
}
return new LogRecord($dateTimeImmutable, $this->groupName, $level, $message, []);
}
/**
* @return array
*/
private function getMultipleRecords(): array
{
return [
$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'),
];
}
}