diff --git a/documentation/components/bridges/symfony-telemetry-bundle.md b/documentation/components/bridges/symfony-telemetry-bundle.md index a83bc4effc..4231a52be1 100644 --- a/documentation/components/bridges/symfony-telemetry-bundle.md +++ b/documentation/components/bridges/symfony-telemetry-bundle.md @@ -1494,6 +1494,12 @@ flow_telemetry: flush_deferred: false # opt-in; see below ``` +Every service tagged `cache.pool` is traced, including the framework's own internal pools +(`cache.system`, `cache.validator`, `cache.serializer`, `cache.property_info`, `cache.app`, +`cache.doctrine.*`). Use `exclude_pools` to opt any of them out - entries are matched as an exact +service id or, when the value is a valid regular expression, as a pattern. Before 0.43.0 those pools +were skipped by a bug; see the [upgrade note](/documentation/upgrading.md#upgrading-from-042x-to-043x). + ##### Deferred writes on a Doctrine DBAL cache pool A cache pool backed by `cache.adapter.doctrine_dbal` defers writes and flushes them from the pool's own diff --git a/documentation/upgrading.md b/documentation/upgrading.md index 60eb72d493..366f49c61b 100644 --- a/documentation/upgrading.md +++ b/documentation/upgrading.md @@ -62,12 +62,12 @@ try { On `d = 2024-01-01, 2024-01-02, 2024-01-03, 2024-01-04` and `s = 100, 200, 300, 400`: -| Before | After | -|-------------------------------------------------------------|--------------------------| -| `sum(ref('s'))->over(window()->orderBy(ref('d')))` → `1000, 1000, 1000, 1000` | `100, 300, 600, 1000` | -| `average()`, `count()` over an ordered window - whole partition | rows up to the current row's peers | -| `window()->partitionBy(ref('dept'))` - whole partition | unchanged | -| empty frame | `sum()`/`average()` → `null`, `count()` → `0` | +| Before | After | +|-------------------------------------------------------------------------------|-----------------------------------------------| +| `sum(ref('s'))->over(window()->orderBy(ref('d')))` → `1000, 1000, 1000, 1000` | `100, 300, 600, 1000` | +| `average()`, `count()` over an ordered window - whole partition | rows up to the current row's peers | +| `window()->partitionBy(ref('dept'))` - whole partition | unchanged | +| empty frame | `sum()`/`average()` → `null`, `count()` → `0` | Restore the previous result: @@ -79,17 +79,17 @@ sum(ref('s'))->over(window()->orderBy(ref('d'))->rowsBetween(unbounded_preceding On `s = 100, 100, 300` ordered by a distinct column: -| Before | After | -|-------------------------------------------------------------------------|----------------------------------------| +| Before | After | +|---------------------------------------------------------------------------|-------------------------------------------------| | `count(ref('s'))` counts rows sharing the current row's value → `2, 2, 1` | counts non-null values in the frame → `1, 2, 3` | -| `count()` threw `Count WindowFunction function requires a reference.` | counts every row in the frame (`COUNT(*)`) | +| `count()` threw `Count WindowFunction function requires a reference.` | counts every row in the frame (`COUNT(*)`) | ### 5) `flow-php/etl` - `partitionBy()` no longer sets `orderBy()` -| Before | After | -|------------------------------------------------------------------------------|---------| -| `window()->orderBy(ref('date'))->partitionBy(ref('dept'))->order()` → `['dept']` | `['date']` | -| `window()->partitionBy(ref('dept'))->order()` → `['dept']` | `[]` | +| Before | After | +|----------------------------------------------------------------------------------------------------------|---------------------------------------------------| +| `window()->orderBy(ref('date'))->partitionBy(ref('dept'))->order()` → `['dept']` | `['date']` | +| `window()->partitionBy(ref('dept'))->order()` → `['dept']` | `[]` | | `rank()`/`dense_rank()`/`row_number()` over a `partitionBy()`-only window ranked by the partition column | throws `... requires to be ordered by one column` | Add the ordering explicitly: @@ -100,15 +100,15 @@ rank()->over(window()->partitionBy(ref('dept'))->orderBy(ref('salary')->desc())) ### 6) `flow-php/etl` - `WindowFunction::apply()` receives a `WindowContext` -| Before | After | -|------------------------------------------------------------|--------------------------------------------------| -| `apply(Row $row, Rows $partition, FlowContext $context)` | `apply(WindowContext $window)` | -| `$row` | `$window->row()` | -| `$partition` | `$window->partition()` | -| `$context` | `$window->flowContext()` | -| — | `$window->frame()` - rows within the current row's frame | -| — | `$window->index()` - position in the ordered partition | -| `row_number()` on duplicate rows → `1, 1, 3` | `1, 2, 3` | +| Before | After | +|----------------------------------------------------------|----------------------------------------------------------| +| `apply(Row $row, Rows $partition, FlowContext $context)` | `apply(WindowContext $window)` | +| `$row` | `$window->row()` | +| `$partition` | `$window->partition()` | +| `$context` | `$window->flowContext()` | +| — | `$window->frame()` - rows within the current row's frame | +| — | `$window->index()` - position in the ordered partition | +| `row_number()` on duplicate rows → `1, 1, 3` | `1, 2, 3` | Implementations must no longer sort; `$window->partition()` and `$window->frame()` are already ordered. @@ -127,6 +127,31 @@ Implementations must no longer sort; `$window->partition()` and `$window->frame( Applies to `flow-php/postgresql` users only through the bundle; `Client\Telemetry` is unchanged. +### 8) `flow-php/symfony-telemetry-bundle` - cache pools and PSR-18 clients that were silently skipped are now traced + +| Before | After | +|------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| `cache.system`, `cache.validator`, `cache.serializer`, `cache.property_info`, `cache.app`, `cache.doctrine.*`, `cache.http_client.pool` - not traced | traced: `cache.*` spans and `flow.cache.hits` / `flow.cache.misses` | +| pool or client whose class is a `%parameter%` - not traced | traced | +| tag-aware pool whose class is a `%parameter%` - got the non-tag-aware decorator | gets `TagAwareTraceableCacheAdapter` | +| PSR-18 client behind an autoconfigured or abstract parent definition - container build failed with *"has a reference to an abstract definition"* | compiles; the client is traced | +| `instrumentation.cache.exclude_pools` entries for framework pools - had no effect | take effect | + +To keep the previous set of traced pools, exclude the framework's own: + +```yaml +flow_telemetry: + instrumentation: + cache: + exclude_pools: + - 'cache.system' + - 'cache.validator' + - 'cache.serializer' + - 'cache.property_info' + - '/^cache\.doctrine\..*/' + - 'cache.http_client.pool' +``` + --- ## Upgrading from 0.41.x to 0.42.x @@ -2378,7 +2403,7 @@ After: ->run(); ``` -### 4) ConfigBuilder::putInputIntoRows () output is now prefixed with _ (underscore) +### 4) ConfigBuilder::putInputIntoRows () output is now prefixed with _ (underscore) In order to avoid collisions with datasets columns, additional columns created after using putInputIntoRows () would now be prefixed with `_` (underscore) symbol. diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/CacheTelemetryPass.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/CacheTelemetryPass.php index b38904d7d0..abef8e56e6 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/CacheTelemetryPass.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/CacheTelemetryPass.php @@ -16,7 +16,6 @@ use Symfony\Component\DependencyInjection\Reference; use function is_a; -use function preg_match; final class CacheTelemetryPass implements CompilerPassInterface { @@ -35,12 +34,16 @@ public function process(ContainerBuilder $container): void ? $container->getParameter('flow.telemetry.cache.exclude_pools') : []; + $excluded = new ServiceIdPatterns($excludePools); + + $resolver = new DefinitionClassResolver($container); + $taggedServices = $container->findTaggedServiceIds('cache.pool'); $innerPools = []; foreach ($taggedServices as $serviceId => $_tags) { - if ($this->isExcluded($serviceId, $excludePools)) { + if ($excluded->matches($serviceId)) { continue; } @@ -50,7 +53,7 @@ public function process(ContainerBuilder $container): void continue; } - $serviceClass = $serviceDefinition->getClass(); + $serviceClass = $resolver->resolve($serviceDefinition); if ($serviceClass === null) { continue; @@ -91,29 +94,4 @@ private function flushDeferredEnabled(ContainerBuilder $container): bool && $container->getParameter('flow.telemetry.cache.flush_deferred') === true ); } - - /** - * @param array $patterns - */ - private function isExcluded(string $serviceId, array $patterns): bool - { - foreach ($patterns as $pattern) { - if ($this->matchesPattern($serviceId, $pattern)) { - return true; - } - } - - return false; - } - - private function matchesPattern(string $serviceId, string $pattern): bool - { - $result = @preg_match($pattern, $serviceId); - - if ($result !== false) { - return (bool) $result; - } - - return $serviceId === $pattern; - } } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/DefinitionClassResolver.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/DefinitionClassResolver.php new file mode 100644 index 0000000000..d5a1d0b1d2 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/DefinitionClassResolver.php @@ -0,0 +1,52 @@ +getClass() === null && $definition instanceof ChildDefinition) { + $parent = $definition->getParent(); + + if (array_key_exists($parent, $seen) || !$this->container->has($parent)) { + return null; + } + + $seen[$parent] = true; + $definition = $this->container->findDefinition($parent); + } + + try { + /** @var mixed $class */ + $class = $this->container->getParameterBag()->resolveValue($definition->getClass()); + } catch (ParameterNotFoundException) { + return null; + } + + if (!is_string($class)) { + return null; + } + + return $this->container->getReflectionClass($class, false)?->getName(); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/HttpClientTelemetryPass.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/HttpClientTelemetryPass.php index e08746e8e6..e5b3602022 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/HttpClientTelemetryPass.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/HttpClientTelemetryPass.php @@ -11,8 +11,6 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use function preg_match; - final class HttpClientTelemetryPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void @@ -30,10 +28,12 @@ public function process(ContainerBuilder $container): void ? $container->getParameter('flow.telemetry.http_client.exclude_clients') : []; + $excluded = new ServiceIdPatterns($excludeClients); + $taggedServices = $container->findTaggedServiceIds('http_client.client'); foreach ($taggedServices as $serviceId => $_tags) { - if ($this->isExcluded($serviceId, $excludeClients)) { + if ($excluded->matches($serviceId)) { continue; } @@ -49,29 +49,4 @@ public function process(ContainerBuilder $container): void $container->setDefinition($decoratorId, $definition); } } - - /** - * @param array $patterns - */ - private function isExcluded(string $serviceId, array $patterns): bool - { - foreach ($patterns as $pattern) { - if ($this->matchesPattern($serviceId, $pattern)) { - return true; - } - } - - return false; - } - - private function matchesPattern(string $serviceId, string $pattern): bool - { - $result = @preg_match($pattern, $serviceId); - - if ($result !== false) { - return (bool) $result; - } - - return $serviceId === $pattern; - } } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/Psr18ClientTelemetryPass.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/Psr18ClientTelemetryPass.php index f31006ba0d..950f38de18 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/Psr18ClientTelemetryPass.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/Psr18ClientTelemetryPass.php @@ -12,9 +12,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use function class_exists; use function is_a; -use function preg_match; final class Psr18ClientTelemetryPass implements CompilerPassInterface { @@ -33,12 +31,26 @@ public function process(ContainerBuilder $container): void ? $container->getParameter('flow.telemetry.psr18_client.exclude_clients') : []; + $excluded = new ServiceIdPatterns($excludeClients); + + $resolver = new DefinitionClassResolver($container); + foreach ($container->getDefinitions() as $serviceId => $definition) { - if ($this->isExcluded($serviceId, $excludeClients)) { + if ($excluded->matches($serviceId)) { + continue; + } + + if ($definition->isAbstract()) { + continue; + } + + $class = $resolver->resolve($definition); + + if ($class === null || $class === PSR18TraceableClient::class) { continue; } - if (!$this->implementsPsr18Interface($definition)) { + if (!is_a($class, ClientInterface::class, true)) { continue; } @@ -53,48 +65,4 @@ public function process(ContainerBuilder $container): void $container->setDefinition($decoratorId, $decoratorDefinition); } } - - private function implementsPsr18Interface(Definition $definition): bool - { - $class = $definition->getClass(); - - if ($class === null) { - return false; - } - - if ($class === PSR18TraceableClient::class) { - return false; - } - - if (!class_exists($class)) { - return false; - } - - return is_a($class, ClientInterface::class, true); - } - - /** - * @param array $patterns - */ - private function isExcluded(string $serviceId, array $patterns): bool - { - foreach ($patterns as $pattern) { - if ($this->matchesPattern($serviceId, $pattern)) { - return true; - } - } - - return false; - } - - private function matchesPattern(string $serviceId, string $pattern): bool - { - $result = @preg_match($pattern, $serviceId); - - if ($result !== false) { - return (bool) $result; - } - - return $serviceId === $pattern; - } } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/ServiceIdPatterns.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/ServiceIdPatterns.php new file mode 100644 index 0000000000..942c0aaa4b --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/ServiceIdPatterns.php @@ -0,0 +1,34 @@ + $patterns + */ + public function __construct( + private array $patterns, + ) {} + + public function matches(string $serviceId): bool + { + foreach ($this->patterns as $pattern) { + $matched = @preg_match($pattern, $serviceId); + + if ($matched === false && $serviceId === $pattern) { + return true; + } + + if ($matched === 1) { + return true; + } + } + + return false; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Psr18/ResettablePsr18Client.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Psr18/ResettablePsr18Client.php new file mode 100644 index 0000000000..fc21d45897 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Psr18/ResettablePsr18Client.php @@ -0,0 +1,21 @@ +bootKernel([ + 'config' => function (TestKernel $kernel) use ($collector): void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => ['utf8' => true, 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php'], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig( + 'flow_telemetry', + $this->symfonyContext()->fullyPopulatedTelemetryConfig(true), + ); + $kernel->addTestContainerConfigurator(static function (ContainerBuilder $container) use ( + $collector, + ): void { + $container->addCompilerPass($collector, PassConfig::TYPE_AFTER_REMOVING, -1024); + }); + }, + ]); + + static::assertContains('cache.system.flow_telemetry', $collector->ids); + static::assertContains('cache.validator.flow_telemetry', $collector->ids); + static::assertContains('cache.serializer.flow_telemetry', $collector->ids); + static::assertContains('cache.property_info.flow_telemetry', $collector->ids); + static::assertContains('cache.app.flow_telemetry', $collector->ids); + } + + public function test_abstract_pool_templates_are_not_traced_in_a_booted_kernel(): void + { + $collector = new CompiledIdCollectorPass(); + + $this->bootKernel([ + 'config' => function (TestKernel $kernel) use ($collector): void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => ['utf8' => true, 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php'], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig( + 'flow_telemetry', + $this->symfonyContext()->fullyPopulatedTelemetryConfig(true), + ); + $kernel->addTestContainerConfigurator(static function (ContainerBuilder $container) use ( + $collector, + ): void { + $container->addCompilerPass($collector, PassConfig::TYPE_AFTER_REMOVING, -1024); + }); + }, + ]); + + static::assertNotContains('cache.adapter.system.flow_telemetry', $collector->ids); + static::assertNotContains('cache.adapter.filesystem.flow_telemetry', $collector->ids); + } + + public function test_excluded_pools_are_not_traced_in_a_booted_kernel(): void + { + $collector = new CompiledIdCollectorPass(); + + $this->bootKernel([ + 'config' => function (TestKernel $kernel) use ($collector): void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => ['utf8' => true, 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php'], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + ...$this->symfonyContext()->fullyPopulatedTelemetryConfig(true), + 'instrumentation' => [ + 'cache' => [ + 'enabled' => true, + 'exclude_pools' => ['cache.system', '/^cache\.validator.*/'], + ], + ], + ]); + $kernel->addTestContainerConfigurator(static function (ContainerBuilder $container) use ( + $collector, + ): void { + $container->addCompilerPass($collector, PassConfig::TYPE_AFTER_REMOVING, -1024); + }); + }, + ]); + + static::assertNotContains('cache.system.flow_telemetry', $collector->ids); + static::assertNotContains('cache.validator.flow_telemetry', $collector->ids); + static::assertContains('cache.app.flow_telemetry', $collector->ids); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/CachePoolContainerMother.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/CachePoolContainerMother.php new file mode 100644 index 0000000000..135fce1daf --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Mother/CachePoolContainerMother.php @@ -0,0 +1,60 @@ +setParameter('flow.telemetry.cache.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + + $container->register('cache.adapter.system', AdapterInterface::class)->setAbstract(true)->addTag('cache.pool'); + $container + ->register('cache.adapter.filesystem', FilesystemAdapter::class) + ->setAbstract(true) + ->addTag('cache.pool'); + + $container->setDefinition( + 'cache.system', + (new ChildDefinition('cache.adapter.system')) + ->addTag('cache.pool') + ->setPublic(true), + ); + $container->setDefinition( + 'cache.validator', + (new ChildDefinition('cache.system')) + ->addTag('cache.pool') + ->setPublic(true), + ); + $container->setDefinition( + 'cache.app', + (new ChildDefinition('cache.adapter.filesystem')) + ->addTag('cache.pool') + ->setPublic(true), + ); + $container->setDefinition( + 'cache.doctrine.orm.default.result', + (new ChildDefinition('cache.app')) + ->addTag('cache.pool') + ->setPublic(true), + ); + + $container->register('app.redis_pool', RedisTagAwareAdapter::class)->addTag('cache.pool')->setPublic(true); + + $container->setParameter('app.pool.class', FilesystemAdapter::class); + $container->register('app.param_pool', '%app.pool.class%')->addTag('cache.pool')->setPublic(true); + + return $container; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/CacheTelemetryPassTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/CacheTelemetryPassTest.php new file mode 100644 index 0000000000..482765012f --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/CacheTelemetryPassTest.php @@ -0,0 +1,163 @@ +process($container); + + static::assertFalse($container->hasDefinition('cache.adapter.system.flow_telemetry')); + static::assertFalse($container->hasDefinition('cache.adapter.filesystem.flow_telemetry')); + } + + public function test_pool_inheriting_its_class_from_a_parent_is_traced(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + + (new CacheTelemetryPass())->process($container); + + static::assertSame( + TraceableCacheAdapter::class, + $container->getDefinition('cache.system.flow_telemetry')->getClass(), + ); + static::assertSame( + TraceableCacheAdapter::class, + $container->getDefinition('cache.validator.flow_telemetry')->getClass(), + ); + } + + public function test_pool_inheriting_through_multiple_levels_is_traced(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + + (new CacheTelemetryPass())->process($container); + + static::assertSame( + TraceableCacheAdapter::class, + $container->getDefinition('cache.doctrine.orm.default.result.flow_telemetry')->getClass(), + ); + } + + public function test_tag_aware_pool_gets_the_tag_aware_decorator(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + + (new CacheTelemetryPass())->process($container); + + static::assertSame( + TagAwareTraceableCacheAdapter::class, + $container->getDefinition('app.redis_pool.flow_telemetry')->getClass(), + ); + } + + public function test_pool_declared_as_a_parameter_is_traced(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + + (new CacheTelemetryPass())->process($container); + + static::assertSame( + TraceableCacheAdapter::class, + $container->getDefinition('app.param_pool.flow_telemetry')->getClass(), + ); + } + + public function test_tag_aware_pool_declared_as_a_parameter_gets_the_tag_aware_decorator(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + $container->setParameter('app.tag_aware_pool.class', RedisTagAwareAdapter::class); + $container->register('app.param_tag_aware_pool', '%app.tag_aware_pool.class%')->addTag('cache.pool'); + + (new CacheTelemetryPass())->process($container); + + static::assertSame( + TagAwareTraceableCacheAdapter::class, + $container->getDefinition('app.param_tag_aware_pool.flow_telemetry')->getClass(), + ); + } + + public function test_pool_excluded_by_exact_id_is_left_untouched(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + $container->setParameter('flow.telemetry.cache.exclude_pools', ['cache.system']); + + (new CacheTelemetryPass())->process($container); + + static::assertFalse($container->hasDefinition('cache.system.flow_telemetry')); + static::assertTrue($container->hasDefinition('cache.validator.flow_telemetry')); + } + + public function test_pool_excluded_by_regex_is_left_untouched(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + $container->setParameter('flow.telemetry.cache.exclude_pools', ['/^cache\.validator.*/']); + + (new CacheTelemetryPass())->process($container); + + static::assertFalse($container->hasDefinition('cache.validator.flow_telemetry')); + static::assertTrue($container->hasDefinition('cache.system.flow_telemetry')); + } + + public function test_nothing_is_decorated_when_disabled(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + $container->setParameter('flow.telemetry.cache.enabled', false); + $container->setParameter('flow.telemetry.cache.flush_deferred', true); + + (new CacheTelemetryPass())->process($container); + + static::assertFalse($container->hasDefinition('cache.system.flow_telemetry')); + static::assertFalse($container->hasDefinition('app.redis_pool.flow_telemetry')); + static::assertFalse($container->hasDefinition('flow.telemetry.cache.deferred_flush_subscriber')); + } + + public function test_deferred_flush_subscriber_collects_the_newly_traced_pools(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + $container->setParameter('flow.telemetry.cache.flush_deferred', true); + + (new CacheTelemetryPass())->process($container); + + /** @var mixed $innerPools */ + $innerPools = $container->getDefinition('flow.telemetry.cache.deferred_flush_subscriber')->getArgument(0); + + static::assertInstanceOf(IteratorArgument::class, $innerPools); + static::assertContainsEquals(new Reference('cache.system.flow_telemetry.inner'), $innerPools->getValues()); + static::assertContainsEquals( + new Reference('cache.doctrine.orm.default.result.flow_telemetry.inner'), + $innerPools->getValues(), + ); + } + + public function test_the_real_pool_graph_compiles(): void + { + $container = CachePoolContainerMother::frameworkPoolGraph(); + $container->addCompilerPass(new CacheTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertSame(TraceableCacheAdapter::class, $container->getDefinition('cache.system')->getClass()); + static::assertSame( + TagAwareTraceableCacheAdapter::class, + $container->getDefinition('app.redis_pool')->getClass(), + ); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/DefinitionClassResolverTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/DefinitionClassResolverTest.php new file mode 100644 index 0000000000..b2cf4688b5 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/DefinitionClassResolverTest.php @@ -0,0 +1,148 @@ +resolve($container->register( + 'app.client', + MockPsr18Client::class, + )), + ); + } + + public function test_resolves_a_class_from_a_direct_parent(): void + { + $container = new ContainerBuilder(); + $container->register('app.base_client', MockPsr18Client::class)->setAbstract(true); + $container->setDefinition('app.client', new ChildDefinition('app.base_client')); + + static::assertSame( + MockPsr18Client::class, + (new DefinitionClassResolver($container))->resolve($container->getDefinition('app.client')), + ); + } + + public function test_resolves_a_class_through_a_multi_level_parent_chain(): void + { + $container = new ContainerBuilder(); + $container->register('app.grandparent_client', MockPsr18Client::class)->setAbstract(true); + $container->setDefinition( + 'app.parent_client', + (new ChildDefinition('app.grandparent_client'))->setAbstract(true), + ); + $container->setDefinition('app.client', new ChildDefinition('app.parent_client')); + + static::assertSame( + MockPsr18Client::class, + (new DefinitionClassResolver($container))->resolve($container->getDefinition('app.client')), + ); + } + + public function test_resolves_a_class_declared_as_a_parameter(): void + { + $container = new ContainerBuilder(); + $container->setParameter('app.client.class', MockPsr18Client::class); + + static::assertSame( + MockPsr18Client::class, + (new DefinitionClassResolver($container))->resolve($container->register( + 'app.client', + '%app.client.class%', + )), + ); + } + + public function test_returns_null_for_a_circular_parent_chain(): void + { + $container = new ContainerBuilder(); + $container->setDefinition('app.first', new ChildDefinition('app.second')); + $container->setDefinition('app.second', new ChildDefinition('app.first')); + + static::assertNull((new DefinitionClassResolver($container))->resolve($container->getDefinition('app.first'))); + } + + public function test_returns_null_for_a_definition_that_is_its_own_parent(): void + { + $container = new ContainerBuilder(); + $container->setDefinition('app.client', new ChildDefinition('app.client')); + + static::assertNull((new DefinitionClassResolver($container))->resolve($container->getDefinition('app.client'))); + } + + public function test_returns_null_when_the_parent_service_is_missing(): void + { + $container = new ContainerBuilder(); + + static::assertNull((new DefinitionClassResolver($container))->resolve( + new ChildDefinition('app.does_not_exist'), + )); + } + + public function test_returns_null_when_the_class_does_not_exist(): void + { + $container = new ContainerBuilder(); + + static::assertNull((new DefinitionClassResolver($container))->resolve($container->register( + 'app.client', + 'Flow\Bridge\Symfony\TelemetryBundle\Tests\NoSuchClass', + ))); + } + + public function test_returns_null_when_the_class_parameter_does_not_exist(): void + { + $container = new ContainerBuilder(); + + static::assertNull((new DefinitionClassResolver($container))->resolve($container->register( + 'app.client', + '%app.absent.class%', + ))); + } + + public function test_returns_null_when_the_class_parameter_is_not_a_string(): void + { + $container = new ContainerBuilder(); + $container->setParameter('app.client.class', [MockPsr18Client::class]); + + static::assertNull((new DefinitionClassResolver($container))->resolve($container->register( + 'app.client', + '%app.client.class%', + ))); + } + + public function test_resolves_an_interface_declared_as_a_class(): void + { + $container = new ContainerBuilder(); + + static::assertSame( + AdapterInterface::class, + (new DefinitionClassResolver($container))->resolve($container->register( + 'cache.adapter.system', + AdapterInterface::class, + )), + ); + } + + public function test_returns_null_for_a_definition_without_a_class(): void + { + static::assertNull((new DefinitionClassResolver(new ContainerBuilder()))->resolve(new Definition())); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/Psr18ClientTelemetryPassTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/Psr18ClientTelemetryPassTest.php new file mode 100644 index 0000000000..bc9d294e73 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/Psr18ClientTelemetryPassTest.php @@ -0,0 +1,178 @@ +setParameter('flow.telemetry.psr18_client.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + $container->registerForAutoconfiguration(ResetInterface::class)->addTag('kernel.reset', ['method' => 'reset']); + $container->register('app.http_client', ResettablePsr18Client::class)->setAutoconfigured(true)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertSame(PSR18TraceableClient::class, $container->getDefinition('app.http_client')->getClass()); + } + + public function test_child_of_an_abstract_psr18_parent_compiles_and_is_traced(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.base_client', MockPsr18Client::class)->setAbstract(true); + $container->setDefinition('app.child_client', (new ChildDefinition('app.base_client'))->setPublic(true)); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertSame(PSR18TraceableClient::class, $container->getDefinition('app.child_client')->getClass()); + } + + public function test_plain_client_is_traced(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('psr18.http_client', MockPsr18Client::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertSame(PSR18TraceableClient::class, $container->getDefinition('psr18.http_client')->getClass()); + } + + public function test_client_declared_as_a_parameter_is_traced(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->setParameter('app.client.class', MockPsr18Client::class); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.client', '%app.client.class%')->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertSame(PSR18TraceableClient::class, $container->getDefinition('app.client')->getClass()); + } + + public function test_non_existent_class_is_left_untouched(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.ghost', 'Flow\Bridge\Symfony\TelemetryBundle\Tests\NoSuchClient')->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.ghost.flow_telemetry')); + static::assertSame( + 'Flow\Bridge\Symfony\TelemetryBundle\Tests\NoSuchClient', + $container->getDefinition('app.ghost')->getClass(), + ); + } + + public function test_non_psr18_class_is_left_untouched(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.thing', stdClass::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.thing.flow_telemetry')); + static::assertSame(stdClass::class, $container->getDefinition('app.thing')->getClass()); + } + + public function test_traceable_client_is_not_double_wrapped(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.traced', PSR18TraceableClient::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.traced.flow_telemetry')); + } + + public function test_client_excluded_by_exact_id_is_left_untouched(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->setParameter('flow.telemetry.psr18_client.exclude_clients', ['app.client']); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.client', MockPsr18Client::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.client.flow_telemetry')); + static::assertSame(MockPsr18Client::class, $container->getDefinition('app.client')->getClass()); + } + + public function test_client_excluded_by_regex_is_left_untouched(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', true); + $container->setParameter('flow.telemetry.psr18_client.exclude_clients', ['/^app\..*/']); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.client', MockPsr18Client::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.client.flow_telemetry')); + static::assertSame(MockPsr18Client::class, $container->getDefinition('app.client')->getClass()); + } + + public function test_nothing_is_decorated_when_disabled(): void + { + $container = new ContainerBuilder(); + $container->setParameter('flow.telemetry.psr18_client.enabled', false); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.client', MockPsr18Client::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.client.flow_telemetry')); + static::assertSame(MockPsr18Client::class, $container->getDefinition('app.client')->getClass()); + } + + public function test_nothing_is_decorated_when_the_parameter_is_absent(): void + { + $container = new ContainerBuilder(); + $container->register(Telemetry::class)->setSynthetic(true); + $container->register('app.client', MockPsr18Client::class)->setPublic(true); + $container->addCompilerPass(new Psr18ClientTelemetryPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + + $container->compile(); + + static::assertFalse($container->hasDefinition('app.client.flow_telemetry')); + static::assertSame(MockPsr18Client::class, $container->getDefinition('app.client')->getClass()); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/ServiceIdPatternsTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/ServiceIdPatternsTest.php new file mode 100644 index 0000000000..1d8e800bcc --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/Compiler/ServiceIdPatternsTest.php @@ -0,0 +1,42 @@ + $patterns + */ + #[TestWith([[], 'cache.system', false])] + #[TestWith([['cache.system'], 'cache.system', true])] + #[TestWith([['cache.system'], 'cache.validator', false])] + #[TestWith([['/^cache\.validator.*/'], 'cache.validator.expression', true])] + #[TestWith([['/^cache\.validator.*/'], 'cache.system', false])] + #[TestWith([['cache.app', '/^cache\.doctrine\..*/'], 'cache.doctrine.orm.default', true])] + #[TestWith([['cache.app', '/^cache\.doctrine\..*/'], 'cache.app', true])] + #[TestWith([['cache.app', '/^cache\.doctrine\..*/'], 'cache.system', false])] + public function test_matching_a_service_id_against_patterns( + array $patterns, + string $serviceId, + bool $expected, + ): void { + static::assertSame($expected, (new ServiceIdPatterns($patterns))->matches($serviceId)); + } + + #[TestWith(['[unterminated'])] + #[TestWith(['/unterminated'])] + #[TestWith(['cache.system'])] + public function test_an_invalid_regular_expression_is_compared_as_an_exact_service_id(string $pattern): void + { + static::assertTrue((new ServiceIdPatterns([$pattern]))->matches($pattern)); + static::assertFalse((new ServiceIdPatterns([$pattern]))->matches('some.other.service')); + } +}