diff --git a/src/Instrumentation/Laravel/src/CommandWatcher.php b/src/Instrumentation/Laravel/src/CommandWatcher.php deleted file mode 100644 index 35a25492..00000000 --- a/src/Instrumentation/Laravel/src/CommandWatcher.php +++ /dev/null @@ -1,32 +0,0 @@ -listen(CommandFinished::class, [$this, 'recordCommandFinished']); - } - - public function recordCommandFinished(CommandFinished $command): void - { - $scope = Context::storage()->scope(); - if (!$scope) { - return; - } - $span = Span::fromContext($scope->context()); - $span->addEvent('command finished', [ - 'command' => $command->command, - 'exit-code' => $command->exitCode, - ]); - } -} diff --git a/src/Instrumentation/Laravel/src/ConsoleInstrumentation.php b/src/Instrumentation/Laravel/src/ConsoleInstrumentation.php new file mode 100644 index 00000000..01185e04 --- /dev/null +++ b/src/Instrumentation/Laravel/src/ConsoleInstrumentation.php @@ -0,0 +1,90 @@ +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) { + $scope = Context::storage()->scope(); + $span = Span::fromContext($scope->context()); + $span->addEvent('command starting', [ + 'command' => $command->getName(), + ]); + + return $params; + }, + post: static function (Command $command, array $params, ?int $exitCode, ?Throwable $exception) { + $scope = Context::storage()->scope(); + if (!$scope) { + return; + } + $span = Span::fromContext($scope->context()); + $span->addEvent('command finished', [ + 'command' => $command->getName(), + 'exit-code' => $exitCode, + ]); + + if ($exception) { + $span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]); + $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); + } + } + ); + } +} diff --git a/src/Instrumentation/Laravel/src/HttpInstrumentation.php b/src/Instrumentation/Laravel/src/HttpInstrumentation.php new file mode 100644 index 00000000..1d5e9291 --- /dev/null +++ b/src/Instrumentation/Laravel/src/HttpInstrumentation.php @@ -0,0 +1,107 @@ +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 ''; + } +} diff --git a/src/Instrumentation/Laravel/src/LaravelInstrumentation.php b/src/Instrumentation/Laravel/src/LaravelInstrumentation.php index b67e0dd8..f26fab69 100644 --- a/src/Instrumentation/Laravel/src/LaravelInstrumentation.php +++ b/src/Instrumentation/Laravel/src/LaravelInstrumentation.php @@ -4,21 +4,9 @@ namespace OpenTelemetry\Contrib\Instrumentation\Laravel; -use Illuminate\Console\Command; -use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Contracts\Http\Kernel as HttpKernel; -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 LaravelInstrumentation @@ -33,135 +21,20 @@ public static function registerWatchers(Application $app, Watcher $watcher) public static function register(): void { $instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.laravel'); - hook( - HttpKernel::class, - 'handle', - pre: static function (HttpKernel $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 (HttpKernel $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( - ConsoleKernel::class, - 'call', - pre: static function (ConsoleKernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { - /** @psalm-suppress ArgumentTypeCoercion */ - $builder = $instrumentation->tracer() - ->spanBuilder(sprintf('Console %s', $params[0] ?? 'unknown')) - ->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 (ConsoleKernel $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); - } - - $span->end(); - } - ); hook( Application::class, '__construct', 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 CacheWatcher()); - self::registerWatchers($application, new CommandWatcher()); self::registerWatchers($application, new LogWatcher()); self::registerWatchers($application, new QueryWatcher($instrumentation)); }, ); - } - - 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); } } diff --git a/src/Instrumentation/Laravel/tests/Integration/CommandWatcherTest.php b/src/Instrumentation/Laravel/tests/Integration/ConsoleInstrumentationTest.php similarity index 75% rename from src/Instrumentation/Laravel/tests/Integration/CommandWatcherTest.php rename to src/Instrumentation/Laravel/tests/Integration/ConsoleInstrumentationTest.php index 9823f145..307a50e4 100644 --- a/src/Instrumentation/Laravel/tests/Integration/CommandWatcherTest.php +++ b/src/Instrumentation/Laravel/tests/Integration/ConsoleInstrumentationTest.php @@ -6,6 +6,7 @@ use ArrayObject; use Illuminate\Console\Command; +use Illuminate\Contracts\Console\Kernel; use Illuminate\Foundation\Testing\WithConsoleEvents; use OpenTelemetry\API\Instrumentation\Configurator; use OpenTelemetry\Context\ScopeInterface; @@ -15,7 +16,7 @@ use OpenTelemetry\SDK\Trace\TracerProvider; use OpenTelemetry\Tests\Instrumentation\Laravel\TestCase; -class CommandWatcherTest extends TestCase +class ConsoleInstrumentationTest extends TestCase { use WithConsoleEvents; @@ -47,18 +48,24 @@ public function tearDown(): void public function test_command_tracing(): void { $this->assertCount(0, $this->storage); - $exitCode = $this->withoutMockingConsoleOutput()->artisan('about'); + + /** @var Kernel $kernel */ + $kernel = $this->app[Kernel::class]; + $exitCode = $kernel->handle( + new \Symfony\Component\Console\Input\ArrayInput(['optimize:clear']), + new \Symfony\Component\Console\Output\NullOutput(), + ); + $this->assertEquals(Command::SUCCESS, $exitCode); $this->assertCount(1, $this->storage); /** @var ImmutableSpan $span */ $span = $this->storage->offsetGet(0); - $this->assertSame('Console about', $span->getName()); - $this->assertCount(1, $span->getEvents()); + $this->assertSame('Artisan handler', $span->getName()); + $this->assertCount(14, $span->getEvents()); $event = $span->getEvents()[0]; $this->assertSame([ - 'command' => 'about', - 'exit-code' => 0, + 'command' => 'optimize:clear', ], $event->getAttributes()->toArray()); } }