diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..af2d5d2 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,113 @@ +# Upgrade Guide + +## Upgrading to 4.x + +This guide covers upgrading `binarybuilds/laritor-client` to 4.x. + +### Upgrade the package + +Update your Composer constraint, then refresh the lock file: + +```sh +composer require binarybuilds/laritor-client:^4.0 --update-with-all-dependencies +``` + +### Optional: Update custom event filters + +> This step is required only if your application uses a custom Laritor override filter class. If you are unsure how to upgrade your custom override class, rename your current override class, publish the new override class by following https://laritor.com/docs/customization and make any necessary changes after publishing. + +4.x moves event filtering from the point where an event is recorded to just before the event batch is sent. Filters can therefore use final status and duration values. It also replaces the recording and payload environment variables from `config/laritor.php` with methods on an override class. Keep filters side-effect free, since they run while Laritor prepares a batch for delivery. + +Configure 4.x filters in a class extending `BinaryBuilds\LaritorClient\Override\DefaultOverride`. The override receives the request, response, status, duration, user, and other completed-event data needed to make context-aware decisions. + +| Filter or setting | 3.x signature / environment variable | 4.x override method | +| --- | --- | --- | +| Outbound request filter | `recordOutboundRequest($url)` | `recordOutboundRequest($url, $statusCode, $duration)` | +| Query filter | `recordQuery($query, $duration)` | `recordQuery($query, $duration, $path)` | +| Queued-job filter | `recordQueuedJob($job)` | `recordQueuedJob(string $connection, string $queue, string $job, string $status, $duration)` | +| Request filter | `recordRequest($request)` | `recordRequest($request, $response, $status, $duration, $user)` | +| Command / scheduled-task filter | `recordCommandOrScheduledTask($command)` | `recordCommandOrScheduledTask(string $command, string $status, $duration)` | +| Mail filter | `recordMail($message)` | `recordMail($mailable, $to, $subject)` | +| Log filter | _Not available_ | `recordLog($level, $message, array $context = [])` | +| Log level | `LARITOR_LOG_LEVEL` | `recordLog($level, $message, array $context = [])` | +| Context | `LARITOR_RECORD_CONTEXT` | `recordRequestContext()`, `recordCommandContext()`, `recordScheduledTaskContext()`, `recordQueuedJobContext()`, `recordLogContext()` | +| Database schema | `LARITOR_RECORD_DB_SCHEMA` | `recordDatabaseSchema()` | +| Query bindings | `LARITOR_RECORD_QUERY_BINDINGS` | `recordQueryBindings($query, $duration, $path)` | +| Request query string | `LARITOR_RECORD_QUERY_STRING` | `recordRequestQueryParameters()` | +| Request headers / body | `LARITOR_RECORD_REQUEST_HEADERS` / `LARITOR_RECORD_REQUEST_BODY` | `recordRequestHeaders()` / `recordRequestBody()` | +| Response headers / body | `LARITOR_RECORD_REQUEST_RESPONSE_HEADERS` / `LARITOR_RECORD_REQUEST_RESPONSE_BODY` | `recordResponseHeaders()` / `recordResponseBody()` | +| Session data | `LARITOR_RECORD_SESSION_DATA` | `recordSessionData()` | +| Outbound-request headers / body | `LARITOR_RECORD_OUTBOUND_REQUEST_HEADERS` / `LARITOR_RECORD_OUTBOUND_REQUEST_BODY` | `recordOutboundRequestHeaders()` / `recordOutboundRequestBody()` | +| Outbound response headers / body | `LARITOR_RECORD_OUTBOUND_REQUEST_RESPONSE_HEADERS` / `LARITOR_RECORD_OUTBOUND_REQUEST_RESPONSE_BODY` | `recordOutboundRequestResponseHeaders()` / `recordOutboundRequestResponseBody()` | +| Whitelisted vendors | `LARITOR_WHITELISTED_VENDORS` | `whitelistedVendors(): array` | + +If your application implements `LaritorOverride` directly, implement every new payload/context method in the table as well as `recordLog()`. Extending `DefaultOverride` is the recommended migration path: only update the methods you need. + +For example, this override retains the 3.x-style “only errors and above” log policy and disables request headers and session data: + +```php +namespace App\Laritor; + +use BinaryBuilds\LaritorClient\Override\DefaultOverride; + +class LaritorDataFilter extends DefaultOverride +{ + public function recordLog($level, $message, array $context = []): bool + { + return in_array(strtolower($level), ['error', 'critical', 'alert', 'emergency'], true); + } + + public function recordRequestHeaders($request, $response, $status, $duration, $user): bool + { + return false; + } + + public function recordSessionData($request, $response, $status, $duration, $user): bool + { + return false; + } +} +``` + +Bind the override in an application service provider (typically in `register`): + +```php +use App\Laritor\LaritorDataFilter; +use BinaryBuilds\LaritorClient\Override\LaritorOverride; + +$this->app->bind(LaritorOverride::class, LaritorDataFilter::class); +``` + +Review the defaults before deploying. `DefaultOverride` records request/response headers and session data by default; request and response bodies remain disabled. Existing redaction still applies, but applications with stricter data-collection requirements should explicitly return `false` from the relevant methods. + +For example, a request filter can exclude successful health checks while retaining failures: + +```php +public function recordRequest($request, $response, $status, $duration, $user): bool +{ + return ! $request->is('health') || $status >= 400; +} +``` + +### Use a generated filter preset (optional) + +The filter generator now accepts an optional preset and creates `App\Laritor\LaritorDataFilter`: + +```sh +# Full observability (default) +php artisan make:laritor-filter + +# Capture data associated with failures and slow operations +php artisan make:laritor-filter issues-only + +# Capture only exception-related data +php artisan make:laritor-filter exceptions-only +``` + +Bind the generated class as shown above. If a file with that name already exists, review and merge its customizations rather than overwriting it. + +### Other behavior changes + +- The default for `LARITOR_INGEST_EVENTS_WITHOUT_OCCURRENCE` is now `true`. Set it explicitly to `false` if you need the former default behavior. +- Cache events now include the cache store name and a `duration` field. +- The default filters omit Laritor's own cache keys, Laritor HTTP ingestion requests and routes, `QueueHealthCheck` jobs, and Laritor/internal Artisan commands, in addition to common framework and monitoring noise. diff --git a/config/laritor.php b/config/laritor.php index 7d77dd9..a7b6b77 100644 --- a/config/laritor.php +++ b/config/laritor.php @@ -19,51 +19,7 @@ 'server_name' => env('LARITOR_SERVER_NAME'), - 'log_level' => env('LARITOR_LOG_LEVEL', 'debug'), - 'max_events' => env('LARITOR_MAX_EVENTS_PER_OCCURRENCE', 5000), - 'context' => env('LARITOR_RECORD_CONTEXT', true), - - 'db_schema' => env('LARITOR_RECORD_DB_SCHEMA', true), - - 'query_bindings' => env('LARITOR_RECORD_QUERY_BINDINGS', true), - - 'requests' => [ - - 'query_string' => env('LARITOR_RECORD_QUERY_STRING', true), - - 'body' => env('LARITOR_RECORD_REQUEST_BODY', false), - - 'headers' => env('LARITOR_RECORD_REQUEST_HEADERS', false), - - 'response_headers' => env('LARITOR_RECORD_REQUEST_RESPONSE_HEADERS', false), - - 'response_body' => env('LARITOR_RECORD_REQUEST_RESPONSE_BODY', false), - - 'rate_limit' => [ - 'enabled' => env('LARITOR_RATE_LIMIT_REQUESTS', false), - - 'attempts' => env('LARITOR_RATE_LIMIT_REQUESTS_ATTEMPTS', 5), - ], - ], - - 'outbound_requests' => [ - - 'body' => env('LARITOR_RECORD_OUTBOUND_REQUEST_BODY', false), - - 'headers' => env('LARITOR_RECORD_OUTBOUND_REQUEST_HEADERS', false), - - 'response_headers' => env('LARITOR_RECORD_OUTBOUND_REQUEST_RESPONSE_HEADERS', false), - - 'response_body' => env('LARITOR_RECORD_OUTBOUND_REQUEST_RESPONSE_BODY', false), - ], - - 'session' => [ - 'data' => env('LARITOR_RECORD_SESSION_DATA', false), - ], - - 'whitelisted_vendors' => env('LARITOR_WHITELISTED_VENDORS', ''), - - 'ingest_events_without_occurrence' => env('LARITOR_INGEST_EVENTS_WITHOUT_OCCURRENCE', false), + 'ingest_events_without_occurrence' => env('LARITOR_INGEST_EVENTS_WITHOUT_OCCURRENCE', true), ]; \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 9536b2c..e3a4584 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,15 +13,6 @@ - - - - - - - - - diff --git a/src/Commands/DataFilterMakeCommand.php b/src/Commands/DataFilterMakeCommand.php index 788574d..12e8b57 100644 --- a/src/Commands/DataFilterMakeCommand.php +++ b/src/Commands/DataFilterMakeCommand.php @@ -3,6 +3,7 @@ namespace BinaryBuilds\LaritorClient\Commands; use Illuminate\Console\GeneratorCommand; +use Symfony\Component\Console\Input\InputArgument; class DataFilterMakeCommand extends GeneratorCommand { @@ -34,7 +35,11 @@ class DataFilterMakeCommand extends GeneratorCommand */ protected function getStub() { - return __DIR__.'/../../stubs/LaritorDataFilter.stub'; + return match ($this->argument('type')) { + 'issues-only' => __DIR__.'/../../stubs/IssuesOnlyDataFilter.stub', + 'exceptions-only' => __DIR__.'/../../stubs/ExceptionsOnlyDataFilter.stub', + default => __DIR__.'/../../stubs/FullObservabilityDataFilter.stub' + }; } /** @@ -50,7 +55,9 @@ protected function getDefaultNamespace($rootNamespace) protected function getArguments() { - return []; + return [ + ['type', InputArgument::OPTIONAL, 'The type of the filter', 'full-observability'], + ]; } protected function getNameInput() diff --git a/src/Commands/SyncCommand.php b/src/Commands/SyncCommand.php index 836a33e..62a2354 100644 --- a/src/Commands/SyncCommand.php +++ b/src/Commands/SyncCommand.php @@ -2,6 +2,7 @@ namespace BinaryBuilds\LaritorClient\Commands; +use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use BinaryBuilds\LaritorClient\SendOutputToLaritor; use Illuminate\Console\Command; use BinaryBuilds\LaritorClient\Helpers\DatabaseHelper; @@ -61,11 +62,7 @@ public function handle( $health_checks = $healthCheckHelper->getHealthChecks(); - $schema = []; - - if ( config('laritor.db_schema') ) { - $schema = $databaseHelper->getSchema(); - } + $schema = FilterHelper::recordDatabaseSchema() ? $databaseHelper->getSchema() : []; $response = $laritor->sync([ 'scheduled_tasks' => $scheduled_tasks, diff --git a/src/Helpers/DataHelper.php b/src/Helpers/DataHelper.php index 3fba15f..06383ad 100644 --- a/src/Helpers/DataHelper.php +++ b/src/Helpers/DataHelper.php @@ -41,7 +41,7 @@ public static function redactData($text) public static function getRedactedContext() { - if (config('laritor.context') && class_exists(\Illuminate\Support\Facades\Context::class)) { + if (class_exists(\Illuminate\Support\Facades\Context::class)) { return app(DataRedactor::class)->redactArray( \Illuminate\Support\Facades\Context::all() ); diff --git a/src/Helpers/FilterHelper.php b/src/Helpers/FilterHelper.php index dfa0173..002f97c 100644 --- a/src/Helpers/FilterHelper.php +++ b/src/Helpers/FilterHelper.php @@ -3,10 +3,24 @@ namespace BinaryBuilds\LaritorClient\Helpers; use BinaryBuilds\LaritorClient\Override\LaritorOverride; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Str; class FilterHelper { + public static $ignoredCommands = [ + 'horizon', 'pulse:', 'db:seed', 'optimize', 'schedule:work', 'schedule:run', + 'schedule:finish', 'package:discover', 'event:cache', 'view:cache', + 'config:cache', 'queue:work', 'queue:listen', 'octane:install', + 'auth:clear-resets', 'config:cache', 'horizon:snapshot', + 'horizon:status', 'horizon:supervisor', 'inertia:start-ssr', + 'invoke-serialized-closure', 'model:prune', 'nightwatch:agent', + 'nightwatch:status', 'queue:monitor', 'reverb:start', + 'schedule:list', 'laritor:sync', 'laritor:send-metrics', + 'vendor:publish' + ]; + public static function recordEvent(callable $callable, $default = true) { return rescue(function () use ($callable){ @@ -16,7 +30,8 @@ public static function recordEvent(callable $callable, $default = true) public static function recordCacheHit($cacheKey): bool { - return static::recordEvent(function () use ($cacheKey) { + return ! Str::startsWith($cacheKey, ['laritor']) && + static::recordEvent(function () use ($cacheKey) { return app(LaritorOverride::class)->recordCacheHit($cacheKey); }); } @@ -28,38 +43,41 @@ public static function recordException($exception): bool }); } - public static function recordOutboundRequest($url): bool + public static function recordOutboundRequest($url, $status_code, $duration): bool { - return static::recordEvent(function () use ($url) { - return app(LaritorOverride::class)->recordOutboundRequest($url); + return ! Str::contains($url, 'laritor.net') && + static::recordEvent(function () use ($url, $status_code, $duration) { + return app(LaritorOverride::class)->recordOutboundRequest($url, $status_code, $duration); }); } - public static function recordQuery($query, $duration): bool + public static function recordQuery($query, $duration, $path): bool { - return static::recordEvent(function () use ($query, $duration) { - return app(LaritorOverride::class)->recordQuery($query, $duration); + return static::recordEvent(function () use ($query, $duration, $path) { + return app(LaritorOverride::class)->recordQuery($query, $duration, $path); }); } - public static function recordQueuedJob($job): bool + public static function recordQueuedJob(string $connection, string $queue, string $job, string $status, $duration): bool { - return static::recordEvent(function () use ($job) { - return app(LaritorOverride::class)->recordQueuedJob($job); + return ! Str::contains($job, 'QueueHealthCheck') && + static::recordEvent(function () use ($connection, $queue, $job, $status, $duration) { + return app(LaritorOverride::class)->recordQueuedJob($connection, $queue, $job, $status, $duration); }); } - public static function recordRequest($request): bool + public static function recordRequest($request, $response, int $status, $duration): bool { - return static::recordEvent(function () use ($request) { - return app(LaritorOverride::class)->recordRequest($request); + return !$request->is('laritor/*') && static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordRequest($request, $response, $status, $duration, Auth::user()); }); } - public static function recordCommandOrScheduledTask($command): bool + public static function recordCommandOrScheduledTask(string $command, string $status, $duration): bool { - return static::recordEvent(function () use ($command) { - return app(LaritorOverride::class)->recordCommandOrScheduledTask($command); + return ! Str::contains($command, self::$ignoredCommands) && + static::recordEvent(function () use ($command, $status, $duration) { + return app(LaritorOverride::class)->recordCommandOrScheduledTask($command, $status, $duration); }); } @@ -70,10 +88,10 @@ public static function recordTaskScheduler(): bool }); } - public static function recordMail($message): bool + public static function recordMail($mailable, $to, $subject): bool { - return static::recordEvent(function () use ($message) { - return app(LaritorOverride::class)->recordMail($message); + return static::recordEvent(function () use ($mailable, $to, $subject) { + return app(LaritorOverride::class)->recordMail($mailable, $to, $subject); }); } @@ -91,10 +109,141 @@ public static function recordFeatureFlag($flag, $scope): bool }); } + public static function recordLog($level, $message, array $context): bool + { + return static::recordEvent(function () use ($level, $message, $context) { + return app(LaritorOverride::class)->recordLog($level, $message, $context); + }); + } + public static function isBot($request): bool { return static::recordEvent(function () use ($request) { return app(LaritorOverride::class)->isBot($request); }, false); } + + public static function recordCommandContext(string $command, string $status, $duration): bool + { + return static::recordEvent(function () use ($command, $status, $duration) { + return app(LaritorOverride::class)->recordCommandContext($command, $status, $duration); + }, true); + } + + public static function recordScheduledTaskContext(string $task, string $status, $duration): bool + { + return static::recordEvent(function () use ($task, $status, $duration) { + return app(LaritorOverride::class)->recordScheduledTaskContext($task, $status, $duration); + }, true); + } + + public static function recordRequestContext($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordRequestContext($request, $response, $status, $duration, Auth::user()); + }, true); + } + + public static function recordLogContext($level, $message): bool + { + return static::recordEvent(function () use ($level, $message) { + return app(LaritorOverride::class)->recordLogContext($level, $message); + }, true); + } + + public static function recordQueuedJobContext(string $connection, string $queue, string $job, string $status, $duration): bool + { + return static::recordEvent(function () use ($connection, $queue, $job, $status, $duration) { + return app(LaritorOverride::class)->recordQueuedJobContext($connection, $queue, $job, $status, $duration); + }, true); + } + + public static function recordDatabaseSchema(): bool + { + return static::recordEvent(function () { + return app(LaritorOverride::class)->recordDatabaseSchema(); + }, true); + } + + public static function recordQueryBindings($query, $duration, $path): bool + { + return static::recordEvent(function () use ($query, $duration, $path) { + return app(LaritorOverride::class)->recordQueryBindings($query, $duration, $path); + }, true); + } + + public static function recordRequestQueryParameters($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordRequestQueryParameters($request, $response, $status, $duration, Auth::user()); + }, true); + } + + public static function recordRequestHeaders($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordRequestHeaders($request, $response, $status, $duration, Auth::user()); + }, true); + } + + public static function recordRequestBody($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordRequestBody($request, $response, $status, $duration, Auth::user()); + }, false); + } + + public static function recordResponseHeaders($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordResponseHeaders($request, $response, $status, $duration, Auth::user()); + }, true); + } + + public static function recordResponseBody($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordResponseBody($request, $response, $status, $duration, Auth::user()); + }, false); + } + + public static function recordSessionData($request, $response, $status, $duration): bool + { + return static::recordEvent(function () use ($request, $response, $status, $duration) { + return app(LaritorOverride::class)->recordSessionData($request, $response, $status, $duration, Auth::user()); + }, true); + } + + public static function recordOutboundRequestHeaders($url, $status_code, $duration): bool + { + return static::recordEvent(function () use ($url, $status_code, $duration) { + return app(LaritorOverride::class)->recordOutboundRequestHeaders($url, $status_code, $duration); + }, true); + } + + public static function recordOutboundRequestBody($url, $status_code, $duration): bool + { + return static::recordEvent(function () use ($url, $status_code, $duration) { + return app(LaritorOverride::class)->recordOutboundRequestBody($url, $status_code, $duration); + }, false); + } + + public static function recordOutboundRequestResponseHeaders($url, $status_code, $duration): bool + { + return static::recordEvent(function () use ($url, $status_code, $duration) { + return app(LaritorOverride::class)->recordOutboundRequestResponseHeaders($url, $status_code, $duration); + }, true); + } + + public static function recordOutboundRequestResponseBody($url, $status_code, $duration): bool + { + return static::recordEvent(function () use ($url, $status_code, $duration) { + return app(LaritorOverride::class)->recordOutboundRequestResponseBody($url, $status_code, $duration); + }, false); + } + + public static function whitelistedVendors(): array + { + return []; + } } \ No newline at end of file diff --git a/src/Helpers/ScheduledTaskHelper.php b/src/Helpers/ScheduledTaskHelper.php index 9e0e548..e82f8d3 100644 --- a/src/Helpers/ScheduledTaskHelper.php +++ b/src/Helpers/ScheduledTaskHelper.php @@ -25,7 +25,7 @@ public function getScheduledTasks() mb_strpos(Str::replace("'",'', $event->command), 'artisan') ); - if (in_array($task, ['artisan laritor:send-metrics']) || !FilterHelper::recordCommandOrScheduledTask($event->command)) { + if (in_array($task, ['artisan laritor:send-metrics'])) { continue; } diff --git a/src/Laritor.php b/src/Laritor.php index 7989bb3..9e27f87 100644 --- a/src/Laritor.php +++ b/src/Laritor.php @@ -2,7 +2,19 @@ namespace BinaryBuilds\LaritorClient; +use BinaryBuilds\LaritorClient\Helpers\FilterHelper; +use BinaryBuilds\LaritorClient\Recorders\CacheRecorder; +use BinaryBuilds\LaritorClient\Recorders\CommandRecorder; +use BinaryBuilds\LaritorClient\Recorders\ExceptionRecorder; +use BinaryBuilds\LaritorClient\Recorders\FeatureFlagRecorder; use BinaryBuilds\LaritorClient\Recorders\LogRecorder; +use BinaryBuilds\LaritorClient\Recorders\MailRecorder; +use BinaryBuilds\LaritorClient\Recorders\NotificationRecorder; +use BinaryBuilds\LaritorClient\Recorders\OutboundRequestRecorder; +use BinaryBuilds\LaritorClient\Recorders\QueryRecorder; +use BinaryBuilds\LaritorClient\Recorders\QueuedJobRecorder; +use BinaryBuilds\LaritorClient\Recorders\RequestRecorder; +use BinaryBuilds\LaritorClient\Recorders\ScheduledTaskRecorder; use Carbon\Carbon; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Event; @@ -13,7 +25,7 @@ class Laritor { - public const VERSION = '3.0.11'; + public const VERSION = '4.0.0'; /** * @var array @@ -40,6 +52,117 @@ class Laritor public const CUSTOM_EVENT = 'custom'; + private $exception = null; + + private int $requestStatus = 0; + + private int $requestDuration = 0; + + private $failedJob = null; + + private $failedCommand = null; + + private $jobDuration = 0; + + private $commandDuration = 0; + + public function getJobDuration() + { + return $this->jobDuration; + } + + public function setJobDuration($jobDuration): void + { + $this->jobDuration = $jobDuration; + } + + public function getCommandDuration() + { + return $this->commandDuration; + } + + public function setCommandDuration($commandDuration): void + { + $this->commandDuration = $commandDuration; + } + + /** + * @param $failedJob + * @return void + */ + public function setFailedJob($failedJob): void + { + $this->failedJob = $failedJob; + } + + /** + * @param $failedCommand + * @return void + */ + public function setFailedCommand($failedCommand): void + { + $this->failedCommand = $failedCommand; + } + + public function hasFailedJob(): bool + { + return !is_null($this->failedJob); + } + + public function hasFailedCommand(): bool + { + return !is_null($this->failedCommand); + } + + /** + * @param int $status + * @return void + */ + public function setRequestStatus(int $status): void + { + $this->requestStatus = $status; + } + + /** + * @param int $duration + * @return void + */ + public function setRequestDuration(int $duration): void + { + $this->requestDuration = $duration; + } + + public function getRequestStatus(): int + { + return $this->requestStatus; + } + + public function getRequestDuration(): int + { + return $this->requestDuration; + } + + /** + * @return \Throwable|null + */ + public function getException() + { + return $this->exception; + } + + public static function hasException(): bool + { + return !is_null(app(Laritor::class)->getException()); + } + + /** + * @param \Throwable|null $exception + */ + public function setException($exception): void + { + $this->exception = $exception; + } + /** * @return string */ @@ -243,6 +366,13 @@ public function reset() $this->response = 0; $this->context = 'BOOT'; $this->hasCustomLogs = false; + $this->exception = null; + $this->requestStatus = 0; + $this->requestDuration = 0; + $this->failedJob = null; + $this->failedCommand = null; + $this->jobDuration = 0; + $this->commandDuration = 0; } /** @@ -252,7 +382,7 @@ public function sendEvents() { rescue(function () { Event::fakeFor(function (){ - $this->cleanupEvents(); + $this->filterEvents(); if ($this->shouldSendEvents()) { $this->callApi(); } @@ -262,12 +392,42 @@ public function sendEvents() }, null, false); } - public function cleanupEvents() + public function filterEvents() { - if (isset($this->events['outbound_requests'])) { - $this->events['outbound_requests'] = array_values(array_filter($this->events['outbound_requests'], function ($event) { - return !empty($event['completed_at']); - })); + foreach ($this->events as $type => $events) { + $filtered = []; + foreach ($events as $event) { + $shouldAdd = match ($type){ + CacheRecorder::$eventType => FilterHelper::recordCacheHit($event['key']), + CommandRecorder::$eventType => FilterHelper::recordCommandOrScheduledTask($event['command'], $event['code'] === 0 ? 'completed' : 'failed', $event['duration'] ?? 0), + ExceptionRecorder::$eventType => FilterHelper::recordException($this->exception), + FeatureFlagRecorder::$eventType => FilterHelper::recordFeatureFlag($event['flag'], $event['feature_flag_scope']), + LogRecorder::$eventType => FilterHelper::recordLog($event['level'], $event['message'], $event['log_context']), + MailRecorder::$eventType => FilterHelper::recordMail($event['mailable'], $event['to'], $event['subject']), + NotificationRecorder::$eventType => FilterHelper::recordNotification($event['notifiable_instance'], $event['notification']), + OutboundRequestRecorder::$eventType => !empty($event['completed_at']) && FilterHelper::recordOutboundRequest($event['url'], $event['code'], $event['duration']), + QueryRecorder::$eventType => FilterHelper::recordQuery($event['query'], $event['time'], $event['path']), + QueuedJobRecorder::$eventType => FilterHelper::recordQueuedJob($event['connection'], $event['queue'], $event['job'], $event['status'], $event['duration'] ?? 0), + RequestRecorder::$eventType => FilterHelper::recordRequest($event['request_instance'], $event['response_instance'], $event['response']['status_code'], $event['request']['duration']), + ScheduledTaskRecorder::$eventType => FilterHelper::recordCommandOrScheduledTask($event['task'], $event['status'], $event['duration'] ?? 0), + default => false + }; + + if ($shouldAdd) { + unset($event['feature_flag_scope']); + unset($event['notifiable_instance']); + unset($event['request_instance']); + unset($event['response_instance']); + + $filtered[] = $event; + } + } + + if (!empty($filtered)) { + $this->events[$type] = $filtered; + } else { + unset($this->events[$type]); + } } } @@ -378,25 +538,6 @@ public function shouldSendEvents() } } - if (! $hasOccurrence) { - return false; - } - - if (app()->runningInConsole() || ! $this->isRateLimiterEnabled() ) { - return true; - } - - $key = 'laritor-'.Str::slug(request()->path()); - if (! RateLimiter::tooManyAttempts($key, config('laritor.requests.rate_limit.attempts') ) ) { - RateLimiter::hit($key); - return true; - } - - return false; - } - - public function isRateLimiterEnabled() - { - return (bool)config('laritor.requests.rate_limit.enabled', false); + return $hasOccurrence; } } diff --git a/src/Override/DefaultOverride.php b/src/Override/DefaultOverride.php index 5814e86..e2051a4 100644 --- a/src/Override/DefaultOverride.php +++ b/src/Override/DefaultOverride.php @@ -2,12 +2,10 @@ namespace BinaryBuilds\LaritorClient\Override; -use Illuminate\Contracts\Queue\Job; use Illuminate\Http\Request; use Illuminate\Notifications\Notification; use Illuminate\Support\Str; use Jaybizzle\CrawlerDetect\CrawlerDetect; -use Symfony\Component\Mime\Email; class DefaultOverride implements LaritorOverride { @@ -50,20 +48,23 @@ public function recordException($exception): bool } /** - * @param string $url + * @param $url + * @param $status_code + * @param $duration * @return bool */ - public function recordOutboundRequest($url): bool + public function recordOutboundRequest($url, $status_code, $duration): bool { return true; } /** - * @param string $query - * @param int $duration + * @param $query + * @param $duration + * @param $path * @return bool */ - public function recordQuery($query, $duration): bool + public function recordQuery($query, $duration, $path): bool { $ignore = [ "`".config('session.table')."`", @@ -81,19 +82,27 @@ public function recordQuery($query, $duration): bool } /** - * @param Job $job + * @param string $connection + * @param string $queue + * @param string $job + * @param string $status + * @param int $duration * @return bool */ - public function recordQueuedJob($job): bool + public function recordQueuedJob(string $connection, string $queue, string $job, string $status, $duration): bool { return true; } /** - * @param Request $request + * @param $request + * @param $response + * @param $status + * @param $duration + * @param $user * @return bool */ - public function recordRequest($request): bool + public function recordRequest($request, $response, $status, $duration, $user): bool { $ignore = [ 'telescope/*'. @@ -115,9 +124,11 @@ public function recordRequest($request): bool /** * @param string $command + * @param string $status + * @param int $duration * @return bool */ - public function recordCommandOrScheduledTask($command): bool + public function recordCommandOrScheduledTask(string $command, string $status, $duration): bool { return true; } @@ -131,10 +142,12 @@ public function recordTaskScheduler(): bool } /** - * @param Email $message + * @param $mailable + * @param $to + * @param $subject * @return bool */ - public function recordMail($message): bool + public function recordMail($mailable, $to, $subject): bool { return true; } @@ -159,6 +172,17 @@ public function recordFeatureFlag($flag, $scope): bool return true; } + /** + * @param $level + * @param $message + * @param array $context + * @return bool + */ + public function recordLog($level, $message, array $context = []): bool + { + return true; + } + /** * @param Request $request * @return bool @@ -169,4 +193,94 @@ public function isBot($request): bool $crawler = new CrawlerDetect(); return $crawler->isCrawler($userAgent); } + + public function recordCommandContext(string $command, string $status, $duration): bool + { + return true; + } + + public function recordScheduledTaskContext(string $task, string $status, $duration): bool + { + return true; + } + + public function recordRequestContext($request, $response, $status, $duration, $user): bool + { + return true; + } + + public function recordLogContext($level, $message): bool + { + return true; + } + + public function recordQueuedJobContext(string $connection, string $queue, string $job, string $status, $duration): bool + { + return true; + } + + public function recordDatabaseSchema(): bool + { + return true; + } + + public function recordQueryBindings($query, $duration, $path): bool + { + return true; + } + + public function recordRequestQueryParameters($request, $response, $status, $duration, $user): bool + { + return true; + } + + public function recordRequestHeaders($request, $response, $status, $duration, $user): bool + { + return true; + } + + public function recordRequestBody($request, $response, $status, $duration, $user): bool + { + return false; + } + + public function recordResponseHeaders($request, $response, $status, $duration, $user): bool + { + return true; + } + + public function recordResponseBody($request, $response, $status, $duration, $user): bool + { + return false; + } + + public function recordSessionData($request, $response, $status, $duration, $user): bool + { + return true; + } + + public function recordOutboundRequestHeaders($url, $status_code, $duration): bool + { + return true; + } + + public function recordOutboundRequestBody($url, $status_code, $duration): bool + { + return false; + } + + public function recordOutboundRequestResponseHeaders($url, $status_code, $duration): bool + { + return true; + } + + public function recordOutboundRequestResponseBody($url, $status_code, $duration): bool + { + return false; + } + + public function whitelistedVendors(): array + { + return []; + } } \ No newline at end of file diff --git a/src/Override/LaritorOverride.php b/src/Override/LaritorOverride.php index acca918..8455130 100644 --- a/src/Override/LaritorOverride.php +++ b/src/Override/LaritorOverride.php @@ -22,35 +22,48 @@ public function recordCacheHit($cacheKey): bool; public function recordException($exception): bool; /** - * @param string $url + * @param $url + * @param $status_code + * @param $duration * @return bool */ - public function recordOutboundRequest($url): bool; + public function recordOutboundRequest($url, $status_code, $duration): bool; /** - * @param string $query - * @param int $duration + * @param $query + * @param $duration + * @param $path * @return bool */ - public function recordQuery($query, $duration): bool; + public function recordQuery($query, $duration, $path): bool; /** - * @param Job $job + * @param string $connection + * @param string $queue + * @param string $job + * @param string $status + * @param int $duration * @return bool */ - public function recordQueuedJob($job): bool; + public function recordQueuedJob(string $connection, string $queue, string $job, string $status, int $duration): bool; /** - * @param Request $request + * @param $request + * @param $response + * @param $status + * @param $duration + * @param $user * @return bool */ - public function recordRequest($request): bool; + public function recordRequest($request, $response, $status, $duration, $user): bool; /** * @param string $command + * @param string $status + * @param int $duration * @return bool */ - public function recordCommandOrScheduledTask($command): bool; + public function recordCommandOrScheduledTask(string $command, string $status, $duration): bool; /** * @return bool @@ -58,10 +71,12 @@ public function recordCommandOrScheduledTask($command): bool; public function recordTaskScheduler(): bool; /** - * @param Email $message + * @param $mailable + * @param $to + * @param $subject * @return bool */ - public function recordMail($message): bool; + public function recordMail($mailable, $to, $subject): bool; /** * @param mixed $notifiable @@ -77,9 +92,59 @@ public function recordNotification($notifiable, $notification): bool; */ public function recordFeatureFlag($flag, $scope): bool; + /** + * @param $level + * @param $message + * @param array $context + * @return bool + */ + public function recordLog($level, $message, array $context = []): bool; + /** * @param Request $request * @return bool */ public function isBot($request): bool; + + public function recordCommandContext(string $command, string $status, $duration): bool; + + public function recordScheduledTaskContext(string $task, string $status, $duration): bool; + + public function recordRequestContext($request, $response, $status, $duration, $user): bool; + + public function recordLogContext($level, $message): bool; + + public function recordQueuedJobContext(string $connection, string $queue, string $job, string $status, $duration): bool; + + public function recordDatabaseSchema(): bool; + + /** + * @param $query + * @param $duration + * @param $path + * @return bool + */ + public function recordQueryBindings($query, $duration, $path): bool; + + public function recordRequestQueryParameters($request, $response, $status, $duration, $user): bool; + + public function recordRequestHeaders($request, $response, $status, $duration, $user): bool; + + public function recordRequestBody($request, $response, $status, $duration, $user): bool; + + public function recordResponseHeaders($request, $response, $status, $duration, $user): bool; + + public function recordResponseBody($request, $response, $status, $duration, $user): bool; + + public function recordSessionData($request, $response, $status, $duration, $user): bool; + + public function recordOutboundRequestHeaders($url, $status_code, $duration): bool; + + public function recordOutboundRequestBody($url, $status_code, $duration): bool; + + public function recordOutboundRequestResponseHeaders($url, $status_code, $duration): bool; + + public function recordOutboundRequestResponseBody($url, $status_code, $duration): bool; + + public function whitelistedVendors(): array; } \ No newline at end of file diff --git a/src/Override/TestOverride.php b/src/Override/TestOverride.php index 490ab3b..3be7bae 100644 --- a/src/Override/TestOverride.php +++ b/src/Override/TestOverride.php @@ -4,7 +4,7 @@ class TestOverride extends DefaultOverride { - public function recordRequest($request): bool + public function recordRequest($request, $response, $status, $duration, $user): bool { $ignore = [ 'laritor-job', @@ -24,4 +24,24 @@ public function recordException($exception): bool { return !request()->is('laritor-failed-job'); } + + public function recordOutboundRequestBody($url, $status_code, $duration): bool + { + return true; + } + + public function recordOutboundRequestResponseBody($url, $status_code, $duration): bool + { + return true; + } + + public function recordRequestBody($request, $response, $status, $duration, $user): bool + { + return true; + } + + public function recordResponseBody($request, $response, $status, $duration, $user): bool + { + return true; + } } \ No newline at end of file diff --git a/src/Recorders/CacheRecorder.php b/src/Recorders/CacheRecorder.php index 86018a0..be01ab9 100644 --- a/src/Recorders/CacheRecorder.php +++ b/src/Recorders/CacheRecorder.php @@ -2,12 +2,10 @@ namespace BinaryBuilds\LaritorClient\Recorders; -use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use Illuminate\Cache\Events\CacheHit; use Illuminate\Cache\Events\CacheMissed; use Illuminate\Cache\Events\KeyForgotten; use Illuminate\Cache\Events\KeyWritten; -use Illuminate\Support\Str; class CacheRecorder extends Recorder { @@ -32,14 +30,12 @@ class CacheRecorder extends Recorder */ public function trackEvent($event) { - if ( Str::startsWith($event->key, ['laritor']) || - !FilterHelper::recordCacheHit($event->key) - ) { - return; - } - $type = null; - if ($event instanceof CacheHit) { + if (class_exists(\Illuminate\Cache\Events\RetrievingKey::class) && + $event instanceof \Illuminate\Cache\Events\RetrievingKey) { + $type = 'RETRIEVING'; + } + elseif ($event instanceof CacheHit) { $type = 'HIT'; } elseif ($event instanceof CacheMissed) { $type = 'MISS'; @@ -49,11 +45,42 @@ public function trackEvent($event) $type = 'DELETE'; } - $this->laritor->pushEvent(static::$eventType, [ - 'key' => $event->key, - 'type' => $type, - 'occurred_at' => now()->format('Y-m-d H:i:s'), - 'context' => $this->laritor->getContext() - ]); + $eventFound = false; + if ($type !== 'RETRIEVING') { + $events = collect($this->laritor->getEvents(static::$eventType)) + ->map(function ($added) use ($event, $type, &$eventFound) { + if ($added['type'] === 'RETRIEVING' && $added['key'] === $event->key) { + $eventFound = true; + $added['type'] = $type; + $added['duration'] = microtime(true) - $added['timestamp']; + } + return $added; + }); + + $this->laritor->addEvents(static::$eventType, $events); + } + + if (!$eventFound) { + $this->laritor->pushEvent(static::$eventType, [ + 'key' => $event->key, + 'type' => $type, + 'store' => property_exists($event, 'storeName') ? $event->storeName : config('cache.default'), + 'duration' => 0, + 'occurred_at' => now()->format('Y-m-d H:i:s'), + 'context' => $this->laritor->getContext() + ]); + } + } + + /** + * @return void + */ + public static function registerRecorder() + { + if (class_exists(\Illuminate\Cache\Events\RetrievingKey::class)) { + self::$events[] = \Illuminate\Cache\Events\RetrievingKey::class; + } + + parent::registerRecorder(); } } diff --git a/src/Recorders/CommandRecorder.php b/src/Recorders/CommandRecorder.php index 75eed86..aee3138 100644 --- a/src/Recorders/CommandRecorder.php +++ b/src/Recorders/CommandRecorder.php @@ -33,10 +33,6 @@ class CommandRecorder extends Recorder */ public function trackEvent($event) { - if ($this->ignore($event->command) || !FilterHelper::recordCommandOrScheduledTask($event->command)) { - return; - } - if ($event instanceof CommandStarting ) { $this->start($event); } elseif ($event instanceof CommandFinished ) { @@ -93,11 +89,17 @@ public function finish(CommandFinished $event) )->firstWhere('completed_at', '=',null); if ($command) { - $command['duration'] = $command['started_at']->diffInMilliseconds(); + $duration = $command['started_at']->diffInMilliseconds(); + $this->laritor->setCommandDuration($duration); + if ($event->exitCode > 0) { + $this->laritor->setFailedCommand($event->command); + } + + $command['duration'] = $duration; $command['completed_at'] = now()->format('Y-m-d H:i:s'); $command['started_at'] = $command['started_at']->format('Y-m-d H:i:s'); $command['code'] = $event->exitCode; - $command['custom_context'] = DataHelper::getRedactedContext(); + $command['custom_context'] = FilterHelper::recordCommandContext($event->command, $event->exitCode > 0 ? 'failed' : 'completed', $duration) ? DataHelper::getRedactedContext() : []; $command['output'] = app(CommandOutput::class)->getLines(); app(CommandOutput::class)->resetLines(); diff --git a/src/Recorders/ExceptionRecorder.php b/src/Recorders/ExceptionRecorder.php index 6f18684..65a4967 100644 --- a/src/Recorders/ExceptionRecorder.php +++ b/src/Recorders/ExceptionRecorder.php @@ -3,7 +3,7 @@ namespace BinaryBuilds\LaritorClient\Recorders; use BinaryBuilds\LaritorClient\Helpers\DataHelper; -use BinaryBuilds\LaritorClient\Helpers\FilterHelper; +use BinaryBuilds\LaritorClient\Laritor; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Support\Str; use BinaryBuilds\LaritorClient\Helpers\FileHelper; @@ -25,9 +25,7 @@ public function trackEvent($event) { $throwable = $event; - if (!FilterHelper::recordException($throwable)) { - return; - } + app(Laritor::class)->setException($throwable); $data = [ 'message' => DataHelper::redactData($throwable->getMessage()), diff --git a/src/Recorders/FeatureFlagRecorder.php b/src/Recorders/FeatureFlagRecorder.php index 83785e1..c04b4ae 100644 --- a/src/Recorders/FeatureFlagRecorder.php +++ b/src/Recorders/FeatureFlagRecorder.php @@ -2,7 +2,6 @@ namespace BinaryBuilds\LaritorClient\Recorders; -use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use BinaryBuilds\LaritorClient\Laritor; use Illuminate\Support\Facades\Event; @@ -19,11 +18,7 @@ class FeatureFlagRecorder extends Recorder */ public function trackEvent($event) { - if(!FilterHelper::recordFeatureFlag($event->feature, $event->scope)) { - return; - } - - self::recordFeatureCheck($event->feature, $event->value !== false); + self::recordFeatureCheck($event->feature, $event->scope, $event->value !== false); } public static function registerRecorder() @@ -33,13 +28,14 @@ public static function registerRecorder() } } - public static function recordFeatureCheck(string $feature, bool $active = true) + public static function recordFeatureCheck(string $feature, $scope = null, bool $active = true) { $laritor = app(Laritor::class); $laritor->pushEvent(self::$eventType, [ 'feature' => $feature, 'active' => $active, + 'feature_flag_scope' => $scope, 'context' => $laritor->getContext(), 'checked_at' => now()->toDateTimeString(), ]); diff --git a/src/Recorders/FetchesStackTrace.php b/src/Recorders/FetchesStackTrace.php index 26cb914..44e1f7c 100644 --- a/src/Recorders/FetchesStackTrace.php +++ b/src/Recorders/FetchesStackTrace.php @@ -2,6 +2,7 @@ namespace BinaryBuilds\LaritorClient\Recorders; +use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use Illuminate\Support\Str; trait FetchesStackTrace @@ -36,7 +37,7 @@ protected function getCallerFromStackTrace($forgetLines = 0) */ protected function whitelistedVendors(): array { - $whitelist = config('laritor.whitelisted_vendors', '') ? explode(',', config('laritor.whitelisted_vendors', '')) : []; + $whitelist = FilterHelper::whitelistedVendors(); return array_map(function ($path) { return 'vendor/'.$path; diff --git a/src/Recorders/LogRecorder.php b/src/Recorders/LogRecorder.php index c738e9e..5724519 100644 --- a/src/Recorders/LogRecorder.php +++ b/src/Recorders/LogRecorder.php @@ -3,6 +3,7 @@ namespace BinaryBuilds\LaritorClient\Recorders; use BinaryBuilds\LaritorClient\Helpers\DataHelper; +use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use Illuminate\Log\Events\MessageLogged; class LogRecorder extends Recorder @@ -25,35 +26,12 @@ class LogRecorder extends Recorder */ public function trackEvent($event) { - if(!$this->shouldRecordLog($event)) { - return; - } - $this->laritor->pushEvent(static::$eventType, [ 'level' => $event->level, 'message' => DataHelper::redactData($event->message), - 'log_context' => DataHelper::redactArray($event->context), + 'log_context' => FilterHelper::recordLogContext($event->level, $event->message) ? DataHelper::redactArray($event->context) : [], 'occurred_at' => now()->format('Y-m-d H:i:s'), 'context' => $this->laritor->getContext() ]); } - - public function shouldRecordLog($event) - { - $levels = [ - 'DEBUG' => 1, - 'NOTICE' => 2, - 'INFO' => 3, - 'WARNING' => 4, - 'ERROR' => 5, - 'ALERT' => 6, - 'CRITICAL' => 7, - 'EMERGENCY' => 8 - ]; - - $minIndex = $levels[strtoupper(config('laritor.log_level'))]; - $logIndex = $levels[strtoupper($event->level)]; - - return $logIndex >= $minIndex; - } } diff --git a/src/Recorders/MailRecorder.php b/src/Recorders/MailRecorder.php index bdde587..c9d9288 100644 --- a/src/Recorders/MailRecorder.php +++ b/src/Recorders/MailRecorder.php @@ -3,7 +3,6 @@ namespace BinaryBuilds\LaritorClient\Recorders; use BinaryBuilds\LaritorClient\Helpers\DataHelper; -use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Events\MessageSending; use Illuminate\Mail\Events\MessageSent; @@ -31,10 +30,6 @@ class MailRecorder extends Recorder */ public function trackEvent($event) { - if (!FilterHelper::recordMail($event->message)) { - return; - } - if ($event instanceof MessageSending ) { $this->sending($event); } diff --git a/src/Recorders/NotificationRecorder.php b/src/Recorders/NotificationRecorder.php index 1cad862..b5f59cd 100644 --- a/src/Recorders/NotificationRecorder.php +++ b/src/Recorders/NotificationRecorder.php @@ -3,9 +3,7 @@ namespace BinaryBuilds\LaritorClient\Recorders; use BinaryBuilds\LaritorClient\Helpers\DataHelper; -use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use Illuminate\Database\Eloquent\Model; -use Illuminate\Log\Events\MessageLogged; use Illuminate\Notifications\AnonymousNotifiable; use Illuminate\Notifications\Events\NotificationSending; use Illuminate\Notifications\Events\NotificationSent; @@ -35,10 +33,6 @@ class NotificationRecorder extends Recorder */ public function trackEvent($event) { - if (!FilterHelper::recordNotification($event->notifiable, $event->notification)) { - return; - } - if ($event instanceof NotificationSending ) { $this->sending($event); } @@ -56,6 +50,7 @@ public function sending(NotificationSending $event) 'id' => $event->notification->id, 'notification' => get_class($event->notification), 'notifiable' => $this->formatNotifiable($event->notifiable), + 'notifiable_instance' => $event->notifiable, 'context' => $this->laritor->getContext(), 'started_at' => now()->format('Y-m-d H:i:s'), 'completed_at' => null diff --git a/src/Recorders/OutboundRequestRecorder.php b/src/Recorders/OutboundRequestRecorder.php index b42b71f..28e80e5 100644 --- a/src/Recorders/OutboundRequestRecorder.php +++ b/src/Recorders/OutboundRequestRecorder.php @@ -9,7 +9,6 @@ use Illuminate\Http\Client\Events\ResponseReceived; use Illuminate\Http\Client\Request; use Illuminate\Http\Client\Response; -use Illuminate\Support\Str; class OutboundRequestRecorder extends Recorder @@ -46,11 +45,6 @@ public function trackEvent($event) */ public function sending(RequestSending $event) { - if ( Str::contains($event->request->url(), 'laritor.net') || - !FilterHelper::recordOutboundRequest($event->request->url())) { - return; - } - $this->laritor->pushEvent(static::$eventType, [ 'started_at' => now(), 'completed_at' => null, @@ -89,18 +83,20 @@ public function completeOutboundRequest($outboundRequestEvent) if ( $request['status'] === 'sent' && $request['url'] === $outboundRequestEvent->request->url() ) { $started = $request['started_at']; + $duration = $started->diffInMilliseconds(); + $status = $outboundRequestEvent instanceof ResponseReceived ? $outboundRequestEvent->response->status() : 0; $request['started_at'] = $started->format('Y-m-d H:i:s'); $request['completed_at'] = now()->format('Y-m-d H:i:s'); - $request['duration'] = $started->diffInMilliseconds(); - $request['code'] = $outboundRequestEvent instanceof ResponseReceived ? $outboundRequestEvent->response->status() : 0; + $request['duration'] = $duration; + $request['code'] = $status; $request['status'] = 'completed'; $request['request'] = [ - 'body' => $this->getRequestBody($outboundRequestEvent->request), - 'headers' => $this->getRequestHeaders($outboundRequestEvent->request), + 'body' => $this->getRequestBody($outboundRequestEvent->request, $status, $duration), + 'headers' => $this->getRequestHeaders($outboundRequestEvent->request, $status, $duration), ]; $request['response'] = [ - 'body' => $outboundRequestEvent instanceof ConnectionFailed ? false : $this->getResponseBody($outboundRequestEvent->response), - 'headers' => $outboundRequestEvent instanceof ConnectionFailed ? false : $this->getResponseHeaders($outboundRequestEvent->response), + 'body' => $outboundRequestEvent instanceof ConnectionFailed ? false : $this->getResponseBody($outboundRequestEvent->response, $outboundRequestEvent->request->url(), $status, $duration), + 'headers' => $outboundRequestEvent instanceof ConnectionFailed ? false : $this->getResponseHeaders($outboundRequestEvent->response, $outboundRequestEvent->request->url(), $status, $duration), ]; } @@ -110,29 +106,29 @@ public function completeOutboundRequest($outboundRequestEvent) $this->laritor->addEvents(static::$eventType, $outboundRequests); } - protected function getRequestBody(Request $request) + protected function getRequestBody(Request $request, $status, $duration) { - if (config('laritor.outbound_requests.body')) { + if (FilterHelper::recordOutboundRequestBody($request->url(), $status, $duration)) { return $request->isJson() ? DataHelper::redactArray(json_decode($request->body(), true)) : DataHelper::redactData($request->body()); } - return false; + return []; } - protected function getRequestHeaders(Request $request) + protected function getRequestHeaders(Request $request, $status, $duration) { - if (config('laritor.outbound_requests.headers')) { + if (FilterHelper::recordOutboundRequestHeaders($request->url(), $status, $duration)) { return DataHelper::redactHeaders($request->headers()); } - return false; + return []; } - protected function getResponseBody(Response $response) + protected function getResponseBody(Response $response, $url, $status, $duration) { - if (config('laritor.outbound_requests.response_body')) { + if (FilterHelper::recordOutboundRequestResponseBody($url, $status, $duration)) { $body = $response->json(); if (is_array($body)) { @@ -142,15 +138,15 @@ protected function getResponseBody(Response $response) return DataHelper::redactData($response->body()); } - return false; + return []; } - protected function getResponseHeaders(Response $response) + protected function getResponseHeaders(Response $response, $url, $status, $duration) { - if (config('laritor.outbound_requests.response_headers')) { + if (FilterHelper::recordOutboundRequestResponseHeaders($url, $status, $duration)) { return DataHelper::redactHeaders($response->headers()); } - return false; + return []; } } diff --git a/src/Recorders/QueryRecorder.php b/src/Recorders/QueryRecorder.php index 6a6cf0e..346b630 100644 --- a/src/Recorders/QueryRecorder.php +++ b/src/Recorders/QueryRecorder.php @@ -5,7 +5,6 @@ use BinaryBuilds\LaritorClient\Helpers\DataHelper; use BinaryBuilds\LaritorClient\Helpers\FilterHelper; use Illuminate\Database\Events\QueryExecuted; -use Illuminate\Support\Str; use BinaryBuilds\LaritorClient\Helpers\FileHelper; class QueryRecorder extends Recorder @@ -24,18 +23,15 @@ class QueryRecorder extends Recorder */ public function trackEvent($event) { - if (!FilterHelper::recordQuery($event->sql, $event->time)) { - return; - } - if($caller = $this->getCallerFromStackTrace()) { $time = $event->time; + $path = FileHelper::parseFileName($caller['file']) .'@'.$caller['line']; $query = [ 'query' => $event->sql, - 'bindings' => config('laritor.query_bindings') ? DataHelper::redactData($this->replaceBindings($event)) : null, + 'bindings' => FilterHelper::recordQueryBindings($event->sql, $time, $path) ? DataHelper::redactData($this->replaceBindings($event)) : null, 'time' => $time, - 'path' => FileHelper::parseFileName($caller['file']) .'@'.$caller['line'], + 'path' => $path, 'completed_at' => now()->format('Y-m-d H:i:s'), 'context' => $this->laritor->getContext() ]; diff --git a/src/Recorders/QueuedJobRecorder.php b/src/Recorders/QueuedJobRecorder.php index 79942f0..8cc4b94 100644 --- a/src/Recorders/QueuedJobRecorder.php +++ b/src/Recorders/QueuedJobRecorder.php @@ -4,7 +4,6 @@ use BinaryBuilds\LaritorClient\Helpers\DataHelper; use BinaryBuilds\LaritorClient\Helpers\FilterHelper; -use BinaryBuilds\LaritorClient\Jobs\QueueHealthCheck; use Carbon\Carbon; use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Events\JobProcessed; @@ -31,10 +30,6 @@ class QueuedJobRecorder extends Recorder */ public function trackEvent($event) { - if ($event->job instanceof QueueHealthCheck || !FilterHelper::recordQueuedJob($event->job)) { - return; - } - if ($event instanceof JobQueued ) { $this->queued($event); } @@ -42,6 +37,7 @@ public function trackEvent($event) $this->processing($event); } elseif ($event instanceof JobExceptionOccurred) { app(ExceptionRecorder::class)->handle($event->exception); + $this->laritor->setFailedJob($event->job); $this->complete($event); } elseif ($event instanceof JobProcessed ) { $this->complete($event); @@ -61,16 +57,18 @@ public function queued(JobQueued $event) $jobPayload = $event->payload(); } + $queue = $event->job->queue ?? config("queue.connections.{$event->connectionName}.queue", 'default'); + $jobName = isset($jobPayload['displayName']) ? $jobPayload['displayName'] : get_class($event->job); $this->laritor->pushEvent(static::$eventType, [ 'connection' => $event->connectionName, - 'queue' => $event->job->queue ?? config("queue.connections.{$event->connectionName}.queue", 'default'), - 'job' => isset($jobPayload['displayName']) ? $jobPayload['displayName'] : get_class($event->job), + 'queue' => $queue, + 'job' => $jobName, 'id' => $this->resolveJobId($event), 'delay' => isset($event->delay) ? $event->delay : ( isset($jobPayload['delay']) ? $jobPayload['delay'] : 0 ), 'queued_at' => now()->toDateTimeString(), 'status' => 'queued', 'context' => $this->laritor->getContext(), - 'custom_context' => DataHelper::getRedactedContext(), + 'custom_context' => FilterHelper::recordQueuedJobContext($event->connectionName, $queue, $jobName, 'queued', 0) ? DataHelper::getRedactedContext() : [], ]); } @@ -130,12 +128,15 @@ public function complete($event) $jobs = []; foreach ($this->laritor->getEvents(static::$eventType) as $job) { if (isset($job['id']) && $job['id'] === $this->resolveJobId($event)) { + $status = $event instanceof JobExceptionOccurred ? 'failed' : 'processed'; $start = Carbon::parse($job['started_at']); - $job['duration'] = $start->diffInMilliseconds(); + $duration = $start->diffInMilliseconds(); + $this->laritor->setJobDuration($duration); + $job['duration'] = $duration; $job['started_at'] = $start->toDateTimeString(); $job['completed_at'] = now()->toDateTimeString(); - $job['status'] = $event instanceof JobExceptionOccurred ? 'failed' : 'processed'; - $job['custom_context'] = DataHelper::getRedactedContext(); + $job['status'] = $status; + $job['custom_context'] = FilterHelper::recordQueuedJobContext($job['connection'], $job['queue'], $job['job'], $status, $duration) ? DataHelper::getRedactedContext() : []; } $jobs[] = $job; diff --git a/src/Recorders/RequestRecorder.php b/src/Recorders/RequestRecorder.php index 1761a31..c7a909b 100644 --- a/src/Recorders/RequestRecorder.php +++ b/src/Recorders/RequestRecorder.php @@ -32,10 +32,6 @@ public function trackEvent($event) $request = $event->request; $response = $event->response; - if ($request->is('laritor/*') || !FilterHelper::recordRequest($request)) { - return; - } - $isBot = FilterHelper::isBot($request); $this->laritor->responseRenderCompleted(isset($event->response->exception) ? $event->response->exception : null); @@ -48,30 +44,37 @@ public function trackEvent($event) 'data' => [] ]; + $status = $this->getStatusCode($response); + $this->laritor->setRequestDuration($duration); + $this->laritor->setRequestStatus($status); + if ($request->hasSession()) { $session['id'] = $request->session()->getId(); $session['name'] = $request->session()->getName(); - $session['data'] = config('laritor.session.data') ? $request->session()->all() : []; + $session['data'] = FilterHelper::recordSessionData($request, $response, $status, $duration) ? $request->session()->all() : []; } /** @phpstan-ignore-next-line */ $controller = $request->route() ? explode('@', optional($request->route())->getActionName()) : []; + $this->laritor->pushEvent(static::$eventType, [ + 'request_instance' => $request, + 'response_instance' => $response, 'request' => [ 'started_at' => now()->subMilliseconds($duration)->format('Y-m-d H:i:s'), 'completed_at' => now()->format('Y-m-d H:i:s'), 'duration' => $duration, 'memory' => round(memory_get_peak_usage(true) / 1024 / 1024, 1), - 'url' => $this->getUrl($request), + 'url' => $this->getUrl($request, $response, $status, $duration), 'size' => strlen($request->getContent()), - 'headers' => $this->getRequestHeaders($request), - 'body' => $this->getRequestBody($request), + 'headers' => $this->getRequestHeaders($request, $response, $status, $duration), + 'body' => $this->getRequestBody($request, $response, $status, $duration), ], 'response' => [ - 'status_code' => $this->getStatusCode($response), + 'status_code' => $status, 'size' => strlen($response->getContent()), - 'headers' => $this->getResponseHeaders($response), - 'body' => $this->getResponseBody($response), + 'headers' => $this->getResponseHeaders($request, $response, $status, $duration), + 'body' => $this->getResponseBody($request, $response, $status, $duration), ], 'session' => $session, 'user' => [ @@ -89,7 +92,7 @@ public function trackEvent($event) 'controller_method' => isset($controller[1]) ? $controller[1] : 'closure', 'method' => $request->method(), ], - 'custom_context' => $this->getContext($request), + 'custom_context' => $this->getContext($request, $response, $status, $duration), ]); } @@ -102,7 +105,7 @@ private function getStatusCode($response) return $response->getStatusCode(); } - private function getContext($request) + private function getContext($request, $response, $status, $duration) { $context = []; @@ -118,33 +121,36 @@ private function getContext($request) } } - return array_merge($context, DataHelper::getRedactedContext()); + return array_merge( + $context, + FilterHelper::recordRequestContext($request, $response, $status, $duration) ? DataHelper::getRedactedContext() : [] + ); } - protected function getRequestBody($request) + protected function getRequestBody($request, $response, $status, $duration) { - if (config('laritor.requests.body')) { + if (FilterHelper::recordRequestBody($request, $response, $status, $duration)) { $payload = $request->post(); return ! empty($payload) ? DataHelper::redactArray($payload) : DataHelper::redactData(trim($request->getContent())); } - return false; + return []; } - protected function getRequestHeaders($request) + protected function getRequestHeaders($request, $response, $status, $duration) { - if (config('laritor.requests.headers')) { + if (FilterHelper::recordRequestHeaders($request, $response, $status, $duration)) { return DataHelper::redactHeaders($request->headers->all()); } - return false; + return []; } - protected function getResponseBody($response) + protected function getResponseBody($request, $response, $status, $duration) { - if (config('laritor.requests.response_body')) { + if (FilterHelper::recordResponseBody($request, $response, $status, $duration)) { $body = $response->getContent(); @@ -157,16 +163,16 @@ protected function getResponseBody($response) return DataHelper::redactData($body); } - return false; + return []; } - protected function getResponseHeaders($response) + protected function getResponseHeaders($request, $response, $status, $duration) { - if (config('laritor.requests.response_headers')) { + if (FilterHelper::recordResponseHeaders($request, $response, $status, $duration)) { return DataHelper::redactHeaders($response->headers->all()); } - return false; + return []; } private function getAuthenticatedUser() @@ -190,7 +196,7 @@ private function getAuthenticatedUser() return $user; } - private function getUrl($request) + private function getUrl($request, $response, $status, $duration) { if ($this->isLivewireUpdateRequest($request)) { $url = ''; @@ -201,7 +207,7 @@ private function getUrl($request) $url = rtrim($fragments['path'], '/'); } - if (config('laritor.requests.query_string') && isset($fragments['query'])) { + if (FilterHelper::recordRequestQueryParameters($request, $response, $status, $duration) && isset($fragments['query'])) { $url .= '?' . $fragments['query']; } @@ -212,7 +218,7 @@ private function getUrl($request) } $query = ''; - if (config('laritor.requests.query_string')) { + if (FilterHelper::recordRequestQueryParameters($request, $response, $status, $duration)) { $query = $request->getQueryString(); $query = $query ? '?'.$query : ''; diff --git a/src/Recorders/ScheduledTaskRecorder.php b/src/Recorders/ScheduledTaskRecorder.php index 6301792..769788b 100644 --- a/src/Recorders/ScheduledTaskRecorder.php +++ b/src/Recorders/ScheduledTaskRecorder.php @@ -9,7 +9,6 @@ use Illuminate\Console\Events\ScheduledTaskSkipped; use Illuminate\Console\Events\ScheduledTaskStarting; use Illuminate\Console\Scheduling\CallbackEvent; -use Illuminate\Console\Scheduling\Event; use Illuminate\Support\Facades\Context; use Illuminate\Support\Str; @@ -30,18 +29,6 @@ class ScheduledTaskRecorder extends Recorder */ public function trackEvent($event) { - $task = Str::substr( - Str::replace("'",'', $event->task->command), - mb_strpos(Str::replace("'",'', $event->task->command), 'artisan') - ); - - if ( - in_array($task, ['artisan laritor:send-metrics', 'artisan laritor:sync']) || - !FilterHelper::recordCommandOrScheduledTask($event->task->command) - ) { - return; - } - if ($event instanceof ScheduledTaskStarting ) { $this->start($event); } elseif ($event instanceof ScheduledTaskFinished ) { @@ -102,11 +89,11 @@ public function start(ScheduledTaskStarting $event) public function skip(ScheduledTaskSkipped $event) { $event = $event->task; - + $task = $event instanceof CallbackEvent ? 'Closure' : $event->command; $payload = [ 'started_at' => now()->format('Y-m-d H:i:s'), 'duration' => 0, - 'task' => $event instanceof CallbackEvent ? 'Closure' : $event->command, + 'task' => $task, 'expression' => $event->expression, 'timezone' => $event->timezone, 'user' => $event->user, @@ -114,7 +101,7 @@ public function skip(ScheduledTaskSkipped $event) 'maintenance' => $event->evenInMaintenanceMode, 'one_server' => $event->onOneServer, 'status' => 'skipped', - 'custom_context' => DataHelper::getRedactedContext(), + 'custom_context' => FilterHelper::recordScheduledTaskContext($task, 'skipped', 0) ? DataHelper::getRedactedContext() : [], 'scheduled_at_timestamp' => microtime(true), ]; @@ -138,11 +125,12 @@ public function completeScheduledTask($event, $status) $task['task'] === ( $event instanceof CallbackEvent ? 'Closure' : $event->command) && $task['status'] === 'started' ) { + $duration = $task['started_at']->diffInMilliseconds(); $task['status'] = $status; - $task['duration'] = $task['started_at']->diffInMilliseconds(); + $task['duration'] = $duration; $task['completed_at'] = now()->format('Y-m-d H:i:s'); $task['started_at'] = $task['started_at']->format('Y-m-d H:i:s'); - $task['custom_context'] = DataHelper::getRedactedContext(); + $task['custom_context'] = FilterHelper::recordScheduledTaskContext($task, $status, $duration) ? DataHelper::getRedactedContext() : []; } return $task; diff --git a/stubs/ExceptionsOnlyDataFilter.stub b/stubs/ExceptionsOnlyDataFilter.stub new file mode 100644 index 0000000..1250768 --- /dev/null +++ b/stubs/ExceptionsOnlyDataFilter.stub @@ -0,0 +1,239 @@ +getRequestStatus() >= 400 || + $laritor->getRequestDuration() >= 1000; + } + + public function hasFailedJob() + { + $laritor = app(Laritor::class); + + return + $laritor->hasFailedJob() || + $laritor->getJobDuration() >= 60000; + } + + public function hasFailedCommand() + { + $laritor = app(Laritor::class); + + return + $laritor->hasFailedCommand() || + $laritor->getCommandDuration() >= 300000; + } + + /** + * @param string $cacheKey + * @return bool + */ + public function recordCacheHit($cacheKey): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException(); + } + + /** + * @param \Throwable $exception + * @return bool + */ + public function recordException($exception): bool + { + return true; + } + + /** + * @param $url + * @param $status_code + * @param $duration + * @return bool + */ + public function recordOutboundRequest($url, $status_code, $duration): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException() || + $status_code >= 300 || + $duration >= 1000; + } + + /** + * @param $query + * @param $duration + * @param $path + * @return bool + */ + public function recordQuery($query, $duration, $path): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException() || + $duration >= 200; + } + + /** + * @param string $connection + * @param string $queue + * @param string $job + * @param string $status + * @param int $duration + * @return bool + */ + public function recordQueuedJob(string $connection, string $queue, string $job, string $status, $duration): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedCommand() || + Laritor::hasException() || + strtolower($status) === 'failed' || + $duration >= 60000; + } + + /** + * @param $request + * @param $response + * @param $status + * @param $duration + * @param $user + * @return bool + */ + public function recordRequest($request, $response, $status, $duration, $user): bool + { + return + Laritor::hasException() || + $status >= 400 || + $duration >= 1000; + } + + /** + * @param string $command + * @param string $status + * @param int $duration + * @return bool + */ + public function recordCommandOrScheduledTask(string $command, string $status, $duration): bool + { + return strtolower($status) === 'failed' || $duration >= 60000; + } + + /** + * @return bool + */ + public function recordTaskScheduler(): bool + { + return parent::recordTaskScheduler(); + } + + /** + * @param $mailable + * @param $to + * @param $subject + * @return bool + */ + public function recordMail($mailable, $to, $subject): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException(); + } + + /** + * @param mixed $notifiable + * @param Notification $notification + * @return bool + */ + public function recordNotification($notifiable, $notification): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException(); + } + + /** + * @param string $flag + * @param mixed $scope + * @return bool + */ + public function recordFeatureFlag($flag, $scope): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException(); + } + + /** + * @param $level + * @param $message + * @param array $context + * @return bool + */ + public function recordLog($level, $message, array $context = []): bool + { + return + $this->hasFailedRequest() || + $this->hasFailedJob() || + $this->hasFailedCommand() || + Laritor::hasException() || + !in_array(strtoupper($level), ['INFO','DEBUG']); + } + + /** + * @param Request $request + * @return bool + */ + public function isBot($request): bool + { + return parent::isBot($request); + } + + public function recordCommandContext(string $command, string $status, $duration): bool + { + return self::recordCommandOrScheduledTask($command, $status, $duration); + } + + public function recordScheduledTaskContext(string $task, string $status, $duration): bool + { + return self::recordCommandOrScheduledTask($task, $status, $duration); + } + + public function recordRequestContext($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordLogContext($level, $message): bool + { + return self::recordLog($level, $message); + } + + public function recordQueuedJobContext(string $connection, string $queue, string $job, string $status, $duration): bool + { + return self::recordQueuedJob($connection, $queue, $job, $status, $duration); + } + + public function recordDatabaseSchema(): bool + { + return parent::recordDatabaseSchema(); + } + + public function recordQueryBindings($query, $duration, $path): bool + { + return self::recordQuery($query, $duration, $path); + } + + public function recordRequestQueryParameters($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordRequestHeaders($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordRequestBody($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordResponseHeaders($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordResponseBody($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordSessionData($request, $response, $status, $duration, $user): bool + { + return self::recordRequest($request, $response, $status, $duration, $user); + } + + public function recordOutboundRequestHeaders($url, $status_code, $duration): bool + { + return self::recordOutboundRequest($url, $status_code, $duration); + } + + public function recordOutboundRequestBody($url, $status_code, $duration): bool + { + return self::recordOutboundRequest($url, $status_code, $duration); + } + + public function recordOutboundRequestResponseHeaders($url, $status_code, $duration): bool + { + return self::recordOutboundRequest($url, $status_code, $duration); + } + + public function recordOutboundRequestResponseBody($url, $status_code, $duration): bool + { + return self::recordOutboundRequest($url, $status_code, $duration); + } + + public function whitelistedVendors(): array + { + return parent::whitelistedVendors(); + } +} \ No newline at end of file diff --git a/stubs/LaritorDataFilter.stub b/stubs/LaritorDataFilter.stub deleted file mode 100644 index 3f37286..0000000 --- a/stubs/LaritorDataFilter.stub +++ /dev/null @@ -1,122 +0,0 @@ -