$config
+ */
+ public function addTestExtensionConfig(string $extension, array $config) : void
+ {
+ $this->testExtensionConfigs[$extension] = \array_merge(
+ $this->testExtensionConfigs[$extension] ?? [],
+ $config
+ );
+ }
+
+ #[\Override]
+ public function getCacheDir() : string
+ {
+ return __DIR__ . '/../../../../../../../var/flow_telemetry_bundle_test/' . $this->environment . '/cache';
+ }
+
+ #[\Override]
+ public function getLogDir() : string
+ {
+ return __DIR__ . '/../../../../../../../var/flow_telemetry_bundle_test/' . $this->environment . '/log';
+ }
+
+ #[\Override]
+ public function getProjectDir() : string
+ {
+ return __DIR__ . '/..';
+ }
+
+ public function registerBundles() : iterable
+ {
+ yield new FlowTelemetryBundle();
+
+ foreach ($this->testBundles as $bundleClass) {
+ yield new $bundleClass();
+ }
+ }
+
+ public function registerContainerConfiguration(LoaderInterface $loader) : void
+ {
+ foreach ($this->testConfigs as $configPath) {
+ $loader->load($configPath);
+ }
+
+ $loader->load(function (ContainerBuilder $container) : void {
+ foreach ($this->testExtensionConfigs as $extension => $config) {
+ $container->loadFromExtension($extension, $config);
+ }
+
+ $container->setParameter('kernel.secret', 'test_secret_' . $this->testId);
+ });
+ }
+
+ protected function build(ContainerBuilder $container) : void
+ {
+ parent::build($container);
+
+ $container->addCompilerPass(new class implements CompilerPassInterface {
+ public function process(ContainerBuilder $container) : void
+ {
+ foreach ($container->getDefinitions() as $id => $definition) {
+ if (\str_starts_with($id, 'flow.telemetry')) {
+ $definition->setPublic(true);
+ }
+ }
+
+ foreach ($container->getAliases() as $id => $alias) {
+ if ($id === Telemetry::class || \str_starts_with($id, 'flow.telemetry')) {
+ $alias->setPublic(true);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/config/routes.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/config/routes.php
new file mode 100644
index 0000000000..ecd309f0fe
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/config/routes.php
@@ -0,0 +1,9 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => [
+ 'processor' => [
+ 'type' => 'composite',
+ 'processors' => [
+ ['type' => 'memory', 'exporter' => ['type' => 'memory']],
+ ['type' => 'passthrough', 'exporter' => ['type' => 'console']],
+ ],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(CompositeLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor'));
+ }
+
+ public function test_composite_metric_processor() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => [
+ 'processor' => [
+ 'type' => 'composite',
+ 'processors' => [
+ ['type' => 'memory', 'exporter' => ['type' => 'memory']],
+ ['type' => 'passthrough', 'exporter' => ['type' => 'console']],
+ ],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(CompositeMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor'));
+ }
+
+ public function test_composite_span_processor() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'composite',
+ 'processors' => [
+ ['type' => 'memory', 'exporter' => ['type' => 'memory']],
+ ['type' => 'passthrough', 'exporter' => ['type' => 'console']],
+ ],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertInstanceOf(CompositeSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor'));
+ self::assertInstanceOf(MemorySpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor.0.processor'));
+ self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor.1.processor'));
+ }
+
+ public function test_custom_service_reference_for_exporter() : void
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('my.custom.span_exporter', MemorySpanExporter::class)->setPublic(true);
+
+ $extension = new FlowTelemetryExtension();
+ $extension->load([
+ [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'passthrough',
+ 'exporter' => [
+ 'type' => 'service',
+ 'service_id' => 'my.custom.span_exporter',
+ ],
+ ],
+ ],
+ ],
+ ], $container);
+
+ $this->makeFlowServicesPublic($container);
+ $container->compile();
+
+ self::assertSame(
+ $container->get('my.custom.span_exporter'),
+ $container->get('flow.telemetry.tracer_provider.processor.exporter')
+ );
+ }
+
+ public function test_custom_service_reference_for_processor() : void
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('my.custom.span_processor', VoidSpanProcessor::class)->setPublic(true);
+
+ $extension = new FlowTelemetryExtension();
+ $extension->load([
+ [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'service',
+ 'service_id' => 'my.custom.span_processor',
+ ],
+ ],
+ ],
+ ], $container);
+
+ $this->makeFlowServicesPublic($container);
+ $container->compile();
+
+ self::assertSame(
+ $container->get('my.custom.span_processor'),
+ $container->get('flow.telemetry.tracer_provider.processor')
+ );
+ }
+
+ public function test_custom_service_reference_for_sampler() : void
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('my.custom.sampler', AlwaysOffSampler::class)->setPublic(true);
+
+ $extension = new FlowTelemetryExtension();
+ $extension->load([
+ [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'service',
+ 'service_id' => 'my.custom.sampler',
+ ],
+ ],
+ ],
+ ], $container);
+
+ $this->makeFlowServicesPublic($container);
+ $container->compile();
+
+ self::assertSame(
+ $container->get('my.custom.sampler'),
+ $container->get('flow.telemetry.tracer_provider.sampler')
+ );
+ }
+
+ public function test_flow_telemetry_is_aliased_to_telemetry_class() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has(Telemetry::class));
+ self::assertSame($container->get('flow.telemetry'), $container->get(Telemetry::class));
+ }
+
+ public function test_full_configuration_scenario() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => [
+ 'name' => 'my-application',
+ 'version' => '3.0.0',
+ 'attributes' => [
+ 'deployment.environment' => 'staging',
+ ],
+ ],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'trace_id_ratio',
+ 'ratio' => 0.75,
+ ],
+ 'processor' => [
+ 'type' => 'batching',
+ 'batch_size' => 1024,
+ 'exporter' => ['type' => 'console'],
+ ],
+ ],
+ 'meter_provider' => [
+ 'temporality' => 'delta',
+ 'processor' => [
+ 'type' => 'passthrough',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'logger_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'console'],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var resource $resource */
+ $resource = $container->get('flow.telemetry.resource');
+ self::assertSame('my-application', $resource->get('service.name'));
+ self::assertSame('3.0.0', $resource->get('service.version'));
+ self::assertSame('staging', $resource->get('deployment.environment'));
+
+ self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry'));
+
+ self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.tracer_provider'));
+ self::assertInstanceOf(TraceIdRatioBasedSampler::class, $container->get('flow.telemetry.tracer_provider.sampler'));
+ self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor'));
+ self::assertInstanceOf(ConsoleSpanExporter::class, $container->get('flow.telemetry.tracer_provider.processor.exporter'));
+
+ self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.meter_provider'));
+ self::assertInstanceOf(PassThroughMetricProcessor::class, $container->get('flow.telemetry.meter_provider.processor'));
+ self::assertInstanceOf(MemoryMetricExporter::class, $container->get('flow.telemetry.meter_provider.processor.exporter'));
+
+ self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.logger_provider'));
+ self::assertInstanceOf(MemoryLogProcessor::class, $container->get('flow.telemetry.logger_provider.processor'));
+ self::assertInstanceOf(ConsoleLogExporter::class, $container->get('flow.telemetry.logger_provider.processor.exporter'));
+ }
+
+ public function test_log_exporter_console_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(ConsoleLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter'));
+ }
+
+ public function test_log_exporter_memory_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(MemoryLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter'));
+ }
+
+ public function test_log_exporter_void_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(VoidLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter'));
+ }
+
+ public function test_log_processor_batching_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 256, 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(BatchingLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor'));
+ }
+
+ public function test_log_processor_memory_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(MemoryLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor'));
+ }
+
+ public function test_log_processor_passthrough_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(PassThroughLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor'));
+ }
+
+ public function test_log_processor_void_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => ['processor' => ['type' => 'void']],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(VoidLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor'));
+ }
+
+ public function test_metric_exporter_console_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(ConsoleMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter'));
+ }
+
+ public function test_metric_exporter_memory_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(MemoryMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter'));
+ }
+
+ public function test_metric_exporter_void_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(VoidMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter'));
+ }
+
+ public function test_metric_processor_batching_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 200, 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(BatchingMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor'));
+ }
+
+ public function test_metric_processor_memory_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(MemoryMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor'));
+ }
+
+ public function test_metric_processor_passthrough_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(PassThroughMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor'));
+ }
+
+ public function test_metric_processor_void_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => ['processor' => ['type' => 'void']],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(VoidMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor'));
+ }
+
+ public function test_minimal_configuration_creates_telemetry_with_void_processors() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.clock'));
+ self::assertTrue($container->has('flow.telemetry.context_storage'));
+ self::assertTrue($container->has('flow.telemetry.resource'));
+ self::assertTrue($container->has('flow.telemetry'));
+
+ self::assertInstanceOf(SystemClock::class, $container->get('flow.telemetry.clock'));
+ self::assertInstanceOf(MemoryContextStorage::class, $container->get('flow.telemetry.context_storage'));
+ self::assertInstanceOf(Resource::class, $container->get('flow.telemetry.resource'));
+ self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry'));
+
+ self::assertTrue($container->has('flow.telemetry.tracer_provider'));
+ self::assertTrue($container->has('flow.telemetry.meter_provider'));
+ self::assertTrue($container->has('flow.telemetry.logger_provider'));
+
+ self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.tracer_provider'));
+ self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.meter_provider'));
+ self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.logger_provider'));
+
+ self::assertTrue($container->has('flow.telemetry.tracer_provider.processor'));
+ self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor'));
+
+ self::assertTrue($container->has('flow.telemetry.meter_provider.processor'));
+ self::assertInstanceOf(VoidMetricProcessor::class, $container->get('flow.telemetry.meter_provider.processor'));
+
+ self::assertTrue($container->has('flow.telemetry.logger_provider.processor'));
+ self::assertInstanceOf(VoidLogProcessor::class, $container->get('flow.telemetry.logger_provider.processor'));
+ }
+
+ public function test_multiple_named_services_of_same_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'database' => [
+ 'version' => '1.0.0',
+ ],
+ 'http_client' => [
+ 'version' => '2.0.0',
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.database.tracer'));
+ self::assertTrue($container->has('flow.telemetry.http_client.tracer'));
+ self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.database.tracer'));
+ self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.http_client.tracer'));
+ }
+
+ public function test_named_logger_is_registered_as_service() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'loggers' => [
+ 'audit' => [
+ 'version' => '1.0.0',
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.audit.logger'));
+ self::assertInstanceOf(Logger::class, $container->get('flow.telemetry.audit.logger'));
+ }
+
+ public function test_named_meter_is_registered_as_service() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'meters' => [
+ 'etl_pipeline' => [
+ 'version' => '1.0.0',
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.etl_pipeline.meter'));
+ self::assertInstanceOf(Meter::class, $container->get('flow.telemetry.etl_pipeline.meter'));
+ }
+
+ public function test_named_tracer_is_registered_as_service() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'database' => [
+ 'version' => '2.0.0',
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.database.tracer'));
+ self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.database.tracer'));
+ }
+
+ public function test_named_tracer_with_attributes() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'database' => [
+ 'version' => '2.0.0',
+ 'schema_url' => 'https://opentelemetry.io/schemas/1.20.0',
+ 'attributes' => [
+ 'db.system' => 'postgresql',
+ ],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+ $tracer = $container->get('flow.telemetry.database.tracer');
+
+ self::assertInstanceOf(Tracer::class, $tracer);
+ }
+
+ public function test_otlp_availability_pass_sets_parameter_when_otlp_not_configured() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ ]);
+ },
+ ]);
+
+ self::assertTrue($this->getContainer()->hasParameter('flow.telemetry.otlp_available'));
+ }
+
+ public function test_resource_contains_additional_attributes() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => [
+ 'name' => 'my-service',
+ 'attributes' => [
+ 'deployment.environment' => 'production',
+ 'host.name' => 'server-01',
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ /** @var resource $resource */
+ $resource = $this->getContainer()->get('flow.telemetry.resource');
+ self::assertSame('production', $resource->get('deployment.environment'));
+ self::assertSame('server-01', $resource->get('host.name'));
+ }
+
+ public function test_resource_contains_service_name_and_version() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => [
+ 'name' => 'my-service',
+ 'version' => '2.1.0',
+ ],
+ ]);
+ },
+ ]);
+
+ /** @var resource $resource */
+ $resource = $this->getContainer()->get('flow.telemetry.resource');
+ self::assertSame('my-service', $resource->get('service.name'));
+ self::assertSame('2.1.0', $resource->get('service.version'));
+ }
+
+ public function test_same_name_for_different_types_is_allowed() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'database' => ['version' => '1.0.0'],
+ ],
+ 'meters' => [
+ 'database' => ['version' => '1.0.0'],
+ ],
+ 'loggers' => [
+ 'database' => ['version' => '1.0.0'],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.database.tracer'));
+ self::assertTrue($container->has('flow.telemetry.database.meter'));
+ self::assertTrue($container->has('flow.telemetry.database.logger'));
+ self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.database.tracer'));
+ self::assertInstanceOf(Meter::class, $container->get('flow.telemetry.database.meter'));
+ self::assertInstanceOf(Logger::class, $container->get('flow.telemetry.database.logger'));
+ }
+
+ public function test_service_exporter_without_service_id_throws_exception() : void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('service_id is required when exporter type is "service"');
+
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'passthrough',
+ 'exporter' => ['type' => 'service'],
+ ],
+ ],
+ ]);
+ },
+ ]);
+ }
+
+ public function test_service_processor_without_service_id_throws_exception() : void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('service_id is required when processor type is "service"');
+
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => ['type' => 'service'],
+ ],
+ ]);
+ },
+ ]);
+ }
+
+ public function test_service_sampler_without_service_id_throws_exception() : void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('service_id is required when sampler type is "service"');
+
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => ['type' => 'service'],
+ ],
+ ]);
+ },
+ ]);
+ }
+
+ public function test_span_exporter_console_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(ConsoleSpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter'));
+ }
+
+ public function test_span_exporter_memory_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(MemorySpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter'));
+ }
+
+ public function test_span_exporter_void_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(VoidSpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter'));
+ }
+
+ public function test_span_processor_batching_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 100, 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(BatchingSpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor'));
+ }
+
+ public function test_span_processor_memory_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(MemorySpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor'));
+ }
+
+ public function test_span_processor_passthrough_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(PassThroughSpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor'));
+ }
+
+ public function test_span_processor_void_type() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => ['processor' => ['type' => 'void']],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(VoidSpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor'));
+ }
+
+ public function test_tracer_provider_with_always_off_sampler() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => ['type' => 'always_off'],
+ ],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(AlwaysOffSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler'));
+ }
+
+ public function test_tracer_provider_with_always_on_sampler() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => ['type' => 'always_on'],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.tracer_provider.sampler'));
+ self::assertInstanceOf(AlwaysOnSampler::class, $container->get('flow.telemetry.tracer_provider.sampler'));
+ }
+
+ public function test_tracer_provider_with_parent_based_sampler() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => ['type' => 'parent_based'],
+ ],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(ParentBasedSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler'));
+ }
+
+ public function test_tracer_provider_with_trace_id_ratio_sampler() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'trace_id_ratio',
+ 'ratio' => 0.5,
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ self::assertInstanceOf(TraceIdRatioBasedSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler'));
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php
new file mode 100644
index 0000000000..fb84da639e
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php
@@ -0,0 +1,59 @@
+context = new SymfonyContext();
+ }
+
+ protected function tearDown() : void
+ {
+ $this->context->shutdown();
+ }
+
+ /**
+ * @param array{config?: callable(TestKernel): void} $options
+ */
+ protected function bootKernel(array $options = []) : TestKernel
+ {
+ return $this->context->bootKernel($options);
+ }
+
+ protected function getContainer() : ContainerInterface
+ {
+ return $this->context->getContainer();
+ }
+
+ protected function getKernel() : TestKernel
+ {
+ return $this->context->getKernel();
+ }
+
+ protected function makeFlowServicesPublic(ContainerBuilder $container) : void
+ {
+ foreach ($container->getDefinitions() as $id => $definition) {
+ if (\str_starts_with($id, 'flow.telemetry')) {
+ $definition->setPublic(true);
+ }
+ }
+
+ foreach ($container->getAliases() as $id => $alias) {
+ if ($id === Telemetry::class || \str_starts_with($id, 'flow.telemetry')) {
+ $alias->setPublic(true);
+ }
+ }
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php
new file mode 100644
index 0000000000..403cfcf678
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php
@@ -0,0 +1,127 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'batch_size' => 100,
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => true],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new TestCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:command']);
+ $output = new BufferedOutput();
+
+ $exitCode = $application->run($input, $output);
+
+ self::assertSame(0, $exitCode);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanExporter $exporter */
+ $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter');
+ $spans = $exporter->spans();
+
+ self::assertCount(1, $spans, 'Spans should be exported after console terminate when flush is called');
+ }
+
+ public function test_flush_is_not_called_when_console_instrumentation_is_disabled() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'batch_size' => 100,
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new TestCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:command']);
+ $output = new BufferedOutput();
+
+ $application->run($input, $output);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanExporter $exporter */
+ $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter');
+ $spans = $exporter->spans();
+
+ self::assertCount(0, $spans, 'No spans should be exported when instrumentation is disabled');
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php
new file mode 100644
index 0000000000..1a9648ba23
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php
@@ -0,0 +1,305 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new TestCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:command']);
+ $output = new BufferedOutput();
+
+ $application->run($input, $output);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(0, $spans);
+ }
+
+ public function test_excludes_command_with_exact_match() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => [
+ 'enabled' => true,
+ 'exclude_commands' => ['test:command'],
+ ],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new TestCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:command']);
+ $output = new BufferedOutput();
+
+ $application->run($input, $output);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(0, $spans);
+ }
+
+ public function test_excludes_command_with_regex_pattern() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => [
+ 'enabled' => true,
+ 'exclude_commands' => ['/^test:.*/'],
+ ],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new TestCommand());
+ $application->add(new FailingCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:command']);
+ $output = new BufferedOutput();
+
+ $application->run($input, $output);
+
+ $input = new ArrayInput(['command' => 'test:failing']);
+ $application->run($input, $output);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(0, $spans, 'Both test:command and test:failing should be excluded by regex');
+ }
+
+ public function test_traces_failing_console_command() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => true],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new FailingCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:failing']);
+ $output = new BufferedOutput();
+
+ $exitCode = $application->run($input, $output);
+
+ self::assertSame(1, $exitCode);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(1, $spans);
+
+ $span = $spans[0];
+ self::assertSame('test:failing', $span->name());
+
+ $attributes = $span->attributes();
+ self::assertSame(1, $attributes['process.exit_code']);
+
+ $status = $span->status();
+ self::assertNotNull($status);
+ self::assertSame('Exit code: 1', $status->description);
+ }
+
+ public function test_traces_successful_console_command() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => true],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $application = new Application($kernel);
+ $application->add(new TestCommand());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $input = new ArrayInput(['command' => 'test:command']);
+ $output = new BufferedOutput();
+
+ $exitCode = $application->run($input, $output);
+
+ self::assertSame(0, $exitCode);
+
+ $container = $this->getContainer();
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(1, $spans);
+
+ $span = $spans[0];
+ self::assertSame('test:command', $span->name());
+ self::assertSame(SpanKind::INTERNAL, $span->kind());
+
+ $attributes = $span->attributes();
+ self::assertSame('test:command', $attributes['command.name']);
+ self::assertSame(TestCommand::class, $attributes['command.class']);
+ self::assertSame(0, $attributes['process.exit_code']);
+
+ $status = $span->status();
+ self::assertNotNull($status);
+ self::assertTrue($status->isOk());
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php
new file mode 100644
index 0000000000..5fad0e3b48
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php
@@ -0,0 +1,129 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'batch_size' => 100,
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => true],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Router $router */
+ $router = $container->get('router');
+ $routes = $router->getRouteCollection();
+ $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index']));
+
+ $request = Request::create('/test', 'GET');
+ $response = $kernel->handle($request);
+
+ /** @var MemorySpanExporter $exporter */
+ $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter');
+ $spansBeforeTerminate = $exporter->spans();
+
+ self::assertCount(0, $spansBeforeTerminate, 'Spans should not be exported before terminate (batching)');
+
+ $kernel->terminate($request, $response);
+
+ $spansAfterTerminate = $exporter->spans();
+
+ self::assertCount(1, $spansAfterTerminate, 'Spans should be exported after terminate when flush is called');
+ }
+
+ public function test_flush_is_not_called_when_http_kernel_instrumentation_is_disabled() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'batch_size' => 100,
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Router $router */
+ $router = $container->get('router');
+ $routes = $router->getRouteCollection();
+ $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index']));
+
+ $request = Request::create('/test', 'GET');
+ $response = $kernel->handle($request);
+ $kernel->terminate($request, $response);
+
+ /** @var MemorySpanExporter $exporter */
+ $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter');
+ $spans = $exporter->spans();
+
+ self::assertCount(0, $spans, 'No spans should be exported when instrumentation is disabled');
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php
new file mode 100644
index 0000000000..e57803585f
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php
@@ -0,0 +1,194 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Router $router */
+ $router = $container->get('router');
+ $routes = $router->getRouteCollection();
+ $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index']));
+
+ $request = Request::create('/test', 'GET');
+ $response = $kernel->handle($request);
+ $kernel->terminate($request, $response);
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(0, $spans);
+ }
+
+ public function test_traces_http_request_with_error_status() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => true],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Router $router */
+ $router = $container->get('router');
+ $routes = $router->getRouteCollection();
+ $routes->add('test_error', new Route('/error', ['_controller' => TestController::class . '::error']));
+
+ $request = Request::create('/error', 'GET');
+ $response = $kernel->handle($request);
+ $kernel->terminate($request, $response);
+
+ self::assertSame(404, $response->getStatusCode());
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(1, $spans);
+
+ $span = $spans[0];
+ $attributes = $span->attributes();
+ self::assertSame(404, $attributes['http.status_code']);
+
+ $status = $span->status();
+ self::assertNotNull($status);
+ self::assertTrue($status->isError());
+ self::assertSame('HTTP 404', $status->description);
+ }
+
+ public function test_traces_successful_http_request() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : 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', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => true],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Router $router */
+ $router = $container->get('router');
+ $routes = $router->getRouteCollection();
+ $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index']));
+
+ $request = Request::create('/test', 'GET');
+ $response = $kernel->handle($request);
+ $kernel->terminate($request, $response);
+
+ self::assertSame(200, $response->getStatusCode());
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(1, $spans);
+
+ $span = $spans[0];
+ self::assertSame('GET test_index', $span->name());
+ self::assertSame(SpanKind::SERVER, $span->kind());
+
+ $attributes = $span->attributes();
+ self::assertSame('GET', $attributes['http.method']);
+ self::assertSame(200, $attributes['http.status_code']);
+ self::assertSame('test_index', $attributes['http.route']);
+ self::assertSame(TestController::class . '::index', $attributes['controller']);
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php
new file mode 100644
index 0000000000..15e2da1eca
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php
@@ -0,0 +1,206 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'messenger' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertFalse($container->has('flow.telemetry.messenger.middleware'));
+ }
+
+ public function test_middleware_service_is_registered() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'messenger' => true,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.messenger.middleware'));
+ self::assertInstanceOf(TracingMiddleware::class, $container->get('flow.telemetry.messenger.middleware'));
+ }
+
+ public function test_traces_message_dispatch() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => true,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Telemetry $telemetry */
+ $telemetry = $container->get(Telemetry::class);
+
+ $handler = new TestMessageHandler();
+
+ $bus = new MessageBus([
+ new TracingMiddleware($telemetry),
+ new HandleMessageMiddleware(
+ new HandlersLocator([
+ TestMessage::class => [$handler],
+ ])
+ ),
+ ]);
+
+ $message = new TestMessage('test content');
+ $envelope = new Envelope(
+ $message,
+ [new BusNameStamp('command.bus')]
+ );
+
+ $bus->dispatch($envelope);
+
+ self::assertTrue($handler->handled);
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(1, $spans);
+
+ $span = $spans[0];
+ self::assertSame('command.bus TestMessage', $span->name());
+ self::assertSame(SpanKind::PRODUCER, $span->kind());
+
+ $attributes = $span->attributes();
+ self::assertSame('symfony_messenger', $attributes['messaging.system']);
+ self::assertSame('command.bus', $attributes['messaging.destination']);
+ self::assertSame(TestMessage::class, $attributes['messaging.message.class']);
+ self::assertSame('send', $attributes['messaging.operation']);
+
+ $status = $span->status();
+ self::assertNotNull($status);
+ self::assertTrue($status->isOk());
+ }
+
+ public function test_traces_message_with_exception() : void
+ {
+ $kernel = $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => true,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var Telemetry $telemetry */
+ $telemetry = $container->get(Telemetry::class);
+
+ $failingHandler = static function (TestMessage $message) : void {
+ throw new \RuntimeException('Handler failed');
+ };
+
+ $bus = new MessageBus([
+ new TracingMiddleware($telemetry),
+ new HandleMessageMiddleware(
+ new HandlersLocator([
+ TestMessage::class => [$failingHandler],
+ ])
+ ),
+ ]);
+
+ $message = new TestMessage('test content');
+
+ $exceptionThrown = false;
+
+ try {
+ $bus->dispatch($message);
+ } catch (\Throwable) {
+ $exceptionThrown = true;
+ }
+
+ self::assertTrue($exceptionThrown, 'Expected exception was not thrown');
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertCount(1, $spans);
+
+ $span = $spans[0];
+
+ $status = $span->status();
+ self::assertNotNull($status);
+ self::assertTrue($status->isError());
+ self::assertStringContainsString('Handler failed', $status->description ?? '');
+
+ $events = $span->events();
+ self::assertCount(1, $events);
+ self::assertSame('exception', $events[0]->name());
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php
new file mode 100644
index 0000000000..d950ce071d
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php
@@ -0,0 +1,421 @@
+bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => false],
+ 'console' => ['enabled' => false],
+ 'messenger' => false,
+ 'twig' => [
+ 'enabled' => true,
+ 'trace_blocks' => false,
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var TracingTwigExtension $extension */
+ $extension = $container->get('flow.telemetry.twig.extension');
+
+ $loader = new ArrayLoader([
+ 'base.html.twig' => '{% block content %}Default content{% endblock %}',
+ 'child.html.twig' => '{% extends "base.html.twig" %}{% block content %}Child content{% endblock %}',
+ ]);
+
+ $twig = new Environment($loader);
+ $twig->addExtension($extension);
+
+ $result = $twig->render('child.html.twig');
+
+ self::assertSame('Child content', $result);
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ foreach ($spans as $span) {
+ $attributes = $span->attributes();
+ self::assertNotSame('block', $attributes['twig.type'] ?? '', 'Block span should not be traced');
+ }
+ }
+
+ public function test_does_not_trace_excluded_templates() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => false,
+ 'twig' => [
+ 'enabled' => true,
+ 'exclude_templates' => ['excluded.html.twig'],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var TracingTwigExtension $extension */
+ $extension = $container->get('flow.telemetry.twig.extension');
+
+ $loader = new ArrayLoader([
+ 'included.html.twig' => 'Included template',
+ 'excluded.html.twig' => 'Excluded template',
+ ]);
+
+ $twig = new Environment($loader);
+ $twig->addExtension($extension);
+
+ $twig->render('included.html.twig');
+ $twig->render('excluded.html.twig');
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ $templateNames = [];
+
+ foreach ($spans as $span) {
+ $attributes = $span->attributes();
+
+ if (($attributes['twig.type'] ?? '') === 'template') {
+ $templateNames[] = $attributes['twig.template'];
+ }
+ }
+
+ self::assertContains('included.html.twig', $templateNames, 'Included template should be traced');
+ self::assertNotContains('excluded.html.twig', $templateNames, 'Excluded template should not be traced');
+ }
+
+ public function test_does_not_trace_excluded_templates_with_regex() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => false,
+ 'twig' => [
+ 'enabled' => true,
+ 'exclude_templates' => ['/^@Profiler.*/'],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var TracingTwigExtension $extension */
+ $extension = $container->get('flow.telemetry.twig.extension');
+
+ $loader = new ArrayLoader([
+ 'included.html.twig' => 'Included template',
+ '@Profiler/toolbar.html.twig' => 'Profiler toolbar',
+ '@Profiler/panel.html.twig' => 'Profiler panel',
+ ]);
+
+ $twig = new Environment($loader);
+ $twig->addExtension($extension);
+
+ $twig->render('included.html.twig');
+ $twig->render('@Profiler/toolbar.html.twig');
+ $twig->render('@Profiler/panel.html.twig');
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ $templateNames = [];
+
+ foreach ($spans as $span) {
+ $attributes = $span->attributes();
+
+ if (($attributes['twig.type'] ?? '') === 'template') {
+ $templateNames[] = $attributes['twig.template'];
+ }
+ }
+
+ self::assertContains('included.html.twig', $templateNames, 'Included template should be traced');
+ self::assertNotContains('@Profiler/toolbar.html.twig', $templateNames, 'Profiler toolbar should not be traced');
+ self::assertNotContains('@Profiler/panel.html.twig', $templateNames, 'Profiler panel should not be traced');
+ }
+
+ public function test_excluded_template_cascades_to_children() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => false,
+ 'twig' => [
+ 'enabled' => true,
+ 'trace_blocks' => true,
+ 'exclude_templates' => ['excluded.html.twig'],
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var TracingTwigExtension $extension */
+ $extension = $container->get('flow.telemetry.twig.extension');
+
+ $loader = new ArrayLoader([
+ 'child.html.twig' => 'Child content',
+ 'excluded.html.twig' => '{% block content %}Block content{% endblock %}{% include "child.html.twig" %}',
+ ]);
+
+ $twig = new Environment($loader);
+ $twig->addExtension($extension);
+
+ $twig->render('excluded.html.twig');
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ $templateNames = [];
+ $blockNames = [];
+
+ foreach ($spans as $span) {
+ $attributes = $span->attributes();
+ $type = $attributes['twig.type'] ?? '';
+
+ if ($type === 'template') {
+ $templateNames[] = $attributes['twig.template'];
+ } elseif ($type === 'block') {
+ $blockNames[] = $attributes['twig.name'];
+ }
+ }
+
+ self::assertNotContains('excluded.html.twig', $templateNames, 'Excluded template should not be traced');
+ self::assertNotContains('child.html.twig', $templateNames, 'Child template should not be traced when parent is excluded');
+ self::assertNotContains('content', $blockNames, 'Block in excluded template should not be traced');
+ }
+
+ public function test_extension_not_registered_when_disabled() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'twig' => false,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertFalse($container->has('flow.telemetry.twig.extension'));
+ }
+
+ public function test_extension_service_is_registered() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'twig' => true,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ self::assertTrue($container->has('flow.telemetry.twig.extension'));
+ self::assertInstanceOf(TracingTwigExtension::class, $container->get('flow.telemetry.twig.extension'));
+ }
+
+ public function test_traces_blocks_in_templates() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => false,
+ 'twig' => [
+ 'enabled' => true,
+ 'trace_blocks' => true,
+ ],
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var TracingTwigExtension $extension */
+ $extension = $container->get('flow.telemetry.twig.extension');
+
+ $loader = new ArrayLoader([
+ 'base.html.twig' => '{% block content %}Default content{% endblock %}',
+ 'child.html.twig' => '{% extends "base.html.twig" %}{% block content %}Child content{% endblock %}',
+ ]);
+
+ $twig = new Environment($loader);
+ $twig->addExtension($extension);
+
+ $result = $twig->render('child.html.twig');
+
+ self::assertSame('Child content', $result);
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertGreaterThanOrEqual(1, \count($spans));
+
+ $blockSpanFound = false;
+
+ foreach ($spans as $span) {
+ $attributes = $span->attributes();
+
+ if (($attributes['twig.type'] ?? '') === 'block') {
+ $blockSpanFound = true;
+ self::assertSame('content', $attributes['twig.name']);
+ }
+ }
+
+ self::assertTrue($blockSpanFound, 'Expected block span was not found');
+ }
+
+ public function test_traces_template_rendering() : void
+ {
+ $this->bootKernel([
+ 'config' => static function (TestKernel $kernel) : void {
+ $kernel->addTestExtensionConfig('flow_telemetry', [
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'memory',
+ 'exporter' => ['type' => 'memory'],
+ ],
+ ],
+ 'telemetry' => [
+ 'http_kernel' => false,
+ 'console' => false,
+ 'messenger' => false,
+ 'twig' => true,
+ ],
+ ]);
+ },
+ ]);
+
+ $container = $this->getContainer();
+
+ /** @var TracingTwigExtension $extension */
+ $extension = $container->get('flow.telemetry.twig.extension');
+
+ $loader = new ArrayLoader([
+ 'test.html.twig' => 'Hello {{ name }}!',
+ ]);
+
+ $twig = new Environment($loader);
+ $twig->addExtension($extension);
+
+ $result = $twig->render('test.html.twig', ['name' => 'World']);
+
+ self::assertSame('Hello World!', $result);
+
+ /** @var MemorySpanProcessor $processor */
+ $processor = $container->get('flow.telemetry.tracer_provider.processor');
+ $spans = $processor->endedSpans();
+
+ self::assertGreaterThanOrEqual(1, \count($spans));
+
+ $templateSpanFound = false;
+
+ foreach ($spans as $span) {
+ if ($span->name() === 'test.html.twig') {
+ $templateSpanFound = true;
+ $attributes = $span->attributes();
+ self::assertSame('template', $attributes['twig.type']);
+ self::assertSame('test.html.twig', $attributes['twig.template']);
+ }
+ }
+
+ self::assertTrue($templateSpanFound, 'Expected template span was not found');
+ }
+}
diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php
new file mode 100644
index 0000000000..24733a8fb1
--- /dev/null
+++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php
@@ -0,0 +1,606 @@
+processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'composite',
+ 'processors' => [
+ [
+ 'type' => 'memory',
+ ],
+ [
+ 'type' => 'batching',
+ 'batch_size' => 100,
+ ],
+ ],
+ ],
+ ],
+ ]]);
+
+ $processors = $config['tracer_provider']['processor']['processors'];
+ self::assertCount(2, $processors);
+ self::assertSame('memory', $processors[0]['type']);
+ self::assertSame('batching', $processors[1]['type']);
+ self::assertSame(100, $processors[1]['batch_size']);
+ }
+
+ public function test_empty_service_name_is_rejected() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => ''],
+ ]]);
+ }
+
+ public function test_empty_tracers_meters_loggers_config() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [],
+ 'meters' => [],
+ 'loggers' => [],
+ ]]);
+
+ self::assertSame([], $config['tracers']);
+ self::assertSame([], $config['meters']);
+ self::assertSame([], $config['loggers']);
+ }
+
+ public function test_exporter_defaults_to_void() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ ],
+ ],
+ ]]);
+
+ self::assertSame('void', $config['tracer_provider']['processor']['exporter']['type']);
+ }
+
+ public function test_invalid_exporter_type_is_rejected() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'exporter' => [
+ 'type' => 'invalid_exporter',
+ ],
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_invalid_processor_type_is_rejected() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'invalid_processor',
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_invalid_sampler_type_is_rejected() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'invalid_sampler',
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_invalid_severity_level_is_rejected() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => [
+ 'processor' => [
+ 'type' => 'severity_filtering',
+ 'minimum_severity' => 'invalid_level',
+ 'inner_processor' => [
+ 'type' => 'void',
+ ],
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_logger_configuration() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'loggers' => [
+ 'audit' => [
+ 'version' => '1.0.0',
+ 'schema_url' => 'https://example.com/audit-schema/1.0',
+ 'attributes' => [
+ 'log.category' => 'audit',
+ ],
+ ],
+ ],
+ ]]);
+
+ self::assertArrayHasKey('loggers', $config);
+ self::assertArrayHasKey('audit', $config['loggers']);
+ self::assertSame('1.0.0', $config['loggers']['audit']['version']);
+ self::assertSame('https://example.com/audit-schema/1.0', $config['loggers']['audit']['schema_url']);
+ self::assertSame(['log.category' => 'audit'], $config['loggers']['audit']['attributes']);
+ }
+
+ public function test_meter_configuration() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'meters' => [
+ 'etl_pipeline' => [
+ 'version' => '1.0.0',
+ 'attributes' => [
+ 'flow.pipeline' => 'daily_import',
+ ],
+ ],
+ ],
+ ]]);
+
+ self::assertArrayHasKey('meters', $config);
+ self::assertArrayHasKey('etl_pipeline', $config['meters']);
+ self::assertSame('1.0.0', $config['meters']['etl_pipeline']['version']);
+ self::assertNull($config['meters']['etl_pipeline']['schema_url']);
+ self::assertSame(['flow.pipeline' => 'daily_import'], $config['meters']['etl_pipeline']['attributes']);
+ }
+
+ public function test_meter_provider_temporality_can_be_delta() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => [
+ 'temporality' => 'delta',
+ ],
+ ]]);
+
+ self::assertSame('delta', $config['meter_provider']['temporality']);
+ }
+
+ public function test_meter_provider_temporality_defaults_to_cumulative() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'meter_provider' => [],
+ ]]);
+
+ self::assertSame('cumulative', $config['meter_provider']['temporality']);
+ }
+
+ public function test_minimal_config_requires_service_name() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('service');
+
+ (new Processor())->processConfiguration(new Configuration(), [[]]);
+ }
+
+ public function test_minimal_config_with_service_name() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ ]]);
+
+ self::assertSame('test-app', $config['service']['name']);
+ self::assertNull($config['service']['version']);
+ self::assertSame([], $config['service']['attributes']);
+ }
+
+ public function test_multiple_named_items_of_same_type() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'database' => [
+ 'version' => '1.0.0',
+ ],
+ 'http_client' => [
+ 'version' => '2.0.0',
+ ],
+ 'cache' => [],
+ ],
+ ]]);
+
+ self::assertCount(3, $config['tracers']);
+ self::assertSame('1.0.0', $config['tracers']['database']['version']);
+ self::assertSame('2.0.0', $config['tracers']['http_client']['version']);
+ self::assertSame('unknown', $config['tracers']['cache']['version']);
+ }
+
+ public function test_otlp_serializer_defaults_to_json() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'exporter' => [
+ 'type' => 'otlp',
+ 'otlp' => [
+ 'transport' => [],
+ ],
+ ],
+ ],
+ ],
+ ]]);
+
+ $serializer = $config['tracer_provider']['processor']['exporter']['otlp']['transport']['serializer'];
+ self::assertSame('json', $serializer['type']);
+ }
+
+ public function test_otlp_transport_defaults() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'exporter' => [
+ 'type' => 'otlp',
+ 'otlp' => [
+ 'transport' => [],
+ ],
+ ],
+ ],
+ ],
+ ]]);
+
+ $transport = $config['tracer_provider']['processor']['exporter']['otlp']['transport'];
+ self::assertSame('curl', $transport['type']);
+ self::assertSame('http://localhost:4318', $transport['endpoint']);
+ self::assertSame(30, $transport['timeout']);
+ self::assertSame([], $transport['headers']);
+ self::assertTrue($transport['insecure']);
+ }
+
+ public function test_processor_batch_size_default() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ ],
+ ],
+ ]]);
+
+ self::assertSame(512, $config['tracer_provider']['processor']['batch_size']);
+ }
+
+ public function test_processor_batch_size_minimum_validation() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'batching',
+ 'batch_size' => 0,
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_processor_defaults_to_void() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [],
+ ]]);
+
+ self::assertSame('void', $config['tracer_provider']['processor']['type']);
+ }
+
+ public function test_providers_have_defaults() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ ]]);
+
+ self::assertArrayHasKey('tracer_provider', $config);
+ self::assertArrayHasKey('meter_provider', $config);
+ self::assertArrayHasKey('logger_provider', $config);
+ self::assertSame('void', $config['tracer_provider']['processor']['type']);
+ self::assertSame('void', $config['meter_provider']['processor']['type']);
+ self::assertSame('void', $config['logger_provider']['processor']['type']);
+ }
+
+ public function test_sampler_defaults_to_always_on() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [],
+ ]]);
+
+ self::assertSame('always_on', $config['tracer_provider']['sampler']['type']);
+ }
+
+ public function test_sampler_ratio_maximum_validation() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'trace_id_ratio',
+ 'ratio' => 1.1,
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_sampler_ratio_minimum_validation() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'trace_id_ratio',
+ 'ratio' => -0.1,
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_sampler_ratio_validation() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'sampler' => [
+ 'type' => 'trace_id_ratio',
+ 'ratio' => 0.5,
+ ],
+ ],
+ ]]);
+
+ self::assertSame(0.5, $config['tracer_provider']['sampler']['ratio']);
+ }
+
+ public function test_service_config_with_version_and_attributes() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => [
+ 'name' => 'test-app',
+ 'version' => '1.2.3',
+ 'attributes' => [
+ 'environment' => 'production',
+ 'region' => 'us-east-1',
+ ],
+ ],
+ ]]);
+
+ self::assertSame('test-app', $config['service']['name']);
+ self::assertSame('1.2.3', $config['service']['version']);
+ self::assertSame([
+ 'environment' => 'production',
+ 'region' => 'us-east-1',
+ ], $config['service']['attributes']);
+ }
+
+ public function test_severity_filtering_is_only_available_for_log_processors() : void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracer_provider' => [
+ 'processor' => [
+ 'type' => 'severity_filtering',
+ ],
+ ],
+ ]]);
+ }
+
+ public function test_severity_filtering_minimum_severity_default() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => [
+ 'processor' => [
+ 'type' => 'severity_filtering',
+ 'inner_processor' => [
+ 'type' => 'void',
+ ],
+ ],
+ ],
+ ]]);
+
+ self::assertSame('info', $config['logger_provider']['processor']['minimum_severity']);
+ }
+
+ public function test_severity_filtering_processor_for_logs() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'logger_provider' => [
+ 'processor' => [
+ 'type' => 'severity_filtering',
+ 'minimum_severity' => 'warn',
+ 'inner_processor' => [
+ 'type' => 'batching',
+ 'exporter' => ['type' => 'console'],
+ ],
+ ],
+ ],
+ ]]);
+
+ $processor = $config['logger_provider']['processor'];
+ self::assertSame('severity_filtering', $processor['type']);
+ self::assertSame('warn', $processor['minimum_severity']);
+ self::assertSame('batching', $processor['inner_processor']['type']);
+ self::assertSame('console', $processor['inner_processor']['exporter']['type']);
+ }
+
+ public function test_telemetry_can_be_enabled() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => true],
+ 'console' => ['enabled' => true],
+ 'messenger' => true,
+ ],
+ ]]);
+
+ self::assertTrue($config['telemetry']['http_kernel']['enabled']);
+ self::assertTrue($config['telemetry']['console']['enabled']);
+ self::assertTrue($config['telemetry']['messenger']);
+ }
+
+ public function test_telemetry_console_exclude_commands() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'console' => [
+ 'enabled' => true,
+ 'exclude_commands' => ['cache:clear', 'debug:router'],
+ ],
+ ],
+ ]]);
+
+ self::assertTrue($config['telemetry']['console']['enabled']);
+ self::assertSame(['cache:clear', 'debug:router'], $config['telemetry']['console']['exclude_commands']);
+ }
+
+ public function test_telemetry_defaults_to_all_disabled() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ ]]);
+
+ self::assertArrayHasKey('telemetry', $config);
+ self::assertFalse($config['telemetry']['http_kernel']['enabled']);
+ self::assertFalse($config['telemetry']['console']['enabled']);
+ self::assertFalse($config['telemetry']['messenger']);
+ }
+
+ public function test_telemetry_http_kernel_exclude_routes() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'http_kernel' => [
+ 'enabled' => true,
+ 'exclude_routes' => ['_wdt', '_profiler', '/_profiler.*/'],
+ ],
+ ],
+ ]]);
+
+ self::assertTrue($config['telemetry']['http_kernel']['enabled']);
+ self::assertSame(['_wdt', '_profiler', '/_profiler.*/'], $config['telemetry']['http_kernel']['exclude_routes']);
+ }
+
+ public function test_telemetry_partial_config() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'http_kernel' => ['enabled' => true],
+ ],
+ ]]);
+
+ self::assertTrue($config['telemetry']['http_kernel']['enabled']);
+ self::assertFalse($config['telemetry']['console']['enabled']);
+ self::assertFalse($config['telemetry']['messenger']);
+ }
+
+ public function test_telemetry_twig_exclude_templates() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'telemetry' => [
+ 'twig' => [
+ 'enabled' => true,
+ 'exclude_templates' => ['@WebProfiler/Collector/time.html.twig', 'debug/exception.html.twig'],
+ ],
+ ],
+ ]]);
+
+ self::assertTrue($config['telemetry']['twig']['enabled']);
+ self::assertSame(['@WebProfiler/Collector/time.html.twig', 'debug/exception.html.twig'], $config['telemetry']['twig']['exclude_templates']);
+ }
+
+ public function test_tracer_configuration_with_all_options() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'database' => [
+ 'version' => '2.0.0',
+ 'schema_url' => 'https://opentelemetry.io/schemas/1.20.0',
+ 'attributes' => [
+ 'db.system' => 'postgresql',
+ 'db.pool_size' => 10,
+ ],
+ ],
+ ],
+ ]]);
+
+ self::assertArrayHasKey('tracers', $config);
+ self::assertArrayHasKey('database', $config['tracers']);
+ self::assertSame('2.0.0', $config['tracers']['database']['version']);
+ self::assertSame('https://opentelemetry.io/schemas/1.20.0', $config['tracers']['database']['schema_url']);
+ self::assertSame([
+ 'db.system' => 'postgresql',
+ 'db.pool_size' => 10,
+ ], $config['tracers']['database']['attributes']);
+ }
+
+ public function test_tracer_configuration_with_defaults() : void
+ {
+ $config = (new Processor())->processConfiguration(new Configuration(), [[
+ 'service' => ['name' => 'test-app'],
+ 'tracers' => [
+ 'http_client' => [],
+ ],
+ ]]);
+
+ self::assertArrayHasKey('tracers', $config);
+ self::assertArrayHasKey('http_client', $config['tracers']);
+ self::assertSame('unknown', $config['tracers']['http_client']['version']);
+ self::assertNull($config['tracers']['http_client']['schema_url']);
+ self::assertSame([], $config['tracers']['http_client']['attributes']);
+ }
+}
diff --git a/src/core/etl/src/Flow/ETL/Attribute/Module.php b/src/core/etl/src/Flow/ETL/Attribute/Module.php
index fc86d5a705..9c521dc250 100644
--- a/src/core/etl/src/Flow/ETL/Attribute/Module.php
+++ b/src/core/etl/src/Flow/ETL/Attribute/Module.php
@@ -28,6 +28,7 @@ enum Module : string
case PSR7_TELEMETRY_BRIDGE = 'PSR7_TELEMETRY_BRIDGE';
case S3_FILESYSTEM = 'S3_FILESYSTEM';
case SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE = 'SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE';
+ case SYMFONY_TELEMETRY_BUNDLE = 'SYMFONY_TELEMETRY_BUNDLE';
case TELEMETRY = 'TELEMETRY';
case TELEMETRY_OTLP = 'TELEMETRY_OTLP';
case TEXT = 'TEXT';
diff --git a/tools/box/composer.lock b/tools/box/composer.lock
index 71669fcd25..eaf0068830 100644
--- a/tools/box/composer.lock
+++ b/tools/box/composer.lock
@@ -1083,29 +1083,29 @@
},
{
"name": "doctrine/deprecations",
- "version": "1.1.5",
+ "version": "1.1.6",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
- "phpunit/phpunit": "<=7.5 || >=13"
+ "phpunit/phpunit": "<=7.5 || >=14"
},
"require-dev": {
- "doctrine/coding-standard": "^9 || ^12 || ^13",
- "phpstan/phpstan": "1.4.10 || 2.1.11",
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
@@ -1125,9 +1125,9 @@
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
- "source": "https://github.com/doctrine/deprecations/tree/1.1.5"
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
- "time": "2025-04-07T20:06:18+00:00"
+ "time": "2026-02-07T07:09:04+00:00"
},
{
"name": "fidry/console",
diff --git a/tools/infection/phpunit.xml b/tools/infection/phpunit.xml
index 63fe5c1d13..b335a11b44 100644
--- a/tools/infection/phpunit.xml
+++ b/tools/infection/phpunit.xml
@@ -20,6 +20,7 @@
../../src/lib/telemetry/tests/Flow/Telemetry/Tests/Unit
../../src/bridge/monolog/telemetry/tests/Flow/Bridge/Monolog/Telemetry/Tests/Unit
../../src/bridge/symfony/http-foundation-telemetry/tests/Flow/Bridge/Symfony/HttpFoundationTelemetry/Tests/Unit
+ ../../src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit
../../src/bridge/psr7/telemetry/tests/Flow/Bridge/Psr7/Telemetry/Tests/Unit
../../src/bridge/telemetry/otlp/tests/Flow/Bridge/Telemetry/OTLP/Tests/Unit
@@ -36,6 +37,7 @@
../../src/lib/telemetry/src
../../src/bridge/monolog/telemetry/src
../../src/bridge/symfony/http-foundation-telemetry/src
+ ../../src/bridge/symfony/telemetry-bundle/src
../../src/bridge/psr7/telemetry/src
../../src/bridge/telemetry/otlp/src
@@ -51,6 +53,7 @@
../../src/lib/telemetry/src/Flow/Telemetry/DSL
../../src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/DSL
../../src/bridge/symfony/http-foundation-telemetry/src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL
+ ../../src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL
../../src/bridge/psr7/telemetry/src/Flow/Bridge/Psr7/Telemetry/DSL
../../src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL
diff --git a/tools/phpunit/composer.lock b/tools/phpunit/composer.lock
index d38d153743..c7026c892b 100644
--- a/tools/phpunit/composer.lock
+++ b/tools/phpunit/composer.lock
@@ -592,16 +592,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.50",
+ "version": "11.5.51",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3"
+ "reference": "ad14159f92910b0f0e3928c13e9b2077529de091"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3",
- "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091",
+ "reference": "ad14159f92910b0f0e3928c13e9b2077529de091",
"shasum": ""
},
"require": {
@@ -616,7 +616,7 @@
"phar-io/version": "^3.2.1",
"php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.12",
- "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-file-iterator": "^5.1.1",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
@@ -628,6 +628,7 @@
"sebastian/exporter": "^6.3.2",
"sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1",
+ "sebastian/recursion-context": "^6.0.3",
"sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2",
"staabm/side-effects-detector": "^1.0.5"
@@ -673,7 +674,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51"
},
"funding": [
{
@@ -697,7 +698,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-27T05:59:18+00:00"
+ "time": "2026-02-05T07:59:30+00:00"
},
{
"name": "sebastian/cli-parser",
diff --git a/tools/rector/composer.lock b/tools/rector/composer.lock
index e5b40e09b9..75930daf24 100644
--- a/tools/rector/composer.lock
+++ b/tools/rector/composer.lock
@@ -62,21 +62,21 @@
},
{
"name": "rector/rector",
- "version": "2.3.5",
+ "version": "2.3.6",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070"
+ "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070",
- "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b",
+ "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0",
- "phpstan/phpstan": "^2.1.36"
+ "phpstan/phpstan": "^2.1.38"
},
"conflict": {
"rector/rector-doctrine": "*",
@@ -110,7 +110,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.3.5"
+ "source": "https://github.com/rectorphp/rector/tree/2.3.6"
},
"funding": [
{
@@ -118,7 +118,7 @@
"type": "github"
}
],
- "time": "2026-01-28T15:22:48+00:00"
+ "time": "2026-02-06T14:25:06+00:00"
}
],
"aliases": [],
diff --git a/web/landing/assets/codemirror/completions/dsl.js b/web/landing/assets/codemirror/completions/dsl.js
index bec45bfc2e..43d4807ddb 100644
--- a/web/landing/assets/codemirror/completions/dsl.js
+++ b/web/landing/assets/codemirror/completions/dsl.js
@@ -1,7 +1,7 @@
/**
* CodeMirror Completer for Flow PHP DSL Functions
*
- * Total functions: 674
+ * Total functions: 684
*
* This completer provides autocompletion for all Flow PHP DSL functions:
* - Extractors (flow-extractors)
@@ -5387,6 +5387,24 @@ const dslFunctions = [
},
apply: snippet("\\Flow\\Telemetry\\DSL\\logger_provider(" + "$" + "{" + "1:processor" + "}" + ", " + "$" + "{" + "2:clock" + "}" + ", " + "$" + "{" + "3:contextStorage" + "}" + ")"),
boost: 10
+ }, {
+ label: "log_record_converter",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ log_record_converter(SeverityMapper $severityMapper = null, ValueNormalizer $valueNormalizer = null) : LogRecordConverter
+
+
+ Create a LogRecordConverter for converting Monolog LogRecord to Telemetry LogRecord.
The converter handles:
- Severity mapping from Monolog Level to Telemetry Severity
- Message body conversion
- Channel and level name as monolog.* attributes
- Context values as context.* attributes (Throwables use setException())
- Extra values as extra.* attributes
@param null|SeverityMapper $severityMapper Custom severity mapper (defaults to standard mapping)
@param null|ValueNormalizer $valueNormalizer Custom value normalizer (defaults to standard normalizer)
Example usage:
\`\`\`php
$converter = log_record_converter();
$telemetryRecord = $converter->convert($monologRecord);
\`\`\`
Example with custom mapper:
\`\`\`php
$converter = log_record_converter(
severityMapper: severity_mapper([
Level::Debug->value => Severity::TRACE,
])
);
\`\`\`
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\log_record_converter(" + "$" + "{" + "1:severityMapper" + "}" + ", " + "$" + "{" + "2:valueNormalizer" + "}" + ")"),
+ boost: 10
}, {
label: "lower",
type: "function",
@@ -7406,6 +7424,36 @@ const dslFunctions = [
},
apply: snippet("\\Flow\\Filesystem\\DSL\\protocol(" + "$" + "{" + "1:protocol" + "}" + ")"),
boost: 10
+ }, {
+ label: "psr7_request_carrier",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ psr7_request_carrier(ServerRequestInterface $request) : RequestCarrier
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Psr7\\Telemetry\\DSL\\psr7_request_carrier(" + "$" + "{" + "1:request" + "}" + ")"),
+ boost: 10
+ }, {
+ label: "psr7_response_carrier",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ psr7_response_carrier(ResponseInterface $response) : ResponseCarrier
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Psr7\\Telemetry\\DSL\\psr7_response_carrier(" + "$" + "{" + "1:response" + "}" + ")"),
+ boost: 10
}, {
label: "random_string",
type: "function",
@@ -7888,10 +7936,10 @@ const dslFunctions = [
const div = document.createElement("div")
div.innerHTML = `
- resource(array $attributes = []) : Resource
+ resource(Attributes|array $attributes = []) : Resource
- Create a Resource.
@param array|bool|float|int|string> $attributes Resource attributes
+ Create a Resource.
@param array|bool|float|int|string>|Attributes $attributes Resource attributes
`
return div
@@ -8519,6 +8567,42 @@ const dslFunctions = [
},
apply: snippet("\\Flow\\PostgreSql\\DSL\\set_transaction()"),
boost: 10
+ }, {
+ label: "severity_filtering_log_processor",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ severity_filtering_log_processor(LogProcessor $processor, Severity $minimumSeverity = Flow\\Telemetry\\Logger\\Severity::...) : SeverityFilteringLogProcessor
+
+
+ Create a SeverityFilteringLogProcessor.
Filters log entries based on minimum severity level. Only entries at or above
the configured threshold are passed to the wrapped processor.
@param LogProcessor $processor The processor to wrap
@param Severity $minimumSeverity Minimum severity level (default: INFO)
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Telemetry\\DSL\\severity_filtering_log_processor(" + "$" + "{" + "1:processor" + "}" + ", " + "$" + "{" + "2:minimumSeverity" + "}" + ")"),
+ boost: 10
+ }, {
+ label: "severity_mapper",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ severity_mapper(array $customMapping = null) : SeverityMapper
+
+
+ Create a SeverityMapper for mapping Monolog levels to Telemetry severities.
@param null|array $customMapping Optional custom mapping (Monolog Level value => Telemetry Severity)
Example with default mapping:
\`\`\`php
$mapper = severity_mapper();
\`\`\`
Example with custom mapping:
\`\`\`php
use Monolog\\Level;
use Flow\\Telemetry\\Logger\\Severity;
$mapper = severity_mapper([
Level::Debug->value => Severity::DEBUG,
Level::Info->value => Severity::INFO,
Level::Notice->value => Severity::WARN, // Custom: NOTICE → WARN instead of INFO
Level::Warning->value => Severity::WARN,
Level::Error->value => Severity::ERROR,
Level::Critical->value => Severity::FATAL,
Level::Alert->value => Severity::FATAL,
Level::Emergency->value => Severity::FATAL,
]);
\`\`\`
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\severity_mapper(" + "$" + "{" + "1:customMapping" + "}" + ")"),
+ boost: 10
}, {
label: "similar_to",
type: "function",
@@ -8593,15 +8677,15 @@ const dslFunctions = [
const div = document.createElement("div")
div.innerHTML = `
- span_event(string $name, array $attributes = []) : GenericEvent
+ span_event(string $name, DateTimeImmutable $timestamp, Attributes|array $attributes = []) : GenericEvent
- Create a SpanEvent (GenericEvent) with the current timestamp.
@param string $name Event name
@param array|bool|float|int|string> $attributes Event attributes
+ Create a SpanEvent (GenericEvent) with an explicit timestamp.
@param string $name Event name
@param \\DateTimeImmutable $timestamp Event timestamp
@param array|bool|float|int|string>|Attributes $attributes Event attributes
`
return div
},
- apply: snippet("\\Flow\\Telemetry\\DSL\\span_event(" + "$" + "{" + "1:name" + "}" + ", " + "$" + "{" + "2:attributes" + "}" + ")"),
+ apply: snippet("\\Flow\\Telemetry\\DSL\\span_event(" + "$" + "{" + "1:name" + "}" + ", " + "$" + "{" + "2:timestamp" + "}" + ", " + "$" + "{" + "3:attributes" + "}" + ")"),
boost: 10
}, {
label: "span_id",
@@ -8629,10 +8713,10 @@ const dslFunctions = [
const div = document.createElement("div")
div.innerHTML = `
- span_link(SpanContext $context, array $attributes = []) : SpanLink
+ span_link(SpanContext $context, Attributes|array $attributes = []) : SpanLink
- Create a SpanLink.
@param SpanContext $context The linked span context
@param array|bool|float|int|string> $attributes Link attributes
+ Create a SpanLink.
@param SpanContext $context The linked span context
@param array|bool|float|int|string>|Attributes $attributes Link attributes
`
return div
@@ -9389,6 +9473,36 @@ const dslFunctions = [
},
apply: snippet("\\Flow\\Telemetry\\DSL\\superglobal_carrier()"),
boost: 10
+ }, {
+ label: "symfony_request_carrier",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ symfony_request_carrier(Request $request) : RequestCarrier
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Symfony\\HttpFoundationTelemetry\\DSL\\symfony_request_carrier(" + "$" + "{" + "1:request" + "}" + ")"),
+ boost: 10
+ }, {
+ label: "symfony_response_carrier",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ symfony_response_carrier(Response $response) : ResponseCarrier
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Symfony\\HttpFoundationTelemetry\\DSL\\symfony_response_carrier(" + "$" + "{" + "1:response" + "}" + ")"),
+ boost: 10
}, {
label: "table",
type: "function",
@@ -9461,6 +9575,39 @@ const dslFunctions = [
},
apply: snippet("\\Flow\\Telemetry\\DSL\\telemetry(" + "$" + "{" + "1:resource" + "}" + ", " + "$" + "{" + "2:tracerProvider" + "}" + ", " + "$" + "{" + "3:meterProvider" + "}" + ", " + "$" + "{" + "4:loggerProvider" + "}" + ")"),
boost: 10
+ }, {
+ label: "telemetry_handler",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ telemetry_handler(Logger $logger, LogRecordConverter $converter = Flow\\Bridge\\Monolog\\Telemetry\\LogRecordConverter::..., Level $level = Monolog\\Level::..., bool $bubble = true) : TelemetryHandler
+
+
+ Create a TelemetryHandler for forwarding Monolog logs to Flow Telemetry.
@param Logger $logger The Flow Telemetry logger to forward logs to
@param LogRecordConverter $converter Converter to transform Monolog LogRecord to Telemetry LogRecord
@param Level $level The minimum logging level at which this handler will be triggered
@param bool $bubble Whether messages handled by this handler should bubble up to other handlers
Example usage:
\`\`\`php
use Monolog\\Logger as MonologLogger;
use function Flow\\Bridge\\Monolog\\Telemetry\\DSL\\telemetry_handler;
use function Flow\\Telemetry\\DSL\\telemetry;
$telemetry = telemetry();
$logger = $telemetry->logger(\'my-app\');
$monolog = new MonologLogger(\'channel\');
$monolog->pushHandler(telemetry_handler($logger));
$monolog->info(\'User logged in\', [\'user_id\' => 123]);
// → Forwarded to Flow Telemetry with INFO severity
\`\`\`
Example with custom converter:
\`\`\`php
$converter = log_record_converter(
severityMapper: severity_mapper([
Level::Debug->value => Severity::TRACE,
])
);
$monolog->pushHandler(telemetry_handler($logger, $converter));
\`\`\`
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\telemetry_handler(" + "$" + "{" + "1:logger" + "}" + ", " + "$" + "{" + "2:converter" + "}" + ", " + "$" + "{" + "3:level" + "}" + ", " + "$" + "{" + "4:bubble" + "}" + ")"),
+ boost: 10
+ }, {
+ label: "telemetry_options",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ telemetry_options(bool $trace_loading = false, bool $trace_transformations = false, bool $collect_metrics = false) : TelemetryOptions
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\ETL\\DSL\\telemetry_options(" + "$" + "{" + "1:trace_loading" + "}" + ", " + "$" + "{" + "2:trace_transformations" + "}" + ", " + "$" + "{" + "3:collect_metrics" + "}" + ")"),
+ boost: 10
}, {
label: "text_search_match",
type: "function",
@@ -11126,6 +11273,24 @@ const dslFunctions = [
},
apply: snippet("\\Flow\\PostgreSql\\DSL\\values_table(" + "$" + "{" + "1:rows" + "}" + ")"),
boost: 10
+ }, {
+ label: "value_normalizer",
+ type: "function",
+ detail: "flow\u002Ddsl\u002Dhelpers",
+ info: () => {
+ const div = document.createElement("div")
+ div.innerHTML = `
+
+ value_normalizer() : ValueNormalizer
+
+
+ Create a ValueNormalizer for converting arbitrary PHP values to Telemetry attribute types.
The normalizer handles:
- null → \'null\' string
- scalars (string, int, float, bool) → unchanged
- DateTimeInterface → unchanged
- Throwable → unchanged
- arrays → recursively normalized
- objects with __toString() → string cast
- objects without __toString() → class name
- other types → get_debug_type() result
Example usage:
\`\`\`php
$normalizer = value_normalizer();
$normalized = $normalizer->normalize($value);
\`\`\`
+
+ `
+ return div
+ },
+ apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\value_normalizer()"),
+ boost: 10
}, {
label: "void_log_exporter",
type: "function",
diff --git a/web/landing/composer.json b/web/landing/composer.json
index afc733aac0..3d58069692 100644
--- a/web/landing/composer.json
+++ b/web/landing/composer.json
@@ -2,10 +2,67 @@
"name": "flow-php/web",
"description": "Flow PHP ETL - Web",
"type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../../src/core/etl",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/adapter/etl-adapter-http",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/lib/telemetry",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/lib/types",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/lib/array-dot",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/lib/filesystem",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/bridge/symfony/telemetry-bundle",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/bridge/symfony/http-foundation-telemetry",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/bridge/telemetry/otlp",
+ "options": { "symlink": true }
+ },
+ {
+ "type": "path",
+ "url": "../../src/bridge/monolog/telemetry",
+ "options": { "symlink": true }
+ }
+ ],
"require": {
"php": "8.3.*",
- "flow-php/etl": ">=0.23.0",
- "flow-php/etl-adapter-http": ">=0.23.0",
+ "flow-php/etl": "1.x-dev",
+ "flow-php/etl-adapter-http": "1.x-dev",
+ "flow-php/symfony-telemetry-bundle": "1.x-dev",
+ "flow-php/telemetry-otlp-bridge": "*",
+ "flow-php/monolog-telemetry-bridge": "*",
"nyholm/psr7": "^1.8",
"php-http/curl-client": "^2.3",
"psr/http-client": "^1.0",
diff --git a/web/landing/composer.lock b/web/landing/composer.lock
index ef3184284a..a255dadc46 100644
--- a/web/landing/composer.lock
+++ b/web/landing/composer.lock
@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "eba577c63444051a6ecdac007f116e5f",
+ "content-hash": "c4f6c4d36749461dfdcbe46e566327d3",
"packages": [
{
"name": "brick/math",
- "version": "0.14.4",
+ "version": "0.14.6",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "a8b53e6cc4d3a336543f042a4dfa0e3f2f2356a4"
+ "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/a8b53e6cc4d3a336543f042a4dfa0e3f2f2356a4",
- "reference": "a8b53e6cc4d3a336543f042a4dfa0e3f2f2356a4",
+ "url": "https://api.github.com/repos/brick/math/zipball/32498d5e1897e7642c0b961ace2df6d7dc9a3bc3",
+ "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3",
"shasum": ""
},
"require": {
@@ -56,7 +56,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
- "source": "https://github.com/brick/math/tree/0.14.4"
+ "source": "https://github.com/brick/math/tree/0.14.6"
},
"funding": [
{
@@ -64,7 +64,7 @@
"type": "github"
}
],
- "time": "2026-02-02T16:57:31+00:00"
+ "time": "2026-02-05T07:59:58+00:00"
},
{
"name": "clue/stream-filter",
@@ -353,33 +353,31 @@
},
{
"name": "flow-php/array-dot",
- "version": "0.31.0",
- "source": {
- "type": "git",
- "url": "https://github.com/flow-php/array-dot.git",
- "reference": "7c0b7f16b12b6e5239ecf487908bfe78673d1f22"
- },
+ "version": "1.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/flow-php/array-dot/zipball/7c0b7f16b12b6e5239ecf487908bfe78673d1f22",
- "reference": "7c0b7f16b12b6e5239ecf487908bfe78673d1f22",
- "shasum": ""
+ "type": "path",
+ "url": "../../src/lib/array-dot",
+ "reference": "043d107b6cc0e919d6100a84eb513d4b6b81bf55"
},
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0"
},
"type": "library",
"autoload": {
- "files": [
- "src/Flow/ArrayDot/array_dot.php"
- ],
"psr-4": {
"Flow\\": [
"src/Flow"
]
+ },
+ "files": [
+ "src/Flow/ArrayDot/array_dot.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
},
- "notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@@ -393,38 +391,22 @@
"load",
"transform"
],
- "support": {
- "issues": "https://github.com/flow-php/array-dot/issues",
- "source": "https://github.com/flow-php/array-dot/tree/0.31.0"
- },
- "funding": [
- {
- "url": "https://flow-php.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/norberttech",
- "type": "github"
- }
- ],
- "time": "2026-01-19T09:55:20+00:00"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
"name": "flow-php/etl",
- "version": "0.31.0",
- "source": {
- "type": "git",
- "url": "https://github.com/flow-php/etl.git",
- "reference": "34a6e48efe93801f2ac29fefcb2ef2f474f928c9"
- },
+ "version": "1.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/flow-php/etl/zipball/34a6e48efe93801f2ac29fefcb2ef2f474f928c9",
- "reference": "34a6e48efe93801f2ac29fefcb2ef2f474f928c9",
- "shasum": ""
+ "type": "path",
+ "url": "../../src/core/etl",
+ "reference": "101fcb6bc740b481daec219ceacf87d0a66af0a4"
},
"require": {
"brick/math": "^0.11 || ^0.12 || ^0.13 || ^0.14",
+ "composer-runtime-api": "^2.0",
"ext-json": "*",
"flow-php/array-dot": "self.version",
"flow-php/filesystem": "self.version",
@@ -454,7 +436,11 @@
]
}
},
- "notification-url": "https://packagist.org/downloads/",
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
+ }
+ },
"license": [
"MIT"
],
@@ -465,35 +451,18 @@
"load",
"transform"
],
- "support": {
- "issues": "https://github.com/flow-php/etl/issues",
- "source": "https://github.com/flow-php/etl/tree/0.31.0"
- },
- "funding": [
- {
- "url": "https://flow-php.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/norberttech",
- "type": "github"
- }
- ],
- "time": "2026-01-19T12:58:02+00:00"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
"name": "flow-php/etl-adapter-http",
- "version": "0.31.0",
- "source": {
- "type": "git",
- "url": "https://github.com/flow-php/etl-adapter-http.git",
- "reference": "2353aae484d9f4eb07efae8f81c3acbfa006bdea"
- },
+ "version": "1.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/flow-php/etl-adapter-http/zipball/2353aae484d9f4eb07efae8f81c3acbfa006bdea",
- "reference": "2353aae484d9f4eb07efae8f81c3acbfa006bdea",
- "shasum": ""
+ "type": "path",
+ "url": "../../src/adapter/etl-adapter-http",
+ "reference": "a653aa0bdc76bccd4fcf54f98ddea12af4b5ecf4"
},
"require": {
"ext-json": "*",
@@ -507,16 +476,20 @@
},
"type": "library",
"autoload": {
- "files": [
- "src/Flow/ETL/Adapter/Http/DSL/functions.php"
- ],
"psr-4": {
"Flow\\": [
"src/Flow"
]
+ },
+ "files": [
+ "src/Flow/ETL/Adapter/Http/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
},
- "notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@@ -528,35 +501,18 @@
"load",
"transform"
],
- "support": {
- "issues": "https://github.com/flow-php/etl-adapter-http/issues",
- "source": "https://github.com/flow-php/etl-adapter-http/tree/0.31.0"
- },
- "funding": [
- {
- "url": "https://flow-php.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/norberttech",
- "type": "github"
- }
- ],
- "time": "2025-12-13T19:30:32+00:00"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
"name": "flow-php/filesystem",
- "version": "0.31.0",
- "source": {
- "type": "git",
- "url": "https://github.com/flow-php/filesystem.git",
- "reference": "9a92c442feb3ee3e5e266d7c64d331c7edb4654b"
- },
+ "version": "1.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/flow-php/filesystem/zipball/9a92c442feb3ee3e5e266d7c64d331c7edb4654b",
- "reference": "9a92c442feb3ee3e5e266d7c64d331c7edb4654b",
- "shasum": ""
+ "type": "path",
+ "url": "../../src/lib/filesystem",
+ "reference": "41a8aae737a5591e07aefde6b83097ec04e8e7d4"
},
"require": {
"flow-php/types": "self.version",
@@ -565,16 +521,20 @@
},
"type": "library",
"autoload": {
- "files": [
- "src/Flow/Filesystem/DSL/functions.php"
- ],
"psr-4": {
"Flow\\": [
"src/Flow"
]
+ },
+ "files": [
+ "src/Flow/Filesystem/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
},
- "notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@@ -590,35 +550,174 @@
"remote",
"transform"
],
- "support": {
- "issues": "https://github.com/flow-php/filesystem/issues",
- "source": "https://github.com/flow-php/filesystem/tree/0.31.0"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "flow-php/monolog-telemetry-bridge",
+ "version": "1.x-dev",
+ "dist": {
+ "type": "path",
+ "url": "../../src/bridge/monolog/telemetry",
+ "reference": "86126b365e7c90c52d5447928ef1765c622f504e"
},
- "funding": [
- {
- "url": "https://flow-php.com/sponsor",
- "type": "custom"
+ "require": {
+ "flow-php/etl": "self.version",
+ "flow-php/telemetry": "self.version",
+ "monolog/monolog": "^3.0",
+ "php": "~8.3.0 || ~8.4.0 || ~8.5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Flow\\": [
+ "src/Flow"
+ ]
},
- {
- "url": "https://github.com/norberttech",
- "type": "github"
+ "files": [
+ "src/Flow/Bridge/Monolog/Telemetry/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
+ },
+ "license": [
+ "MIT"
],
- "time": "2025-12-12T10:55:46+00:00"
+ "description": "Flow PHP - Monolog Telemetry Bridge",
+ "homepage": "https://github.com/flow-php/flow",
+ "keywords": [
+ "bridge",
+ "flow-php",
+ "monolog",
+ "telemetry"
+ ],
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
- "name": "flow-php/telemetry",
- "version": "0.31.0",
- "source": {
- "type": "git",
- "url": "https://github.com/flow-php/telemetry.git",
- "reference": "32ff43a631896ba6295e674e300541eb65edc9ce"
+ "name": "flow-php/symfony-http-foundation-telemetry-bridge",
+ "version": "1.x-dev",
+ "dist": {
+ "type": "path",
+ "url": "../../src/bridge/symfony/http-foundation-telemetry",
+ "reference": "1f5726349ffadda8d4c1d917cb3d21f5e1de4fd3"
+ },
+ "require": {
+ "flow-php/telemetry": "self.version",
+ "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
+ "symfony/http-foundation": "^6.4 || ^7.3 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Flow\\": [
+ "src/Flow"
+ ]
+ },
+ "files": [
+ "src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
+ }
},
+ "license": [
+ "MIT"
+ ],
+ "description": "Flow PHP - Symfony Http Foundation Telemetry Bridge",
+ "homepage": "https://github.com/flow-php/flow",
+ "keywords": [
+ "bridge",
+ "flow-php",
+ "http-foundation",
+ "symfony",
+ "telemetry"
+ ],
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "flow-php/symfony-telemetry-bundle",
+ "version": "1.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/flow-php/telemetry/zipball/32ff43a631896ba6295e674e300541eb65edc9ce",
- "reference": "32ff43a631896ba6295e674e300541eb65edc9ce",
- "shasum": ""
+ "type": "path",
+ "url": "../../src/bridge/symfony/telemetry-bundle",
+ "reference": "d592dbe7011b8a3521cf0b1ccb6236ca9c4e553e"
+ },
+ "require": {
+ "flow-php/symfony-http-foundation-telemetry-bridge": "self.version",
+ "flow-php/telemetry": "self.version",
+ "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/clock": "^1.0",
+ "symfony/config": "^6.4 || ^7.3 || ^8.0",
+ "symfony/console": "^6.4 || ^7.3 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.3 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "flow-php/telemetry-otlp-bridge": "self.version",
+ "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0",
+ "symfony/messenger": "^6.4 || ^7.3 || ^8.0",
+ "symfony/routing": "^6.4 || ^7.3 || ^8.0"
+ },
+ "suggest": {
+ "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support",
+ "symfony/messenger": "Required for Messenger tracing middleware"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Flow\\": [
+ "src/Flow"
+ ]
+ },
+ "files": [
+ "src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
+ }
+ },
+ "license": [
+ "MIT"
+ ],
+ "description": "Flow PHP - Symfony Telemetry Bundle",
+ "homepage": "https://github.com/flow-php/flow",
+ "keywords": [
+ "bundle",
+ "flow-php",
+ "logging",
+ "metrics",
+ "opentelemetry",
+ "symfony",
+ "telemetry",
+ "tracing"
+ ],
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "flow-php/telemetry",
+ "version": "1.x-dev",
+ "dist": {
+ "type": "path",
+ "url": "../../src/lib/telemetry",
+ "reference": "17ec7b5760d93ff7437551d74c73fc06607b5618"
},
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
@@ -626,71 +725,124 @@
},
"type": "library",
"autoload": {
- "files": [
- "src/Flow/Telemetry/DSL/functions.php"
- ],
"psr-4": {
"Flow\\": [
"src/Flow"
]
+ },
+ "files": [
+ "src/Flow/Telemetry/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
},
- "notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Flow PHP - Telemetry library for metrics and tracing",
"keywords": [
- "Metrics",
+ "metrics",
"php",
"telemetry",
"tracing"
],
- "support": {
- "issues": "https://github.com/flow-php/telemetry/issues",
- "source": "https://github.com/flow-php/telemetry/tree/0.31.0"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "flow-php/telemetry-otlp-bridge",
+ "version": "1.x-dev",
+ "dist": {
+ "type": "path",
+ "url": "../../src/bridge/telemetry/otlp",
+ "reference": "27c9acf702f6b246c32a9a838e3d36b1f549e8bb"
},
- "funding": [
- {
- "url": "https://flow-php.com/sponsor",
- "type": "custom"
+ "require": {
+ "flow-php/telemetry": "self.version",
+ "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "require-dev": {
+ "google/protobuf": "^4.0",
+ "grpc/grpc": "^1.74",
+ "nyholm/psr7": "^1.8",
+ "open-telemetry/gen-otlp-protobuf": "^1.8",
+ "symfony/http-client": "^6.4 || ^7.3 || ^8.0"
+ },
+ "suggest": {
+ "ext-grpc": "Required for gRPC transport",
+ "google/protobuf": "Required for gRPC transport with binary protobuf encoding",
+ "open-telemetry/gen-otlp-protobuf": "Generated PHP classes for OTLP protobuf messages (required for gRPC transport)"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Flow\\": [
+ "src/Flow"
+ ]
},
- {
- "url": "https://github.com/norberttech",
- "type": "github"
+ "files": [
+ "src/Flow/Bridge/Telemetry/OTLP/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
+ },
+ "license": [
+ "MIT"
],
- "time": "2026-01-16T22:44:33+00:00"
+ "description": "Flow PHP Telemetry - OTLP Exporter Bridge",
+ "homepage": "https://github.com/flow-php/flow",
+ "keywords": [
+ "flow-php",
+ "logging",
+ "metrics",
+ "observability",
+ "opentelemetry",
+ "otlp",
+ "telemetry",
+ "tracing"
+ ],
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
"name": "flow-php/types",
- "version": "0.31.0",
- "source": {
- "type": "git",
- "url": "https://github.com/flow-php/types.git",
- "reference": "4af1b09a0a379ce33e251fe56c8a90fe39c67522"
- },
+ "version": "1.x-dev",
"dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/flow-php/types/zipball/4af1b09a0a379ce33e251fe56c8a90fe39c67522",
- "reference": "4af1b09a0a379ce33e251fe56c8a90fe39c67522",
- "shasum": ""
+ "type": "path",
+ "url": "../../src/lib/types",
+ "reference": "08d95eb6cdc6fa8e1af02a3457a34ccbcac8dbb3"
},
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0"
},
"type": "library",
"autoload": {
- "files": [
- "src/Flow/Types/DSL/functions.php"
- ],
"psr-4": {
"Flow\\": [
"src/Flow"
]
+ },
+ "files": [
+ "src/Flow/Types/DSL/functions.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Flow\\": "tests/Flow"
}
},
- "notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@@ -699,21 +851,10 @@
"php",
"types"
],
- "support": {
- "issues": "https://github.com/flow-php/types/issues",
- "source": "https://github.com/flow-php/types/tree/0.31.0"
- },
- "funding": [
- {
- "url": "https://flow-php.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/norberttech",
- "type": "github"
- }
- ],
- "time": "2025-12-20T17:43:26+00:00"
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
},
{
"name": "league/commonmark",
@@ -1074,16 +1215,16 @@
},
{
"name": "nette/utils",
- "version": "v4.1.1",
+ "version": "v4.1.2",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
+ "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
- "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
+ "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5",
+ "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5",
"shasum": ""
},
"require": {
@@ -1096,7 +1237,7 @@
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
- "phpstan/phpstan-nette": "^2.0@stable",
+ "phpstan/phpstan": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -1157,9 +1298,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.1.1"
+ "source": "https://github.com/nette/utils/tree/v4.1.2"
},
- "time": "2025-12-22T12:14:32+00:00"
+ "time": "2026-02-03T17:21:09+00:00"
},
{
"name": "nyholm/psr7",
@@ -7045,11 +7186,14 @@
}
],
"aliases": [],
- "minimum-stability": "stable",
+ "minimum-stability": "dev",
"stability-flags": {
+ "flow-php/etl": 20,
+ "flow-php/etl-adapter-http": 20,
+ "flow-php/symfony-telemetry-bundle": 20,
"norberttech/static-content-generator-bundle": 20
},
- "prefer-stable": false,
+ "prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "8.3.*"
diff --git a/web/landing/config/bundles.php b/web/landing/config/bundles.php
index 860aa432fb..b81b94cdf2 100644
--- a/web/landing/config/bundles.php
+++ b/web/landing/config/bundles.php
@@ -10,4 +10,5 @@
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
\Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
\Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true],
+ Flow\Bridge\Symfony\TelemetryBundle\FlowTelemetryBundle::class => ['all' => true],
];
diff --git a/web/landing/config/packages/flow_telemetry.yaml b/web/landing/config/packages/flow_telemetry.yaml
new file mode 100644
index 0000000000..e079fd5de9
--- /dev/null
+++ b/web/landing/config/packages/flow_telemetry.yaml
@@ -0,0 +1,68 @@
+flow_telemetry:
+ service:
+ name: "flow-website"
+ version: "1.0.0"
+ attributes:
+ deployment.environment: "%kernel.environment%"
+ telemetry:
+ http_kernel:
+ enabled: true
+ exclude_routes:
+ - '_wdt'
+ - '_profiler'
+ - '/_profiler.*/'
+ console:
+ enabled: true
+ exclude_commands:
+ - 'cache:clear'
+ - 'debug:router'
+ twig:
+ enabled: true
+ trace_templates: true
+ trace_blocks: true
+ trace_macros: true
+ exclude_templates:
+ - '@WebProfiler/Profiler/toolbar.html.twig'
+
+ tracer_provider:
+ sampler:
+ type: always_on
+ processor:
+ type: batching
+ batch_size: 512
+ exporter:
+ type: otlp
+ otlp:
+ transport:
+ type: curl
+ endpoint: "http://localhost:4318"
+ timeout: 30
+ serializer:
+ type: json
+
+ meter_provider:
+ temporality: cumulative
+ processor:
+ type: batching
+ exporter:
+ type: otlp
+ otlp:
+ transport:
+ type: curl
+ endpoint: "http://localhost:4318"
+ timeout: 30
+ serializer:
+ type: json
+
+ logger_provider:
+ processor:
+ type: batching
+ exporter:
+ type: otlp
+ otlp:
+ transport:
+ type: curl
+ endpoint: "http://localhost:4318"
+ timeout: 30
+ serializer:
+ type: json
diff --git a/web/landing/config/packages/monolog.yaml b/web/landing/config/packages/monolog.yaml
index efc4ce8ba6..9c340dcd02 100644
--- a/web/landing/config/packages/monolog.yaml
+++ b/web/landing/config/packages/monolog.yaml
@@ -1,6 +1,10 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
+ handlers:
+ telemetry:
+ type: service
+ id: Flow\Bridge\Monolog\Telemetry\TelemetryHandler
when@dev:
monolog:
diff --git a/web/landing/config/packages/prod/flow_telemetry.yaml b/web/landing/config/packages/prod/flow_telemetry.yaml
new file mode 100644
index 0000000000..efbe8ef4fc
--- /dev/null
+++ b/web/landing/config/packages/prod/flow_telemetry.yaml
@@ -0,0 +1,21 @@
+flow_telemetry:
+ logger_provider:
+ processor:
+ type: severity_filtering
+ minimum_severity: warn
+ inner_processor:
+ type: batching
+ exporter:
+ type: otlp
+ otlp:
+ transport:
+ type: curl
+ endpoint: "http://localhost:4318"
+ timeout: 30
+ serializer:
+ type: json
+
+ telemetry:
+ twig:
+ trace_blocks: false
+ trace_macros: false
diff --git a/web/landing/config/packages/test/flow_telemetry.yaml b/web/landing/config/packages/test/flow_telemetry.yaml
new file mode 100644
index 0000000000..24fcef89e2
--- /dev/null
+++ b/web/landing/config/packages/test/flow_telemetry.yaml
@@ -0,0 +1,20 @@
+flow_telemetry:
+ tracer_provider:
+ processor:
+ type: void
+
+ meter_provider:
+ processor:
+ type: void
+
+ logger_provider:
+ processor:
+ type: void
+
+ telemetry:
+ http_kernel:
+ enabled: false
+ console:
+ enabled: false
+ twig:
+ enabled: false
diff --git a/web/landing/config/services.yaml b/web/landing/config/services.yaml
index b44b952083..352ea06abc 100644
--- a/web/landing/config/services.yaml
+++ b/web/landing/config/services.yaml
@@ -71,4 +71,14 @@ services:
$projectDir: '%kernel.project_dir%'
twig.markdown.league_common_mark_converter_factory:
- class: Flow\Website\Service\Markdown\LeagueCommonMarkConverterFactory
\ No newline at end of file
+ class: Flow\Website\Service\Markdown\LeagueCommonMarkConverterFactory
+
+ flow.telemetry.monolog.logger:
+ class: Flow\Telemetry\Logger\Logger
+ factory: ['@Flow\Telemetry\Telemetry', 'logger']
+ arguments:
+ - 'monolog'
+
+ Flow\Bridge\Monolog\Telemetry\TelemetryHandler:
+ arguments:
+ - '@flow.telemetry.monolog.logger'
\ No newline at end of file
diff --git a/web/landing/src/Flow/Website/Model/Documentation/Module.php b/web/landing/src/Flow/Website/Model/Documentation/Module.php
index 6e47845c1f..82681253c4 100644
--- a/web/landing/src/Flow/Website/Model/Documentation/Module.php
+++ b/web/landing/src/Flow/Website/Model/Documentation/Module.php
@@ -27,6 +27,7 @@ enum Module : string
case PSR7_TELEMETRY_BRIDGE = 'PSR-7 Telemetry Bridge';
case S3_FILESYSTEM = 'S3 Filesystem';
case SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE = 'Symfony HttpFoundation Telemetry Bridge';
+ case SYMFONY_TELEMETRY_BUNDLE = 'Symfony Telemetry Bundle';
case TELEMETRY = 'Telemetry';
case TELEMETRY_OTLP = 'Telemetry OTLP';
case TEXT = 'Text';
@@ -66,7 +67,8 @@ public function priority() : int
self::TELEMETRY_OTLP => 21,
self::MONOLOG_TELEMETRY_BRIDGE => 22,
self::SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE => 23,
- self::PSR7_TELEMETRY_BRIDGE => 24,
+ self::SYMFONY_TELEMETRY_BUNDLE => 24,
+ self::PSR7_TELEMETRY_BRIDGE => 25,
default => 99,
};
}
diff --git a/web/landing/src/Flow/Website/Twig/DSLExtension.php b/web/landing/src/Flow/Website/Twig/DSLExtension.php
index 53d0583e73..0211e65d86 100644
--- a/web/landing/src/Flow/Website/Twig/DSLExtension.php
+++ b/web/landing/src/Flow/Website/Twig/DSLExtension.php
@@ -18,6 +18,7 @@ public function dsl() : string
return file_get_contents($this->dslPath);
}
+ #[\Override]
public function getFunctions()
{
return [
diff --git a/web/landing/src/Flow/Website/Twig/FlowExtension.php b/web/landing/src/Flow/Website/Twig/FlowExtension.php
index 6bef99c2fe..582203e087 100644
--- a/web/landing/src/Flow/Website/Twig/FlowExtension.php
+++ b/web/landing/src/Flow/Website/Twig/FlowExtension.php
@@ -9,6 +9,7 @@
final class FlowExtension extends AbstractExtension
{
+ #[\Override]
public function getFilters() : array
{
return [
diff --git a/web/landing/src/Flow/Website/Twig/HumanizerExtension.php b/web/landing/src/Flow/Website/Twig/HumanizerExtension.php
index baf4c86e48..dfeb80a8a4 100644
--- a/web/landing/src/Flow/Website/Twig/HumanizerExtension.php
+++ b/web/landing/src/Flow/Website/Twig/HumanizerExtension.php
@@ -10,6 +10,7 @@
final class HumanizerExtension extends AbstractExtension
{
+ #[\Override]
public function getFilters()
{
return [
diff --git a/web/landing/src/Flow/Website/Twig/SlugifyExtension.php b/web/landing/src/Flow/Website/Twig/SlugifyExtension.php
index 2b9f7f3e43..0b21fec4be 100644
--- a/web/landing/src/Flow/Website/Twig/SlugifyExtension.php
+++ b/web/landing/src/Flow/Website/Twig/SlugifyExtension.php
@@ -10,6 +10,7 @@
final class SlugifyExtension extends AbstractExtension
{
+ #[\Override]
public function getFilters() : array
{
return [
diff --git a/web/landing/templates/documentation/dsl.html.twig b/web/landing/templates/documentation/dsl.html.twig
index ef65a63ade..77954c8d16 100644
--- a/web/landing/templates/documentation/dsl.html.twig
+++ b/web/landing/templates/documentation/dsl.html.twig
@@ -90,10 +90,8 @@
{% apply spaceless %}
-
>
-
+
+ {{- definition.toString | escape('html') -}}
{% endapply %}
diff --git a/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php b/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php
index f065a05068..efe41ab9e4 100644
--- a/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php
+++ b/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php
@@ -36,6 +36,7 @@ public function test_documentation_dsl_page() : void
self::assertEquals(12, $client->getCrawler()->filter('[data-dsl-type]')->count());
}
+ #[\Override]
protected static function getKernelClass() : string
{
return Kernel::class;