From 153fbb9db83e1c1dcab81e6a7f65caa58eec1cf8 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Thu, 18 Jun 2026 13:57:51 +0200 Subject: [PATCH 1/2] feat(flow-php/symfony-telemetry-bundle): optional http-foundation telemetry bridge - wire RequestCarrier/ResponseCarrier into the http kernel subscriber for in/out W3C trace propagation - move the bridge from require to require-dev/suggest - disable context propagation gracefully when the bridge is absent --- .../packages/symfony-telemetry-bundle.md | 2 +- documentation/upgrading.md | 22 +++- .../symfony/telemetry-bundle/composer.json | 3 +- .../TelemetryBundle/FlowTelemetryBundle.php | 6 +- .../HttpKernel/HttpKernelSpanSubscriber.php | 31 ++--- .../HttpKernelSpanSubscriberTest.php | 109 ++++++++++++++++++ 6 files changed, 154 insertions(+), 19 deletions(-) diff --git a/documentation/installation/packages/symfony-telemetry-bundle.md b/documentation/installation/packages/symfony-telemetry-bundle.md index 42cc2c227f..a4beb1da60 100644 --- a/documentation/installation/packages/symfony-telemetry-bundle.md +++ b/documentation/installation/packages/symfony-telemetry-bundle.md @@ -20,7 +20,6 @@ composer require flow-php/symfony-telemetry-bundle:~--FLOW_PHP_VERSION-- ## Core Dependencies - [flow-php/telemetry](/documentation/installation/packages/telemetry.md) -- [flow-php/symfony-http-foundation-telemetry-bridge](/documentation/installation/packages/symfony-http-foundation-telemetry-bridge.md) - [symfony/config](https://packagist.org/packages/symfony/config) - [symfony/console](https://packagist.org/packages/symfony/console) - [symfony/dependency-injection](https://packagist.org/packages/symfony/dependency-injection) @@ -29,6 +28,7 @@ composer require flow-php/symfony-telemetry-bundle:~--FLOW_PHP_VERSION-- ## Suggested Dependencies - [flow-php/symfony-postgresql-bundle](/documentation/installation/packages/symfony-postgresql-bundle.md) — for PostgreSQL database management and migrations with telemetry support +- [flow-php/symfony-http-foundation-telemetry-bridge](/documentation/installation/packages/symfony-http-foundation-telemetry-bridge.md) — for HTTP trace context propagation (extract incoming / inject outgoing W3C trace headers) - [flow-php/psr18-telemetry-bridge](/documentation/installation/packages/psr18-telemetry-bridge.md) — for PSR-18 HTTP client tracing - [flow-php/telemetry-otlp-bridge](/documentation/installation/packages/telemetry-otlp-bridge.md) — for OTLP exporter support - [symfony/messenger](https://packagist.org/packages/symfony/messenger) — for Messenger tracing middleware diff --git a/documentation/upgrading.md b/documentation/upgrading.md index 772a44f61a..d293882854 100644 --- a/documentation/upgrading.md +++ b/documentation/upgrading.md @@ -11,7 +11,9 @@ Please follow the instructions for your specific version to ensure a smooth upgr ### 1) Removal of Elasticsearch Adapter -The Elasticsearch adapter has been removed from Flow PHP and replaced by the [SEAL](https://php-cmsig.github.io/search/) adapter (`flow-php/etl-adapter-seal`), a search engine abstraction layer that supports Elasticsearch, OpenSearch, Meilisearch, Solr, Typesense, Algolia, RediSearch and Loupe. +The Elasticsearch adapter has been removed from Flow PHP and replaced by the [SEAL](https://php-cmsig.github.io/search/) +adapter (`flow-php/etl-adapter-seal`), a search engine abstraction layer that supports Elasticsearch, OpenSearch, +Meilisearch, Solr, Typesense, Algolia, RediSearch and Loupe. To migrate, install the SEAL adapter together with the engine adapter for your backend: @@ -19,7 +21,8 @@ To migrate, install the SEAL adapter together with the engine adapter for your b composer require flow-php/etl-adapter-seal cmsig/seal-elasticsearch-adapter ``` -Then build a `CmsIg\Seal\Engine` and pass it to `to_seal_upsert()` instead of the previous `to_es_bulk_index()` (or Meilisearch) DSL functions: +Then build a `CmsIg\Seal\Engine` and pass it to `to_seal_upsert()` instead of the previous `to_es_bulk_index()` (or +Meilisearch) DSL functions: ```php use CmsIg\Seal\Engine; @@ -38,6 +41,21 @@ data_frame() ->run(); ``` +### 2) `flow-php/symfony-telemetry-bundle` - + +`flow-php/symfony-http-foundation-telemetry-bridge` is now an optional dependency + +| Before | After | +|--------------------------------------|-------------------------------------------------------------| +| installed transitively by the bundle | install explicitly to enable HTTP trace context propagation | + +`instrumentation.http_kernel.context_propagation` is silently disabled when the bridge is absent. To keep extracting +incoming and injecting outgoing W3C trace headers: + +``` +composer require flow-php/symfony-http-foundation-telemetry-bridge +``` + --- ## Upgrading from 0.39.x to 0.40.x diff --git a/src/bridge/symfony/telemetry-bundle/composer.json b/src/bridge/symfony/telemetry-bundle/composer.json index 19872ca495..7d3114c02c 100644 --- a/src/bridge/symfony/telemetry-bundle/composer.json +++ b/src/bridge/symfony/telemetry-bundle/composer.json @@ -17,7 +17,6 @@ "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0", "flow-php/psr3-telemetry-bridge": "self.version", - "flow-php/symfony-http-foundation-telemetry-bridge": "self.version", "flow-php/telemetry": "self.version", "psr/clock": "^1.0", "symfony/config": "^6.4 || ^7.4 || ^8.0", @@ -32,6 +31,7 @@ "composer/semver": "^3.4", "doctrine/dbal": "^4.4", "flow-php/psr18-telemetry-bridge": "self.version", + "flow-php/symfony-http-foundation-telemetry-bridge": "self.version", "flow-php/telemetry-otlp-bridge": "self.version", "nyholm/psr7": "^1.8", "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0", @@ -44,6 +44,7 @@ "suggest": { "doctrine/dbal": "Required for Doctrine DBAL tracing (^4.4)", "flow-php/psr18-telemetry-bridge": "Required for PSR-18 HTTP client tracing", + "flow-php/symfony-http-foundation-telemetry-bridge": "Required for HTTP trace context propagation (extract incoming / inject outgoing W3C trace headers)", "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support", "symfony/messenger": "Required for Messenger tracing middleware", "symfony/web-profiler-bundle": "Required for the dev-only Flow Telemetry Web Profiler panel", diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php index 71dbf8d025..61aa680a0c 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php @@ -131,6 +131,8 @@ final class FlowTelemetryBundle extends AbstractBundle private const string HTTP_CLIENT_INTERFACE = 'Symfony\\Contracts\\HttpClient\\HttpClientInterface'; + private const string HTTP_FOUNDATION_REQUEST_CARRIER = 'Flow\\Bridge\\Symfony\\HttpFoundationTelemetry\\RequestCarrier'; + private const string MESSENGER_MIDDLEWARE_INTERFACE = 'Symfony\\Component\\Messenger\\Middleware\\MiddlewareInterface'; private const string PSR18_CLIENT_INTERFACE = 'Psr\\Http\\Client\\ClientInterface'; @@ -471,7 +473,7 @@ public function configure(DefinitionConfigurator $definition): void ->end() ->end() ->booleanNode('context_propagation') - ->info('Enable context propagation from incoming HTTP headers (requires propagator)') + ->info('Extract trace context from incoming request headers and inject it into outgoing response headers (requires flow-php/symfony-http-foundation-telemetry-bridge; silently disabled when absent)') ->defaultTrue() ->end() ->end() @@ -2537,7 +2539,7 @@ private function registerInstrumentation(array $config, ContainerConfigurator $c ); $builder->setParameter( 'flow.telemetry.http_kernel.context_propagation', - $httpKernelConfig['context_propagation'] ?? true, + ($httpKernelConfig['context_propagation'] ?? true) && class_exists(self::HTTP_FOUNDATION_REQUEST_CARRIER), ); $container->import(__DIR__ . '/Resources/config/instrumentation/http_kernel.php'); } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/HttpKernel/HttpKernelSpanSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/HttpKernel/HttpKernelSpanSubscriber.php index d1ae9371c8..e5633bdd2d 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/HttpKernel/HttpKernelSpanSubscriber.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Instrumentation/HttpKernel/HttpKernelSpanSubscriber.php @@ -6,10 +6,12 @@ use Closure; use DateTimeImmutable; +use Flow\Bridge\Symfony\HttpFoundationTelemetry\RequestCarrier; +use Flow\Bridge\Symfony\HttpFoundationTelemetry\ResponseCarrier; use Flow\Telemetry\Context\Context; use Flow\Telemetry\Context\ContextStorage; use Flow\Telemetry\PackageVersion; -use Flow\Telemetry\Propagation\ArrayCarrier; +use Flow\Telemetry\Propagation\PropagationContext; use Flow\Telemetry\Propagation\Propagator; use Flow\Telemetry\Telemetry; use Flow\Telemetry\Tracer\Span; @@ -18,6 +20,7 @@ use Flow\Telemetry\Tracer\Tracer; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -48,7 +51,7 @@ public function __construct( array $excludePaths, private ContextStorage $contextStorage, private Propagator $propagator, - private bool $extractContext = true, + private bool $contextPropagation = true, ) { $this->excludePathRules = array_map( static fn(array $config): PathExclusionRule => PathExclusionRule::fromConfig($config), @@ -111,7 +114,7 @@ public function onRequest(RequestEvent $event): void return; } - if ($event->isMainRequest() && $this->extractContext) { + if ($event->isMainRequest() && $this->contextPropagation) { $this->extractContextFromRequest($request); } @@ -149,6 +152,10 @@ public function onResponse(ResponseEvent $event): void } else { $span->setStatus(SpanStatus::ok()); } + + if ($event->isMainRequest() && $this->contextPropagation) { + $this->injectContextIntoResponse($span, $response); + } } public function onTerminate(TerminateEvent $event): void @@ -173,16 +180,7 @@ public function onTerminate(TerminateEvent $event): void private function extractContextFromRequest(Request $request): void { - $headers = []; - - foreach ($request->headers->all() as $key => $values) { - if (count($values) > 0 && is_string($values[0])) { - $headers[$key] = $values[0]; - } - } - - $carrier = new ArrayCarrier($headers); - $propagationContext = $this->propagator->extract($carrier); + $propagationContext = $this->propagator->extract(new RequestCarrier($request)); $spanContext = $propagationContext->spanContext; @@ -198,6 +196,13 @@ private function extractContextFromRequest(Request $request): void } } + private function injectContextIntoResponse(Span $span, Response $response): void + { + $propagationContext = new PropagationContext($span->context(), $this->contextStorage->current()->baggage); + + $this->propagator->inject($propagationContext, new ResponseCarrier($response)); + } + /** * @param array|callable|object $controller */ diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Instrumentation/HttpKernel/HttpKernelSpanSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Instrumentation/HttpKernel/HttpKernelSpanSubscriberTest.php index 39dcf7cfc6..42c4c56d38 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Instrumentation/HttpKernel/HttpKernelSpanSubscriberTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Instrumentation/HttpKernel/HttpKernelSpanSubscriberTest.php @@ -572,4 +572,113 @@ public function test_traces_successful_http_request(): void static::assertSame('test_index', $attributes['http.route']); static::assertSame(TestController::class . '::index', $attributes['controller']); } + + public function test_injects_context_into_response_when_propagation_enabled(): 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', [ + 'resource' => [], + 'exporters' => ['memory' => ['memory' => null], 'void' => ['void' => null]], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => 'memory', + ], + ], + 'instrumentation' => [ + 'http_kernel' => [ + 'enabled' => true, + 'context_propagation' => 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); + + static::assertSame(200, $response->getStatusCode()); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + static::assertCount(1, $spans); + + $span = $spans[0]; + static::assertSame( + "00-{$span->context()->traceId->toHex()}-{$span->context()->spanId->toHex()}-01", + $response->headers->get('traceparent'), + ); + } + + public function test_does_not_inject_context_into_response_when_propagation_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', [ + 'resource' => [], + 'exporters' => ['memory' => ['memory' => null], 'void' => ['void' => null]], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => 'memory', + ], + ], + 'instrumentation' => [ + 'http_kernel' => [ + 'enabled' => true, + 'context_propagation' => 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); + + static::assertSame(200, $response->getStatusCode()); + static::assertFalse($response->headers->has('traceparent')); + } } From 478327ae120d335362956f52ff6dc25707775117 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Thu, 18 Jun 2026 15:29:29 +0200 Subject: [PATCH 2/2] fix(flow-php/symfony-telemetry-bundle): metric attributes render as a toggleable table row --- .../Resources/views/Collector/telemetry.html.twig | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Resources/views/Collector/telemetry.html.twig b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Resources/views/Collector/telemetry.html.twig index 285ddd2677..7bca35b058 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Resources/views/Collector/telemetry.html.twig +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Resources/views/Collector/telemetry.html.twig @@ -174,7 +174,10 @@ {% if collector.metrics is empty %}

No metrics were recorded for this request.

{% else %} - + +
@@ -198,12 +201,16 @@ — {% else %} Attributes ({{ metric.attributes|length }}) - {% endif %} + {% if metric.attributes is not empty %} + + + + {% endif %} {% endfor %}
Name