Skip to content

Commit 49a2b51

Browse files
committed
feat: test view instrumentation
1 parent 20305d9 commit 49a2b51

File tree

8 files changed

+279
-1
lines changed

8 files changed

+279
-1
lines changed

src/Instrumentation/Laravel/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
"lock": false,
4949
"sort-packages": true,
5050
"allow-plugins": {
51-
"php-http/discovery": false
51+
"php-http/discovery": false,
52+
"tbachert/spi": false
5253
}
5354
}
5455
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\Illuminate\View;
6+
7+
use Illuminate\View\View as LaravelView;
8+
use OpenTelemetry\API\Trace\Span;
9+
use OpenTelemetry\API\Trace\SpanKind;
10+
use OpenTelemetry\Context\Context;
11+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHook;
12+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHookTrait;
13+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\PostHookTrait;
14+
use OpenTelemetry\Contrib\Instrumentation\Laravel\LaravelInstrumentation;
15+
use function OpenTelemetry\Instrumentation\hook;
16+
use OpenTelemetry\SemConv\TraceAttributes;
17+
use Throwable;
18+
19+
/**
20+
* Enhanced instrumentation for Laravel's view rendering.
21+
*/
22+
class View implements LaravelHook
23+
{
24+
use LaravelHookTrait;
25+
use PostHookTrait;
26+
27+
public function instrument(): void
28+
{
29+
$this->hookRender();
30+
}
31+
32+
/**
33+
* Hook into View::render to track view rendering.
34+
*/
35+
protected function hookRender(): bool
36+
{
37+
return hook(
38+
LaravelView::class,
39+
'render',
40+
pre: function (LaravelView $view, array $params, string $class, string $function, ?string $filename, ?int $lineno) {
41+
/** @psalm-suppress ArgumentTypeCoercion */
42+
$builder = $this->instrumentation
43+
->tracer()
44+
->spanBuilder('laravel.view.render')
45+
->setSpanKind(SpanKind::KIND_INTERNAL)
46+
->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function)
47+
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
48+
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
49+
->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno)
50+
->setAttribute('view.name', $view->getName());
51+
52+
$parent = Context::getCurrent();
53+
$span = $builder->startSpan();
54+
Context::storage()->attach($span->storeInContext($parent));
55+
56+
return $params;
57+
},
58+
post: function (LaravelView $view, array $params, mixed $response, ?Throwable $exception) {
59+
$this->endSpan($exception);
60+
}
61+
);
62+
}
63+
}

src/Instrumentation/Laravel/src/LaravelInstrumentation.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static function register(): void
3232
Hooks\Illuminate\Queue\Queue::hook($instrumentation);
3333
Hooks\Illuminate\Queue\Worker::hook($instrumentation);
3434
Hooks\Illuminate\Routing\Route::hook($instrumentation);
35+
Hooks\Illuminate\View\View::hook($instrumentation);
3536
}
3637

3738
public static function shouldTraceCli(): bool

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,48 @@ protected function assertTraceStructure(array $expectedStructure, string $messag
367367
);
368368
}
369369

370+
// Verify events if present
371+
if (isset($expected['events'])) {
372+
$actualEvents = $actual->getEvents();
373+
$this->assertCount(
374+
count($expected['events']),
375+
$actualEvents,
376+
$message ?: sprintf('Expected %d events for span %s, got %d',
377+
count($expected['events']),
378+
$actual->getSpanId(),
379+
count($actualEvents)
380+
)
381+
);
382+
383+
foreach ($expected['events'] as $index => $expectedEvent) {
384+
$actualEvent = $actualEvents[$index];
385+
386+
if (isset($expectedEvent['name'])) {
387+
$this->assertEquals(
388+
$expectedEvent['name'],
389+
$actualEvent->getName(),
390+
$message ?: sprintf('Event name mismatch for event %d in span %s', $index, $actual->getSpanId())
391+
);
392+
}
393+
394+
if (isset($expectedEvent['attributes'])) {
395+
$actualAttributes = $actualEvent->getAttributes()->toArray();
396+
foreach ($expectedEvent['attributes'] as $key => $value) {
397+
$this->assertArrayHasKey(
398+
$key,
399+
$actualAttributes,
400+
$message ?: sprintf('Missing attribute %s for event %d in span %s', $key, $index, $actual->getSpanId())
401+
);
402+
$this->assertEquals(
403+
$value,
404+
$actualAttributes[$key],
405+
$message ?: sprintf('Attribute %s mismatch for event %d in span %s', $key, $index, $actual->getSpanId())
406+
);
407+
}
408+
}
409+
}
410+
}
411+
370412
// Verify children if present
371413
if (isset($expected['children'])) {
372414
$children = $actualStructure['spanMap'][$actual->getSpanId()]['children'] ?? [];
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\View;
6+
7+
use Exception;
8+
use Illuminate\Routing\Router;
9+
use Illuminate\View\ViewException;
10+
use OpenTelemetry\API\Trace\SpanKind;
11+
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\TestCase;
12+
13+
/** @psalm-suppress UnusedClass */
14+
class ViewTest extends TestCase
15+
{
16+
public function setUp(): void
17+
{
18+
parent::setUp();
19+
20+
// Configure Laravel to find views in our test resources directory
21+
$this->app['view']->addLocation(__DIR__ . '/../../resources/views');
22+
}
23+
24+
public function test_it_records_view_rendering(): void
25+
{
26+
// Create a test view
27+
$view = view('test-view', ['text' => 'Hello World']);
28+
29+
// Render the view
30+
$this->storage->exchangeArray([]);
31+
$content = $view->render();
32+
33+
// Assert the view was rendered
34+
$this->assertEquals('Hello World', $content);
35+
36+
// Assert trace structure
37+
$this->assertTraceStructure([
38+
[
39+
'name' => 'laravel.view.render',
40+
'attributes' => [
41+
'code.function.name' => 'render',
42+
'code.namespace' => 'Illuminate\View\View',
43+
'view.name' => 'test-view',
44+
],
45+
'kind' => SpanKind::KIND_INTERNAL,
46+
],
47+
]);
48+
}
49+
50+
public function test_it_records_view_rendering_with_exception(): void
51+
{
52+
// Create a view that will throw an exception during rendering
53+
$view = view('test-view-exception');
54+
55+
// Attempt to render the view
56+
$this->storage->exchangeArray([]);
57+
try {
58+
$view->render();
59+
$this->fail('Expected ViewException was not thrown');
60+
} catch (ViewException $e) {
61+
// Expected exception
62+
$this->assertEquals('View rendering failed (View: /usr/src/myapp/src/Instrumentation/Laravel/tests/resources/views/test-view-exception.blade.php)', $e->getMessage());
63+
}
64+
65+
// Assert trace structure
66+
$this->assertTraceStructure([
67+
[
68+
'name' => 'laravel.view.render',
69+
'attributes' => [
70+
'code.function.name' => 'render',
71+
'code.namespace' => 'Illuminate\View\View',
72+
'view.name' => 'test-view-exception',
73+
],
74+
'kind' => SpanKind::KIND_INTERNAL,
75+
'status' => [
76+
'code' => 'error',
77+
],
78+
'events' => [
79+
[
80+
'name' => 'exception',
81+
'attributes' => [
82+
'exception.message' => 'View rendering failed (View: /usr/src/myapp/src/Instrumentation/Laravel/tests/resources/views/test-view-exception.blade.php)',
83+
'exception.type' => ViewException::class,
84+
],
85+
],
86+
],
87+
],
88+
]);
89+
}
90+
91+
public function test_it_records_view_rendering_in_request_context(): void
92+
{
93+
// Define a route that renders a view
94+
$this->router()->get('/view-test', function () {
95+
return view('test-view', ['text' => 'Hello World']);
96+
});
97+
98+
// Make a request to the route
99+
$this->storage->exchangeArray([]);
100+
$response = $this->call('GET', '/view-test');
101+
102+
// Assert response
103+
$this->assertEquals(200, $response->status());
104+
$this->assertEquals('Hello World', $response->getContent());
105+
106+
// Assert trace structure
107+
$this->assertTraceStructure([
108+
[
109+
'name' => 'GET /view-test',
110+
'attributes' => [
111+
'code.function.name' => 'handle',
112+
'code.namespace' => 'Illuminate\Foundation\Http\Kernel',
113+
'url.full' => 'http://localhost/view-test',
114+
'http.request.method' => 'GET',
115+
'url.scheme' => 'http',
116+
'network.protocol.version' => '1.1',
117+
'network.peer.address' => '127.0.0.1',
118+
'url.path' => 'view-test',
119+
'server.address' => 'localhost',
120+
'server.port' => 80,
121+
'user_agent.original' => 'Symfony',
122+
'http.route' => 'view-test',
123+
'http.response.status_code' => 200,
124+
],
125+
'kind' => SpanKind::KIND_SERVER,
126+
'children' => [
127+
[
128+
'name' => 'Illuminate\Foundation\Http\Middleware\ValidatePostSize::handle',
129+
'attributes' => [
130+
'laravel.middleware.class' => 'Illuminate\Foundation\Http\Middleware\ValidatePostSize',
131+
'http.response.status_code' => 200,
132+
],
133+
'kind' => SpanKind::KIND_INTERNAL,
134+
'children' => [
135+
[
136+
'name' => 'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::handle',
137+
'attributes' => [
138+
'laravel.middleware.class' => 'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull',
139+
'http.response.status_code' => 200,
140+
],
141+
'kind' => SpanKind::KIND_INTERNAL,
142+
'children' => [
143+
[
144+
'name' => 'laravel.view.render',
145+
'attributes' => [
146+
'code.function.name' => 'render',
147+
'code.namespace' => 'Illuminate\View\View',
148+
'view.name' => 'test-view',
149+
],
150+
'kind' => SpanKind::KIND_INTERNAL,
151+
],
152+
],
153+
],
154+
],
155+
],
156+
],
157+
],
158+
]);
159+
}
160+
161+
private function router(): Router
162+
{
163+
/** @psalm-suppress PossiblyNullReference */
164+
return $this->app->make(Router::class);
165+
}
166+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ $text }}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@php
2+
throw new Exception('View rendering failed');
3+
@endphp
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ $text }}

0 commit comments

Comments
 (0)