Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Laravel Instrumentation #184

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
347a001
Laravel Instrumentation:
ChrisLightfootWild Aug 11, 2023
1221735
Added `mockery/mockery` to allow testing console commands in Laravel …
ChrisLightfootWild Aug 11, 2023
24ae2ea
Added CommandWatcher to Laravel instrumentation.
ChrisLightfootWild Aug 11, 2023
3b24774
Laravel instrumentation: fixed LaravelInstrumentationTest namespace.
ChrisLightfootWild Aug 11, 2023
cb5eea9
Laravel instrumentation: linting.
ChrisLightfootWild Aug 11, 2023
4cc513c
Laravel instrumentation: remove mockery dev dependency.
ChrisLightfootWild Aug 12, 2023
7c02e60
Laravel instrumentation: prevent real Http requests from leaking.
ChrisLightfootWild Aug 12, 2023
f49d613
Laravel instrumentation: first pass at instrumenting Console\Kernel.
ChrisLightfootWild Aug 12, 2023
da40eaf
Laravel instrumentation: linting.
ChrisLightfootWild Aug 13, 2023
e376229
Merge remote-tracking branch 'upstream/main' into laravel/hook/applic…
ChrisLightfootWild Aug 13, 2023
792a80d
Laravel instrumentation: ordered_imports fix.
ChrisLightfootWild Aug 13, 2023
b52e707
Merge remote-tracking branch 'upstream/main' into laravel/hook/applic…
ChrisLightfootWild Aug 23, 2023
1eadc5c
Laravel contrib: added "ext-json" to dependencies.
ChrisLightfootWild Aug 23, 2023
f2b5e31
Laravel instrumentation Console/Http split.
ChrisLightfootWild Aug 28, 2023
4ed45f8
Laravel console instrumentation checks scope before using it in comma…
ChrisLightfootWild Aug 28, 2023
632df00
Laravel linting.
ChrisLightfootWild Aug 28, 2023
d3e55ba
Laravel: removed now redundant WithConsoleEvents trait.
ChrisLightfootWild Aug 28, 2023
973f695
Laravel: removed unused `use ($instrumentation)`.
ChrisLightfootWild Aug 28, 2023
a24857e
Merge remote-tracking branch 'upstream/main' into laravel/hook/applic…
ChrisLightfootWild Aug 29, 2023
bc4c216
Laravel: Moved Watchers into \OpenTelemetry\Contrib\Instrumentation\L…
ChrisLightfootWild Sep 2, 2023
2c18066
Laravel: ConsoleInstrumentation updates.
ChrisLightfootWild Sep 3, 2023
22dd5b0
Laravel: end span post Command::execute hook.
ChrisLightfootWild Sep 19, 2023
a8138ad
Laravel: fixed ConsoleInstrumentationTest.
ChrisLightfootWild Sep 20, 2023
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
18 changes: 10 additions & 8 deletions src/Instrumentation/Laravel/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,28 @@
"minimum-stability": "dev",
"require": {
"php": "^8.0",
"laravel/framework": ">=6.0",
"ext-json": "*",
"ext-opentelemetry": "*",
"laravel/framework": ">=6.0",
"open-telemetry/api": "^1.0.0beta10",
"open-telemetry/sem-conv": "^1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3",
"guzzlehttp/guzzle": "*",
"laravel/sail": "*",
"laravel/sanctum": "*",
"laravel/tinker": "*",
"nunomaduro/collision": "*",
"friendsofphp/php-cs-fixer": "^3",
"open-telemetry/sdk": "^1.0",
"phan/phan": "^5.0",
"php-http/mock-client": "*",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"psalm/plugin-phpunit": "^0.16",
"open-telemetry/sdk": "^1.0",
"phpunit/phpunit": "^9.5",
"vimeo/psalm": "^4.0",
"psalm/plugin-phpunit": "^0.16",
"spatie/laravel-ignition": "*",
"laravel/sail": "*",
"laravel/tinker": "*",
"guzzlehttp/guzzle": "*"
"vimeo/psalm": "^4.0"
},
"autoload": {
"psr-4": {
Expand All @@ -46,6 +47,7 @@
}
},
"config": {
"sort-packages": true,
brettmc marked this conversation as resolved.
Show resolved Hide resolved
"allow-plugins": {
"php-http/discovery": false
}
Expand Down
101 changes: 101 additions & 0 deletions src/Instrumentation/Laravel/src/ConsoleInstrumentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Laravel;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
use Throwable;

class ConsoleInstrumentation
{
public static function register(CachedInstrumentation $instrumentation): void
{
hook(
Kernel::class,
'handle',
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
/** @psalm-suppress ArgumentTypeCoercion */
$builder = $instrumentation->tracer()
->spanBuilder('Artisan handler')
->setSpanKind(SpanKind::KIND_PRODUCER)
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);

$parent = Context::getCurrent();
$span = $builder->startSpan();
Context::storage()->attach($span->storeInContext($parent));

return $params;
},
post: static function (Kernel $kernel, array $params, ?int $exitCode, ?Throwable $exception) {
$scope = Context::storage()->scope();
if (!$scope) {
return;
}

$scope->detach();
$span = Span::fromContext($scope->context());
if ($exception) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
} elseif ($exitCode !== Command::SUCCESS) {
$span->setStatus(StatusCode::STATUS_ERROR);
} else {
$span->setStatus(StatusCode::STATUS_OK);
}

$span->end();
}
);

hook(
Command::class,
'execute',
pre: static function (Command $command, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
/** @psalm-suppress ArgumentTypeCoercion */
$builder = $instrumentation->tracer()
->spanBuilder(sprintf('Command %s', $command->getName() ?: 'unknown'))
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);

$parent = Context::getCurrent();
$span = $builder->startSpan();
Context::storage()->attach($span->storeInContext($parent));

return $params;
},
post: static function (Command $command, array $params, ?int $exitCode, ?Throwable $exception) {
$scope = Context::storage()->scope();
if (!$scope) {
return;
}

$scope->detach();
$span = Span::fromContext($scope->context());
$span->addEvent('command finished', [
'exit-code' => $exitCode,
]);

if ($exception) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}

$span->end();
}
);
}
}
107 changes: 107 additions & 0 deletions src/Instrumentation/Laravel/src/HttpInstrumentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Laravel;

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class HttpInstrumentation
{
public static function register(CachedInstrumentation $instrumentation): void
{
hook(
Kernel::class,
'handle',
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
$request = ($params[0] instanceof Request) ? $params[0] : null;
/** @psalm-suppress ArgumentTypeCoercion */
$builder = $instrumentation->tracer()
->spanBuilder(sprintf('HTTP %s', $request?->method() ?? 'unknown'))
->setSpanKind(SpanKind::KIND_SERVER)
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);
$parent = Context::getCurrent();
if ($request) {
$parent = Globals::propagator()->extract($request, HeadersPropagator::instance());
$span = $builder
->setParent($parent)
->setAttribute(TraceAttributes::HTTP_URL, $request->fullUrl())
->setAttribute(TraceAttributes::HTTP_METHOD, $request->method())
->setAttribute(TraceAttributes::HTTP_REQUEST_CONTENT_LENGTH, $request->header('Content-Length'))
->setAttribute(TraceAttributes::HTTP_SCHEME, $request->getScheme())
->setAttribute(TraceAttributes::HTTP_FLAVOR, $request->getProtocolVersion())
->setAttribute(TraceAttributes::HTTP_CLIENT_IP, $request->ip())
->setAttribute(TraceAttributes::HTTP_TARGET, self::httpTarget($request))
->setAttribute(TraceAttributes::NET_HOST_NAME, self::httpHostName($request))
->setAttribute(TraceAttributes::NET_HOST_PORT, $request->getPort())
->setAttribute(TraceAttributes::NET_PEER_PORT, $request->server('REMOTE_PORT'))
->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->userAgent())
->startSpan();
$request->attributes->set(SpanInterface::class, $span);
} else {
$span = $builder->startSpan();
}
Context::storage()->attach($span->storeInContext($parent));

return [$request];
},
post: static function (Kernel $kernel, array $params, ?Response $response, ?Throwable $exception) {
$scope = Context::storage()->scope();
if (!$scope) {
return;
}
$scope->detach();
$span = Span::fromContext($scope->context());
if ($exception) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}
if ($response) {
if ($response->getStatusCode() >= 400) {
$span->setStatus(StatusCode::STATUS_ERROR);
}
$span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, $response->getStatusCode());
$span->setAttribute(TraceAttributes::HTTP_FLAVOR, $response->getProtocolVersion());
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_CONTENT_LENGTH, $response->headers->get('Content-Length'));
}

$span->end();
}
);
}

private static function httpTarget(Request $request): string
{
$query = $request->getQueryString();
$question = $request->getBaseUrl() . $request->getPathInfo() === '/' ? '/?' : '?';

return $query ? $request->path() . $question . $query : $request->path();
}

private static function httpHostName(Request $request): string
{
if (method_exists($request, 'host')) {
return $request->host();
}
if (method_exists($request, 'getHost')) {
return $request->getHost();
}

return '';
}
}
117 changes: 16 additions & 101 deletions src/Instrumentation/Laravel/src/LaravelInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@

namespace OpenTelemetry\Contrib\Instrumentation\Laravel;

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Http\Kernel;
use Illuminate\Http\Request;
use OpenTelemetry\API\Globals;
use Illuminate\Contracts\Foundation\Application;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Watcher is already typed against \Illuminate\Contracts\Foundation\Application, so this maintains that type for consistency.

use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\CacheWatcher;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\ClientRequestWatcher;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\ExceptionWatcher;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\LogWatcher;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\QueryWatcher;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\Watcher;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class LaravelInstrumentation
Expand All @@ -31,101 +27,20 @@ public static function registerWatchers(Application $app, Watcher $watcher)
public static function register(): void
{
$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.laravel');
hook(
Kernel::class,
'handle',
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
$request = ($params[0] instanceof Request) ? $params[0] : null;
/** @psalm-suppress ArgumentTypeCoercion */
$builder = $instrumentation->tracer()
->spanBuilder(sprintf('HTTP %s', $request?->method() ?? 'unknown'))
->setSpanKind(SpanKind::KIND_SERVER)
->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINENO, $lineno);
$parent = Context::getCurrent();
if ($request) {
$parent = Globals::propagator()->extract($request, HeadersPropagator::instance());
$span = $builder
->setParent($parent)
->setAttribute(TraceAttributes::HTTP_URL, $request->fullUrl())
->setAttribute(TraceAttributes::HTTP_METHOD, $request->method())
->setAttribute(TraceAttributes::HTTP_REQUEST_CONTENT_LENGTH, $request->header('Content-Length'))
->setAttribute(TraceAttributes::HTTP_SCHEME, $request->getScheme())
->setAttribute(TraceAttributes::HTTP_FLAVOR, $request->getProtocolVersion())
->setAttribute(TraceAttributes::HTTP_CLIENT_IP, $request->ip())
->setAttribute(TraceAttributes::HTTP_TARGET, self::httpTarget($request))
->setAttribute(TraceAttributes::NET_HOST_NAME, self::httpHostName($request))
->setAttribute(TraceAttributes::NET_HOST_PORT, $request->getPort())
->setAttribute(TraceAttributes::NET_PEER_PORT, $request->server('REMOTE_PORT'))
->setAttribute(TraceAttributes::USER_AGENT_ORIGINAL, $request->userAgent())
->startSpan();
$request->attributes->set(SpanInterface::class, $span);
} else {
$span = $builder->startSpan();
}
Context::storage()->attach($span->storeInContext($parent));

return [$request];
},
post: static function (Kernel $kernel, array $params, ?Response $response, ?Throwable $exception) {
$scope = Context::storage()->scope();
if (!$scope) {
return;
}
$scope->detach();
$span = Span::fromContext($scope->context());
if ($exception) {
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}
if ($response) {
if ($response->getStatusCode() >= 400) {
$span->setStatus(StatusCode::STATUS_ERROR);
}
$span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, $response->getStatusCode());
$span->setAttribute(TraceAttributes::HTTP_FLAVOR, $response->getProtocolVersion());
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_CONTENT_LENGTH, $response->headers->get('Content-Length'));
}

$span->end();
}
);
hook(
Kernel::class,
Application::class,
'__construct',
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
$app = $params[0];
$app->booted(static function (Application $app) use ($instrumentation) {
self::registerWatchers($app, new ClientRequestWatcher($instrumentation));
self::registerWatchers($app, new ExceptionWatcher());
self::registerWatchers($app, new CacheWatcher());
self::registerWatchers($app, new LogWatcher());
self::registerWatchers($app, new QueryWatcher($instrumentation));
});
post: static function (Application $application, array $params, mixed $returnValue, ?Throwable $exception) use ($instrumentation) {
self::registerWatchers($application, new CacheWatcher());
self::registerWatchers($application, new ClientRequestWatcher($instrumentation));
self::registerWatchers($application, new ExceptionWatcher());
self::registerWatchers($application, new LogWatcher());
self::registerWatchers($application, new QueryWatcher($instrumentation));
},
post: null
);
}

private static function httpTarget(Request $request): string
{
$query = $request->getQueryString();
$question = $request->getBaseUrl() . $request->getPathInfo() === '/' ? '/?' : '?';

return $query ? $request->path() . $question . $query : $request->path();
}

private static function httpHostName(Request $request): string
{
if (method_exists($request, 'host')) {
return $request->host();
}
if (method_exists($request, 'getHost')) {
return $request->getHost();
}

return '';
ConsoleInstrumentation::register($instrumentation);
HttpInstrumentation::register($instrumentation);
}
}
Loading
Loading