Skip to content

Commit 9c7ad1c

Browse files
authored
initial laravel auto instrumentation, root span, kernel::handle hook (#116)
* initial laravel auto instrumentation, root span, kernel::handle hook * update laravel version * ignore vendor directory
0 parents  commit 9c7ad1c

File tree

10 files changed

+354
-0
lines changed

10 files changed

+354
-0
lines changed

.php-cs-fixer.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
$finder = PhpCsFixer\Finder::create()
3+
->exclude('vendor')
4+
->exclude('var/cache')
5+
->in(__DIR__);
6+
7+
$config = new PhpCsFixer\Config();
8+
return $config->setRules([
9+
'concat_space' => ['spacing' => 'one'],
10+
'declare_equal_normalize' => ['space' => 'none'],
11+
'is_null' => true,
12+
'modernize_types_casting' => true,
13+
'ordered_imports' => true,
14+
'php_unit_construct' => true,
15+
'single_line_comment_style' => true,
16+
'yoda_style' => false,
17+
'@PSR2' => true,
18+
'array_syntax' => ['syntax' => 'short'],
19+
'blank_line_after_opening_tag' => true,
20+
'blank_line_before_statement' => true,
21+
'cast_spaces' => true,
22+
'declare_strict_types' => true,
23+
'function_typehint_space' => true,
24+
'include' => true,
25+
'lowercase_cast' => true,
26+
'new_with_braces' => true,
27+
'no_extra_blank_lines' => true,
28+
'no_leading_import_slash' => true,
29+
'echo_tag_syntax' => true,
30+
'no_unused_imports' => true,
31+
'no_useless_else' => true,
32+
'no_useless_return' => true,
33+
'phpdoc_order' => true,
34+
'phpdoc_scalar' => true,
35+
'phpdoc_types' => true,
36+
'short_scalar_cast' => true,
37+
'single_blank_line_before_namespace' => true,
38+
'single_quote' => true,
39+
'trailing_comma_in_multiline' => true,
40+
])
41+
->setRiskyAllowed(true)
42+
->setFinder($finder);
43+

_register.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
assert(extension_loaded('otel_instrumentation'));
6+
7+
\OpenTelemetry\Contrib\Instrumentation\Laravel\LaravelInstrumentation::register();

composer.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "open-telemetry/opentelemetry-auto-laravel",
3+
"description": "OpenTelemetry auto-instrumentation for Laravel",
4+
"keywords": ["opentelemetry", "otel", "open-telemetry", "tracing", "laravel", "instrumentation"],
5+
"type": "library",
6+
"homepage": "https://opentelemetry.io/docs/php",
7+
"readme": "./README.md",
8+
"license": "Apache-2.0",
9+
"minimum-stability": "dev",
10+
"require": {
11+
"php": "^8.0",
12+
"laravel/framework": "^9.52.1",
13+
"ext-otel_instrumentation": "*",
14+
"open-telemetry/api": "^1"
15+
},
16+
"require-dev": {
17+
"friendsofphp/php-cs-fixer": "^3",
18+
"phan/phan": "^5.0",
19+
"php-http/mock-client": "*",
20+
"phpstan/phpstan": "^1.1",
21+
"phpstan/phpstan-phpunit": "^1.0",
22+
"psalm/plugin-phpunit": "^0.16",
23+
"open-telemetry/sdk": "^1.0",
24+
"phpunit/phpunit": "^9.5",
25+
"vimeo/psalm": "^4.0"
26+
},
27+
"autoload": {
28+
"psr-4": {
29+
"OpenTelemetry\\Contrib\\Instrumentation\\Laravel\\": "src/"
30+
},
31+
"files": [
32+
"_register.php"
33+
]
34+
},
35+
"autoload-dev": {
36+
"psr-4": {
37+
"OpenTelemetry\\Tests\\Instrumentation\\Laravel\\": "tests/"
38+
}
39+
},
40+
"config": {
41+
"allow-plugins": {
42+
"php-http/discovery": false
43+
}
44+
}
45+
}

phpstan.neon.dist

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
includes:
2+
- vendor/phpstan/phpstan-phpunit/extension.neon
3+
4+
parameters:
5+
tmpDir: var/cache/phpstan
6+
level: 5
7+
paths:
8+
- src
9+
- tests

phpunit.xml.dist

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
6+
backupGlobals="false"
7+
backupStaticAttributes="false"
8+
cacheResult="false"
9+
colors="false"
10+
convertErrorsToExceptions="true"
11+
convertNoticesToExceptions="true"
12+
convertWarningsToExceptions="true"
13+
forceCoversAnnotation="false"
14+
processIsolation="false"
15+
stopOnError="false"
16+
stopOnFailure="false"
17+
stopOnIncomplete="false"
18+
stopOnSkipped="false"
19+
stopOnRisky="false"
20+
timeoutForSmallTests="1"
21+
timeoutForMediumTests="10"
22+
timeoutForLargeTests="60"
23+
verbose="true">
24+
25+
<coverage processUncoveredFiles="true" disableCodeCoverageIgnore="false">
26+
<include>
27+
<directory>src</directory>
28+
</include>
29+
</coverage>
30+
31+
<php>
32+
<ini name="date.timezone" value="UTC" />
33+
<ini name="display_errors" value="On" />
34+
<ini name="display_startup_errors" value="On" />
35+
<ini name="error_reporting" value="E_ALL" />
36+
</php>
37+
38+
<testsuites>
39+
<testsuite name="unit">
40+
<directory>tests/Unit</directory>
41+
</testsuite>
42+
<testsuite name="integration">
43+
<directory>tests/Integration</directory>
44+
</testsuite>
45+
</testsuites>
46+
47+
</phpunit>

psalm.xml.dist

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0"?>
2+
<psalm
3+
errorLevel="3"
4+
cacheDirectory="var/cache/psalm"
5+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
6+
xmlns="https://getpsalm.org/schema/config"
7+
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd">
8+
<projectFiles>
9+
<ignoreFiles>
10+
<directory name="vendor"/>
11+
</ignoreFiles>
12+
<directory name="src"/>
13+
<directory name="tests"/>
14+
</projectFiles>
15+
<plugins>
16+
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
17+
</plugins>
18+
</psalm>

src/HeadersPropagator.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel;
6+
7+
use function assert;
8+
use Illuminate\Http\Request;
9+
use OpenTelemetry\Context\Propagation\PropagationGetterInterface;
10+
11+
/**
12+
* @internal
13+
*/
14+
class HeadersPropagator implements PropagationGetterInterface
15+
{
16+
public static function instance(): self
17+
{
18+
static $instance;
19+
20+
return $instance ??= new self();
21+
}
22+
23+
/** @psalm-suppress InvalidReturnType */
24+
public function keys($carrier): array
25+
{
26+
assert($carrier instanceof Request);
27+
/** @psalm-suppress InvalidReturnStatement */
28+
return $carrier->headers->keys();
29+
}
30+
31+
public function get($carrier, string $key) : ?string
32+
{
33+
assert($carrier instanceof Request);
34+
35+
return $carrier->headers->get($key);
36+
}
37+
}

src/LaravelInstrumentation.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel;
6+
7+
use Illuminate\Foundation\Http\Kernel;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Http\Response;
10+
use OpenTelemetry\API\Common\Instrumentation\CachedInstrumentation;
11+
use OpenTelemetry\API\Common\Instrumentation\Globals;
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;
17+
use function OpenTelemetry\Instrumentation\hook;
18+
use OpenTelemetry\SemConv\TraceAttributes;
19+
use Throwable;
20+
21+
class LaravelInstrumentation
22+
{
23+
public static function register(): void
24+
{
25+
$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.laravel');
26+
hook(
27+
Kernel::class,
28+
'handle',
29+
pre: static function (Kernel $kernel, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
30+
$request = ($params[0] instanceof Request) ? $params[0] : null;
31+
/** @psalm-suppress ArgumentTypeCoercion */
32+
$builder = $instrumentation->tracer()
33+
->spanBuilder(sprintf('HTTP %s', $request?->method() ?? 'unknown'))
34+
->setSpanKind(SpanKind::KIND_SERVER)
35+
->setAttribute('code.function', $function)
36+
->setAttribute('code.namespace', $class)
37+
->setAttribute('code.filepath', $filename)
38+
->setAttribute('code.lineno', $lineno);
39+
$parent = Context::getCurrent();
40+
if ($request) {
41+
$parent = Globals::propagator()->extract($request, HeadersPropagator::instance());
42+
$span = $builder
43+
->setParent($parent)
44+
->setAttribute(TraceAttributes::HTTP_URL, $request->url())
45+
->setAttribute(TraceAttributes::HTTP_METHOD, $request->method())
46+
->setAttribute(TraceAttributes::HTTP_REQUEST_CONTENT_LENGTH, $request->headers->get('Content-Length'))
47+
->setAttribute(TraceAttributes::HTTP_SCHEME, $request->getScheme())
48+
->startSpan();
49+
$request->attributes->set(SpanInterface::class, $span);
50+
} else {
51+
$span = $builder->startSpan();
52+
}
53+
Context::storage()->attach($span->storeInContext($parent));
54+
55+
return [$request];
56+
},
57+
post: static function (Kernel $kernel, array $params, ?Response $response, ?Throwable $exception) {
58+
$scope = Context::storage()->scope();
59+
if (!$scope) {
60+
return;
61+
}
62+
$scope->detach();
63+
$span = Span::fromContext($scope->context());
64+
if ($exception) {
65+
$span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
66+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
67+
}
68+
if ($response) {
69+
if ($response->getStatusCode() >= 400) {
70+
$span->setStatus(StatusCode::STATUS_ERROR);
71+
}
72+
$span->setAttribute(TraceAttributes::HTTP_STATUS_CODE, $response->getStatusCode());
73+
$span->setAttribute(TraceAttributes::HTTP_FLAVOR, $response->getProtocolVersion());
74+
$span->setAttribute(TraceAttributes::HTTP_RESPONSE_CONTENT_LENGTH, $response->headers->get('Content-Length'));
75+
}
76+
77+
$span->end();
78+
}
79+
);
80+
}
81+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Tests\Instrumentation\Laravel\tests\Integration;
6+
7+
use ArrayObject;
8+
use Illuminate\Container\Container;
9+
use Illuminate\Events\Dispatcher;
10+
use Illuminate\Foundation\Application;
11+
use Illuminate\Foundation\Http\Kernel;
12+
use Illuminate\Http\Request;
13+
use Illuminate\Http\Response;
14+
use Illuminate\Routing\Router;
15+
use OpenTelemetry\API\Common\Instrumentation\Configurator;
16+
use OpenTelemetry\Context\ScopeInterface;
17+
use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter;
18+
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
19+
use OpenTelemetry\SDK\Trace\TracerProvider;
20+
use PHPUnit\Framework\TestCase;
21+
22+
class ByPassRouterKernel extends Kernel
23+
{
24+
protected function sendRequestThroughRouter($request)
25+
{
26+
return new Response();
27+
}
28+
}
29+
30+
class LaravelInstrumentationTest extends TestCase
31+
{
32+
private ScopeInterface $scope;
33+
private ArrayObject $storage;
34+
private TracerProvider $tracerProvider;
35+
36+
public function setUp(): void
37+
{
38+
$this->storage = new ArrayObject();
39+
$this->tracerProvider = new TracerProvider(
40+
new SimpleSpanProcessor(
41+
new InMemoryExporter($this->storage)
42+
)
43+
);
44+
45+
$this->scope = Configurator::create()
46+
->withTracerProvider($this->tracerProvider)
47+
->activate();
48+
}
49+
50+
public function tearDown(): void
51+
{
52+
$this->scope->detach();
53+
}
54+
public function test_http_kernel_handle(): void
55+
{
56+
$app = new Application('.');
57+
58+
$container = new Container();
59+
$events_dispatcher = new Dispatcher();
60+
$router = new Router($events_dispatcher, $container);
61+
$kernel = new ByPassRouterKernel($app, $router);
62+
$request = Request::create('/', 'GET');
63+
$this->assertCount(0, $this->storage);
64+
$kernel->handle($request);
65+
$this->assertCount(1, $this->storage);
66+
}
67+
}

tests/Unit/Unit/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)