Skip to content

Commit 6620b3a

Browse files
authored
Merge pull request #3 from pascalbaljet/trim-response-body
Support for trimming the response body
2 parents ac1ec4d + d5fc09c commit 6620b3a

File tree

4 files changed

+162
-7
lines changed

4 files changed

+162
-7
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ You may override its configuration in your `.env` - the following environment va
3535
- `HTTP_CLIENT_GLOBAL_LOGGER_RESPONSE_FORMAT` (string)
3636
- `HTTP_CLIENT_GLOBAL_LOGGER_OBFUSCATE_ENABLED` (bool)
3737
- `HTTP_CLIENT_GLOBAL_LOGGER_OBFUSCATE_REPLACEMENT` (string)
38+
- `HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_ENABLED` (bool)
39+
- `HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_LIMIT` (int)
40+
- `HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_CONTENT_TYPE_WHITELIST` (string)
3841

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

4144
## Features
4245

@@ -45,6 +48,8 @@ Using the logger will log both the request and response of an external HTTP requ
4548
- Multi-line log records that contain full request/response information (including all headers and body)
4649
- 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.
4750
- Full support of [Guzzle MessageFormatter](https://github.com/guzzle/guzzle/blob/master/src/MessageFormatter.php) variable substitutions for highly customized log messages.
51+
- Basic obfuscation of credentials in HTTP Client requests
52+
- Trimming of response body content to a certain length with support for Content-Type whitelisting
4853
- **Variant 1: Global logging** (default)
4954
- Zero-configuration: Global logging is enabled by default in this package.
5055
- Simple and performant implementation using `RequestSending` / `ResponseReceived` event listeners
@@ -166,6 +171,7 @@ Both packages provide a different feature set and have those advantages:
166171
- auto-configured log channel `http-client` to log to a separate `http-client.log` file
167172
- Full support of [Guzzle MessageFormatter](https://github.com/guzzle/guzzle/blob/master/src/MessageFormatter.php) variable substitutions for highly customized log messages.
168173
- basic obfuscation of credentials in HTTP Client requests
174+
- trimming of response body content
169175
- [bilfeldt/laravel-http-client-logger](https://github.com/bilfeldt/laravel-http-client-logger)
170176
- conditional logging using `logWhen($condition)`
171177
- filtering of logs by HTTP response codes

config/http-client-global-logger.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,24 @@
107107
'pass,password,token,apikey,access_token,refresh_token,client_secret'
108108
)),
109109
],
110+
111+
/*
112+
|--------------------------------------------------------------------------
113+
| Trim response body
114+
|--------------------------------------------------------------------------
115+
|
116+
| Trim response body to a certain length. This is useful when you are logging
117+
| large responses, and you don't want to fill up your log files.
118+
|
119+
| NOTE the leading comma in trim_response_body.content_type_whitelist default value:
120+
| it's there to whitelist empty content types (e.g. when no Content-Type header is set).
121+
*/
122+
'trim_response_body' => [
123+
'enabled' => env('HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_ENABLED', false),
124+
'limit' => env('HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_LIMIT', 200),
125+
'content_type_whitelist' => explode(',', env(
126+
'HTTP_CLIENT_GLOBAL_LOGGER_TRIM_RESPONSE_BODY_CONTENT_TYPE_WHITELIST',
127+
',application/json'
128+
)),
129+
],
110130
];

src/Listeners/LogResponseReceived.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
namespace Onlime\LaravelHttpClientGlobalLogger\Listeners;
44

55
use GuzzleHttp\MessageFormatter;
6+
use GuzzleHttp\Psr7\Response;
7+
use GuzzleHttp\Psr7\Utils;
68
use Illuminate\Http\Client\Events\ResponseReceived;
79
use Illuminate\Support\Facades\Log;
10+
use Illuminate\Support\Str;
811
use Onlime\LaravelHttpClientGlobalLogger\EventHelper;
12+
use Psr\Http\Message\MessageInterface;
913
use Saloon\Laravel\Events\SentSaloonRequest;
1014

1115
class LogResponseReceived
@@ -22,7 +26,44 @@ public function handle(ResponseReceived|SentSaloonRequest $event): void
2226
$formatter = new MessageFormatter(config('http-client-global-logger.format.response'));
2327
Log::channel(config('http-client-global-logger.channel'))->info($formatter->format(
2428
EventHelper::getPsrRequest($event),
25-
EventHelper::getPsrResponse($event),
29+
$this->trimBody(EventHelper::getPsrResponse($event))
2630
));
2731
}
32+
33+
/**
34+
* Trim the response body when it's too long.
35+
*/
36+
private function trimBody(Response $psrResponse): Response|MessageInterface
37+
{
38+
// Check if trimming is enabled
39+
if (! config('http-client-global-logger.trim_response_body.enabled')) {
40+
return $psrResponse;
41+
}
42+
43+
// E.g.: application/json; charset=utf-8 => application/json
44+
$contentTypeHeader = Str::of($psrResponse->getHeaderLine('Content-Type'))
45+
->before(';')
46+
->trim()
47+
->lower()
48+
->value();
49+
50+
$whiteListedContentTypes = array_map(
51+
fn (string $type) => trim(strtolower($type)),
52+
config('http-client-global-logger.trim_response_body.content_type_whitelist')
53+
);
54+
55+
// Check if the content type is whitelisted
56+
if (in_array($contentTypeHeader, $whiteListedContentTypes)) {
57+
return $psrResponse;
58+
}
59+
60+
$limit = config('http-client-global-logger.trim_response_body.limit');
61+
62+
// Check if the body size exceeds the limit
63+
return ($psrResponse->getBody()->getSize() <= $limit)
64+
? $psrResponse
65+
: $psrResponse->withBody(Utils::streamFor(
66+
Str::limit($psrResponse->getBody(), $limit)
67+
));
68+
}
2869
}

tests/HttpClientLoggerTest.php

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@
22

33
use Illuminate\Support\Facades\Http;
44
use Illuminate\Support\Facades\Log;
5+
use Mockery\MockInterface;
56
use Onlime\LaravelHttpClientGlobalLogger\HttpClientLogger;
67
use Psr\Http\Message\RequestInterface;
78
use Psr\Log\LoggerInterface;
89

9-
it('can add a global request middleware to log the requests', function () {
10-
Http::globalRequestMiddleware(
11-
fn (RequestInterface $psrRequest) => $psrRequest->withHeader('X-Test', 'test')
12-
);
13-
10+
function setupLogger(): MockInterface
11+
{
1412
HttpClientLogger::addRequestMiddleware();
1513

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

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

18+
return $logger;
19+
}
20+
21+
it('can add a global request middleware to log the requests', function () {
22+
Http::globalRequestMiddleware(
23+
fn (RequestInterface $psrRequest) => $psrRequest->withHeader('X-Test', 'test')
24+
);
25+
26+
$logger = setupLogger();
27+
2028
$logger->shouldReceive('info')->withArgs(function ($message) {
2129
expect($message)
2230
->toContain('REQUEST: GET https://example.com')
@@ -35,3 +43,83 @@
3543

3644
Http::fake()->get('https://example.com');
3745
});
46+
47+
it('can trim the body response', function (array $config, string $contentType, bool $shouldTrim, bool $addCharsetToContentType) {
48+
config(['http-client-global-logger.trim_response_body' => $config]);
49+
50+
$logger = setupLogger();
51+
52+
$logger->shouldReceive('info')->withArgs(function ($message) {
53+
expect($message)->toContain('REQUEST: GET https://example.com');
54+
return true;
55+
})->once();
56+
57+
$logger->shouldReceive('info')->withArgs(function ($message) use ($shouldTrim) {
58+
expect($message)->toContain($shouldTrim ? 'verylongbo...' : 'verylongbody');
59+
return true;
60+
})->once();
61+
62+
Http::fake([
63+
'*' => Http::response('verylongbody', 200, [
64+
'Content-Type' => $contentType.($addCharsetToContentType ? '; charset=UTF-8' : ''),
65+
]),
66+
])->get('https://example.com');
67+
})->with(
68+
[
69+
'disabled' => [
70+
'config' => [
71+
'enabled' => false,
72+
'limit' => 10,
73+
'content_type_whitelist' => ['application/json'],
74+
],
75+
'contentType' => 'application/octet-stream',
76+
'shouldTrim' => false,
77+
],
78+
'below_limit' => [
79+
'config' => [
80+
'enabled' => true,
81+
'limit' => 20,
82+
'content_type_whitelist' => ['application/json'],
83+
],
84+
'contentType' => 'application/octet-stream',
85+
'shouldTrim' => false,
86+
],
87+
'content_type_whitelisted' => [
88+
'config' => [
89+
'enabled' => true,
90+
'limit' => 10,
91+
'content_type_whitelist' => ['application/octet-stream'],
92+
],
93+
'contentType' => 'application/octet-stream',
94+
'shouldTrim' => false,
95+
],
96+
'trim' => [
97+
'config' => [
98+
'enabled' => true,
99+
'limit' => 10,
100+
'content_type_whitelist' => ['application/json'],
101+
],
102+
'contentType' => 'application/octet-stream',
103+
'shouldTrim' => true,
104+
],
105+
'no_content_type_trim' => [
106+
'config' => [
107+
'enabled' => true,
108+
'limit' => 10,
109+
'content_type_whitelist' => ['application/octet-stream'],
110+
],
111+
'contentType' => '',
112+
'shouldTrim' => true,
113+
],
114+
'no_content_type_whitelisted' => [
115+
'config' => [
116+
'enabled' => true,
117+
'limit' => 10,
118+
'content_type_whitelist' => ['', 'application/octet-stream'],
119+
],
120+
'contentType' => '',
121+
'shouldTrim' => false,
122+
],
123+
],
124+
[true, false]
125+
);

0 commit comments

Comments
 (0)