Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\Database\Eloquent;

use Illuminate\Support\Facades\DB;
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel;
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\TestCase;

/**
* Integration test for Eloquent\Model hooks
* @psalm-suppress UnusedClass
*/
class ModelTest extends TestCase
{
protected function getEnvironmentSetUp($app): void
{
// Setup default database to use sqlite :memory:
$app['config']->set('database.default', 'testbench');
$app['config']->set('database.connections.testbench', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}

public function setUp(): void
{
parent::setUp();

// Setup database table for fixture model
DB::statement('CREATE TABLE IF NOT EXISTS test_models(
id BIGINT,
name VARCHAR(255),
created_at DATETIME,
updated_at DATETIME
)
');
}

/**
* @return \OpenTelemetry\SDK\Trace\ImmutableSpan[]
*/
private function filterOnlyEloquentSpans(): array
{
// SQL spans be mixed up in storage because \Illuminate\Support\Facades\DB called.
// So, filtering only spans has attribute named 'laravel.eloquent.operation'.
return array_values(
array_filter(
iterator_to_array($this->storage),
fn ($span) =>
$span instanceof \OpenTelemetry\SDK\Trace\ImmutableSpan &&
$span->getAttributes()->has('laravel.eloquent.operation')
)
);
}

public function test_create(): void
{
TestModel::create(['id' => 1, 'name' => 'test']);

$spans = $this->filterOnlyEloquentSpans();

$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::create', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('create', $span->getAttributes()->get('laravel.eloquent.operation'));
}

public function test_find(): void
{
TestModel::find(1);

// spans contains 2 eloquent spans for 'find' and 'get', because it method internally calls 'getModels' method.
// So, filtering span only find span.
$spans = array_values(
array_filter(
$this->filterOnlyEloquentSpans(),
fn ($span) => $span->getAttributes()->get('laravel.eloquent.operation') === 'find'
)
);

$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::find', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('find', $span->getAttributes()->get('laravel.eloquent.operation'));

}

public function test_perform_insert(): void
{
// Illuminate\Database\Eloquent\Model::performInsert is called from Illuminate\Database\Eloquent\Model::save.
// Mark as exists = false required, because performUpdate called if exists = true.
$model = (new TestModel())->newInstance(['id' => 1, 'name' => 'test'], false);
$model->save();

$spans = $this->filterOnlyEloquentSpans();
$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::create', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('create', $span->getAttributes()->get('laravel.eloquent.operation'));
}

public function test_perform_update(): void
{
// Illuminate\Database\Eloquent\Model::performInsert is called from Illuminate\Database\Eloquent\Model::save.
// Mark as exists = true required, because performInsert called if exists = false.
$model = (new TestModel())->newInstance(['id' => 1, 'name' => 'test'], true);
$model->save();

$spans = $this->filterOnlyEloquentSpans();
$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::update', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('update', $span->getAttributes()->get('laravel.eloquent.operation'));
}

public function test_delete(): void
{
$model = new TestModel();
$model->delete(); // no effect

$spans = $this->filterOnlyEloquentSpans();
$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::delete', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('delete', $span->getAttributes()->get('laravel.eloquent.operation'));
}

public function test_get_models(): void
{
TestModel::get();

$spans = $this->filterOnlyEloquentSpans();
$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::get', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('get', $span->getAttributes()->get('laravel.eloquent.operation'));
}

public function test_destory(): void
{
TestModel::destroy([1]);

// spans contains 2 eloquent spans for 'destroy' and 'get', because it method internally calls 'getModels' method.
// So, filtering span only 'destroy' span.
$spans = array_values(
array_filter(
$this->filterOnlyEloquentSpans(),
fn ($span) => $span->getAttributes()->get('laravel.eloquent.operation') === 'destroy'
)
);

$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::destroy', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('destroy', $span->getAttributes()->get('laravel.eloquent.operation'));
}

public function test_refresh(): void
{
$model = new TestModel();
$model->refresh(); // no effect

$spans = $this->filterOnlyEloquentSpans();
$this->assertCount(1, $spans);

$span = $spans[0];
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::refresh', $span->getName());
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
$this->assertSame('refresh', $span->getAttributes()->get('laravel.eloquent.operation'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use OpenTelemetry\SemConv\TraceAttributes;
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel;

/** @psalm-suppress UnusedClass */
class LaravelInstrumentationTest extends TestCase
Expand Down Expand Up @@ -85,148 +84,6 @@ 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());

$eloquentSpans = $this->resolveEloquentOperationSpans();

// 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_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');
Expand Down Expand Up @@ -254,25 +111,4 @@ private function router(): Router
/** @psalm-suppress PossiblyNullReference */
return $this->app->make(Router::class);
}

/**
* @return array<int, \OpenTelemetry\SDK\Trace\ImmutableSpan>
*/
private function resolveEloquentOperationSpans(): array
{
// Verify spans for each Eloquent operation
/** @var array<int, \OpenTelemetry\SDK\Trace\ImmutableSpan> $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;
}
}