Skip to content

Commit

Permalink
Laravel Instrumentation (#184)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
ChrisLightfootWild authored Sep 20, 2023
1 parent 294929c commit 4224034
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 116 deletions.
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,
"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;
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

0 comments on commit 4224034

Please sign in to comment.