diff --git a/src/Instrumentation/Laravel/src/Hooks/Illuminate/Database/Eloquent/Model.php b/src/Instrumentation/Laravel/src/Hooks/Illuminate/Database/Eloquent/Model.php index 610cdb8ab..64151b426 100644 --- a/src/Instrumentation/Laravel/src/Hooks/Illuminate/Database/Eloquent/Model.php +++ b/src/Instrumentation/Laravel/src/Hooks/Illuminate/Database/Eloquent/Model.php @@ -189,7 +189,11 @@ private function hookDestroy(): void hook( EloquentModel::class, 'destroy', - pre: function ($model, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + pre: function (string $modelClassName, array $params, string $class, string $function, ?string $filename, ?int $lineno) { + // The class-string is passed to the $model argument, because \Illuminate\Database\Eloquent\Model::destroy is static method. + // Therefore, create a class instance from a class-string, and then get the table name from the getTable function. + $model = new $modelClassName(); + $builder = $this->instrumentation ->tracer() ->spanBuilder($model::class . '::destroy') diff --git a/src/Instrumentation/Laravel/tests/Integration/LaravelInstrumentationTest.php b/src/Instrumentation/Laravel/tests/Integration/LaravelInstrumentationTest.php index f3928c17d..0b1b59f92 100644 --- a/src/Instrumentation/Laravel/tests/Integration/LaravelInstrumentationTest.php +++ b/src/Instrumentation/Laravel/tests/Integration/LaravelInstrumentationTest.php @@ -102,16 +102,16 @@ public function test_eloquent_operations(): void 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([ @@ -127,18 +127,7 @@ public function test_eloquent_operations(): void } $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(), '::') - )); + $eloquentSpans = $this->resolveEloquentOperationSpans(); // Sort spans by operation type to ensure consistent order usort($eloquentSpans, function ($a, $b) { @@ -186,6 +175,58 @@ public function test_eloquent_operations(): void $this->assertSame('delete', $deleteSpan->getAttributes()->get('laravel.eloquent.operation')); } + public function test_eloquent_static_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 { + // create record for Test destroy + $created = TestModel::create(['name' => 'test']); + + // Test destroy + TestModel::destroy([$created->id]); + + 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()); + + $eloquentSpans = $this->resolveEloquentOperationSpans(); + + // Destroy span + $destroySpans = array_values(array_filter($eloquentSpans, function ($span) { + return $span->getAttributes()->get('laravel.eloquent.operation') === 'destroy'; + })); + + $this->assertCount(1, $destroySpans); + + $destroySpan = $destroySpans[0]; + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::destroy', $destroySpan->getName()); + $this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $destroySpan->getAttributes()->get('laravel.eloquent.model')); + $this->assertSame('test_models', $destroySpan->getAttributes()->get('laravel.eloquent.table')); + $this->assertSame('destroy', $destroySpan->getAttributes()->get('laravel.eloquent.operation')); + } + public function test_low_cardinality_route_span_name(): void { $this->router()->get('/hello/{name}', fn () => null)->name('hello-name'); @@ -213,4 +254,25 @@ private function router(): Router /** @psalm-suppress PossiblyNullReference */ return $this->app->make(Router::class); } + + /** + * @return array + */ + private function resolveEloquentOperationSpans(): array + { + // 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(), '::') + )); + + return $eloquentSpans; + } }