diff --git a/composer.json b/composer.json index 41a1d0532..33428c5fa 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.4.5", "psr/http-message": "^1.1||^2.0", - "psr/cache": "^2.0||^3.0" + "psr/cache": "^2.0||^3.0", + "psr/log": "^3.0" }, "require-dev": { "guzzlehttp/promises": "^2.0", diff --git a/src/ApplicationDefaultCredentials.php b/src/ApplicationDefaultCredentials.php index 80437c8c9..5474effc5 100644 --- a/src/ApplicationDefaultCredentials.php +++ b/src/ApplicationDefaultCredentials.php @@ -23,12 +23,15 @@ use Google\Auth\Credentials\ServiceAccountCredentials; use Google\Auth\HttpHandler\HttpClientCache; use Google\Auth\HttpHandler\HttpHandlerFactory; +use Google\Auth\Logging\StdOutLogger; use Google\Auth\Middleware\AuthTokenMiddleware; use Google\Auth\Middleware\ProxyAuthTokenMiddleware; use Google\Auth\Subscriber\AuthTokenSubscriber; use GuzzleHttp\Client; use InvalidArgumentException; +use PHPUnit\TextUI\XmlConfiguration\Logging\Logging; use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; /** * ApplicationDefaultCredentials obtains the default credentials for @@ -69,6 +72,8 @@ */ class ApplicationDefaultCredentials { + private const SDK_DEBUG_FLAG = 'GOOGLE_SDK_DEBUG_LOGGING'; + /** * @deprecated * @@ -145,7 +150,8 @@ public static function getMiddleware( * user-defined scopes exist, expressed either as an Array or as a * space-delimited string. * @param string $universeDomain Specifies a universe domain to use for the - * calling client library + * calling client library. + * @param null|false|LoggerInterface $logger A PSR3 compliant LoggerInterface. * * @return FetchAuthTokenInterface * @throws DomainException if no implementation can be obtained. @@ -157,7 +163,8 @@ public static function getCredentials( CacheItemPoolInterface $cache = null, $quotaProject = null, $defaultScope = null, - string $universeDomain = null + string $universeDomain = null, + null|false|LoggerInterface $logger = null, ) { $creds = null; $jsonKey = CredentialsLoader::fromEnv() @@ -170,7 +177,7 @@ public static function getCredentials( HttpClientCache::setHttpClient($client); } - $httpHandler = HttpHandlerFactory::build($client); + $httpHandler = HttpHandlerFactory::build($client, $logger); } if (is_null($quotaProject)) { @@ -321,6 +328,40 @@ public static function getIdTokenCredentials( return $creds; } + /** + * Returns a StdOutLogger instance + * + * @return null|LoggerInterface + */ + public static function getDefaultLogger(): null|LoggerInterface + { + $loggingFlag = getenv(self::SDK_DEBUG_FLAG); + + // Env var is not set + if (!is_string($loggingFlag)) { + if (is_array($loggingFlag)) { + trigger_error('The ' . self::SDK_DEBUG_FLAG . ' is set, but it is set to another value than false, true, 0 or 1. Logging is disabled'); + return null; + } + + return null; + } + + $loggingFlag = strtolower($loggingFlag); + + // Env Var is not true + if ($loggingFlag !== 'true' && $loggingFlag !== '1') { + // Env var is set to a non valid value + if ($loggingFlag !== 'false' && $loggingFlag !== '0') { + trigger_error('The ' . self::SDK_DEBUG_FLAG . ' is set, but it is set to another value than false, true, 0 or 1. Logging is disabled'); + } + + return null; + } + + return new StdOutLogger(); + } + /** * @return string */ diff --git a/src/HttpHandler/Guzzle6HttpHandler.php b/src/HttpHandler/Guzzle6HttpHandler.php index 53a8865fd..644d94b08 100644 --- a/src/HttpHandler/Guzzle6HttpHandler.php +++ b/src/HttpHandler/Guzzle6HttpHandler.php @@ -16,23 +16,35 @@ */ namespace Google\Auth\HttpHandler; +use Google\Auth\Logging\LogEvent; +use Google\Auth\Logging\LoggingTrait; use GuzzleHttp\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Log\LoggerInterface; class Guzzle6HttpHandler { + use LoggingTrait; + /** * @var ClientInterface */ private $client; + /** + * @var null|LoggerInterface + */ + private $logger; + /** * @param ClientInterface $client + * @param null|LoggerInterface $logger */ - public function __construct(ClientInterface $client) + public function __construct(ClientInterface $client, LoggerInterface $logger = null) { $this->client = $client; + $this->logger = $logger; } /** @@ -44,7 +56,38 @@ public function __construct(ClientInterface $client) */ public function __invoke(RequestInterface $request, array $options = []) { - return $this->client->send($request, $options); + $requestEvent = null; + + if ($this->logger) { + $requestEvent = new LogEvent(); + + $requestEvent->method = $request->getMethod(); + $requestEvent->url = $request->getUri()->__toString(); + $requestEvent->headers = $request->getHeaders(); + $requestEvent->payload = $request->getBody()->getContents(); + $requestEvent->retryAttempt = $options['retryAttempt'] ?? null; + $requestEvent->serviceName = $options['serviceName'] ?? null; + $requestEvent->clientId = spl_object_id($this->client); + $requestEvent->requestId = spl_object_id($request); + + $this->logRequest($requestEvent); + } + + $response = $this->client->send($request, $options); + + if ($this->logger) { + $responseEvent = new LogEvent($requestEvent->timestamp); + + $responseEvent->headers = $response->getHeaders(); + $responseEvent->payload = $response->getBody()->getContents(); + $responseEvent->status = $response->getStatusCode(); + $responseEvent->clientId = $requestEvent->clientId; + $responseEvent->requestId = $requestEvent->requestId; + + $this->logResponse($responseEvent); + } + + return $response; } /** @@ -57,6 +100,41 @@ public function __invoke(RequestInterface $request, array $options = []) */ public function async(RequestInterface $request, array $options = []) { - return $this->client->sendAsync($request, $options); + $requestEvent = null; + + if ($this->logger) { + $requestEvent = new LogEvent(); + + $requestEvent->method = $request->getMethod(); + $requestEvent->url = $request->getUri()->__toString(); + $requestEvent->headers = $request->getHeaders(); + $requestEvent->payload = $request->getBody()->getContents(); + $requestEvent->retryAttempt = $options['retryAttempt'] ?? null; + $requestEvent->serviceName = $options['serviceName'] ?? null; + $requestEvent->clientId = spl_object_id($this->client); + $requestEvent->requestId = spl_object_id($request); + + $this->logRequest($requestEvent); + } + + $promise = $this->client->sendAsync($request, $options); + + if ($this->logger) { + $promise->then(function (ResponseInterface $response) use ($requestEvent) { + $responseEvent = new LogEvent($requestEvent->timestamp); + + $responseEvent->headers = $response->getHeaders(); + $responseEvent->payload = $response->getBody()->getContents(); + $responseEvent->status = $response->getStatusCode(); + $responseEvent->clientId = $requestEvent->clientId; + $responseEvent->requestId = $requestEvent->requestId; + + $this->logResponse($responseEvent); + + return $response; + }); + } + + return $promise; } } diff --git a/src/HttpHandler/HttpHandlerFactory.php b/src/HttpHandler/HttpHandlerFactory.php index f19f87443..d08e73192 100644 --- a/src/HttpHandler/HttpHandlerFactory.php +++ b/src/HttpHandler/HttpHandlerFactory.php @@ -16,11 +16,13 @@ */ namespace Google\Auth\HttpHandler; +use Google\Auth\ApplicationDefaultCredentials; use GuzzleHttp\BodySummarizer; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; +use Psr\Log\LoggerInterface; class HttpHandlerFactory { @@ -28,10 +30,14 @@ class HttpHandlerFactory * Builds out a default http handler for the installed version of guzzle. * * @param ClientInterface $client + * @param null|false|LoggerInterface $logger * @return Guzzle6HttpHandler|Guzzle7HttpHandler * @throws \Exception */ - public static function build(ClientInterface $client = null) + public static function build( + ClientInterface $client = null, + null|false|LoggerInterface $logger = null, + ) { if (is_null($client)) { $stack = null; @@ -45,6 +51,12 @@ public static function build(ClientInterface $client = null) $client = new Client(['handler' => $stack]); } + if ($logger === false) { + $logger = null; + } else { + $logger = $logger ?? ApplicationDefaultCredentials::getDefaultLogger(); + } + $version = null; if (defined('GuzzleHttp\ClientInterface::MAJOR_VERSION')) { $version = ClientInterface::MAJOR_VERSION; @@ -54,9 +66,9 @@ public static function build(ClientInterface $client = null) switch ($version) { case 6: - return new Guzzle6HttpHandler($client); + return new Guzzle6HttpHandler($client, $logger); case 7: - return new Guzzle7HttpHandler($client); + return new Guzzle7HttpHandler($client, $logger); default: throw new \Exception('Version not supported'); } diff --git a/src/Logging/LogEvent.php b/src/Logging/LogEvent.php new file mode 100644 index 000000000..3007ce2e4 --- /dev/null +++ b/src/Logging/LogEvent.php @@ -0,0 +1,121 @@ + + */ + public null|array $headers = null; + + /** + * An array representation of JSON for the response or request + * + * @var null|string|array + */ + public null|string|array $payload = null; + + /** + * Status code for REST or gRPC methods + * + * @var null|int|string + */ + public null|int|string $status = null; + + /** + * The latency in miliseconds + * + * @var null|int + */ + public null|int $latency = null; + + /** + * The retry attempt number + * + * @var null|int + */ + public null|int $retryAttempt = null; + + /** + * The name of the gRPC method being called + * + * @var null|string + */ + public null|string $rpcName = null; + + /** + * The Service Name of the gRPC + * + * @var null|string $serviceName + */ + public null|string $serviceName = null; + + /** + * The Client Id for easy trace + * + * @var null|int $clientId + */ + public null|int $clientId = null; + + /** + * The Request id for easy trace + * + * @var null|int $requestId; + */ + public null|int $requestId = null; + + /** + * Creates an object with all the fields required for logging + * Passing a string representation of a timestamp calculates the difference between + * these two times and sets the latency field with the result. + * + * @param null|string $startTime (Optional) Parameter to calculate the latency + */ + public function __construct(null|string $startTime = null) + { + $this->timestamp = date(DATE_RFC3339); + + if ($startTime) { + $this->latency = (int) strtotime($this->timestamp) - strtotime($startTime); + } + } +} diff --git a/src/Logging/LoggingTrait.php b/src/Logging/LoggingTrait.php new file mode 100644 index 000000000..f0198c35a --- /dev/null +++ b/src/Logging/LoggingTrait.php @@ -0,0 +1,166 @@ + $event->timestamp, + 'severity' => strtoupper(LogLevel::DEBUG), + 'clientId' => $event->clientId, + 'requestId' => $event->requestId ?? null, + ]; + + $debugEvent = array_filter($debugEvent, fn ($value) => !is_null($value)); + + $jsonPayload = [ + 'request.method' => $event->method, + 'request.url' => $event->url, + 'request.headers' => $event->headers, + 'request.payload' => $event->payload, + 'request.jwt' => $this->getJwtToken($event->headers ?? []), + 'retryAttempt' => $event->retryAttempt + ]; + + // Remove null values + $debugEvent['jsonPayload'] = array_filter($jsonPayload, fn ($value) => !is_null($value)); + + $stringifiedEvent = json_encode($debugEvent); + + // There was an error stringifying the event, return to not break execution + if ($stringifiedEvent === false) { + return; + } + + $this->logger->debug($stringifiedEvent); + } + + /** + * @param LogEvent $event + */ + private function logResponse(LogEvent $event): void + { + $debugEvent = [ + 'timestamp' => $event->timestamp, + 'severity' => strtoupper(LogLevel::DEBUG), + 'clientId' => $event->clientId, + 'requestId' => $event->requestId ?? null, + 'jsonPayload' => [ + 'response.headers' => $event->headers, + 'response.payload' => $event->payload, + 'latency' => $event->latency, + ] + ]; + + // Remove null values + $debugEvent = array_filter($debugEvent, fn ($value) => !is_null($value)); + $debugEvent['jsonPayload'] = array_filter( + $debugEvent['jsonPayload'], + fn ($value) => !is_null($value) + ); + + $stringifiedEvent = json_encode($debugEvent); + + // There was an error stringifying the event, return to not break execution + if ($stringifiedEvent === false) { + return; + } + + $this->logger->debug($stringifiedEvent); + + if ($event->status) { + $infoEvent = [ + 'timestamp' => $event->timestamp, + 'severity' => LogLevel::INFO, + 'clientId' => $event->clientId, + 'requestId' => $event->requestId ?? null, + 'jsonPayload' => [ + 'response.status' => $event->status + ] + ]; + + // Remove null values + $infoEvent = array_filter($infoEvent, fn ($value) => !is_null($value)); + + $stringifiedEvent = json_encode($infoEvent); + + // There was an error stringifying the event, return to not break execution + if ($stringifiedEvent === false) { + return; + } + + $this->logger->info($stringifiedEvent); + } + } + + /** + * @param LogEvent $event + */ + private function logStatus(LogEvent $event): void + { + $infoEvent = [ + 'timestamp' => $event->timestamp, + 'severity' => LogLevel::INFO, + 'clientId' => $event->clientId, + 'requestId' => $event->requestId ?? null, + 'jsonPayload' => [ + 'response.status' => $event->status + ] + ]; + + $infoEvent = array_filter($infoEvent, fn ($value) => !is_null($value)); + $infoEvent['jsonPayload'] = array_filter( + $infoEvent['jsonPayload'], + fn ($value) => !is_null($value) + ); + + $this->logger->info((string) json_encode($infoEvent)); + } + + /** + * @param array $headers + * @return null|array + */ + private function getJwtToken(array $headers): null|array + { + if (empty($headers)) { + return null; + } + + $tokenHeader = $headers['Authorization'] ?? ''; + $token = str_replace('Bearer ', '', $tokenHeader); + + if (substr_count($token, '.') !== 2) { + return null; + } + + [$header, $token, $_] = explode('.', $token); + + return [ + 'header' => base64_decode($header), + 'token' => base64_decode($token) + ]; + } +} diff --git a/src/Logging/StdOutLogger.php b/src/Logging/StdOutLogger.php new file mode 100644 index 000000000..220136757 --- /dev/null +++ b/src/Logging/StdOutLogger.php @@ -0,0 +1,144 @@ + + */ + private array $levelMapping = [ + LogLevel::EMERGENCY => 7, + LogLevel::ALERT => 6, + LogLevel::CRITICAL => 5, + LogLevel::ERROR => 4, + LogLevel::WARNING => 3, + LogLevel::NOTICE => 2, + LogLevel::INFO => 1, + LogLevel::DEBUG => 0, + ]; + private int $level; + + /** + * Constructs a basic PSR-3 logger class that logs into StdOut for GCP Logging + * + * @param string $level The level of the logger instance. + */ + public function __construct(string $level = LogLevel::DEBUG) + { + $this->level = $this->getLevelMap($level); + } + + /** + * {@inheritdoc} + */ + public function emergency(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::EMERGENCY, $message); + } + + /** + * {@inheritdoc} + */ + public function alert(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message); + } + + /** + * {@inheritdoc} + */ + public function critical(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message); + } + + /** + * {@inheritdoc} + */ + public function error(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message); + } + + /** + * {@inheritdoc} + */ + public function warning(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message); + } + + /** + * {@inheritdoc} + */ + public function notice(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message); + } + + /** + * {@inheritdoc} + */ + public function info(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::INFO, $message); + } + + /** + * {@inheritdoc} + */ + public function debug(string|Stringable $message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message); + } + + /** + * {@inheritdoc} + */ + public function log($level, string|Stringable $message, array $context = []): void + { + if ($this->getLevelMap($level) < $this->level) { + return; + } + + print($message . "\n"); + } + + /** + * @param string $levelName + * @return int + * @throws InvalidArgumentException + */ + private function getLevelMap(string $levelName): int + { + if (!array_key_exists($levelName, $this->levelMapping)) { + throw new InvalidArgumentException('The level supplied to the Logger is not valid'); + } + + return $this->levelMapping[$levelName]; + } +} diff --git a/tests/ApplicationDefaultCredentialsTest.php b/tests/ApplicationDefaultCredentialsTest.php index fa537691f..38fae0640 100644 --- a/tests/ApplicationDefaultCredentialsTest.php +++ b/tests/ApplicationDefaultCredentialsTest.php @@ -25,9 +25,11 @@ use Google\Auth\CredentialsLoader; use Google\Auth\CredentialSource; use Google\Auth\GCECache; +use Google\Auth\Logging\StdOutLogger; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Utils; +use PHPUnit\Framework\Error\Notice; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use ReflectionClass; @@ -43,6 +45,7 @@ class ApplicationDefaultCredentialsTest extends TestCase private $targetAudience = 'a target audience'; private $quotaProject = 'a-quota-project'; private $originalServiceAccount; + private const SDK_DEBUG_FLAG = 'GOOGLE_SDK_DEBUG_LOGGING'; public function testGetCredentialsFailsIfEnvSpecifiesNonExistentFile() { @@ -775,6 +778,39 @@ public function testExternalAccountCredentials(string $jsonFile, string $expecte $this->assertInstanceOf($expectedCredSource, $subjectTokenFetcher); } + public function testGetDefaultLoggerReturnStdOutLoggerIfEnvVarIsPresent() + { + putenv($this::SDK_DEBUG_FLAG . '=true'); + $logger = ApplicationDefaultCredentials::getDefaultLogger(); + $this->assertTrue($logger instanceof StdOutLogger); + + putenv($this::SDK_DEBUG_FLAG . '=1'); + $logger = ApplicationDefaultCredentials::getDefaultLogger(); + $this->assertTrue($logger instanceof StdOutLogger); + } + + public function testGetDefaultLoggerReturnsNullIfNotEnvVar() + { + putenv($this::SDK_DEBUG_FLAG . '=false'); + $logger = ApplicationDefaultCredentials::getDefaultLogger(); + + $this->assertNull($logger); + + putenv($this::SDK_DEBUG_FLAG . '=0'); + $logger = ApplicationDefaultCredentials::getDefaultLogger(); + + $this->assertNull($logger); + } + + public function testGetDefaultLoggerRaiseAWarningIfMisconfiguredAndReturnsNull() + { + putenv($this::SDK_DEBUG_FLAG . '=invalid'); + $this->expectException(Notice::class); + $logger = ApplicationDefaultCredentials::getDefaultLogger(); + + $this->assertNull($logger); + } + public function provideExternalAccountCredentials() { return [ diff --git a/tests/HttpHandler/Guzzle7HttpHandlerTest.php b/tests/HttpHandler/Guzzle7HttpHandlerTest.php index 375f72cbf..732583be3 100644 --- a/tests/HttpHandler/Guzzle7HttpHandlerTest.php +++ b/tests/HttpHandler/Guzzle7HttpHandlerTest.php @@ -18,6 +18,13 @@ namespace Google\Auth\Tests\HttpHandler; use Google\Auth\HttpHandler\Guzzle7HttpHandler; +use Google\Auth\Logging\StdOutLogger; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Promise\Promise; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use Prophecy\Argument; +use Psr\Log\LoggerInterface; /** * @group http-handler @@ -31,4 +38,49 @@ public function setUp(): void $this->client = $this->prophesize('GuzzleHttp\ClientInterface'); $this->handler = new Guzzle7HttpHandler($this->client->reveal()); } + + public function testLoggerGetsCalledIfLoggerIsPassed() + { + $requestPromise = new Promise(function() use (&$requestPromise) { + $response = new Response(200); + $requestPromise->resolve($response); + }); + + $mockLogger = $this->prophesize(StdOutLogger::class); + $mockLogger->debug(Argument::cetera()) + ->shouldBeCalledTimes(2); + $mockLogger->info(Argument::cetera()) + ->shouldBeCalledTimes(1); + + $this->client->sendAsync(Argument::cetera()) + ->willReturn($requestPromise); + + $request = new Request('GET', 'https://domain.tld'); + $options = ['key' => 'value']; + + $handler = new Guzzle7HttpHandler($this->client->reveal(), $mockLogger->reveal()); + $handler->async($request, $options)->wait(); + } + + public function testLoggerDoesNotGetsCalledIfLoggerIsNotPassed() + { + $requestPromise = new Promise(function () use (&$requestPromise) { + $response = new Response(200); + $requestPromise->resolve($response); + }); + + $this->client->sendAsync(Argument::cetera()) + ->willReturn($requestPromise) + ->shouldBeCalledTimes(1); + + $request = new Request('GET', 'https://domain.tld'); + $options = ['key' => 'value']; + + /** + * @var LoggerInterface $mockLogger + * @var ClientInterface $mockClient + */ + $handler = new Guzzle7HttpHandler($this->client->reveal()); + $handler->async($request, $options)->wait(); + } } diff --git a/tests/Logging/LogEventTest.php b/tests/Logging/LogEventTest.php new file mode 100644 index 000000000..5b29b7eb0 --- /dev/null +++ b/tests/Logging/LogEventTest.php @@ -0,0 +1,42 @@ +assertNotNull($item->timestamp); + } + + public function testConstructorWithoutParameterHasNoLatency() + { + $item = new LogEvent(); + $this->assertNull($item->latency); + } + + public function testConstructorWithParameterHasLatencySet() + { + $item = new LogEvent(date(DATE_RFC3339)); + $this->assertNotNull($item->latency); + } +} diff --git a/tests/Logging/LoggingTraitTest.php b/tests/Logging/LoggingTraitTest.php new file mode 100644 index 000000000..f162b4760 --- /dev/null +++ b/tests/Logging/LoggingTraitTest.php @@ -0,0 +1,136 @@ +loggerContainer = new MockClassWithLogger(); + } + + public function testLogRequest() + { + ob_start(); + + $event = $this->getNewLogEvent(); + $this->loggerContainer->logRequestEvent($event); + + $buffer = ob_get_contents(); + ob_end_clean(); + $jsonParsed = json_decode($buffer, true); + + $this->assertEquals($event->timestamp, $jsonParsed['timestamp']); + $this->assertEquals($event->clientId, $jsonParsed['clientId']); + $this->assertEquals($event->method, $jsonParsed['jsonPayload']['request.method']); + $this->assertEquals($event->url, $jsonParsed['jsonPayload']['request.url']); + $this->assertEquals($event->headers, $jsonParsed['jsonPayload']['request.headers']); + $this->assertArrayHasKey('request.jwt', $jsonParsed['jsonPayload']); + } + + public function testRequestWithoutJwtShouldNotPrintAJwt() + { + ob_start(); + + $event = $this->getNewLogEvent(); + $event->headers = ['no jwt' => true]; + $this->loggerContainer->logRequestEvent($event); + + $buffer = ob_get_contents(); + ob_end_clean(); + $jsonParsed = json_decode($buffer, true); + + $this->assertArrayNotHasKey('request.jwt', $jsonParsed['jsonPayload']); + } + + public function testLogResponse() + { + ob_start(); + + $event = $this->getNewLogEvent(); + $this->loggerContainer->logResponseEvent($event); + + $buffer = ob_get_contents(); + ob_end_clean(); + + $buffer = str_replace("\n", '', $buffer); + + // The LogResponse method logs two evnets, one for info and one for debug. + [$debugEvent, $infoEvent] = explode('}{', $buffer); + $debugEvent .= '}'; + $infoEvent = '{' . $infoEvent; + + $parsedDebugEvent = json_decode($debugEvent, true); + $this->assertEquals($event->clientId, $parsedDebugEvent['clientId']); + $this->assertEquals($event->requestId, $parsedDebugEvent['requestId']); + $this->assertEquals($event->headers, $parsedDebugEvent['jsonPayload']['response.headers']); + + $parsedInfoEvent = json_decode($infoEvent, true); + $this->assertEquals($event->status, $parsedInfoEvent['jsonPayload']['response.status']); + } + + private function getNewLogEvent(): LogEvent + { + $event = new LogEvent(); + $event->clientId = 123; + $event->method = 'get'; + $event->url = 'test.com'; + $event->headers = [ + 'header1' => 'test', + 'Authorization' => 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ' + ]; + $event->payload = ['param' => 'test']; + $event->status = 200; + $event->retryAttempt = 0; + $event->rpcName = 'Rpc NameTest'; + $event->serviceName = 'Service Name'; + $event->requestId = 321; + $event->latency = 555; + + return $event; + } +} + +class MockClassWithLogger +{ + use LoggingTrait; + private LoggerInterface $logger; + + public function __construct() + { + $this->logger = new StdOutLogger(); + } + + public function logRequestEvent(LogEvent $event): void + { + $this->logRequest($event); + } + + public function logResponseEvent(LogEvent $event): void + { + $this->logResponse($event); + } +} diff --git a/tests/Logging/StdOutLoggerTest.php b/tests/Logging/StdOutLoggerTest.php new file mode 100644 index 000000000..f5f603313 --- /dev/null +++ b/tests/Logging/StdOutLoggerTest.php @@ -0,0 +1,74 @@ +expectException(InvalidArgumentException::class); + new StdOutLogger('invalid level'); + } + + public function testLoggingOnSameLevelWritesToStdOut() + { + ob_start(); + + $logger = new StdOutLogger(LogLevel::DEBUG); + $expectedString = 'test'; + $logger->debug($expectedString); + $buffer = ob_get_contents(); + + $this->assertEquals($expectedString . "\n", $buffer); + + ob_end_clean(); + } + + public function testLoggingOnHigherLeverWritesToStdOut() + { + ob_start(); + + $logger = new StdOutLogger(LogLevel::WARNING); + $expectedString = 'test'; + $logger->error($expectedString); + $buffer = ob_get_contents(); + + $this->assertEquals($expectedString . "\n", $buffer); + + ob_end_clean(); + } + + public function testLoggingOnLowerLeverDoesNotWriteToStdOut() + { + ob_start(); + + $logger = new StdOutLogger(LogLevel::WARNING); + $expectedString = 'test'; + $logger->debug($expectedString); + $buffer = ob_get_contents(); + + $this->assertEmpty($buffer); + + ob_end_clean(); + } +}