Skip to content

Support for trimming the response body #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 16, 2024
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ You may override its configuration in your `.env` - the following environment va
- `HTTP_CLIENT_GLOBAL_LOGGER_RESPONSE_FORMAT` (string)
- `HTTP_CLIENT_GLOBAL_LOGGER_OBFUSCATE_ENABLED` (bool)
- `HTTP_CLIENT_GLOBAL_LOGGER_OBFUSCATE_REPLACEMENT` (string)
- `HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_ENABLED` (bool)
- `HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_LIMIT` (int)
- `HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_CONTENT_TYPE_WHITELIST` (string)

(look into `config/http-client-global-logger.php` for further configuration and explanation)
(look into `config/http-client-global-logger.php` for defaults, further configuration, and explanation)

## Features

Expand All @@ -45,6 +48,8 @@ Using the logger will log both the request and response of an external HTTP requ
- Multi-line log records that contain full request/response information (including all headers and body)
- Logging into separate logfile `http-client.log`. You're free to override this and use your own logging channel or just log to a different logfile.
- Full support of [Guzzle MessageFormatter](https://github.com/guzzle/guzzle/blob/master/src/MessageFormatter.php) variable substitutions for highly customized log messages.
- Basic obfuscation of credentials in HTTP Client requests
- Trimming of response body content to a certain length with support for Content-Type whitelisting
- **Variant 1: Global logging** (default)
- Zero-configuration: Global logging is enabled by default in this package.
- Simple and performant implementation using `RequestSending` / `ResponseReceived` event listeners
Expand Down Expand Up @@ -166,6 +171,7 @@ Both packages provide a different feature set and have those advantages:
- auto-configured log channel `http-client` to log to a separate `http-client.log` file
- Full support of [Guzzle MessageFormatter](https://github.com/guzzle/guzzle/blob/master/src/MessageFormatter.php) variable substitutions for highly customized log messages.
- basic obfuscation of credentials in HTTP Client requests
- trimming of response body content
- [bilfeldt/laravel-http-client-logger](https://github.com/bilfeldt/laravel-http-client-logger)
- conditional logging using `logWhen($condition)`
- filtering of logs by HTTP response codes
Expand Down
20 changes: 20 additions & 0 deletions config/http-client-global-logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,24 @@
'pass,password,token,apikey,access_token,refresh_token,client_secret'
)),
],

/*
|--------------------------------------------------------------------------
| Trim response body
|--------------------------------------------------------------------------
|
| Trim response body to a certain length. This is useful when you are logging
| large responses, and you don't want to fill up your log files.
|
| NOTE the leading comma in trim_response_body.content_type_whitelist default value:
| it's there to whitelist empty content types (e.g. when no Content-Type header is set).
*/
'trim_response_body' => [
'enabled' => env('HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_ENABLED', false),
'limit' => env('HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_LIMIT', 200),
'content_type_whitelist' => explode(',', env(
'HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_CONTENT_TYPE_WHITELIST',
',application/json'
)),
],
];
43 changes: 42 additions & 1 deletion src/Listeners/LogResponseReceived.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
namespace Onlime\LaravelHttpClientGlobalLogger\Listeners;

use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Onlime\LaravelHttpClientGlobalLogger\EventHelper;
use Psr\Http\Message\MessageInterface;
use Saloon\Laravel\Events\SentSaloonRequest;

class LogResponseReceived
Expand All @@ -22,7 +26,44 @@ public function handle(ResponseReceived|SentSaloonRequest $event): void
$formatter = new MessageFormatter(config('http-client-global-logger.format.response'));
Log::channel(config('http-client-global-logger.channel'))->info($formatter->format(
EventHelper::getPsrRequest($event),
EventHelper::getPsrResponse($event),
$this->trimBody(EventHelper::getPsrResponse($event))
));
}

/**
* Trim the response body when it's too long.
*/
private function trimBody(Response $psrResponse): Response|MessageInterface
{
// Check if trimming is enabled
if (! config('http-client-global-logger.trim_response_body.enabled')) {
return $psrResponse;
}

// E.g.: application/json; charset=utf-8 => application/json
$contentTypeHeader = Str::of($psrResponse->getHeaderLine('Content-Type'))
->before(';')
->trim()
->lower()
->value();

$whiteListedContentTypes = array_map(
fn (string $type) => trim(strtolower($type)),
config('http-client-global-logger.trim_response_body.content_type_whitelist')
);

// Check if the content type is whitelisted
if (in_array($contentTypeHeader, $whiteListedContentTypes)) {
return $psrResponse;
}

$limit = config('http-client-global-logger.trim_response_body.limit');

// Check if the body size exceeds the limit
return ($psrResponse->getBody()->getSize() <= $limit)
? $psrResponse
: $psrResponse->withBody(Utils::streamFor(
Str::limit($psrResponse->getBody(), $limit)
));
}
}
98 changes: 93 additions & 5 deletions tests/HttpClientLoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Mockery\MockInterface;
use Onlime\LaravelHttpClientGlobalLogger\HttpClientLogger;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;

it('can add a global request middleware to log the requests', function () {
Http::globalRequestMiddleware(
fn (RequestInterface $psrRequest) => $psrRequest->withHeader('X-Test', 'test')
);

function setupLogger(): MockInterface
{
HttpClientLogger::addRequestMiddleware();

$logger = Mockery::mock(LoggerInterface::class);

Log::shouldReceive('channel')->with('http-client')->andReturn($logger);

return $logger;
}

it('can add a global request middleware to log the requests', function () {
Http::globalRequestMiddleware(
fn (RequestInterface $psrRequest) => $psrRequest->withHeader('X-Test', 'test')
);

$logger = setupLogger();

$logger->shouldReceive('info')->withArgs(function ($message) {
expect($message)
->toContain('REQUEST: GET https://example.com')
Expand All @@ -35,3 +43,83 @@

Http::fake()->get('https://example.com');
});

it('can trim the body response', function (array $config, string $contentType, bool $shouldTrim, bool $addCharsetToContentType) {
config(['http-client-global-logger.trim_response_body' => $config]);

$logger = setupLogger();

$logger->shouldReceive('info')->withArgs(function ($message) {
expect($message)->toContain('REQUEST: GET https://example.com');
return true;
})->once();

$logger->shouldReceive('info')->withArgs(function ($message) use ($shouldTrim) {
expect($message)->toContain($shouldTrim ? 'verylongbo...' : 'verylongbody');
return true;
})->once();

Http::fake([
'*' => Http::response('verylongbody', 200, [
'Content-Type' => $contentType.($addCharsetToContentType ? '; charset=UTF-8' : ''),
]),
])->get('https://example.com');
})->with(
[
'disabled' => [
'config' => [
'enabled' => false,
'limit' => 10,
'content_type_whitelist' => ['application/json'],
],
'contentType' => 'application/octet-stream',
'shouldTrim' => false,
],
'below_limit' => [
'config' => [
'enabled' => true,
'limit' => 20,
'content_type_whitelist' => ['application/json'],
],
'contentType' => 'application/octet-stream',
'shouldTrim' => false,
],
'content_type_whitelisted' => [
'config' => [
'enabled' => true,
'limit' => 10,
'content_type_whitelist' => ['application/octet-stream'],
],
'contentType' => 'application/octet-stream',
'shouldTrim' => false,
],
'trim' => [
'config' => [
'enabled' => true,
'limit' => 10,
'content_type_whitelist' => ['application/json'],
],
'contentType' => 'application/octet-stream',
'shouldTrim' => true,
],
'no_content_type_trim' => [
'config' => [
'enabled' => true,
'limit' => 10,
'content_type_whitelist' => ['application/octet-stream'],
],
'contentType' => '',
'shouldTrim' => true,
],
'no_content_type_whitelisted' => [
'config' => [
'enabled' => true,
'limit' => 10,
'content_type_whitelist' => ['', 'application/octet-stream'],
],
'contentType' => '',
'shouldTrim' => false,
],
],
[true, false]
);