Skip to content

Commit de86b84

Browse files
rjd22Jean85ste93cry
authored
Implement distributed tracing support for Twig (getsentry#430)
Co-authored-by: Alessandro Lai <[email protected]> Co-authored-by: Stefano Arlandini <[email protected]>
1 parent d526d9d commit de86b84

22 files changed

+345
-12
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ jobs:
104104
restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer-${{ matrix.dependencies }}-
105105

106106
- name: Remove optional packages
107-
run: composer remove doctrine/dbal doctrine/doctrine-bundle symfony/messenger --dev --no-update
107+
run: composer remove doctrine/dbal doctrine/doctrine-bundle symfony/messenger symfony/twig-bundle --dev --no-update
108108

109109
- name: Install highest dependencies
110110
run: composer update --no-progress --no-interaction --prefer-dist

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Add support for distributed tracing of Twig template rendering (#430)
56
- Add support for distributed tracing of SQL queries while using Doctrine DBAL (#426)
67
- Added missing `capture-soft-fails` config schema option (#417)
78
- Deprecate the `Sentry\SentryBundle\EventListener\ConsoleCommandListener` class in favor of its parent class `Sentry\SentryBundle\EventListener\ConsoleListener` (#429)

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ psalm:
1515
test:
1616
vendor/bin/phpunit
1717

18-
pre-commit-check: cs phpstan test
18+
pre-commit-check: cs phpstan psalm test
1919

2020
setup-git:
2121
git config branch.autosetuprebase always

composer.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
"jean85/pretty-package-versions": "^1.5 || ^2.0",
2424
"php-http/discovery": "^1.11",
2525
"sentry/sdk": "^3.1",
26-
"symfony/config": "^3.4.44||^4.4.11||^5.0.11",
27-
"symfony/console": "^3.4.44||^4.4.11||^5.0.11",
28-
"symfony/dependency-injection": "^3.4.44||^4.4.11||^5.0.11",
29-
"symfony/event-dispatcher": "^3.4.44||^4.4.11||^5.0.11",
30-
"symfony/http-kernel": "^3.4.44||^4.4.11||^5.0.11",
26+
"symfony/config": "^3.4.44||^4.4.12||^5.0.11",
27+
"symfony/console": "^3.4.44||^4.4.12||^5.0.11",
28+
"symfony/dependency-injection": "^3.4.44||^4.4.12||^5.0.11",
29+
"symfony/event-dispatcher": "^3.4.44||^4.4.12||^5.0.11",
30+
"symfony/http-kernel": "^3.4.44||^4.4.12||^5.0.11",
3131
"symfony/psr-http-message-bridge": "^2.0",
32-
"symfony/security-core": "^3.4.44||^4.4.11||^5.0.11"
32+
"symfony/security-core": "^3.4.44||^4.4.12||^5.0.11"
3333
},
3434
"require-dev": {
3535
"doctrine/dbal": "^2.10||^3.0",
@@ -46,16 +46,18 @@
4646
"symfony/browser-kit": "^3.4.44||^4.4.12||^5.0.11",
4747
"symfony/dom-crawler": "^3.4.44||^4.4.12||^5.0.11",
4848
"symfony/framework-bundle": "^3.4.44||^4.4.12||^5.0.11",
49-
"symfony/messenger": "^4.4.11||^5.0.11",
49+
"symfony/messenger": "^4.4.12||^5.0.11",
5050
"symfony/monolog-bundle": "^3.4",
5151
"symfony/phpunit-bridge": "^5.0",
5252
"symfony/polyfill-php80": "^1.22",
53-
"symfony/yaml": "^3.4.44||^4.4.11||^5.0.11",
53+
"symfony/twig-bundle": "^3.4.44||^4.4.12||^5.0.11",
54+
"symfony/yaml": "^3.4.44||^4.4.12||^5.0.11",
5455
"vimeo/psalm": "^4.3"
5556
},
5657
"suggest": {
5758
"monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.",
58-
"doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry."
59+
"doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.",
60+
"symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry."
5961
},
6062
"autoload": {
6163
"files": [

phpstan-baseline.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,3 @@ parameters:
219219
message: "#^Trying to mock an undefined method convertException\\(\\) on class Sentry\\\\SentryBundle\\\\Tests\\\\Tracing\\\\Doctrine\\\\DBAL\\\\StubExceptionConverterDriverInterface\\.$#"
220220
count: 1
221221
path: tests/Tracing/Doctrine/DBAL/TracingDriverTest.php
222-

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ parameters:
1111
dynamicConstantNames:
1212
- Symfony\Component\HttpKernel\Kernel::VERSION
1313
- Doctrine\DBAL\Version::VERSION
14+
stubFiles:
15+
- tests/Stubs/Profile.phpstub

psalm.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
<directory name="vendor" />
1414
</ignoreFiles>
1515
</projectFiles>
16+
17+
<stubs>
18+
<file name="tests/Stubs/Profile.phpstub"/>
19+
</stubs>
1620
</psalm>

src/DependencyInjection/Configuration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ private function addDistributedTracingSection(ArrayNodeDefinition $rootNode): vo
163163
->end()
164164
->end()
165165
->end()
166+
->arrayNode('twig')
167+
->canBeEnabled()
168+
->end()
166169
->end()
167170
->end()
168171
->end();

src/DependencyInjection/SentryExtension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Sentry\SentryBundle\SentryBundle;
2020
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator;
2121
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware;
22+
use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension;
2223
use Sentry\Serializer\RepresentationSerializer;
2324
use Sentry\Serializer\Serializer;
2425
use Sentry\Transport\TransportFactoryInterface;
@@ -62,6 +63,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
6263
$this->registerErrorListenerConfiguration($container, $mergedConfig);
6364
$this->registerMessengerListenerConfiguration($container, $mergedConfig['messenger']);
6465
$this->registerTracingConfiguration($container, $mergedConfig['tracing']);
66+
$this->registerTracingTwigExtensionConfiguration($container, $mergedConfig['tracing']);
6567
}
6668

6769
/**
@@ -173,6 +175,18 @@ private function registerTracingConfiguration(ContainerBuilder $container, array
173175
}
174176
}
175177

178+
/**
179+
* @param array<string, mixed> $config
180+
*/
181+
private function registerTracingTwigExtensionConfiguration(ContainerBuilder $container, array $config): void
182+
{
183+
$isConfigEnabled = $this->isConfigEnabled($container, $config['twig']);
184+
185+
if (!$isConfigEnabled) {
186+
$container->removeDefinition(TwigTracingExtension::class);
187+
}
188+
}
189+
176190
/**
177191
* @param string[] $integrations
178192
* @param array<string, mixed> $config

src/Resources/config/schema/sentry-1.0.xsd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<xsd:complexType name="tracing">
8585
<xsd:choice maxOccurs="unbounded">
8686
<xsd:element name="dbal" type="tracing-dbal" minOccurs="0" maxOccurs="1" />
87+
<xsd:element name="twig" type="tracing-twig" minOccurs="0" maxOccurs="1" />
8788
</xsd:choice>
8889
</xsd:complexType>
8990

@@ -94,4 +95,8 @@
9495

9596
<xsd:attribute name="enabled" type="xsd:boolean" />
9697
</xsd:complexType>
98+
99+
<xsd:complexType name="tracing-twig">
100+
<xsd:attribute name="enabled" type="xsd:boolean" />
101+
</xsd:complexType>
97102
</xsd:schema>

src/Resources/config/services.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
<argument type="service" id="Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware" />
7474
</service>
7575

76+
<service id="Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension" class="Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension">
77+
<argument type="service" id="Sentry\State\HubInterface" />
78+
79+
<tag name="twig.extension" />
80+
</service>
81+
7682
<service id="Sentry\Integration\RequestFetcherInterface" class="Sentry\SentryBundle\Integration\RequestFetcher">
7783
<argument type="service" id="Symfony\Component\HttpFoundation\RequestStack" />
7884
<argument type="service" id="Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface" on-invalid="null" />
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tracing\Twig;
6+
7+
use Sentry\State\HubInterface;
8+
use Sentry\Tracing\Span;
9+
use Sentry\Tracing\SpanContext;
10+
use SplObjectStorage;
11+
use Twig\Extension\AbstractExtension;
12+
use Twig\Profiler\NodeVisitor\ProfilerNodeVisitor;
13+
use Twig\Profiler\Profile;
14+
15+
final class TwigTracingExtension extends AbstractExtension
16+
{
17+
/**
18+
* @var HubInterface The current hub
19+
*/
20+
private $hub;
21+
22+
/**
23+
* @var SplObjectStorage<object, Span> The currently active spans
24+
*/
25+
private $spans;
26+
27+
/**
28+
* @param HubInterface $hub The current hub
29+
*/
30+
public function __construct(HubInterface $hub)
31+
{
32+
$this->hub = $hub;
33+
$this->spans = new SplObjectStorage();
34+
}
35+
36+
/**
37+
* This method is called before the execution of a block, a macro or a
38+
* template.
39+
*
40+
* @param Profile $profile The profiling data
41+
*/
42+
public function enter(Profile $profile): void
43+
{
44+
$transaction = $this->hub->getTransaction();
45+
46+
if (null === $transaction) {
47+
return;
48+
}
49+
50+
$spanContext = new SpanContext();
51+
$spanContext->setOp('view.render');
52+
$spanContext->setDescription($this->getSpanDescription($profile));
53+
54+
$this->spans[$profile] = $transaction->startChild($spanContext);
55+
}
56+
57+
/**
58+
* This method is called when the execution of a block, a macro or a
59+
* template is finished.
60+
*
61+
* @param Profile $profile The profiling data
62+
*/
63+
public function leave(Profile $profile): void
64+
{
65+
if (!isset($this->spans[$profile])) {
66+
return;
67+
}
68+
69+
$this->spans[$profile]->finish();
70+
71+
unset($this->spans[$profile]);
72+
}
73+
74+
/**
75+
* {@inheritdoc}
76+
*/
77+
public function getNodeVisitors(): array
78+
{
79+
return [
80+
new ProfilerNodeVisitor(self::class),
81+
];
82+
}
83+
84+
/**
85+
* Gets a short description for the span.
86+
*
87+
* @param Profile $profile The profiling data
88+
*/
89+
private function getSpanDescription(Profile $profile): string
90+
{
91+
switch (true) {
92+
case $profile->isRoot():
93+
return $profile->getName();
94+
95+
case $profile->isTemplate():
96+
return $profile->getTemplate();
97+
98+
default:
99+
return sprintf('%s::%s(%s)', $profile->getTemplate(), $profile->getType(), $profile->getName());
100+
}
101+
}
102+
}

tests/DependencyInjection/ConfigurationTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public function testProcessConfigurationWithDefaultConfiguration(): void
4141
'enabled' => false,
4242
'connections' => class_exists(DoctrineBundle::class) ? ['%doctrine.default_connection%'] : [],
4343
],
44+
'twig' => [
45+
'enabled' => false,
46+
],
4447
],
4548
];
4649

tests/DependencyInjection/Fixtures/php/full.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,8 @@
4747
'enabled' => false,
4848
'connections' => ['default'],
4949
],
50+
'twig' => [
51+
'enabled' => false,
52+
],
5053
],
5154
]);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\DependencyInjection\ContainerBuilder;
6+
7+
/** @var ContainerBuilder $container */
8+
$container->loadFromExtension('sentry', [
9+
'tracing' => [
10+
'twig' => [
11+
'enabled' => true,
12+
],
13+
],
14+
]);

tests/DependencyInjection/Fixtures/xml/full.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<sentry:dbal enabled="false">
4242
<sentry:connection>default</sentry:connection>
4343
</sentry:dbal>
44+
<sentry:twig enabled="false" />
4445
</sentry:tracing>
4546
</sentry:config>
4647
</container>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:sentry="https://sentry.io/schema/dic/sentry-symfony"
6+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
7+
https://sentry.io/schema/dic/sentry-symfony https://sentry.io/schema/dic/sentry-symfony/sentry-1.0.xsd">
8+
9+
<sentry:config>
10+
<sentry:tracing>
11+
<sentry:twig enabled="true" />
12+
</sentry:tracing>
13+
</sentry:config>
14+
</container>

tests/DependencyInjection/Fixtures/yml/full.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ sentry:
4242
enabled: false
4343
connections:
4444
- enabled
45+
twig:
46+
enabled: false
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sentry:
2+
tracing:
3+
twig:
4+
enabled: true

tests/DependencyInjection/SentryExtensionTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Sentry\SentryBundle\SentryBundle;
2020
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator;
2121
use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware;
22+
use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension;
2223
use Sentry\Serializer\RepresentationSerializer;
2324
use Sentry\Serializer\Serializer;
2425
use Sentry\Transport\TransportFactoryInterface;
@@ -286,6 +287,20 @@ public function testTracingDriverMiddlewareIsRemovedWhenDbalTracingIsDisabled():
286287
$this->assertEmpty($container->getParameter('sentry.tracing.dbal.connections'));
287288
}
288289

290+
public function testTwigTracingExtensionIsConfiguredWhenTwigTracingIsEnabled(): void
291+
{
292+
$container = $this->createContainerFromFixture('twig_tracing_enabled');
293+
294+
$this->assertTrue($container->hasDefinition(TwigTracingExtension::class));
295+
}
296+
297+
public function testTwigTracingExtensionIsRemovedWhenTwigTracingIsDisabled(): void
298+
{
299+
$container = $this->createContainerFromFixture('full');
300+
301+
$this->assertFalse($container->hasDefinition(TwigTracingExtension::class));
302+
}
303+
289304
private function createContainerFromFixture(string $fixtureFile): ContainerBuilder
290305
{
291306
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([

tests/Stubs/Profile.phpstub

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Twig\Profiler;
4+
5+
/**
6+
* @template-implements \IteratorAggregate<Profile, Profile>
7+
*/
8+
final class Profile implements \IteratorAggregate, \Serializable
9+
{
10+
}

0 commit comments

Comments
 (0)