Skip to content

Commit 34d218f

Browse files
Laravel Instrumentation (#184)
* Laravel Instrumentation: Hook into `\Illuminate\Contracts\Foundation\Application`. Allows for watcher registration in console Kernel contexts. * Added `mockery/mockery` to allow testing console commands in Laravel instrumentation. * Added CommandWatcher to Laravel instrumentation. * Laravel instrumentation: fixed LaravelInstrumentationTest namespace. * Laravel instrumentation: linting. * Laravel instrumentation: remove mockery dev dependency. * Laravel instrumentation: prevent real Http requests from leaking. * Laravel instrumentation: first pass at instrumenting Console\Kernel. * Laravel instrumentation: linting. * Laravel instrumentation: ordered_imports fix. * Laravel contrib: added "ext-json" to dependencies. * Laravel instrumentation Console/Http split. * Laravel console instrumentation checks scope before using it in command execute pre-hook. * Laravel linting. * Laravel: removed now redundant WithConsoleEvents trait. * Laravel: removed unused `use ($instrumentation)`. * Laravel: Moved Watchers into \OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers. * Laravel: ConsoleInstrumentation updates. * Laravel: end span post Command::execute hook. * Laravel: fixed ConsoleInstrumentationTest.
1 parent 901ea4f commit 34d218f

12 files changed

+318
-116
lines changed

composer.json

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,28 @@
99
"minimum-stability": "dev",
1010
"require": {
1111
"php": "^8.0",
12-
"laravel/framework": ">=6.0",
12+
"ext-json": "*",
1313
"ext-opentelemetry": "*",
14+
"laravel/framework": ">=6.0",
1415
"open-telemetry/api": "^1.0.0beta10",
1516
"open-telemetry/sem-conv": "^1"
1617
},
1718
"require-dev": {
19+
"friendsofphp/php-cs-fixer": "^3",
20+
"guzzlehttp/guzzle": "*",
21+
"laravel/sail": "*",
1822
"laravel/sanctum": "*",
23+
"laravel/tinker": "*",
1924
"nunomaduro/collision": "*",
20-
"friendsofphp/php-cs-fixer": "^3",
25+
"open-telemetry/sdk": "^1.0",
2126
"phan/phan": "^5.0",
2227
"php-http/mock-client": "*",
2328
"phpstan/phpstan": "^1.1",
2429
"phpstan/phpstan-phpunit": "^1.0",
25-
"psalm/plugin-phpunit": "^0.16",
26-
"open-telemetry/sdk": "^1.0",
2730
"phpunit/phpunit": "^9.5",
28-
"vimeo/psalm": "^4.0",
31+
"psalm/plugin-phpunit": "^0.16",
2932
"spatie/laravel-ignition": "*",
30-
"laravel/sail": "*",
31-
"laravel/tinker": "*",
32-
"guzzlehttp/guzzle": "*"
33+
"vimeo/psalm": "^4.0"
3334
},
3435
"autoload": {
3536
"psr-4": {
@@ -46,6 +47,7 @@
4647
}
4748
},
4849
"config": {
50+
"sort-packages": true,
4951
"allow-plugins": {
5052
"php-http/discovery": false
5153
}

src/ConsoleInstrumentation.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel;
6+
7+
use Illuminate\Console\Command;
8+
use Illuminate\Contracts\Console\Kernel;
9+
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
10+
use OpenTelemetry\API\Trace\Span;
11+
use OpenTelemetry\API\Trace\SpanKind;
12+
use OpenTelemetry\API\Trace\StatusCode;
13+
use OpenTelemetry\Context\Context;
14+
use function OpenTelemetry\Instrumentation\hook;
15+
use OpenTelemetry\SemConv\TraceAttributes;
16+
use Throwable;
17+
18+
class ConsoleInstrumentation
19+
{
20+
public static function register(CachedInstrumentation $instrumentation): void
21+
{
22+
hook(
23+
Kernel::class,
24+
'handle',
25+
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
26+
/** @psalm-suppress ArgumentTypeCoercion */
27+
$builder = $instrumentation->tracer()
28+
->spanBuilder('Artisan handler')
29+
->setSpanKind(SpanKind::KIND_PRODUCER)
30+
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
31+
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
32+
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
33+
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);
34+
35+
$parent = Context::getCurrent();
36+
$span = $builder->startSpan();
37+
Context::storage()->attach($span->storeInContext($parent));
38+
39+
return $params;
40+
},
41+
post: static function (Kernel $kernel, array $params, ?int $exitCode, ?Throwable $exception) {
42+
$scope = Context::storage()->scope();
43+
if (!$scope) {
44+
return;
45+
}
46+
47+
$scope->detach();
48+
$span = Span::fromContext($scope->context());
49+
if ($exception) {
50+
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
51+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
52+
} elseif ($exitCode !== Command::SUCCESS) {
53+
$span->setStatus(StatusCode::STATUS_ERROR);
54+
} else {
55+
$span->setStatus(StatusCode::STATUS_OK);
56+
}
57+
58+
$span->end();
59+
}
60+
);
61+
62+
hook(
63+
Command::class,
64+
'execute',
65+
pre: static function (Command $command, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
66+
/** @psalm-suppress ArgumentTypeCoercion */
67+
$builder = $instrumentation->tracer()
68+
->spanBuilder(sprintf('Command %s', $command->getName() ?: 'unknown'))
69+
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
70+
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
71+
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
72+
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);
73+
74+
$parent = Context::getCurrent();
75+
$span = $builder->startSpan();
76+
Context::storage()->attach($span->storeInContext($parent));
77+
78+
return $params;
79+
},
80+
post: static function (Command $command, array $params, ?int $exitCode, ?Throwable $exception) {
81+
$scope = Context::storage()->scope();
82+
if (!$scope) {
83+
return;
84+
}
85+
86+
$scope->detach();
87+
$span = Span::fromContext($scope->context());
88+
$span->addEvent('command finished', [
89+
'exit-code' => $exitCode,
90+
]);
91+
92+
if ($exception) {
93+
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
94+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
95+
}
96+
97+
$span->end();
98+
}
99+
);
100+
}
101+
}

src/HttpInstrumentation.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel;
6+
7+
use Illuminate\Contracts\Http\Kernel;
8+
use Illuminate\Http\Request;
9+
use OpenTelemetry\API\Globals;
10+
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
11+
use OpenTelemetry\API\Trace\Span;
12+
use OpenTelemetry\API\Trace\SpanInterface;
13+
use OpenTelemetry\API\Trace\SpanKind;
14+
use OpenTelemetry\API\Trace\StatusCode;
15+
use OpenTelemetry\Context\Context;
16+
use function OpenTelemetry\Instrumentation\hook;
17+
use OpenTelemetry\SemConv\TraceAttributes;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Throwable;
20+
21+
class HttpInstrumentation
22+
{
23+
public static function register(CachedInstrumentation $instrumentation): void
24+
{
25+
hook(
26+
Kernel::class,
27+
'handle',
28+
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
29+
$request = ($params[0] instanceof Request) ? $params[0] : null;
30+
/** @psalm-suppress ArgumentTypeCoercion */
31+
$builder = $instrumentation->tracer()
32+
->spanBuilder(sprintf('HTTP %s', $request?->method() ?? 'unknown'))
33+
->setSpanKind(SpanKind::KIND_SERVER)
34+
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
35+
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
36+
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
37+
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);
38+
$parent = Context::getCurrent();
39+
if ($request) {
40+
$parent = Globals::propagator()->extract($request, HeadersPropagator::instance());
41+
$span = $builder
42+
->setParent($parent)
43+
->setAttribute(TraceAttributes::HTTP_URL, $request->fullUrl())
44+
->setAttribute(TraceAttributes::HTTP_METHOD, $request->method())
45+
->setAttribute(TraceAttributes::HTTP_REQUEST_CONTENT_LENGTH, $request->header('Content-Length'))
46+
->setAttribute(TraceAttributes::HTTP_SCHEME, $request->getScheme())
47+
->setAttribute(TraceAttributes::HTTP_FLAVOR, $request->getProtocolVersion())
48+
->setAttribute(TraceAttributes::HTTP_CLIENT_IP, $request->ip())
49+
->setAttribute(TraceAttributes::HTTP_TARGET, self::httpTarget($request))
50+
->setAttribute(TraceAttributes::NET_HOST_NAME, self::httpHostName($request))
51+
->setAttribute(TraceAttributes::NET_HOST_PORT, $request->getPort())
52+
->setAttribute(TraceAttributes::NET_PEER_PORT, $request->server('REMOTE_PORT'))
53+
->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->userAgent())
54+
->startSpan();
55+
$request->attributes->set(SpanInterface::class, $span);
56+
} else {
57+
$span = $builder->startSpan();
58+
}
59+
Context::storage()->attach($span->storeInContext($parent));
60+
61+
return [$request];
62+
},
63+
post: static function (Kernel $kernel, array $params, ?Response $response, ?Throwable $exception) {
64+
$scope = Context::storage()->scope();
65+
if (!$scope) {
66+
return;
67+
}
68+
$scope->detach();
69+
$span = Span::fromContext($scope->context());
70+
if ($exception) {
71+
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
72+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
73+
}
74+
if ($response) {
75+
if ($response->getStatusCode() >= 400) {
76+
$span->setStatus(StatusCode::STATUS_ERROR);
77+
}
78+
$span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, $response->getStatusCode());
79+
$span->setAttribute(TraceAttributes::HTTP_FLAVOR, $response->getProtocolVersion());
80+
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_CONTENT_LENGTH, $response->headers->get('Content-Length'));
81+
}
82+
83+
$span->end();
84+
}
85+
);
86+
}
87+
88+
private static function httpTarget(Request $request): string
89+
{
90+
$query = $request->getQueryString();
91+
$question = $request->getBaseUrl() . $request->getPathInfo() === '/' ? '/?' : '?';
92+
93+
return $query ? $request->path() . $question . $query : $request->path();
94+
}
95+
96+
private static function httpHostName(Request $request): string
97+
{
98+
if (method_exists($request, 'host')) {
99+
return $request->host();
100+
}
101+
if (method_exists($request, 'getHost')) {
102+
return $request->getHost();
103+
}
104+
105+
return '';
106+
}
107+
}

src/LaravelInstrumentation.php

Lines changed: 16 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44

55
namespace OpenTelemetry\Contrib\Instrumentation\Laravel;
66

7-
use Illuminate\Foundation\Application;
8-
use Illuminate\Foundation\Http\Kernel;
9-
use Illuminate\Http\Request;
10-
use OpenTelemetry\API\Globals;
7+
use Illuminate\Contracts\Foundation\Application;
118
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
12-
use OpenTelemetry\API\Trace\Span;
13-
use OpenTelemetry\API\Trace\SpanInterface;
14-
use OpenTelemetry\API\Trace\SpanKind;
15-
use OpenTelemetry\API\Trace\StatusCode;
16-
use OpenTelemetry\Context\Context;
9+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\CacheWatcher;
10+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\ClientRequestWatcher;
11+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\ExceptionWatcher;
12+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\LogWatcher;
13+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\QueryWatcher;
14+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\Watcher;
1715
use function OpenTelemetry\Instrumentation\hook;
18-
use OpenTelemetry\SemConv\TraceAttributes;
19-
use Symfony\Component\HttpFoundation\Response;
2016
use Throwable;
2117

2218
class LaravelInstrumentation
@@ -31,101 +27,20 @@ public static function registerWatchers(Application $app, Watcher $watcher)
3127
public static function register(): void
3228
{
3329
$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.laravel');
34-
hook(
35-
Kernel::class,
36-
'handle',
37-
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
38-
$request = ($params[0] instanceof Request) ? $params[0] : null;
39-
/** @psalm-suppress ArgumentTypeCoercion */
40-
$builder = $instrumentation->tracer()
41-
->spanBuilder(sprintf('HTTP %s', $request?->method() ?? 'unknown'))
42-
->setSpanKind(SpanKind::KIND_SERVER)
43-
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
44-
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
45-
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
46-
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);
47-
$parent = Context::getCurrent();
48-
if ($request) {
49-
$parent = Globals::propagator()->extract($request, HeadersPropagator::instance());
50-
$span = $builder
51-
->setParent($parent)
52-
->setAttribute(TraceAttributes::HTTP_URL, $request->fullUrl())
53-
->setAttribute(TraceAttributes::HTTP_METHOD, $request->method())
54-
->setAttribute(TraceAttributes::HTTP_REQUEST_CONTENT_LENGTH, $request->header('Content-Length'))
55-
->setAttribute(TraceAttributes::HTTP_SCHEME, $request->getScheme())
56-
->setAttribute(TraceAttributes::HTTP_FLAVOR, $request->getProtocolVersion())
57-
->setAttribute(TraceAttributes::HTTP_CLIENT_IP, $request->ip())
58-
->setAttribute(TraceAttributes::HTTP_TARGET, self::httpTarget($request))
59-
->setAttribute(TraceAttributes::NET_HOST_NAME, self::httpHostName($request))
60-
->setAttribute(TraceAttributes::NET_HOST_PORT, $request->getPort())
61-
->setAttribute(TraceAttributes::NET_PEER_PORT, $request->server('REMOTE_PORT'))
62-
->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->userAgent())
63-
->startSpan();
64-
$request->attributes->set(SpanInterface::class, $span);
65-
} else {
66-
$span = $builder->startSpan();
67-
}
68-
Context::storage()->attach($span->storeInContext($parent));
69-
70-
return [$request];
71-
},
72-
post: static function (Kernel $kernel, array $params, ?Response $response, ?Throwable $exception) {
73-
$scope = Context::storage()->scope();
74-
if (!$scope) {
75-
return;
76-
}
77-
$scope->detach();
78-
$span = Span::fromContext($scope->context());
79-
if ($exception) {
80-
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
81-
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
82-
}
83-
if ($response) {
84-
if ($response->getStatusCode() >= 400) {
85-
$span->setStatus(StatusCode::STATUS_ERROR);
86-
}
87-
$span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, $response->getStatusCode());
88-
$span->setAttribute(TraceAttributes::HTTP_FLAVOR, $response->getProtocolVersion());
89-
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_CONTENT_LENGTH, $response->headers->get('Content-Length'));
90-
}
9130

92-
$span->end();
93-
}
94-
);
9531
hook(
96-
Kernel::class,
32+
Application::class,
9733
'__construct',
98-
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
99-
$app = $params[0];
100-
$app->booted(static function (Application $app) use ($instrumentation) {
101-
self::registerWatchers($app, new ClientRequestWatcher($instrumentation));
102-
self::registerWatchers($app, new ExceptionWatcher());
103-
self::registerWatchers($app, new CacheWatcher());
104-
self::registerWatchers($app, new LogWatcher());
105-
self::registerWatchers($app, new QueryWatcher($instrumentation));
106-
});
34+
post: static function (Application $application, array $params, mixed $returnValue, ?Throwable $exception) use ($instrumentation) {
35+
self::registerWatchers($application, new CacheWatcher());
36+
self::registerWatchers($application, new ClientRequestWatcher($instrumentation));
37+
self::registerWatchers($application, new ExceptionWatcher());
38+
self::registerWatchers($application, new LogWatcher());
39+
self::registerWatchers($application, new QueryWatcher($instrumentation));
10740
},
108-
post: null
10941
);
110-
}
111-
112-
private static function httpTarget(Request $request): string
113-
{
114-
$query = $request->getQueryString();
115-
$question = $request->getBaseUrl() . $request->getPathInfo() === '/' ? '/?' : '?';
116-
117-
return $query ? $request->path() . $question . $query : $request->path();
118-
}
119-
120-
private static function httpHostName(Request $request): string
121-
{
122-
if (method_exists($request, 'host')) {
123-
return $request->host();
124-
}
125-
if (method_exists($request, 'getHost')) {
126-
return $request->getHost();
127-
}
12842

129-
return '';
43+
ConsoleInstrumentation::register($instrumentation);
44+
HttpInstrumentation::register($instrumentation);
13045
}
13146
}

0 commit comments

Comments
 (0)