diff --git a/.gitignore b/.gitignore index 9a43686..1e6806a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ testbench.yaml vendor node_modules .php-cs-fixer.cache +.phpunit.cache/ diff --git a/config/request-logger.php b/config/request-logger.php index 8ea95eb..5e1cf7a 100644 --- a/config/request-logger.php +++ b/config/request-logger.php @@ -1,6 +1,7 @@ + */ + protected array $logs = []; + + /** @inheritDoc */ + public function log(Request $request, $response, ?int $duration = null, ?int $memory = null): void + { + $this->logs[] = [ + 'uuid' => $request->getUniqueId(), + 'correlation_id' => $this->truncateToLength($request->getCorrelationId()), + 'client_request_id' => $this->truncateToLength($request->getClientRequestId()), + 'ip' => $request->ip(), + 'session' => $request->hasSession() ? $request->session()->getId() : null, + 'middleware' => array_values(optional($request->route())->gatherMiddleware() ?? []), + 'method' => $request->getMethod(), + 'route' => $this->truncateToLength(optional($request->route())->getName() ?? optional($request->route())->uri()), + 'path' => $this->truncateToLength($request->path()), + 'status' => $response->getStatusCode(), + 'headers' => $this->getFiltered($request->headers->all()) ?: null, + 'payload' => $this->getFiltered($request->input()) ?: null, + 'response_headers' => $this->getFiltered($response->headers->all()) ?: null, + 'response_body' => $this->getLoggableResponseContent($response), + 'duration' => $duration, + 'memory' => round($memory / 1024 / 1024, 2), + ]; + } + + /** + * Get logs + * + * @return array + */ + public function getLogs(): array + { + return $this->logs; + } +} diff --git a/src/EventServiceProvider.php b/src/EventServiceProvider.php deleted file mode 100644 index 9e8c4c7..0000000 --- a/src/EventServiceProvider.php +++ /dev/null @@ -1,26 +0,0 @@ - [ - LogRequest::class, - ], - ]; - - /** - * Register any events for your application. - * - * @return void - */ - public function boot() - { - parent::boot(); - } -} diff --git a/src/Listeners/LogRequest.php b/src/LogHandler.php similarity index 71% rename from src/Listeners/LogRequest.php rename to src/LogHandler.php index 6405469..bcba83d 100644 --- a/src/Listeners/LogRequest.php +++ b/src/LogHandler.php @@ -1,35 +1,34 @@ request->server('REQUEST_TIME_FLOAT'); + $startTime = defined('LARAVEL_START') ? LARAVEL_START : $request->server('REQUEST_TIME_FLOAT'); $duration = $startTime ? floor((microtime(true) - $startTime) * 1000) : null; $memory = memory_get_peak_usage(true); - if ($this->shouldLog($event->request, $event->response)) { - $event->request->enableLog(); + if ($this->shouldLog($request, $response)) { + $request->enableLog(); } - foreach (array_unique($event->request->attributes->get('log', [])) as $driver) { + foreach (array_unique($request->attributes->get('log', [])) as $driver) { RequestLoggerFacade::driver($driver)->log( - $event->request, - $event->response, + $request, + $response, $duration, $memory ); diff --git a/src/Models/RequestLog.php b/src/Models/RequestLog.php index f7b8581..73c5694 100644 --- a/src/Models/RequestLog.php +++ b/src/Models/RequestLog.php @@ -4,23 +4,19 @@ use Bilfeldt\RequestLogger\Contracts\RequestLoggerInterface; use Bilfeldt\RequestLogger\Database\Factories\RequestLogFactory; -use Bilfeldt\RequestLogger\RequestLoggerFacade; +use Bilfeldt\RequestLogger\Traits\Loggable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\MassPrunable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\Http\Response; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\Date; -use Illuminate\Support\Str; -use Illuminate\View\View; class RequestLog extends Model implements RequestLoggerInterface { use HasFactory; use MassPrunable; + use Loggable; /** * The attributes that are mass assignable. @@ -137,73 +133,6 @@ protected function getRequestTeam(Request $request): ?Model return null; } - protected function getLoggableResponseContent(\Symfony\Component\HttpFoundation\Response $response): array - { - $content = $response->getContent(); - - if (is_string($content)) { - if (is_array(json_decode($content, true)) && - json_last_error() === JSON_ERROR_NONE) { - return $this->contentWithinLimits($content) - ? $this->getFiltered(json_decode($content, true)) - : ['Purged By bilfeldt/laravel-request-logger']; - } - - if (Str::startsWith(strtolower($response->headers->get('Content-Type')), 'text/plain')) { - return $this->contentWithinLimits($content) ? [$content] : ['purge' => 'bilfeldt/laravel-request-logger']; - } - } - - if ($response instanceof RedirectResponse) { - return ['redirect' => $response->getTargetUrl()]; - } - - if ($response instanceof Response && $response->getOriginalContent() instanceof View) { - return [ - 'view' => $response->getOriginalContent()->getPath(), - //'data' => $this->extractDataFromView($response->getOriginalContent()), - ]; - } - - return ['html' => 'non-json']; - } - - protected function contentWithinLimits(string $content): bool - { - return intdiv(mb_strlen($content), 1000) <= 64; - } - - protected function getFiltered(array $data) - { - return $this->replaceParameters($data, RequestLoggerFacade::getFilters()); - } - - protected function replaceParameters(array $array, array $hidden, string $value = '********'): array - { - foreach ($hidden as $parameter) { - if (Arr::get($array, $parameter)) { - Arr::set($array, $parameter, '********'); - } - } - - return $array; - } - - protected function truncateToLength(?string $string, int $length = 255): ?string - { - if (! $string) { - return $string; - } - - $truncator = '...'; - - if (mb_strwidth($string, 'UTF-8') <= $length) { - return $string; - } - - return Str::limit($string, $length - mb_strwidth($truncator, 'UTF-8'), $truncator); - } - protected static function newFactory() { return RequestLogFactory::new(); diff --git a/src/RequestLogger.php b/src/RequestLogger.php index b41be04..d073fb5 100755 --- a/src/RequestLogger.php +++ b/src/RequestLogger.php @@ -29,6 +29,11 @@ public function createNullDriver(): NullLogger return new NullLogger(); } + public function createArrayDriver(): ArrayLogger + { + return app()->make(ArrayLogger::class); + } + public function createModelDriver(): RequestLoggerInterface { $model = config('request-logger.drivers.model.class'); diff --git a/src/RequestLoggerServiceProvider.php b/src/RequestLoggerServiceProvider.php index 172fa1e..d5cb517 100644 --- a/src/RequestLoggerServiceProvider.php +++ b/src/RequestLoggerServiceProvider.php @@ -16,8 +16,6 @@ class RequestLoggerServiceProvider extends ServiceProvider public function register() { $this->mergeConfigFrom(__DIR__.'/../config/request-logger.php', 'request-logger'); - - $this->app->register(EventServiceProvider::class); } /** @@ -40,9 +38,13 @@ public function boot() ]); } + $this->app->terminating(new LogHandler); + $this->registerMiddlewareAlias(); $this->bootMacros(); + $this->app->scoped(ArrayLogger::class); + // TODO: Register command PruneRequestLogsCommand::class); // TODO: Register EventServiceProvider::class } diff --git a/src/Traits/Loggable.php b/src/Traits/Loggable.php new file mode 100644 index 0000000..abfbb0e --- /dev/null +++ b/src/Traits/Loggable.php @@ -0,0 +1,80 @@ +getContent(); + + if (is_string($content)) { + if (is_array(json_decode($content, true)) && + json_last_error() === JSON_ERROR_NONE) { + return $this->contentWithinLimits($content) + ? $this->getFiltered(json_decode($content, true)) + : ['Purged By bilfeldt/laravel-request-logger']; + } + + if (Str::startsWith(strtolower($response->headers->get('Content-Type')), 'text/plain')) { + return $this->contentWithinLimits($content) ? [$content] : ['purge' => 'bilfeldt/laravel-request-logger']; + } + } + + if ($response instanceof RedirectResponse) { + return ['redirect' => $response->getTargetUrl()]; + } + + if ($response instanceof Response && $response->getOriginalContent() instanceof View) { + return [ + 'view' => $response->getOriginalContent()->getPath(), + //'data' => $this->extractDataFromView($response->getOriginalContent()), + ]; + } + + return ['html' => 'non-json']; + } + + protected function contentWithinLimits(string $content): bool + { + return intdiv(mb_strlen($content), 1000) <= 64; + } + + protected function getFiltered(array $data) + { + return $this->replaceParameters($data, RequestLoggerFacade::getFilters()); + } + + protected function replaceParameters(array $array, array $hidden, string $value = '********'): array + { + foreach ($hidden as $parameter) { + if (Arr::get($array, $parameter)) { + Arr::set($array, $parameter, '********'); + } + } + + return $array; + } + + protected function truncateToLength(?string $string, int $length = 255): ?string + { + if (! $string) { + return $string; + } + + $truncator = '...'; + + if (mb_strwidth($string, 'UTF-8') <= $length) { + return $string; + } + + return Str::limit($string, $length - mb_strwidth($truncator, 'UTF-8'), $truncator); + } +} diff --git a/tests/Feature/LogHandlerTest.php b/tests/Feature/LogHandlerTest.php new file mode 100644 index 0000000..af314cc --- /dev/null +++ b/tests/Feature/LogHandlerTest.php @@ -0,0 +1,47 @@ +assertEmpty($logger->getLogs()); + + $this + ->get(route('test')) + ->assertOk(); + + $logs = $logger->getLogs(); + + $this->assertNotEmpty($logs); + $this->assertCount(1, $logs); + + $this->assertArrayHasKey('uuid', $logs[0]); + $this->assertArrayHasKey('correlation_id', $logs[0]); + $this->assertArrayHasKey('client_request_id', $logs[0]); + $this->assertArrayHasKey('ip', $logs[0]); + $this->assertArrayHasKey('session', $logs[0]); + $this->assertArrayHasKey('middleware', $logs[0]); + $this->assertArrayHasKey('method', $logs[0]); + $this->assertArrayHasKey('route', $logs[0]); + $this->assertArrayHasKey('path', $logs[0]); + $this->assertArrayHasKey('status', $logs[0]); + $this->assertArrayHasKey('headers', $logs[0]); + $this->assertArrayHasKey('payload', $logs[0]); + $this->assertArrayHasKey('response_headers', $logs[0]); + $this->assertArrayHasKey('response_body', $logs[0]); + $this->assertArrayHasKey('duration', $logs[0]); + $this->assertArrayHasKey('memory', $logs[0]); + + $this->assertEquals($logs[0]['method'], 'GET'); + $this->assertEquals($logs[0]['route'], 'test'); + $this->assertEquals($logs[0]['path'], '/'); + $this->assertEquals($logs[0]['status'], 200); + } +} diff --git a/tests/Feature/LogRequestListenerTest.php b/tests/Feature/LogRequestListenerTest.php deleted file mode 100644 index fe09f91..0000000 --- a/tests/Feature/LogRequestListenerTest.php +++ /dev/null @@ -1,21 +0,0 @@ -set('database.default', 'testing'); + config()->set('request-logger.default', 'array'); $migration = include __DIR__.'/../database/migrations/create_request_logs_table.php.stub'; $migration->up(); } + + protected function defineRoutes($router) + { + $router->get('/', static fn () => 'Test!')->middleware(LogRequestMiddleware::class)->name('test'); + } }