Skip to content
Draft
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
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
3 changes: 2 additions & 1 deletion src/Instrumentation/Laravel/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"lock": false,
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": false
"php-http/discovery": false,
"tbachert/spi": false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\Illuminate\Contracts\Debug;

use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHook;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHookTrait;
use function OpenTelemetry\Instrumentation\hook;
use Throwable;

/**
* Enhanced instrumentation for Laravel's exception handler.
*/
class ExceptionHandler implements LaravelHook
{
use LaravelHookTrait;

public function instrument(): void
{
$this->hookRender();
$this->hookReport();
}

/**
* Hook into the render method to name the transaction when exceptions occur.
*/
protected function hookRender(): bool
{
return hook(
ExceptionHandlerContract::class,
'render',
pre: function (ExceptionHandlerContract $handler, array $params, string $class, string $function, ?string $filename, ?int $lineno) {
$exception = $params[0] ?? null;

// Name the transaction after the exception handler and method
$spanName = $class . '@' . $function;

// Try to get the current span
$scope = Context::storage()->scope();
if (!$scope) {
return;
}

// Get the current span
$span = Span::fromContext($scope->context());
$span->updateName($spanName);

// Record exception information
if ($exception instanceof Throwable) {
$span->recordException($exception);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
$span->setAttribute('exception.class', get_class($exception));
$span->setAttribute('exception.message', $exception->getMessage());

// Add file and line number where the exception occurred
$span->setAttribute('exception.file', $exception->getFile());
$span->setAttribute('exception.line', $exception->getLine());
}
}
);
}

/**
* Hook into the report method to record traced errors for unhandled exceptions.
*/
protected function hookReport(): bool
{
return hook(
ExceptionHandlerContract::class,
'report',
pre: function (ExceptionHandlerContract $handler, array $params, string $class, string $function, ?string $filename, ?int $lineno) {
$exception = $params[0] ?? null;
if (!$exception instanceof Throwable) {
return;
}

// Check if this exception should be reported
// Laravel's default handler has a shouldReport method that returns false
// if the exception should be ignored
if (method_exists($handler, 'shouldReport') && !$handler->shouldReport($exception)) {
return;
}

// Get the current span (or create a new one)
$scope = Context::storage()->scope();
if (!$scope) {
return;
}

$span = Span::fromContext($scope->context());

// Record the exception details
$span->recordException($exception);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
$span->setAttribute('exception.class', get_class($exception));
$span->setAttribute('exception.message', $exception->getMessage());
$span->setAttribute('exception.file', $exception->getFile());
$span->setAttribute('exception.line', $exception->getLine());
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected function hookHandle(): bool
/** @psalm-suppress ArgumentTypeCoercion */
$builder = $this->instrumentation
->tracer()
->spanBuilder(sprintf('%s', $request?->method() ?? 'unknown'))
->spanBuilder(sprintf('HTTP %s', $request?->method() ?? 'unknown'))
->setSpanKind(SpanKind::KIND_SERVER)
->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
Expand Down Expand Up @@ -87,7 +87,14 @@ protected function hookHandle(): bool
$route = $request?->route();

if ($request && $route instanceof Route) {
$span->updateName("{$request->method()} /" . ltrim($route->uri, '/'));
if (method_exists($route, 'getName') && $route->getName() && strpos($route->getName(), 'generated::') !== 0) {
$span->updateName("{$request->method()} " . $route->getName());
$span->setAttribute('laravel.route.name', $route->getName());
} elseif (method_exists($route, 'uri')) {
$span->updateName("{$request->method()} /" . ltrim($route->uri, '/'));
} else {
$span->updateName("HTTP {$request->method()}");
}
$span->setAttribute(TraceAttributes::HTTP_ROUTE, $route->uri);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\Illuminate\Foundation\Http;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Http\Kernel as HttpKernel;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHook;
use OpenTelemetry\Contrib\Instrumentation\Laravel\Hooks\LaravelHookTrait;
use function OpenTelemetry\Instrumentation\hook;
use OpenTelemetry\SemConv\TraceAttributes;
use ReflectionClass;
use Throwable;

/**
* Enhanced instrumentation for Laravel's middleware components.
*/
class Middleware implements LaravelHook
{
use LaravelHookTrait;

private $middlewareClasses = [];

public function instrument(): void
{
$this->setupMiddlewareHooks();
}

/**
* Find and hook all global middleware classes.
*/
protected function setupMiddlewareHooks(): void
{
hook(
Application::class,
'boot',
post: function (Application $app, array $params, $result, ?Throwable $exception) {
// Abort if there was an exception
if ($exception) {
return;
}

try {
// Get the HTTP kernel and its middleware
if (!$app->has(HttpKernel::class)) {
return;
}

/** @var HttpKernel $kernel */
$kernel = $app->make(HttpKernel::class);

// Get middleware property using reflection (different between Laravel versions)
$reflectionClass = new ReflectionClass($kernel);
$middlewareProperty = null;

if ($reflectionClass->hasProperty('middleware')) {
$middlewareProperty = $reflectionClass->getProperty('middleware');
$middlewareProperty->setAccessible(true);
$middleware = $middlewareProperty->getValue($kernel);
} elseif (method_exists($kernel, 'getMiddleware')) {
$middleware = $kernel->getMiddleware();
} else {
return;
}

// Hook each middleware
if (is_array($middleware)) {
foreach ($middleware as $middlewareClass) {
if (is_string($middlewareClass) && class_exists($middlewareClass)) {
if (!in_array($middlewareClass, $this->middlewareClasses)) {
$this->middlewareClasses[] = $middlewareClass;
$this->hookMiddlewareClass($middlewareClass);
}
}
}
}

// Also hook middleware groups
if ($reflectionClass->hasProperty('middlewareGroups')) {
$middlewareGroupsProperty = $reflectionClass->getProperty('middlewareGroups');
$middlewareGroupsProperty->setAccessible(true);
$middlewareGroups = $middlewareGroupsProperty->getValue($kernel);

if (is_array($middlewareGroups)) {
foreach ($middlewareGroups as $groupName => $middlewareList) {
if (is_array($middlewareList)) {
foreach ($middlewareList as $middlewareItem) {
if (is_string($middlewareItem) && class_exists($middlewareItem)) {
if (!in_array($middlewareItem, $this->middlewareClasses)) {
$this->middlewareClasses[] = $middlewareItem;
$this->hookMiddlewareClass($middlewareItem, $groupName);
}
}
}
}
}
}
}
} catch (Throwable $e) {
// Swallow exceptions to prevent breaking the application
}
}
);
}

/**
* Hook an individual middleware class.
*/
protected function hookMiddlewareClass(string $middlewareClass, ?string $group = null): void
{
// Check if the class exists and has a handle method
if (!class_exists($middlewareClass) || !method_exists($middlewareClass, 'handle')) {
return;
}

// Hook the handle method
hook(
$middlewareClass,
'handle',
pre: function (object $middleware, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($group) {
$spanName = $class . '::' . $function;
$parent = Context::getCurrent();
$parentSpan = Span::fromContext($parent);

// Don't create a new span if we're handling an exception
if ($params[0] instanceof Throwable) {
return;
}

$span = $this->instrumentation
->tracer()
->spanBuilder($spanName)
->setParent($parent)
->setSpanKind(SpanKind::KIND_INTERNAL)
->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, $function)
->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
->setAttribute(TraceAttributes::CODE_LINE_NUMBER, $lineno)
->setAttribute('laravel.middleware.class', $class);

if ($group) {
$span->setAttribute('laravel.middleware.group', $group);
}

$newSpan = $span->startSpan();
$context = $newSpan->storeInContext($parent);
Context::storage()->attach($context);
},
post: function (object $middleware, array $params, $response, ?Throwable $exception) {
$scope = Context::storage()->scope();
if (!$scope) {
return;
}

$span = Span::fromContext($scope->context());

// Record any exceptions
if ($exception) {
$span->recordException($exception);
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
}

// If this middleware short-circuits the request by returning a response,
// capture the response information
if ($response && method_exists($response, 'getStatusCode')) {
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_STATUS_CODE, $response->getStatusCode());

// Mark 5xx responses as errors
if ($response->getStatusCode() >= 500) {
$span->setStatus(StatusCode::STATUS_ERROR);
}
}

$scope->detach();
// End the span
$span->end();
}
);
}
}
Loading
Loading