Skip to content

Commit e0c6ed6

Browse files
authored
Improve openentelemetry-auto-laravel test definitions for Eloquent hooks. (#394)
* create test boilerplate * add testcases * remove replaced integration test cases * format and replace class path * update namespace * assert only scoped spans * fix test errors * add psalm-suppress annotation * remove table dropping with tearDown (already initialized by in-memory DB) * add test case for delete hook
1 parent a3fb3a4 commit e0c6ed6

File tree

2 files changed

+196
-164
lines changed

2 files changed

+196
-164
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\Database\Eloquent;
6+
7+
use Illuminate\Support\Facades\DB;
8+
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel;
9+
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\TestCase;
10+
11+
/**
12+
* Integration test for Eloquent\Model hooks
13+
* @psalm-suppress UnusedClass
14+
*/
15+
class ModelTest extends TestCase
16+
{
17+
protected function getEnvironmentSetUp($app): void
18+
{
19+
// Setup default database to use sqlite :memory:
20+
$app['config']->set('database.default', 'testbench');
21+
$app['config']->set('database.connections.testbench', [
22+
'driver' => 'sqlite',
23+
'database' => ':memory:',
24+
'prefix' => '',
25+
]);
26+
}
27+
28+
public function setUp(): void
29+
{
30+
parent::setUp();
31+
32+
// Setup database table for fixture model
33+
DB::statement('CREATE TABLE IF NOT EXISTS test_models(
34+
id BIGINT,
35+
name VARCHAR(255),
36+
created_at DATETIME,
37+
updated_at DATETIME
38+
)
39+
');
40+
}
41+
42+
/**
43+
* @return \OpenTelemetry\SDK\Trace\ImmutableSpan[]
44+
*/
45+
private function filterOnlyEloquentSpans(): array
46+
{
47+
// SQL spans be mixed up in storage because \Illuminate\Support\Facades\DB called.
48+
// So, filtering only spans has attribute named 'laravel.eloquent.operation'.
49+
return array_values(
50+
array_filter(
51+
iterator_to_array($this->storage),
52+
fn ($span) =>
53+
$span instanceof \OpenTelemetry\SDK\Trace\ImmutableSpan &&
54+
$span->getAttributes()->has('laravel.eloquent.operation')
55+
)
56+
);
57+
}
58+
59+
public function test_create(): void
60+
{
61+
TestModel::create(['id' => 1, 'name' => 'test']);
62+
63+
$spans = $this->filterOnlyEloquentSpans();
64+
65+
$this->assertCount(1, $spans);
66+
67+
$span = $spans[0];
68+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::create', $span->getName());
69+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
70+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
71+
$this->assertSame('create', $span->getAttributes()->get('laravel.eloquent.operation'));
72+
}
73+
74+
public function test_find(): void
75+
{
76+
TestModel::find(1);
77+
78+
// spans contains 2 eloquent spans for 'find' and 'get', because it method internally calls 'getModels' method.
79+
// So, filtering span only find span.
80+
$spans = array_values(
81+
array_filter(
82+
$this->filterOnlyEloquentSpans(),
83+
fn ($span) => $span->getAttributes()->get('laravel.eloquent.operation') === 'find'
84+
)
85+
);
86+
87+
$this->assertCount(1, $spans);
88+
89+
$span = $spans[0];
90+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::find', $span->getName());
91+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
92+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
93+
$this->assertSame('find', $span->getAttributes()->get('laravel.eloquent.operation'));
94+
95+
}
96+
97+
public function test_perform_insert(): void
98+
{
99+
// Illuminate\Database\Eloquent\Model::performInsert is called from Illuminate\Database\Eloquent\Model::save.
100+
// Mark as exists = false required, because performUpdate called if exists = true.
101+
$model = (new TestModel())->newInstance(['id' => 1, 'name' => 'test'], false);
102+
$model->save();
103+
104+
$spans = $this->filterOnlyEloquentSpans();
105+
$this->assertCount(1, $spans);
106+
107+
$span = $spans[0];
108+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::create', $span->getName());
109+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
110+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
111+
$this->assertSame('create', $span->getAttributes()->get('laravel.eloquent.operation'));
112+
}
113+
114+
public function test_perform_update(): void
115+
{
116+
// Illuminate\Database\Eloquent\Model::performInsert is called from Illuminate\Database\Eloquent\Model::save.
117+
// Mark as exists = true required, because performInsert called if exists = false.
118+
$model = (new TestModel())->newInstance(['id' => 1, 'name' => 'test'], true);
119+
$model->save();
120+
121+
$spans = $this->filterOnlyEloquentSpans();
122+
$this->assertCount(1, $spans);
123+
124+
$span = $spans[0];
125+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::update', $span->getName());
126+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
127+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
128+
$this->assertSame('update', $span->getAttributes()->get('laravel.eloquent.operation'));
129+
}
130+
131+
public function test_delete(): void
132+
{
133+
$model = new TestModel();
134+
$model->delete(); // no effect
135+
136+
$spans = $this->filterOnlyEloquentSpans();
137+
$this->assertCount(1, $spans);
138+
139+
$span = $spans[0];
140+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::delete', $span->getName());
141+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
142+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
143+
$this->assertSame('delete', $span->getAttributes()->get('laravel.eloquent.operation'));
144+
}
145+
146+
public function test_get_models(): void
147+
{
148+
TestModel::get();
149+
150+
$spans = $this->filterOnlyEloquentSpans();
151+
$this->assertCount(1, $spans);
152+
153+
$span = $spans[0];
154+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::get', $span->getName());
155+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
156+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
157+
$this->assertSame('get', $span->getAttributes()->get('laravel.eloquent.operation'));
158+
}
159+
160+
public function test_destory(): void
161+
{
162+
TestModel::destroy([1]);
163+
164+
// spans contains 2 eloquent spans for 'destroy' and 'get', because it method internally calls 'getModels' method.
165+
// So, filtering span only 'destroy' span.
166+
$spans = array_values(
167+
array_filter(
168+
$this->filterOnlyEloquentSpans(),
169+
fn ($span) => $span->getAttributes()->get('laravel.eloquent.operation') === 'destroy'
170+
)
171+
);
172+
173+
$this->assertCount(1, $spans);
174+
175+
$span = $spans[0];
176+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::destroy', $span->getName());
177+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
178+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
179+
$this->assertSame('destroy', $span->getAttributes()->get('laravel.eloquent.operation'));
180+
}
181+
182+
public function test_refresh(): void
183+
{
184+
$model = new TestModel();
185+
$model->refresh(); // no effect
186+
187+
$spans = $this->filterOnlyEloquentSpans();
188+
$this->assertCount(1, $spans);
189+
190+
$span = $spans[0];
191+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::refresh', $span->getName());
192+
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $span->getAttributes()->get('laravel.eloquent.model'));
193+
$this->assertSame('test_models', $span->getAttributes()->get('laravel.eloquent.table'));
194+
$this->assertSame('refresh', $span->getAttributes()->get('laravel.eloquent.operation'));
195+
}
196+
}

src/Instrumentation/Laravel/tests/Integration/LaravelInstrumentationTest.php

Lines changed: 0 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Illuminate\Support\Facades\Http;
1010
use Illuminate\Support\Facades\Log;
1111
use OpenTelemetry\SemConv\TraceAttributes;
12-
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel;
1312

1413
/** @psalm-suppress UnusedClass */
1514
class LaravelInstrumentationTest extends TestCase
@@ -85,148 +84,6 @@ public function test_cache_log_db(): void
8584
$this->assertSame(json_encode(['test' => true]), $logRecord->getAttributes()->toArray()['context']);
8685
}
8786

88-
public function test_eloquent_operations(): void
89-
{
90-
// Assert storage is empty before interacting with the database
91-
$this->assertCount(0, $this->storage);
92-
93-
// Create the test_models table
94-
DB::statement('CREATE TABLE IF NOT EXISTS test_models (
95-
id INTEGER PRIMARY KEY AUTOINCREMENT,
96-
name TEXT,
97-
created_at DATETIME,
98-
updated_at DATETIME
99-
)');
100-
101-
$this->router()->get('/eloquent', function () {
102-
try {
103-
// Test create
104-
$created = TestModel::create(['name' => 'test']);
105-
106-
// Test find
107-
$found = TestModel::find($created->id);
108-
109-
// Test update
110-
$found->update(['name' => 'updated']);
111-
112-
// Test delete
113-
$found->delete();
114-
115-
return response()->json(['status' => 'ok']);
116-
} catch (\Exception $e) {
117-
return response()->json([
118-
'error' => $e->getMessage(),
119-
'trace' => $e->getTraceAsString(),
120-
], 500);
121-
}
122-
});
123-
124-
$response = $this->call('GET', '/eloquent');
125-
if ($response->status() !== 200) {
126-
$this->fail('Request failed: ' . $response->content());
127-
}
128-
$this->assertEquals(200, $response->status());
129-
130-
$eloquentSpans = $this->resolveEloquentOperationSpans();
131-
132-
// Sort spans by operation type to ensure consistent order
133-
usort($eloquentSpans, function ($a, $b) {
134-
$operations = ['create' => 0, 'find' => 1, 'update' => 2, 'delete' => 3];
135-
$aOp = $a->getAttributes()->get('laravel.eloquent.operation');
136-
$bOp = $b->getAttributes()->get('laravel.eloquent.operation');
137-
138-
return ($operations[$aOp] ?? 999) <=> ($operations[$bOp] ?? 999);
139-
});
140-
141-
// Create span
142-
$createSpan = array_values(array_filter($eloquentSpans, function ($span) {
143-
return $span->getAttributes()->get('laravel.eloquent.operation') === 'create';
144-
}))[0];
145-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::create', $createSpan->getName());
146-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $createSpan->getAttributes()->get('laravel.eloquent.model'));
147-
$this->assertSame('test_models', $createSpan->getAttributes()->get('laravel.eloquent.table'));
148-
$this->assertSame('create', $createSpan->getAttributes()->get('laravel.eloquent.operation'));
149-
150-
// Find span
151-
$findSpan = array_values(array_filter($eloquentSpans, function ($span) {
152-
return $span->getAttributes()->get('laravel.eloquent.operation') === 'find';
153-
}))[0];
154-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::find', $findSpan->getName());
155-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $findSpan->getAttributes()->get('laravel.eloquent.model'));
156-
$this->assertSame('test_models', $findSpan->getAttributes()->get('laravel.eloquent.table'));
157-
$this->assertSame('find', $findSpan->getAttributes()->get('laravel.eloquent.operation'));
158-
159-
// Update span
160-
$updateSpan = array_values(array_filter($eloquentSpans, function ($span) {
161-
return $span->getAttributes()->get('laravel.eloquent.operation') === 'update';
162-
}))[0];
163-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::update', $updateSpan->getName());
164-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $updateSpan->getAttributes()->get('laravel.eloquent.model'));
165-
$this->assertSame('test_models', $updateSpan->getAttributes()->get('laravel.eloquent.table'));
166-
$this->assertSame('update', $updateSpan->getAttributes()->get('laravel.eloquent.operation'));
167-
168-
// Delete span
169-
$deleteSpan = array_values(array_filter($eloquentSpans, function ($span) {
170-
return $span->getAttributes()->get('laravel.eloquent.operation') === 'delete';
171-
}))[0];
172-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::delete', $deleteSpan->getName());
173-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $deleteSpan->getAttributes()->get('laravel.eloquent.model'));
174-
$this->assertSame('test_models', $deleteSpan->getAttributes()->get('laravel.eloquent.table'));
175-
$this->assertSame('delete', $deleteSpan->getAttributes()->get('laravel.eloquent.operation'));
176-
}
177-
178-
public function test_eloquent_static_operations(): void
179-
{
180-
// Assert storage is empty before interacting with the database
181-
$this->assertCount(0, $this->storage);
182-
183-
// Create the test_models table
184-
DB::statement('CREATE TABLE IF NOT EXISTS test_models (
185-
id INTEGER PRIMARY KEY AUTOINCREMENT,
186-
name TEXT,
187-
created_at DATETIME,
188-
updated_at DATETIME
189-
)');
190-
191-
$this->router()->get('/eloquent', function () {
192-
try {
193-
// create record for Test destroy
194-
$created = TestModel::create(['name' => 'test']);
195-
196-
// Test destroy
197-
TestModel::destroy([$created->id]);
198-
199-
return response()->json(['status' => 'ok']);
200-
} catch (\Exception $e) {
201-
return response()->json([
202-
'error' => $e->getMessage(),
203-
'trace' => $e->getTraceAsString(),
204-
], 500);
205-
}
206-
});
207-
208-
$response = $this->call('GET', '/eloquent');
209-
if ($response->status() !== 200) {
210-
$this->fail('Request failed: ' . $response->content());
211-
}
212-
$this->assertEquals(200, $response->status());
213-
214-
$eloquentSpans = $this->resolveEloquentOperationSpans();
215-
216-
// Destroy span
217-
$destroySpans = array_values(array_filter($eloquentSpans, function ($span) {
218-
return $span->getAttributes()->get('laravel.eloquent.operation') === 'destroy';
219-
}));
220-
221-
$this->assertCount(1, $destroySpans);
222-
223-
$destroySpan = $destroySpans[0];
224-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel::destroy', $destroySpan->getName());
225-
$this->assertSame('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Fixtures\Models\TestModel', $destroySpan->getAttributes()->get('laravel.eloquent.model'));
226-
$this->assertSame('test_models', $destroySpan->getAttributes()->get('laravel.eloquent.table'));
227-
$this->assertSame('destroy', $destroySpan->getAttributes()->get('laravel.eloquent.operation'));
228-
}
229-
23087
public function test_low_cardinality_route_span_name(): void
23188
{
23289
$this->router()->get('/hello/{name}', fn () => null)->name('hello-name');
@@ -254,25 +111,4 @@ private function router(): Router
254111
/** @psalm-suppress PossiblyNullReference */
255112
return $this->app->make(Router::class);
256113
}
257-
258-
/**
259-
* @return array<int, \OpenTelemetry\SDK\Trace\ImmutableSpan>
260-
*/
261-
private function resolveEloquentOperationSpans(): array
262-
{
263-
// Verify spans for each Eloquent operation
264-
/** @var array<int, \OpenTelemetry\SDK\Trace\ImmutableSpan> $spans */
265-
$spans = array_values(array_filter(
266-
iterator_to_array($this->storage),
267-
fn ($item) => $item instanceof \OpenTelemetry\SDK\Trace\ImmutableSpan
268-
));
269-
270-
// Filter out SQL spans and keep only Eloquent spans
271-
$eloquentSpans = array_values(array_filter(
272-
$spans,
273-
fn ($span) => str_contains($span->getName(), '::')
274-
));
275-
276-
return $eloquentSpans;
277-
}
278114
}

0 commit comments

Comments
 (0)