Skip to content

Commit 3dbd04b

Browse files
authored
initial symfony auto instrumentation (#114)
* initial symfony auto instrumentation * update composer.json * add test * add to test matrix * php-cs-fixer * downgrade symfony/http-kernel version * style fixes * satisfying psaml
0 parents  commit 3dbd04b

File tree

10 files changed

+357
-0
lines changed

10 files changed

+357
-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\Symfony\SymfonyInstrumentation::register();

composer.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "open-telemetry/opentelemetry-auto-symfony",
3+
"description": "OpenTelemetry auto-instrumentation for Symfony",
4+
"keywords": ["opentelemetry", "otel", "open-telemetry", "tracing", "symfony", "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+
"ext-otel_instrumentation": "*",
13+
"open-telemetry/api": "^1",
14+
"symfony/http-kernel": "^6.0.20"
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\\Symfony\\": "src/"
30+
},
31+
"files": [
32+
"_register.php"
33+
]
34+
},
35+
"autoload-dev": {
36+
"psr-4": {
37+
"OpenTelemetry\\Tests\\Instrumentation\\Symfony\\": "tests/"
38+
}
39+
}
40+
}

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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
<directory name="src"/>
10+
<directory name="tests"/>
11+
</projectFiles>
12+
<plugins>
13+
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
14+
</plugins>
15+
</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\Symfony;
6+
7+
use function assert;
8+
use OpenTelemetry\Context\Propagation\PropagationGetterInterface;
9+
use Symfony\Component\HttpFoundation\Request;
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/SymfonyInstrumentation.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\Symfony;
6+
7+
use OpenTelemetry\API\Common\Instrumentation\CachedInstrumentation;
8+
use OpenTelemetry\API\Common\Instrumentation\Globals;
9+
use OpenTelemetry\API\Trace\Span;
10+
use OpenTelemetry\API\Trace\SpanInterface;
11+
use OpenTelemetry\API\Trace\SpanKind;
12+
use OpenTelemetry\API\Trace\StatusCode;
13+
use OpenTelemetry\Context\Context;
14+
use function OpenTelemetry\Instrumentation\hook;
15+
use OpenTelemetry\SemConv\TraceAttributes;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\HttpKernel;
19+
use Throwable;
20+
21+
class SymfonyInstrumentation
22+
{
23+
public static function register(): void
24+
{
25+
$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.symfony');
26+
hook(
27+
HttpKernel::class,
28+
'handle',
29+
pre: static function (HttpKernel $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?->getMethod() ?? '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->getUri())
45+
->setAttribute(TraceAttributes::HTTP_METHOD, $request->getMethod())
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 (HttpKernel $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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Tests\Instrumentation\Symfony\tests\Integration;
6+
7+
use ArrayObject;
8+
use OpenTelemetry\API\Common\Instrumentation\Configurator;
9+
use OpenTelemetry\Context\ScopeInterface;
10+
use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter;
11+
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
12+
use OpenTelemetry\SDK\Trace\TracerProvider;
13+
use PHPUnit\Framework\TestCase;
14+
use Symfony\Component\EventDispatcher\EventDispatcher;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\RequestStack;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
20+
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
21+
use Symfony\Component\HttpKernel\HttpKernel;
22+
use Symfony\Component\HttpKernel\HttpKernelInterface;
23+
24+
class SymfonyInstrumentationTest extends TestCase
25+
{
26+
private ScopeInterface $scope;
27+
private ArrayObject $storage;
28+
private TracerProvider $tracerProvider;
29+
30+
public function setUp(): void
31+
{
32+
$this->storage = new ArrayObject();
33+
$this->tracerProvider = new TracerProvider(
34+
new SimpleSpanProcessor(
35+
new InMemoryExporter($this->storage)
36+
)
37+
);
38+
39+
$this->scope = Configurator::create()
40+
->withTracerProvider($this->tracerProvider)
41+
->activate();
42+
}
43+
44+
public function tearDown(): void
45+
{
46+
$this->scope->detach();
47+
}
48+
49+
public function test_http_kernel_handle(): void
50+
{
51+
$this->expectException(\RuntimeException::class);
52+
$kernel = $this->getHttpKernel(new EventDispatcher(), function () {
53+
throw new \RuntimeException();
54+
});
55+
$this->assertCount(0, $this->storage);
56+
$kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true);
57+
$this->assertCount(1, $this->storage);
58+
}
59+
60+
private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, RequestStack $requestStack = null, array $arguments = [])
61+
{
62+
$controller ??= fn () => new Response('Hello');
63+
64+
$controllerResolver = $this->createMock(ControllerResolverInterface::class);
65+
$controllerResolver
66+
->expects($this->any())
67+
->method('getController')
68+
->willReturn($controller);
69+
70+
$argumentResolver = $this->createMock(ArgumentResolverInterface::class);
71+
$argumentResolver
72+
->expects($this->any())
73+
->method('getArguments')
74+
->willReturn($arguments);
75+
76+
return new HttpKernel($eventDispatcher, $controllerResolver, $requestStack, $argumentResolver);
77+
}
78+
}

tests/Unit/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)