├── .gitattributes ├── .gitignore ├── .phpunit.result.cache ├── LICENSE ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Casts │ └── ArrayOrText.php ├── Console │ └── Commands │ │ └── CleanRequestResponseLogs.php ├── Jobs │ └── LogResponseRequest.php ├── Middleware │ └── LogRequestsAndResponses.php ├── Models │ └── RequestResponseLog.php ├── Providers │ └── RequestResponseLoggerServiceProvider.php ├── Support │ └── Logging │ │ ├── Contracts │ │ └── ShouldLogContract.php │ │ ├── LogAll.php │ │ ├── LogClientErrorsOnly.php │ │ └── LogSuccessOnly.php ├── config │ └── log-requests-and-responses.php └── database │ └── migrations │ └── create_request_response_logs_table.php └── tests ├── LogClientErrorResponsesOnlyTest.php ├── LogSuccessfulResponsesOnlyTest.php ├── RequestResponseLoggerTest.php └── dummy └── test.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | .DS_Store -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"RequestResponseLoggerTest::canInstantiateClass":4,"RequestResponseLoggerTest::canInstantiateClassFromHelper":4,"RequestResponseLoggerTest::canInstantiateClassFromFacade":4,"RequestResponseLoggerTest::canSetNewEndPoint":4,"RequestResponseLoggerTest::canSwitchEnvironment":4,"RequestResponseLoggerTest::canGetVendorThroughCallMethod":4,"RequestResponseLoggerTest::canSkipParametersMethod":4,"RequestResponseLoggerTest::canGetVendorThroughMagicMethod":4,"RequestResponseLoggerTest::middlewareSavesALog":4,"RequestResponseLoggerTest::middlewareSavesHeaderData":4,"RequestResponseLoggerTest::middlewareRetrievesHeaderDataAsObject":5,"RequestResponseLoggerTest::middlewareRetrievesDataAsObject":4,"RequestResponseLoggerTest::middlewareSavesAGetRequest":4,"RequestResponseLoggerTest::middlewareSavesAPostRequest":4,"RequestResponseLoggerTest::middlewareSavesPostData":4,"RequestResponseLoggerTest::middlewareSavesJsonPostData":4,"RequestResponseLoggerTest::middlewareRetrievesJsonAsObject":4,"RequestResponseLoggerTest::middlewareSavesXmlRequestData":4,"RequestResponseLoggerTest::modelSuccessfulScopeWorks":4,"RequestResponseLoggerTest::middlewareSavesXmlPostData":4,"RequestResponseLoggerTest::modelFailedScopeWorks":4},"times":{"RequestResponseLoggerTest::canInstantiateClass":0.078,"RequestResponseLoggerTest::canInstantiateClassFromHelper":0.004,"RequestResponseLoggerTest::canInstantiateClassFromFacade":0.003,"RequestResponseLoggerTest::canSetNewEndPoint":0.003,"RequestResponseLoggerTest::canSwitchEnvironment":0.003,"RequestResponseLoggerTest::canGetVendorThroughCallMethod":0.003,"RequestResponseLoggerTest::canSkipParametersMethod":0.003,"RequestResponseLoggerTest::canGetVendorThroughMagicMethod":0.003,"RequestResponseLoggerTest::middlewareSavesALog":0.12,"RequestResponseLoggerTest::middlewareSavesAGetRequest":0.299,"RequestResponseLoggerTest::middlewareSavesAPostRequest":0.008,"RequestResponseLoggerTest::middlewareSavesPostData":0.008,"RequestResponseLoggerTest::middlewareSavesHeaderData":0.008,"RequestResponseLoggerTest::middlewareSavesJsonPostData":0.007,"RequestResponseLoggerTest::middlewareRetrievesHeaderDataAsObject":0.008,"RequestResponseLoggerTest::middlewareRetrievesDataAsObject":0.007,"RequestResponseLoggerTest::middlewareRetrievesJsonAsObject":0.008,"RequestResponseLoggerTest::middlewareSavesXmlRequestData":0.008,"RequestResponseLoggerTest::middlewareSavesXmlPostData":0.009,"RequestResponseLoggerTest::modelSuccessfulScopeWorks":0.007,"RequestResponseLoggerTest::modelFailedScopeWorks":0.007}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Mark Townsend 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Easily capture every incoming request and the corresponding outgoing response in your Laravel app. 6 | 7 | *This package is designed to work only with the Laravel framework.* 8 | 9 | ## Installation 10 | 11 | Install via composer: 12 | 13 | ``` 14 | composer require mtownsend/laravel-request-response-logger 15 | ``` 16 | 17 | ## Upgrading from v1.0.X -> v2.0.X 18 | 19 | [Please see the release notes here.](https://github.com/mtownsend5512/laravel-request-response-logger/releases/tag/2.0.0) 20 | 21 | ### Registering the service provider (Laravel users) 22 | 23 | For Laravel 5.4 and lower, add the following line to your ``config/app.php``: 24 | 25 | ```php 26 | /* 27 | * Package Service Providers... 28 | */ 29 | Mtownsend\RequestResponseLogger\Providers\RequestResponseLoggerServiceProvider::class, 30 | ``` 31 | 32 | For Laravel 5.5 and greater, the package will auto register the provider for you. 33 | 34 | 35 | ### Publish the migration and config files 36 | 37 | You will need to publish the migration and configuration file before you can begin using the package. To do so run the following in your console: 38 | 39 | ```` 40 | php artisan vendor:publish --provider="Mtownsend\RequestResponseLogger\Providers\RequestResponseLoggerServiceProvider" 41 | ```` 42 | 43 | Next, you need to run the migration for the new database table. Run the following in your console: 44 | 45 | ```` 46 | php artisan migrate 47 | ```` 48 | 49 | ### Setting up the middleware (important!) 50 | 51 | In order to begin logging requests and responses you will have to attach the middleware to the routes you want to log. The package *does not* make this assumption for you, since not everyone may want to log every route. 52 | 53 | Now, navigate to your `/app/Http/Kernel.php` 54 | 55 | You can choose which method you would like to use. We've provided a few different options below: 56 | 57 | #### OPTION 1: Bind the middleware to a name you can call only on the routes you want 58 | 59 | ```php 60 | protected $routeMiddleware = [ 61 | ... 62 | 'log.requests.responses' => \Mtownsend\RequestResponseLogger\Middleware\LogRequestsAndResponses::class, 63 | ]; 64 | ``` 65 | 66 | Then apply the named middleware to whatever routes you want: 67 | 68 | ```php 69 | Route::post('/some/route', SomeController::class)->middleware(['log.requests.responses']); 70 | ``` 71 | 72 | #### OPTION 2: Assign the middleware to a route group 73 | 74 | ```php 75 | protected $middlewareGroups = [ 76 | ... 77 | 'api' => [ 78 | ... 79 | \Mtownsend\RequestResponseLogger\Middleware\LogRequestsAndResponses::class, 80 | ], 81 | ]; 82 | ``` 83 | 84 | #### OPTION 3: Assign the middleware to every route 85 | 86 | ```php 87 | protected $middleware = [ 88 | ... 89 | \Mtownsend\RequestResponseLogger\Middleware\LogRequestsAndResponses::class, 90 | ]; 91 | ``` 92 | 93 | That's it! The middleware will log every incoming request it receives! 94 | 95 | ### Customizing your configuration (optional) 96 | 97 | This package provides a few customizations you can make. 98 | 99 | When you navigation to `app/config` you will see a `log-requests-and-responses.php` file. It will contain the following: 100 | 101 | ```php 102 | return [ 103 | 104 | /** 105 | * The model used to manage the database table for storing request and response logs. 106 | */ 107 | 'logging_model' => \Mtownsend\RequestResponseLogger\Models\RequestResponseLog::class, 108 | 109 | /** 110 | * When logging requests and responses, should the logging action be 111 | * passed off to the queue (true) or run synchronously (false)? 112 | */ 113 | 'logging_should_queue' => false, 114 | 115 | /** 116 | * If stored json should be transformed into an array when retrieved from the database. 117 | * Set to `false` to receive as a regular php object. 118 | */ 119 | 'get_json_values_as_array' => true, 120 | 121 | /** 122 | * The class responsible for determining if a request should be logged. 123 | * 124 | * Out of the box options are: 125 | * Mtownsend\RequestResponseLogger\Support\Logging\LogAll::class, 126 | * Mtownsend\RequestResponseLogger\Support\Logging\LogClientErrorsOnly::class, 127 | * Mtownsend\RequestResponseLogger\Support\Logging\LogSuccessOnly::class, 128 | */ 129 | 'should_log_handler' => \Mtownsend\RequestResponseLogger\Support\Logging\LogAll::class, 130 | 131 | ]; 132 | ``` 133 | 134 | ## Housekeeping 135 | 136 | You may want to utilize some housekeeping to prevent your logs table from getting too large. This package supplies a preregistered command for wiping the table clean. You may run it manually 137 | 138 | ```` 139 | php artisan request-response-logger:clean 140 | ```` 141 | 142 | You may also schedule it to be run automatically in `app/Console/Kernel.php`: 143 | 144 | ```php 145 | protected function schedule(Schedule $schedule) 146 | { 147 | ... 148 | $schedule->command('request-response-logger:clean')->quarterly(); 149 | } 150 | ``` 151 | 152 | # Advanced Usage 153 | 154 | ## Conditional logging 155 | 156 | This package provides support for specifying custom conditions before a request/response is logged to the database. 157 | 158 | It comes with 3 default options out of the box: 159 | 160 | * LogAll - log request & response 161 | * LogClientErrorsOnly - log only responses that have an http status code of 4XX 162 | * LogSuccessOnly - log only responses that have an http status code of 2XX 163 | * ...or your own! 164 | 165 | Creating your own conditional logic is pretty straightforward and can be done in 2 simple steps: 166 | 167 | 1. First, create a custom class that will perform your conditional checks for logging. For demonstration purposes let's say we're going to create a conditional logic check to only log requests made from external services and not your own web app. You can use the following code as a template. 168 | 169 | ```php 170 | request = $request; 185 | $this->response = $response; 186 | } 187 | 188 | /** 189 | * Return a truth-y value to log the request and response. 190 | * Return false-y value to skip logging. 191 | * 192 | * @return bool 193 | */ 194 | public function shouldLog(): bool 195 | { 196 | // Custom logic goes here... 197 | } 198 | } 199 | ``` 200 | 201 | 2. Open up your `config/log-requests-and-responses.php` and set the `should_log_handler` key to your class. 202 | 203 | ```php 204 | [ 205 | //... 206 | 'should_log_handler' => \App\Support\Logging\LogExternalRequests::class, 207 | ] 208 | ``` 209 | 210 | ...and that's it! Your custom logging logic will now be used any time the middleware is executed. 211 | 212 | ## Model scopes 213 | 214 | If you would like to work with the data you've logged, you may want to retrieve data based on the http code your app returned for that request. 215 | 216 | ```php 217 | use Mtownsend\RequestResponseLogger\Models\RequestResponseLog; 218 | 219 | // Get every logged item with an http response code of 2XX: 220 | RequestResponseLog::successful()->get(); 221 | 222 | // Get every logged item with an http response code that ISN'T 2XX: 223 | RequestResponseLog::failed()->get(); 224 | ``` 225 | 226 | ## Replacing the `RequestResponseLog` model with your own 227 | 228 | You may want to extend the base `RequestResponseLog` model with your own. This is entirely possible. 229 | 230 | First, create your own model 231 | 232 | ```` 233 | php artisan make:model RequestResponseLog 234 | ```` 235 | 236 | Then in your model, extend the base model: 237 | 238 | ```php 239 | \App\Models\RequestResponseLog::class, 255 | ``` 256 | 257 | Now the package will utilize your model instead of the default one. 258 | 259 | 260 | ## Testing 261 | 262 | You can run the tests with: 263 | 264 | ```bash 265 | vendor/bin/phpunit 266 | ``` 267 | 268 | ## License 269 | 270 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 271 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mtownsend/laravel-request-response-logger", 3 | "description": "Easily capture every incoming request and the corresponding outgoing response in your Laravel app.", 4 | "keywords": [ 5 | "request", 6 | "response", 7 | "logging", 8 | "debugging" 9 | ], 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Mark Townsend", 14 | "email": "mtownsend5512@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4": { 20 | "Mtownsend\\RequestResponseLogger\\": "src" 21 | }, 22 | "files": [] 23 | }, 24 | "repositories": [], 25 | "require": { 26 | "php": ">=7.0|^8.0" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^9.5", 30 | "orchestra/testbench": "^6.17" 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Mtownsend\\RequestResponseLogger\\Test\\": "tests/" 35 | } 36 | }, 37 | "scripts": { 38 | "post-autoload-dump": [ 39 | "@php ./vendor/bin/testbench package:discover --ansi" 40 | ] 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "Mtownsend\\RequestResponseLogger\\Providers\\RequestResponseLoggerServiceProvider" 46 | ] 47 | } 48 | }, 49 | "minimum-stability": "dev" 50 | } 51 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Casts/ArrayOrText.php: -------------------------------------------------------------------------------- 1 | isJson($value)) { 21 | $value = json_decode($value, config('log-requests-and-responses.get_json_values_as_array')); 22 | } 23 | return $value; 24 | } 25 | 26 | /** 27 | * Prepare the given value for storage. 28 | * 29 | * @param \Illuminate\Database\Eloquent\Model $model 30 | * @param string $key 31 | * @param mixed $value 32 | * @param array $attributes 33 | * @return mixed 34 | */ 35 | public function set($model, $key, $value = null, $attributes = []) 36 | { 37 | if (is_array($value)) { 38 | $value = json_encode($value); 39 | } 40 | return $value; 41 | } 42 | 43 | /** 44 | * Checks if the string is valid json. 45 | * 46 | * @param string $string 47 | * @return boolean 48 | */ 49 | public function isJson($string): bool 50 | { 51 | if (!is_string($string)) { 52 | return false; 53 | } 54 | json_decode($string); 55 | return json_last_error() === JSON_ERROR_NONE; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Console/Commands/CleanRequestResponseLogs.php: -------------------------------------------------------------------------------- 1 | truncate(); 42 | 43 | $this->info('Request and response log table has been cleaned.'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Jobs/LogResponseRequest.php: -------------------------------------------------------------------------------- 1 | data = $data; 26 | } 27 | 28 | /** 29 | * Execute the job. 30 | * 31 | * @return void 32 | */ 33 | public function handle(): void 34 | { 35 | $model = config('log-requests-and-responses.logging_model'); 36 | (new $model)->create($this->data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Middleware/LogRequestsAndResponses.php: -------------------------------------------------------------------------------- 1 | $request->method(), 35 | 'request_headers' => Collection::make($request->headers->all())->transform(function ($item) { 36 | return head($item); 37 | }) ?? [], 38 | 'request_body' => $this->handleRequestBody($request), 39 | 'request_url' => $request->url(), 40 | 'response_headers' => Collection::make($response->headers->all())->transform(function ($item) { 41 | return head($item); 42 | }) ?? [], 43 | 'response_body' => $response->getContent(), 44 | 'response_http_code' => $response->status() 45 | ]; 46 | 47 | $handler = config('log-requests-and-responses.should_log_handler'); 48 | $shouldLog = (new $handler($request, $response))->shouldLog(); 49 | 50 | if ($shouldLog) { 51 | $method = $this->dispatchType(); 52 | LogResponseRequest::$method($data); 53 | } 54 | } 55 | 56 | /** 57 | * Legacy support for older versions of Laravel that named the synchronous 58 | * `dispatchNow` as well as newer versions that use `dispatchSync`. 59 | * 60 | * @return string 61 | */ 62 | private function dispatchType(): string 63 | { 64 | if (app()->version() < 8 && config('log-requests-and-responses.logging_should_queue')) { 65 | return 'dispatchNow'; 66 | } elseif (app()->version() >= 8 && config('log-requests-and-responses.logging_should_queue')) { 67 | return 'dispatchSync'; 68 | } else { 69 | return 'dispatch'; 70 | } 71 | } 72 | 73 | /** 74 | * Determine if the incoming request is json, arrayable (by Laravel) 75 | * or is XML or some other form of plain text that isn't arrayble. 76 | * 77 | * @param \Illuminate\Http\Request $request 78 | * @return mixed 79 | */ 80 | private function handleRequestBody($request) 81 | { 82 | if (!empty($request->all())) { 83 | return $request->all(); 84 | } elseif (empty($request->all()) && !empty($request->getContent())) { 85 | return $request->getContent(); 86 | } 87 | 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Models/RequestResponseLog.php: -------------------------------------------------------------------------------- 1 | ArrayOrText::class, 17 | 'response_headers' => ArrayOrText::class, 18 | 'request_body' => ArrayOrText::class, 19 | 'response_body' => ArrayOrText::class, 20 | ]; 21 | 22 | /** 23 | * Scope a query to only include failed http code responses. 24 | * 25 | * @param \Illuminate\Database\Eloquent\Builder $query 26 | * @return \Illuminate\Database\Eloquent\Builder 27 | */ 28 | public function scopeFailed($query) 29 | { 30 | return $query->where('response_http_code', 'NOT LIKE', 2 . '%'); 31 | } 32 | 33 | /** 34 | * Scope a query to only include successful http code responses. 35 | * 36 | * @param \Illuminate\Database\Eloquent\Builder $query 37 | * @return \Illuminate\Database\Eloquent\Builder 38 | */ 39 | public function scopeSuccessful($query) 40 | { 41 | return $query->where('response_http_code', 'LIKE', 2 . '%'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Providers/RequestResponseLoggerServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__ . '/../config/log-requests-and-responses.php' => config_path('log-requests-and-responses.php') 21 | ], 'config'); 22 | 23 | if (! $this->migrationExists()) { 24 | $this->publishes([ 25 | __DIR__ . '/../database/migrations/create_request_response_logs_table.php' => database_path('migrations/' . date('Y_m_d_His_', time()) . 'create_request_response_logs_table.php') 26 | ], 'migrations'); 27 | } 28 | } 29 | 30 | /** 31 | * Register any application services. 32 | * 33 | * @return void 34 | */ 35 | public function register() 36 | { 37 | $this->commands([ 38 | CleanRequestResponseLogs::class 39 | ]); 40 | } 41 | 42 | /** 43 | * Determine if the user has already published the migration before. 44 | * 45 | * @return bool 46 | */ 47 | private function migrationExists() 48 | { 49 | $files = (new Filesystem())->files(database_path('migrations')); 50 | foreach ($files as $file) { 51 | if (Str::endsWith(basename($file), '_create_request_response_logs_table.php')) { 52 | return true; 53 | } else { 54 | continue; 55 | } 56 | } 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Support/Logging/Contracts/ShouldLogContract.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | $this->response = $response; 17 | } 18 | 19 | /** 20 | * Return a truth-y value to log the request and response. 21 | * Return false-y value to skip logging. 22 | * 23 | * @return bool 24 | */ 25 | public function shouldLog(): bool 26 | { 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Support/Logging/LogClientErrorsOnly.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | $this->response = $response; 17 | } 18 | 19 | /** 20 | * Return a truth-y value to log the request and response. 21 | * Return false-y value to skip logging. 22 | * 23 | * @return bool 24 | */ 25 | public function shouldLog(): bool 26 | { 27 | $code = (int) $this->response->status(); 28 | return (bool) ($code >= 400 && $code <= 499); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Support/Logging/LogSuccessOnly.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | $this->response = $response; 17 | } 18 | 19 | /** 20 | * Return a truth-y value to log the request and response. 21 | * Return false-y value to skip logging. 22 | * 23 | * @return bool 24 | */ 25 | public function shouldLog(): bool 26 | { 27 | $code = (int) $this->response->status(); 28 | return (bool) ($code >= 200 && $code <= 299); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/config/log-requests-and-responses.php: -------------------------------------------------------------------------------- 1 | \Mtownsend\RequestResponseLogger\Models\RequestResponseLog::class, 9 | 10 | /** 11 | * When logging requests and responses, should the logging action be 12 | * passed off to the queue (true) or run synchronously (false)? 13 | */ 14 | 'logging_should_queue' => false, 15 | 16 | /** 17 | * If stored json should be transformed into an array when retrieved from the database. 18 | * Set to `false` to receive as a regular php object. 19 | */ 20 | 'get_json_values_as_array' => true, 21 | 22 | /** 23 | * The class responsible for determining if a request should be logged. 24 | * 25 | * Out of the box options are: 26 | * Mtownsend\RequestResponseLogger\Support\Logging\LogAll::class, 27 | * Mtownsend\RequestResponseLogger\Support\Logging\LogClientErrorsOnly::class, 28 | * Mtownsend\RequestResponseLogger\Support\Logging\LogSuccessOnly::class, 29 | */ 30 | 'should_log_handler' => \Mtownsend\RequestResponseLogger\Support\Logging\LogAll::class, 31 | 32 | ]; 33 | -------------------------------------------------------------------------------- /src/database/migrations/create_request_response_logs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | 19 | $table->string('request_method')->nullable(); 20 | $table->longText('request_headers')->nullable(); 21 | $table->longText('request_body')->nullable(); 22 | $table->string('request_url')->nullable(); 23 | $table->longText('response_headers')->nullable(); 24 | $table->longText('response_body')->nullable(); 25 | $table->string('response_http_code')->nullable(); 26 | 27 | $table->nullableTimestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('request_response_logs'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/LogClientErrorResponsesOnlyTest.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 72 | $app['config']->set('database.connections.testbench', [ 73 | 'driver' => 'sqlite', 74 | 'database' => ':memory:', 75 | 'prefix' => '' 76 | ]); 77 | 78 | $app['config']->set('log-requests-and-responses', [ 79 | 'logging_model' => RequestResponseLog::class, 80 | 'logging_should_queue' => false, 81 | 'get_json_values_as_array' => true, 82 | 'should_log_handler' => LogClientErrorsOnly::class, 83 | ]); 84 | } 85 | 86 | /** 87 | * @param \Illuminate\Foundation\Application $app 88 | */ 89 | protected function setUpDatabase($app) 90 | { 91 | include_once __DIR__ . '/../src/database/migrations/create_request_response_logs_table.php'; 92 | 93 | (new CreateRequestResponseLogsTable())->up(); 94 | } 95 | 96 | /** 97 | * Define routes setup. 98 | * 99 | * @param \Illuminate\Routing\Router $router 100 | * 101 | * @return void 102 | */ 103 | protected function defineRoutes($router) 104 | { 105 | $router->any('/test/{code?}', function ($code = 200) { 106 | return response()->json([ 107 | 'status' => 'success', 108 | 'received' => request()->all() 109 | ], $code); 110 | })->middleware(LogRequestsAndResponses::class); 111 | } 112 | 113 | /** 114 | * Setup the test environment. 115 | */ 116 | protected function setUp(): void 117 | { 118 | // Code before application created. 119 | 120 | parent::setUp(); 121 | 122 | $this->setUpDatabase($this->app); 123 | 124 | // Code after application created. 125 | } 126 | 127 | /** 128 | * @test 129 | */ 130 | public function logClientErrorResponsesOnly() 131 | { 132 | $request = $this->post('/test/200'); // success 133 | $request = $this->post('/test/401'); // error 134 | $request = $this->post('/test/201'); // success 135 | $log = RequestResponseLog::count(); 136 | 137 | $this->assertSame($log, 1); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/LogSuccessfulResponsesOnlyTest.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 72 | $app['config']->set('database.connections.testbench', [ 73 | 'driver' => 'sqlite', 74 | 'database' => ':memory:', 75 | 'prefix' => '' 76 | ]); 77 | 78 | $app['config']->set('log-requests-and-responses', [ 79 | 'logging_model' => RequestResponseLog::class, 80 | 'logging_should_queue' => false, 81 | 'get_json_values_as_array' => true, 82 | 'should_log_handler' => LogSuccessOnly::class, 83 | ]); 84 | } 85 | 86 | /** 87 | * @param \Illuminate\Foundation\Application $app 88 | */ 89 | protected function setUpDatabase($app) 90 | { 91 | include_once __DIR__ . '/../src/database/migrations/create_request_response_logs_table.php'; 92 | 93 | (new CreateRequestResponseLogsTable())->up(); 94 | } 95 | 96 | /** 97 | * Define routes setup. 98 | * 99 | * @param \Illuminate\Routing\Router $router 100 | * 101 | * @return void 102 | */ 103 | protected function defineRoutes($router) 104 | { 105 | $router->any('/test/{code?}', function ($code = 200) { 106 | return response()->json([ 107 | 'status' => 'success', 108 | 'received' => request()->all() 109 | ], $code); 110 | })->middleware(LogRequestsAndResponses::class); 111 | } 112 | 113 | /** 114 | * Setup the test environment. 115 | */ 116 | protected function setUp(): void 117 | { 118 | // Code before application created. 119 | 120 | parent::setUp(); 121 | 122 | $this->setUpDatabase($this->app); 123 | 124 | // Code after application created. 125 | } 126 | 127 | /** 128 | * @test 129 | */ 130 | public function logSuccessfulResponsesOnly() 131 | { 132 | $request = $this->post('/test/401'); // error 133 | $request = $this->post('/test/200'); // success 134 | $request = $this->post('/test/404'); // error 135 | $log = RequestResponseLog::count(); 136 | 137 | $this->assertSame($log, 1); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/RequestResponseLoggerTest.php: -------------------------------------------------------------------------------- 1 | RequestResponseLog::class, 20 | 'logging_should_queue' => false, 21 | 'get_json_values_as_array' => true, 22 | 'should_log_handler' => LogAll::class, 23 | ]; 24 | 25 | /** 26 | * Ignore package discovery from. 27 | * 28 | * @return array 29 | */ 30 | public function ignorePackageDiscoveriesFrom() 31 | { 32 | return []; 33 | } 34 | 35 | /** 36 | * Get package providers. 37 | * 38 | * @param \Illuminate\Foundation\Application $app 39 | * 40 | * @return array 41 | */ 42 | protected function getPackageProviders($app) 43 | { 44 | return []; 45 | } 46 | 47 | /** 48 | * Override application aliases. 49 | * 50 | * @param \Illuminate\Foundation\Application $app 51 | * 52 | * @return array 53 | */ 54 | protected function overrideApplicationProviders($app) 55 | { 56 | return []; 57 | } 58 | 59 | /** 60 | * Get application timezone. 61 | * 62 | * @param \Illuminate\Foundation\Application $app 63 | * @return string|null 64 | */ 65 | protected function getApplicationTimezone($app) 66 | { 67 | return 'UTC'; 68 | } 69 | 70 | /** 71 | * Define environment setup. 72 | * 73 | * @param \Illuminate\Foundation\Application $app 74 | * @return void 75 | */ 76 | protected function defineEnvironment($app) 77 | { 78 | $app['config']->set('database.default', 'testbench'); 79 | $app['config']->set('database.connections.testbench', [ 80 | 'driver' => 'sqlite', 81 | 'database' => ':memory:', 82 | 'prefix' => '' 83 | ]); 84 | 85 | $app['config']->set('log-requests-and-responses', [ 86 | 'logging_model' => RequestResponseLog::class, 87 | 'logging_should_queue' => false, 88 | 'get_json_values_as_array' => true, 89 | 'should_log_handler' => LogAll::class, 90 | ]); 91 | } 92 | 93 | /** 94 | * @param \Illuminate\Foundation\Application $app 95 | */ 96 | protected function setUpDatabase($app) 97 | { 98 | include_once __DIR__ . '/../src/database/migrations/create_request_response_logs_table.php'; 99 | 100 | (new CreateRequestResponseLogsTable())->up(); 101 | } 102 | 103 | /** 104 | * Define routes setup. 105 | * 106 | * @param \Illuminate\Routing\Router $router 107 | * 108 | * @return void 109 | */ 110 | protected function defineRoutes($router) 111 | { 112 | $router->any('/test/{code?}', function ($code = 200) { 113 | return response()->json([ 114 | 'status' => 'success', 115 | 'received' => request()->all() 116 | ], $code); 117 | })->middleware(LogRequestsAndResponses::class); 118 | 119 | $router->any('/html/{code?}', function ($code = 200) { 120 | return response('

An HTML header

Some paragraph text below it

', $code); 121 | })->middleware(LogRequestsAndResponses::class); 122 | } 123 | 124 | /** 125 | * Setup the test environment. 126 | */ 127 | protected function setUp(): void 128 | { 129 | // Code before application created. 130 | 131 | parent::setUp(); 132 | 133 | $this->setUpDatabase($this->app); 134 | 135 | $this->requestHeaders = [ 136 | 'Accept' => 'application/json', 137 | 'Content-Type' => 'application/json', 138 | 'X-API-Token' => '39a0jg08j-4f8as0-f9a83jd' 139 | ]; 140 | 141 | $this->requestData = [ 142 | 'email' => 'john.doe@email.com', 143 | 'name' => 'John Doe', 144 | 'password' => 'secret' 145 | ]; 146 | 147 | // Code after application created. 148 | } 149 | 150 | /** @test */ 151 | public function middlewareSavesAGetRequest() 152 | { 153 | $request = $this->get('/test'); 154 | $log = RequestResponseLog::first(); 155 | 156 | $this->assertSame($log->request_method, 'GET'); 157 | } 158 | 159 | /** @test */ 160 | public function middlewareSavesAPostRequest() 161 | { 162 | $request = $this->post('/test'); 163 | $log = RequestResponseLog::first(); 164 | 165 | $this->assertSame($log->request_method, 'POST'); 166 | } 167 | 168 | /** @test */ 169 | public function middlewareSavesPostData() 170 | { 171 | $request = $this->post('/test', $this->requestData); 172 | $log = RequestResponseLog::first(); 173 | 174 | $this->assertSame($log->request_body['email'], $this->requestData['email']); 175 | } 176 | 177 | /** @test */ 178 | public function middlewareSavesJsonPostData() 179 | { 180 | $request = $this->postJson('/test', $this->requestData); 181 | $log = RequestResponseLog::first(); 182 | 183 | $this->assertSame($log->request_body['email'], $this->requestData['email']); 184 | } 185 | 186 | /** @test */ 187 | public function middlewareSavesXmlPostData() 188 | { 189 | $headers = array_merge($this->requestHeaders, [ 190 | 'Content-Type' => 'text/xml' 191 | ]); 192 | $xml = file_get_contents(__DIR__ . '/dummy/test.xml'); 193 | $request = $this->call('post', 194 | '/test', 195 | [], 196 | [], 197 | [], 198 | $this->transformHeadersToServerVars($headers), 199 | $xml 200 | ); 201 | $log = RequestResponseLog::first(); 202 | 203 | $this->assertSame($log->request_body, $xml); 204 | } 205 | 206 | /** @test */ 207 | public function middlewareSavesHeaderData() 208 | { 209 | $request = $this->postJson('/test', [], $this->requestHeaders); 210 | $log = RequestResponseLog::first(); 211 | 212 | $this->assertSame($log->request_headers['x-api-token'], $this->requestHeaders['X-API-Token']); 213 | } 214 | 215 | /** @test */ 216 | public function modelSuccessfulScopeWorks() 217 | { 218 | $badLog = RequestResponseLog::create([ 219 | 'response_http_code' => 401 220 | ]); 221 | $goodLogs = RequestResponseLog::insert([[ 222 | 'response_http_code' => 201, 223 | 'created_at' => now(), 224 | 'updated_at' => now() 225 | ], [ 226 | 'response_http_code' => 200, 227 | 'created_at' => now(), 228 | 'updated_at' => now() 229 | ]]); 230 | 231 | $this->assertSame(RequestResponseLog::successful()->count(), 2); 232 | } 233 | 234 | /** @test */ 235 | public function modelFailedScopeWorks() 236 | { 237 | $goodLog = RequestResponseLog::create([ 238 | 'response_http_code' => 200 239 | ]); 240 | $badLogs = RequestResponseLog::insert([[ 241 | 'response_http_code' => 401, 242 | 'created_at' => now(), 243 | 'updated_at' => now() 244 | ], [ 245 | 'response_http_code' => 500, 246 | 'created_at' => now(), 247 | 'updated_at' => now() 248 | ]]); 249 | 250 | $this->assertSame(RequestResponseLog::failed()->count(), 2); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /tests/dummy/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gambardella, Matthew 5 | XML Developer's Guide 6 | Computer 7 | 44.95 8 | 2000-10-01 9 | An in-depth look at creating applications 10 | with XML. 11 | 12 | 13 | Ralls, Kim 14 | Midnight Rain 15 | Fantasy 16 | 5.95 17 | 2000-12-16 18 | A former architect battles corporate zombies, 19 | an evil sorceress, and her own childhood to become queen 20 | of the world. 21 | 22 | --------------------------------------------------------------------------------