diff --git a/composer.json b/composer.json index 7521678..53e906d 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "henrikbjorn/phpspec-code-coverage" : "^1.0", "php-http/authentication": "^0.1@dev", "php-http/cookie": "^0.1@dev", - "symfony/stopwatch": "^2.3" + "symfony/stopwatch": "^2.3", + "psr/log": "^1.0" }, "autoload": { "psr-4": { @@ -30,7 +31,8 @@ "suggest": { "php-http/authentication": "Allow to use the AuthenticationPlugin", "php-http/cookie": "Allow to use CookiePlugin", - "symfony/stopwatch": "Allow to use the StopwatchPlugin" + "symfony/stopwatch": "Allow to use the StopwatchPlugin", + "psr/log-implementation": "Allow to use the LoggerPlugin" }, "scripts": { "test": "vendor/bin/phpspec run", diff --git a/spec/LoggerPluginSpec.php b/spec/LoggerPluginSpec.php new file mode 100644 index 0000000..62ee914 --- /dev/null +++ b/spec/LoggerPluginSpec.php @@ -0,0 +1,96 @@ +beConstructedWith($logger); + } + + function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Plugin\LoggerPlugin'); + } + + function it_is_a_plugin() + { + $this->shouldImplement('Http\Client\Plugin\Plugin'); + } + + function it_logs_request_and_response(LoggerInterface $logger, RequestInterface $request, ResponseInterface $response) + { + $logger->info('Emit request: "GET / 1.1"', ['request' => $request])->shouldBeCalled(); + $logger->info('Receive response: "200 Ok 1.1" for request: "GET / 1.1"', ['request' => $request, 'response' => $response])->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/'); + $request->getProtocolVersion()->willReturn('1.1'); + + $response->getReasonPhrase()->willReturn('Ok'); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn('200'); + + $next = function () use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_logs_exception(LoggerInterface $logger, RequestInterface $request) + { + $exception = new NetworkException('Cannot connect', $request->getWrappedObject()); + + $logger->info('Emit request: "GET / 1.1"', ['request' => $request])->shouldBeCalled(); + $logger->error('Error: "Cannot connect" when emitting request: "GET / 1.1"', ['request' => $request, 'exception' => $exception])->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/'); + $request->getProtocolVersion()->willReturn('1.1'); + + $next = function () use ($exception) { + return new RejectedPromise($exception); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_logs_response_within_exception(LoggerInterface $logger, RequestInterface $request, ResponseInterface $response) + { + $exception = new HttpException('Forbidden', $request->getWrappedObject(), $response->getWrappedObject()); + + $logger->info('Emit request: "GET / 1.1"', ['request' => $request])->shouldBeCalled(); + $logger->error('Error: "Forbidden" with response: "403 Forbidden 1.1" when emitting request: "GET / 1.1"', [ + 'request' => $request, + 'response' => $response, + 'exception' => $exception + ])->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/'); + $request->getProtocolVersion()->willReturn('1.1'); + + $response->getReasonPhrase()->willReturn('Forbidden'); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn('403'); + + $next = function () use ($exception) { + return new RejectedPromise($exception); + }; + + $this->handleRequest($request, $next, function () {}); + } +} diff --git a/spec/Normalizer/NormalizerSpec.php b/spec/Normalizer/NormalizerSpec.php new file mode 100644 index 0000000..e119c4b --- /dev/null +++ b/spec/Normalizer/NormalizerSpec.php @@ -0,0 +1,36 @@ +shouldHaveType('Http\Client\Plugin\Normalizer\Normalizer'); + } + + function it_normalize_request_to_string(RequestInterface $request) + { + $request->getMethod()->willReturn('GET'); + $request->getRequestTarget()->willReturn('/'); + $request->getProtocolVersion()->willReturn('1.1'); + + $this->normalizeRequestToString($request)->shouldReturn('GET / 1.1'); + } + + function it_normalize_response_to_string(ResponseInterface $response) + { + $response->getReasonPhrase()->willReturn('Ok'); + $response->getProtocolVersion()->willReturn('1.1'); + $response->getStatusCode()->willReturn('200'); + + $this->normalizeResponseToString($response)->shouldReturn('200 Ok 1.1'); + } +} diff --git a/src/LoggerPlugin.php b/src/LoggerPlugin.php new file mode 100644 index 0000000..f0dafa3 --- /dev/null +++ b/src/LoggerPlugin.php @@ -0,0 +1,81 @@ + + */ +class LoggerPlugin implements Plugin +{ + /** + * Logger to log request / response / exception for a http call + * + * @var LoggerInterface + */ + private $logger; + + /** + * Normalize request and response to string or array + * + * @var Normalizer + */ + private $normalizer; + + /** + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + $this->normalizer = new Normalizer(); + } + + /** + * {@inheritdoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first) + { + $this->logger->info(sprintf('Emit request: "%s"', $this->normalizer->normalizeRequestToString($request)), ['request' => $request]); + + return $next($request)->then(function (ResponseInterface $response) use($request) { + $this->logger->info( + sprintf('Receive response: "%s" for request: "%s"', $this->normalizer->normalizeResponseToString($response), $this->normalizer->normalizeRequestToString($request)), + [ + 'request' => $request, + 'response' => $response, + ] + ); + + return $response; + }, function (Exception $exception) use($request) { + if ($exception instanceof Exception\HttpException) { + $this->logger->error( + sprintf('Error: "%s" with response: "%s" when emitting request: "%s"', $exception->getMessage(), $this->normalizer->normalizeResponseToString($exception->getResponse()), $this->normalizer->normalizeRequestToString($request)), + [ + 'request' => $request, + 'response' => $exception->getResponse(), + 'exception' => $exception + ] + ); + } else { + $this->logger->error( + sprintf('Error: "%s" when emitting request: "%s"', $exception->getMessage(), $this->normalizer->normalizeRequestToString($request)), + [ + 'request' => $request, + 'exception' => $exception + ] + ); + } + + throw $exception; + }); + } +} diff --git a/src/Normalizer/Normalizer.php b/src/Normalizer/Normalizer.php new file mode 100644 index 0000000..480a393 --- /dev/null +++ b/src/Normalizer/Normalizer.php @@ -0,0 +1,40 @@ + + * + * @internal Should not be used outside of the logger plugin + */ +class Normalizer +{ + /** + * Normalize a request to string + * + * @param RequestInterface $request + * + * @return string + */ + public function normalizeRequestToString(RequestInterface $request) + { + return sprintf('%s %s %s', $request->getMethod(), $request->getRequestTarget(), $request->getProtocolVersion()); + } + + /** + * Normalize a response to string + * + * @param ResponseInterface $response + * + * @return string + */ + public function normalizeResponseToString(ResponseInterface $response) + { + return sprintf("%s %s %s", $response->getStatusCode(), $response->getReasonPhrase(), $response->getProtocolVersion()); + } +}