diff --git a/src/Hooks/Illuminate/Contracts/Http/Kernel.php b/src/Hooks/Illuminate/Contracts/Http/Kernel.php index 2b59751..b0c0eb9 100644 --- a/src/Hooks/Illuminate/Contracts/Http/Kernel.php +++ b/src/Hooks/Illuminate/Contracts/Http/Kernel.php @@ -61,11 +61,12 @@ protected function hookHandle(): bool ->setAttribute(TraceAttributes::HTTP_REQUEST_BODY_SIZE, $request->header('Content-Length')) ->setAttribute(TraceAttributes::URL_SCHEME, $request->getScheme()) ->setAttribute(TraceAttributes::NETWORK_PROTOCOL_VERSION, $request->getProtocolVersion()) - ->setAttribute(TraceAttributes::NETWORK_PEER_ADDRESS, $request->ip()) + ->setAttribute(TraceAttributes::NETWORK_PEER_ADDRESS, $request->server('REMOTE_ADDR')) ->setAttribute(TraceAttributes::URL_PATH, $this->httpTarget($request)) ->setAttribute(TraceAttributes::SERVER_ADDRESS, $this->httpHostName($request)) ->setAttribute(TraceAttributes::SERVER_PORT, $request->getPort()) ->setAttribute(TraceAttributes::CLIENT_PORT, $request->server('REMOTE_PORT')) + ->setAttribute(TraceAttributes::CLIENT_ADDRESS, $request->ip()) ->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->userAgent()) ->startSpan(); $request->attributes->set(SpanInterface::class, $span); diff --git a/src/Hooks/Illuminate/Database/Eloquent/Model.php b/src/Hooks/Illuminate/Database/Eloquent/Model.php new file mode 100644 index 0000000..70de1bf --- /dev/null +++ b/src/Hooks/Illuminate/Database/Eloquent/Model.php @@ -0,0 +1,252 @@ +hookFind(); + $this->hookPerformInsert(); + $this->hookPerformUpdate(); + $this->hookDelete(); + $this->hookGetModels(); + $this->hookDestroy(); + $this->hookRefresh(); + } + + private function hookFind(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + \Illuminate\Database\Eloquent\Builder::class, + 'find', + pre: function ($builder, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $model = $builder->getModel(); + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::find') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'find'); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function ($builder, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } + + private function hookPerformUpdate(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + EloquentModel::class, + 'performUpdate', + pre: function (EloquentModel $model, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::update') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'update'); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function (EloquentModel $model, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } + + private function hookPerformInsert(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + EloquentModel::class, + 'performInsert', + pre: function (EloquentModel $model, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::create') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'create'); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function (EloquentModel $model, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } + + private function hookDelete(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + EloquentModel::class, + 'delete', + pre: function (EloquentModel $model, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::delete') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'delete'); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function (EloquentModel $model, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } + + private function hookGetModels(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + \Illuminate\Database\Eloquent\Builder::class, + 'getModels', + pre: function ($builder, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $model = $builder->getModel(); + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::get') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'get') + ->setAttribute('db.statement', $builder->getQuery()->toSql()); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function ($builder, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } + + private function hookDestroy(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + EloquentModel::class, + 'destroy', + pre: function ($model, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::destroy') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'destroy'); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function ($model, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } + + private function hookRefresh(): void + { + /** @psalm-suppress UnusedFunctionCall */ + hook( + EloquentModel::class, + 'refresh', + pre: function (EloquentModel $model, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + $builder = $this->instrumentation + ->tracer() + ->spanBuilder($model::class . '::refresh') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno) + ->setAttribute('laravel.eloquent.model', $model::class) + ->setAttribute('laravel.eloquent.table', $model->getTable()) + ->setAttribute('laravel.eloquent.operation', 'refresh'); + + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + + return $params; + }, + post: function (EloquentModel $model, array $params, $result, ?Throwable $exception) { + $this->endSpan($exception); + } + ); + } +} diff --git a/src/LaravelInstrumentation.php b/src/LaravelInstrumentation.php index df54324..efd32ed 100644 --- a/src/LaravelInstrumentation.php +++ b/src/LaravelInstrumentation.php @@ -29,6 +29,7 @@ public static function register(): void Hooks\Illuminate\Queue\SyncQueue::hook($instrumentation); Hooks\Illuminate\Queue\Queue::hook($instrumentation); Hooks\Illuminate\Queue\Worker::hook($instrumentation); + Hooks\Illuminate\Database\Eloquent\Model::hook($instrumentation); } public static function shouldTraceCli(): bool diff --git a/tests/Fixtures/Models/TestModel.php b/tests/Fixtures/Models/TestModel.php new file mode 100644 index 0000000..fd9f7ea --- /dev/null +++ b/tests/Fixtures/Models/TestModel.php @@ -0,0 +1,16 @@ +set('database.default', 'testbench'); + $app['config']->set('database.connections.testbench', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + public function test_request_response(): void { $this->router()->get('/', fn () => null); @@ -73,6 +85,107 @@ public function test_cache_log_db(): void $this->assertSame(json_encode(['test' => true]), $logRecord->getAttributes()->toArray()['context']); } + public function test_eloquent_operations(): void + { + // Assert storage is empty before interacting with the database + $this->assertCount(0, $this->storage); + + // Create the test_models table + DB::statement('CREATE TABLE IF NOT EXISTS test_models ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + created_at DATETIME, + updated_at DATETIME + )'); + + $this->router()->get('/eloquent', function () { + try { + // Test create + $created = TestModel::create(['name' => 'test']); + + // Test find + $found = TestModel::find($created->id); + + // Test update + $found->update(['name' => 'updated']); + + // Test delete + $found->delete(); + + return response()->json(['status' => 'ok']); + } catch (\Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ], 500); + } + }); + + $response = $this->call('GET', '/eloquent'); + if ($response->status() !== 200) { + $this->fail('Request failed: ' . $response->content()); + } + $this->assertEquals(200, $response->status()); + + // Verify spans for each Eloquent operation + /** @var array $spans */ + $spans = array_values(array_filter( + iterator_to_array($this->storage), + fn ($item) => $item instanceof \OpenTelemetry\SDK\Trace\ImmutableSpan + )); + + // Filter out SQL spans and keep only Eloquent spans + $eloquentSpans = array_values(array_filter( + $spans, + fn ($span) => str_contains($span->getName(), '::') + )); + + // Sort spans by operation type to ensure consistent order + usort($eloquentSpans, function ($a, $b) { + $operations = ['create' => 0, 'find' => 1, 'update' => 2, 'delete' => 3]; + $aOp = $a->getAttributes()->get('laravel.eloquent.operation'); + $bOp = $b->getAttributes()->get('laravel.eloquent.operation'); + + return ($operations[$aOp] ?? 999) <=> ($operations[$bOp] ?? 999); + }); + + // Create span + $createSpan = array_values(array_filter($eloquentSpans, function ($span) { + return $span->getAttributes()->get('laravel.eloquent.operation') === 'create'; + }))[0]; + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::create', $createSpan->getName()); + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $createSpan->getAttributes()->get('laravel.eloquent.model')); + $this->assertSame('test_models', $createSpan->getAttributes()->get('laravel.eloquent.table')); + $this->assertSame('create', $createSpan->getAttributes()->get('laravel.eloquent.operation')); + + // Find span + $findSpan = array_values(array_filter($eloquentSpans, function ($span) { + return $span->getAttributes()->get('laravel.eloquent.operation') === 'find'; + }))[0]; + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::find', $findSpan->getName()); + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $findSpan->getAttributes()->get('laravel.eloquent.model')); + $this->assertSame('test_models', $findSpan->getAttributes()->get('laravel.eloquent.table')); + $this->assertSame('find', $findSpan->getAttributes()->get('laravel.eloquent.operation')); + + // Update span + $updateSpan = array_values(array_filter($eloquentSpans, function ($span) { + return $span->getAttributes()->get('laravel.eloquent.operation') === 'update'; + }))[0]; + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::update', $updateSpan->getName()); + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $updateSpan->getAttributes()->get('laravel.eloquent.model')); + $this->assertSame('test_models', $updateSpan->getAttributes()->get('laravel.eloquent.table')); + $this->assertSame('update', $updateSpan->getAttributes()->get('laravel.eloquent.operation')); + + // Delete span + $deleteSpan = array_values(array_filter($eloquentSpans, function ($span) { + return $span->getAttributes()->get('laravel.eloquent.operation') === 'delete'; + }))[0]; + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::delete', $deleteSpan->getName()); + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $deleteSpan->getAttributes()->get('laravel.eloquent.model')); + $this->assertSame('test_models', $deleteSpan->getAttributes()->get('laravel.eloquent.table')); + $this->assertSame('delete', $deleteSpan->getAttributes()->get('laravel.eloquent.operation')); + } + public function test_low_cardinality_route_span_name(): void { $this->router()->get('/hello/{name}', fn () => null)->name('hello-name'); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy