diff --git a/.gitignore b/.gitignore index 6dcd135..df533d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -/vendor/ +.phpunit.cache +.phpunit.result.cache +.pint.cache.json /.idea /composer.lock /tests/_coverage -.phpunit.result.cache -.pint.cache.json +/vendor/ +build +coverage +phpunit.xml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 407ec74..0e1922d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,13 @@ # CHANGELOG -## [v1.0.x (Unreleased)](https://github.com/onlime/laravel-http-client-global-logger/compare/v1.0.1...main) +## [v1.1.x (Unreleased)](https://github.com/onlime/laravel-http-client-global-logger/compare/v1.1.0...main) - ... +## [v1.1.0 (2023-11-09)](https://github.com/onlime/laravel-http-client-global-logger/compare/v1.0.2...v1.1.0) + +- Feature | HTTP Request Middleware to log Requests after Global Middleware by @pascalbaljet in #1 + ## [v1.0.2 (2023-07-17)](https://github.com/onlime/laravel-http-client-global-logger/compare/v1.0.1...v1.0.2) - Drop Laravel 9 support diff --git a/README.md b/README.md index a172469..6470208 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,14 @@ Using the logger will log both the request and response of an external HTTP requ - **Variant 2: Mixin** (`HTTP_CLIENT_GLOBAL_LOGGER_MIXIN=true`) - Enabled only on individual HTTP Client instances, using `Http::log()` - no global logging. - Log channel name can be set per HTTP Client instance by passing a name to `Http::log($name)` +- **Variant 3: Global HTTP Middleware** + - Can be used in combination with other `Http::globalRequestMiddleware()` calls in your `AppServiceProvider`'s `boot()` method, after registering your [Global Middleware](https://laravel.com/docs/10.x/http-client#global-middleware). + ## Usage +> **NOTE:** For all 3 variants below, you need to keep the HTTP Client Global Logger enabled (not setting `HTTP_CLIENT_GLOBAL_LOGGER_ENABLED=false` in your `.env`). The `http-client-global-logger.enabled` config option is a global on/off switch for all 3 variants, not just the "global" variants. Our project name might be misleading in that context. + ### Variant 1: Global Logging **Just use Laravel HTTP Client as always - no need to configure anything!** @@ -103,6 +108,31 @@ $client = Http::log('my-api')->withOptions([ $response = $client->get('/user'); ``` +### Variant 3: Global HTTP Middleware + +If you use [Global Middleware](https://laravel.com/docs/10.x/http-client#global-middleware) (`Http::globalRequestMiddleware()` and `Http::globalResponseMiddleware()` methods), you should be aware that *Variant 1* uses Laravel's `RequestSending` event to log HTTP requests. This event is fired **before** Global Middleware is executed. Therefore, any modifications to the request made by Global Middleware will not be logged. To overcome this, this package provides a middleware that you may add after your Global Middleware. + +You may add the middleware using the static `addRequestMiddleware()` method on the `HttpClientLogger` class: + +```php +use Onlime\LaravelHttpClientGlobalLogger\HttpClientLogger; + +HttpClientLogger::addRequestMiddleware(); +``` + +For example, you may add this to your `AppServiceProvider`'s `boot()` method after registering your Global Middleware: + +```php +use Illuminate\Support\Facades\Http; +use Onlime\LaravelHttpClientGlobalLogger\HttpClientLogger; + +Http::globalRequestMiddleware(fn ($request) => $request->withHeader( + 'User-Agent', 'My Custom User Agent' +)); + +HttpClientLogger::addRequestMiddleware(); +``` + ## Logging example By default, logs are written to a separate logfile `http-client.log`. @@ -146,9 +176,9 @@ So, my recommendation: If you need global logging without any extra configuratio ## Caveats - This package currently uses two different implementations for logging. In the preferred variant 1 (global logging), it is currently not possible to configure the [log channel name](https://laravel.com/docs/logging#configuring-the-channel-name) which defaults to current environment, such as `production` or `local`. If you with to use Laravel HTTP Client to access multiple different external APIs, it is nice to explicitely distinguish between them by different log channel names. - + As a workaround, I have implemented another way of logging through `Http::log()` method as mixin. But of course, we should combine both variants into a single one for a cleaner codebase. - + - Very basic obfuscation support using regex with lookbehind assertions (e.g. `/(?<=Authorization:\sBearer ).*/m`, modifying formatted log output. It's currently not possible to directly modify request headers or JSON data in request body. - Obfuscation currently only works in variant 1 (global logging). diff --git a/composer.json b/composer.json index 5a417f8..5aaee98 100644 --- a/composer.json +++ b/composer.json @@ -34,15 +34,32 @@ }, "require-dev": { "laravel/framework": "^10.0", - "laravel/pint": "^1.10" + "laravel/pint": "^1.10", + "orchestra/testbench": "^8.8", + "pestphp/pest": "^2.20", + "pestphp/pest-plugin-arch": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0" }, "autoload": { "psr-4": { "Onlime\\LaravelHttpClientGlobalLogger\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Onlime\\LaravelHttpClientGlobalLogger\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage", + "format": "vendor/bin/pint" + }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } }, "extra": { "laravel": { @@ -53,4 +70,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..2db54ad --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,38 @@ + + + + + tests + + + + + + + + + + + + + + + ./src + + + \ No newline at end of file diff --git a/src/HttpClientLogger.php b/src/HttpClientLogger.php new file mode 100644 index 0000000..ab02085 --- /dev/null +++ b/src/HttpClientLogger.php @@ -0,0 +1,46 @@ + tap($psrRequest, function (RequestInterface $psrRequest) { + // Wrap PSR-7 request into Laravel's HTTP Client Request object + $clientRequest = new Request($psrRequest); + + // Instantiate event and listener + $event = new RequestSending($clientRequest); + $listener = new LogRequestSending; + + // Handle event + $listener->handleEvent($event); + }) + ); + + self::$addedRequestMiddleware = true; + } + + public static function requestMiddlewareWasAdded(): bool + { + return self::$addedRequestMiddleware; + } +} diff --git a/src/Listeners/LogRequestSending.php b/src/Listeners/LogRequestSending.php index 02526c6..57f2c0c 100644 --- a/src/Listeners/LogRequestSending.php +++ b/src/Listeners/LogRequestSending.php @@ -5,16 +5,25 @@ use GuzzleHttp\MessageFormatter; use Illuminate\Http\Client\Events\RequestSending; use Illuminate\Support\Facades\Log; +use Onlime\LaravelHttpClientGlobalLogger\HttpClientLogger; use Psr\Http\Message\RequestInterface; class LogRequestSending { + /** + * Handle the event if the middleware was not added manually. + */ + public function handle(RequestSending $event): void + { + if (!HttpClientLogger::requestMiddlewareWasAdded()) { + $this->handleEvent($event); + } + } + /** * Handle the event. - * - * @return void */ - public function handle(RequestSending $event) + public function handleEvent(RequestSending $event): void { $obfuscate = config('http-client-global-logger.obfuscate.enabled'); $psrRequest = $event->request->toPsrRequest(); diff --git a/tests/HttpClientLoggerTest.php b/tests/HttpClientLoggerTest.php new file mode 100644 index 0000000..3b10f87 --- /dev/null +++ b/tests/HttpClientLoggerTest.php @@ -0,0 +1,37 @@ + $psrRequest->withHeader('X-Test', 'test') + ); + + HttpClientLogger::addRequestMiddleware(); + + $logger = Mockery::mock(LoggerInterface::class); + + Log::shouldReceive('channel')->with('http-client')->andReturn($logger); + + $logger->shouldReceive('info')->withArgs(function ($message) { + expect($message) + ->toContain('REQUEST: GET https://example.com') + ->and($message) + ->toContain('X-Test: test'); + + return true; + })->once()->andReturnSelf(); + + $logger->shouldReceive('info')->withArgs(function ($message) { + expect($message) + ->toContain('RESPONSE: HTTP/1.1 200 OK'); + + return true; + })->once()->andReturnSelf(); + + Http::fake()->get('https://example.com'); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..0be1cfa --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..be802e9 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,16 @@ +