Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
96 changes: 96 additions & 0 deletions spec/LoggerPluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace spec\Http\Client\Plugin;

use Http\Client\Exception\HttpException;
use Http\Client\Exception\NetworkException;
use Http\Client\Plugin\Normalizer\Normalizer;
use Http\Client\Utils\Promise\FulfilledPromise;
use Http\Client\Utils\Promise\RejectedPromise;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;

class LoggerPluginSpec extends ObjectBehavior
{
function let(LoggerInterface $logger)
{
$this->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 () {});
}
}
36 changes: 36 additions & 0 deletions spec/Normalizer/NormalizerSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace spec\Http\Client\Plugin\Normalizer;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;

class NormalizerSpec extends ObjectBehavior
{
function it_is_initializable(LoggerInterface $logger)
{
$this->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');
}
}
81 changes: 81 additions & 0 deletions src/LoggerPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Http\Client\Plugin;

use Http\Client\Exception;
use Http\Client\Plugin\Normalizer\Normalizer;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;

/**
* Log request, response and exception for a HTTP Client
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the promise is handling the throwing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by catching the exception, i mean.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes when calling one of the callable passed to the then method, if it returns something then the value will go into the next onFulfilled callable, if it throw an exception it will go into the next onRejected callable.

});
}
}
40 changes: 40 additions & 0 deletions src/Normalizer/Normalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Http\Client\Plugin\Normalizer;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Normalize a request or a response into a string or an array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author info

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please also explain in the class docblock that this is @internal and should not be used outside this package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum i add @internal on the method, but this normalizer is injected into the plugin, to allow user to change how to render the log line, so it's not that "internal" ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is...for now. We should replace it in the future with some kind of Formatter interface with different kind of formatters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i see. hm. it does make sense to inject the string converter. but it will be a BC break when we start a separate tool and switch to that. could we for now simply instantiate Normalizer in the plugin and not have an argument for Normalizer? then we can add an optional argument once we have built the separate package to convert requests to strings, without creating a BC break for users.

*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*
* @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());
}
}