Skip to content

Commit 20305d9

Browse files
committed
feat: hook controller method
1 parent b7c727d commit 20305d9

File tree

4 files changed

+182
-1
lines changed

4 files changed

+182
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\Illuminate\Routing;
6+
7+
use Illuminate\Routing\Route as LaravelRoute;
8+
use OpenTelemetry\API\Trace\Span;
9+
use OpenTelemetry\API\Trace\SpanKind;
10+
use OpenTelemetry\API\Trace\StatusCode;
11+
use OpenTelemetry\Context\Context;
12+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHook;
13+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHookTrait;
14+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\PostHookTrait;
15+
use function OpenTelemetry\Instrumentation\hook;
16+
use OpenTelemetry\SemConv\TraceAttributes;
17+
use Throwable;
18+
19+
/**
20+
* Enhanced instrumentation for Laravel's route execution.
21+
*/
22+
class Route implements LaravelHook
23+
{
24+
use LaravelHookTrait;
25+
use PostHookTrait;
26+
27+
public function instrument(): void
28+
{
29+
$this->hookRun();
30+
}
31+
32+
/**
33+
* Hook into Route::run to track controller execution.
34+
*/
35+
protected function hookRun(): bool
36+
{
37+
return hook(
38+
LaravelRoute::class,
39+
'run',
40+
pre: function (LaravelRoute $route, array $params, string $class, string $function, ?string $filename, ?int $lineno) {
41+
// Get the route action
42+
$action = $route->getAction();
43+
44+
// Check if this is a controller route
45+
if (is_array($action)) {
46+
$controllerClass = null;
47+
$method = null;
48+
49+
// Handle array format ['Controller', 'method']
50+
if (isset($action[0]) && isset($action[1])) {
51+
$controllerClass = $action[0];
52+
$method = $action[1];
53+
}
54+
// Handle array format ['controller' => 'Controller@method']
55+
elseif (isset($action['controller'])) {
56+
$controller = $action['controller'];
57+
if (is_string($controller) && str_contains($controller, '@')) {
58+
[$controllerClass, $method] = explode('@', $controller);
59+
}
60+
}
61+
62+
if ($controllerClass && $method) {
63+
// Hook into the controller method execution
64+
hook(
65+
$controllerClass,
66+
$method,
67+
pre: function ($controller, array $params, string $class, string $function, ?string $filename, ?int $lineno) {
68+
$spanBuilder = $this->instrumentation->tracer()
69+
->spanBuilder("Controller::{$function}");
70+
$span = $spanBuilder->setSpanKind(SpanKind::KIND_INTERNAL)->startSpan();
71+
72+
// Add code attributes
73+
$span->setAttribute('code.function.name', $function);
74+
$span->setAttribute('code.namespace', $class);
75+
76+
Context::storage()->attach($span->storeInContext(Context::getCurrent()));
77+
78+
return $params;
79+
},
80+
post: function ($controller, array $params, mixed $response, ?Throwable $exception) {
81+
$scope = Context::storage()->scope();
82+
if (!$scope) {
83+
return;
84+
}
85+
$scope->detach();
86+
87+
$span = Span::fromContext($scope->context());
88+
89+
if ($exception) {
90+
$span->recordException($exception);
91+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
92+
}
93+
94+
$span->end();
95+
}
96+
);
97+
}
98+
}
99+
100+
return $params;
101+
}
102+
);
103+
}
104+
}

src/Instrumentation/Laravel/src/Hooks/Illuminate/Routing/Router.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Illuminate\Routing\Router as LaravelRouter;
88
use Illuminate\Routing\RouteCollection;
99
use OpenTelemetry\API\Trace\Span;
10-
use OpenTelemetry\API\Trace\SpanInterface;
1110
use OpenTelemetry\API\Trace\StatusCode;
1211
use OpenTelemetry\Context\Context;
1312
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHook;

src/Instrumentation/Laravel/src/LaravelInstrumentation.php

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

3637
public static function shouldTraceCli(): bool

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,83 @@ public function test_route_span_name_if_not_found(): void
309309
]);
310310
}
311311

312+
public function test_controller_execution(): void
313+
{
314+
// Define the controller class if it doesn't exist
315+
if (!class_exists('OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\TestController')) {
316+
eval('
317+
namespace OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration;
318+
319+
class TestController
320+
{
321+
public function index()
322+
{
323+
return "Hello from controller";
324+
}
325+
}
326+
');
327+
}
328+
329+
$this->router()->get('/controller', [TestController::class, 'index']);
330+
331+
$this->assertCount(0, $this->storage);
332+
$response = $this->call('GET', '/controller');
333+
$this->assertEquals(200, $response->status());
334+
$this->assertCount(4, $this->storage);
335+
336+
$this->assertTraceStructure([
337+
[
338+
'name' => 'GET /controller',
339+
'attributes' => [
340+
'code.function.name' => 'handle',
341+
'code.namespace' => 'Illuminate\Foundation\Http\Kernel',
342+
'url.full' => 'http://localhost/controller',
343+
'http.request.method' => 'GET',
344+
'url.scheme' => 'http',
345+
'network.protocol.version' => '1.1',
346+
'network.peer.address' => '127.0.0.1',
347+
'url.path' => 'controller',
348+
'server.address' => 'localhost',
349+
'server.port' => 80,
350+
'user_agent.original' => 'Symfony',
351+
'http.route' => 'controller',
352+
'http.response.status_code' => 200,
353+
],
354+
'kind' => \OpenTelemetry\API\Trace\SpanKind::KIND_SERVER,
355+
'children' => [
356+
[
357+
'name' => 'Illuminate\Foundation\Http\Middleware\ValidatePostSize::handle',
358+
'attributes' => [
359+
'laravel.middleware.class' => 'Illuminate\Foundation\Http\Middleware\ValidatePostSize',
360+
'http.response.status_code' => 200,
361+
],
362+
'kind' => \OpenTelemetry\API\Trace\SpanKind::KIND_INTERNAL,
363+
'children' => [
364+
[
365+
'name' => 'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::handle',
366+
'attributes' => [
367+
'laravel.middleware.class' => 'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull',
368+
'http.response.status_code' => 200,
369+
],
370+
'kind' => \OpenTelemetry\API\Trace\SpanKind::KIND_INTERNAL,
371+
'children' => [
372+
[
373+
'name' => 'Controller::index',
374+
'attributes' => [
375+
'code.function.name' => 'index',
376+
'code.namespace' => 'OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\TestController',
377+
],
378+
'kind' => \OpenTelemetry\API\Trace\SpanKind::KIND_INTERNAL,
379+
],
380+
],
381+
],
382+
],
383+
],
384+
],
385+
],
386+
]);
387+
}
388+
312389
private function router(): Router
313390
{
314391
/** @psalm-suppress PossiblyNullReference */

0 commit comments

Comments
 (0)