From fd245fdfa3ee1f6bd31057ef1dca31d8299d32e3 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Thu, 5 Feb 2026 11:06:19 +0100 Subject: [PATCH 1/7] feature: initialized symfony telemetry bundle --- .github/workflows/monorepo-split.yml | 2 + bin/docs.php | 1 + composer.json | 13 + composer.lock | 1062 +++++++++++++++-- .../bridges/symfony-telemetry-bundle.md | 63 + infection.json | 4 +- phpdoc/bridge.symfony.telemetry.xml | 24 + phpstan.neon | 2 + phpunit.xml.dist | 6 + .../symfony/telemetry-bundle/.gitattributes | 9 + .../.github/workflows/readonly.yaml | 17 + .../symfony/telemetry-bundle/CONTRIBUTING.md | 6 + src/bridge/symfony/telemetry-bundle/LICENSE | 19 + src/bridge/symfony/telemetry-bundle/README.md | 20 + .../symfony/telemetry-bundle/composer.json | 51 + .../Symfony/TelemetryBundle/DSL/functions.php | 5 + .../TelemetryBundle/Exception/Exception.php | 9 + .../Exception/RuntimeException.php | 9 + .../TelemetryBundle/FlowTelemetryBundle.php | 11 + .../Tests/Integration/.gitkeep | 0 .../TelemetryBundle/Tests/Unit/.gitkeep | 0 .../etl/src/Flow/ETL/Attribute/Module.php | 1 + tools/infection/phpunit.xml | 3 + .../Website/Model/Documentation/Module.php | 4 +- 24 files changed, 1214 insertions(+), 127 deletions(-) create mode 100644 documentation/components/bridges/symfony-telemetry-bundle.md create mode 100644 phpdoc/bridge.symfony.telemetry.xml create mode 100644 src/bridge/symfony/telemetry-bundle/.gitattributes create mode 100644 src/bridge/symfony/telemetry-bundle/.github/workflows/readonly.yaml create mode 100644 src/bridge/symfony/telemetry-bundle/CONTRIBUTING.md create mode 100644 src/bridge/symfony/telemetry-bundle/LICENSE create mode 100644 src/bridge/symfony/telemetry-bundle/README.md create mode 100644 src/bridge/symfony/telemetry-bundle/composer.json create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Exception/Exception.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Exception/RuntimeException.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/FlowTelemetryBundle.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/.gitkeep create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/.gitkeep diff --git a/.github/workflows/monorepo-split.yml b/.github/workflows/monorepo-split.yml index a24f90732..f0a84ed70 100644 --- a/.github/workflows/monorepo-split.yml +++ b/.github/workflows/monorepo-split.yml @@ -94,6 +94,8 @@ jobs: split_repository: 'symfony-http-foundation-bridge' - local_path: 'src/bridge/symfony/http-foundation-telemetry' split_repository: 'symfony-http-foundation-telemetry-bridge' + - local_path: 'src/bridge/symfony/telemetry-bundle' + split_repository: 'symfony-telemetry-bundle' - local_path: 'src/bridge/telemetry/otlp' split_repository: 'telemetry-otlp-bridge' diff --git a/bin/docs.php b/bin/docs.php index 62942d744..467feeafa 100755 --- a/bin/docs.php +++ b/bin/docs.php @@ -68,6 +68,7 @@ public function execute(InputInterface $input, OutputInterface $output) : int __DIR__ . '/../src/bridge/filesystem/async-aws/src/Flow/Filesystem/Bridge/AsyncAWS/DSL/functions.php', __DIR__ . '/../src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/DSL/functions.php', __DIR__ . '/../src/bridge/symfony/http-foundation-telemetry/src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL/functions.php', + __DIR__ . '/../src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php', __DIR__ . '/../src/bridge/psr7/telemetry/src/Flow/Bridge/Psr7/Telemetry/DSL/functions.php', __DIR__ . '/../src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL/functions.php', ]; diff --git a/composer.json b/composer.json index c1830de37..7e433f95a 100644 --- a/composer.json +++ b/composer.json @@ -40,8 +40,12 @@ "psr/http-message": "^1.0 || ^2.0", "psr/log": "^2.0 || ^3.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.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/event-dispatcher": "^6.4 || ^7.3 || ^8.0", "symfony/http-foundation": "^6.4 || ^7.3 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.3 || ^8.0", "symfony/string": "^6.4 || ^7.3 || ^8.0", "symfony/uid": "^6.4 || ^7.3 || ^8.0", "webmozart/glob": "^3.0 || ^4.0" @@ -100,6 +104,7 @@ "flow-php/snappy": "self.version", "flow-php/symfony-http-foundation-bridge": "self.version", "flow-php/symfony-http-foundation-telemetry-bridge": "self.version", + "flow-php/symfony-telemetry-bundle": "self.version", "flow-php/psr7-telemetry-bridge": "self.version", "flow-php/telemetry": "self.version", "flow-php/telemetry-otlp-bridge": "self.version", @@ -133,6 +138,7 @@ "src/bridge/psr7/telemetry/src/Flow", "src/bridge/symfony/http-foundation/src/Flow", "src/bridge/symfony/http-foundation-telemetry/src/Flow", + "src/bridge/symfony/telemetry-bundle/src/Flow", "src/bridge/telemetry/otlp/src/Flow", "src/cli/src/Flow", "src/core/etl/src/Flow", @@ -179,6 +185,7 @@ "src/bridge/psr7/telemetry/src/Flow/Bridge/Psr7/Telemetry/DSL/functions.php", "src/bridge/symfony/http-foundation/src/Flow/Bridge/Symfony/HttpFoundation/functions.php", "src/bridge/symfony/http-foundation-telemetry/src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL/functions.php", + "src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php", "src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL/functions.php", "src/cli/src/Flow/CLI/DSL/functions.php", "src/core/etl/src/Flow/ETL/DSL/functions.php", @@ -221,6 +228,7 @@ "src/bridge/psr7/telemetry/tests/Flow", "src/bridge/symfony/http-foundation/tests/Flow", "src/bridge/symfony/http-foundation-telemetry/tests/Flow", + "src/bridge/symfony/telemetry-bundle/tests/Flow", "src/bridge/telemetry/otlp/tests/Flow", "src/cli/tests/Flow", "src/core/etl/tests/Flow", @@ -292,6 +300,7 @@ "@test:bridge:psr7-telemetry", "@test:bridge:symfony-http-foundation", "@test:bridge:symfony-http-foundation-telemetry", + "@test:bridge:symfony-telemetry-bundle", "@test:bridge:telemetry-otlp" ], "test:adapters": [ @@ -381,6 +390,10 @@ "tools/phpunit/vendor/bin/phpunit --testsuite=bridge-symfony-http-foundation-telemetry-unit --log-junit ./var/phpunit/logs/bridge-symfony-http-foundation-telemetry-unit.junit.xml --coverage-clover=./var/phpunit/coverage/clover/bridge-symfony-http-foundation-telemetry-unit.coverage.xml --coverage-html=./var/phpunit/coverage/html/bridge-symfony-http-foundation-telemetry-unit", "tools/phpunit/vendor/bin/phpunit --testsuite=bridge-symfony-http-foundation-telemetry-integration --log-junit ./var/phpunit/logs/bridge-symfony-http-foundation-telemetry-integration.junit.xml --coverage-clover=./var/phpunit/coverage/clover/bridge-symfony-http-foundation-telemetry-integration.coverage.xml --coverage-html=./var/phpunit/coverage/html/bridge-symfony-http-foundation-telemetry-integration" ], + "test:bridge:symfony-telemetry-bundle": [ + "tools/phpunit/vendor/bin/phpunit --testsuite=bridge-symfony-telemetry-bundle-unit --log-junit ./var/phpunit/logs/bridge-symfony-telemetry-bundle-unit.junit.xml --coverage-clover=./var/phpunit/coverage/clover/bridge-symfony-telemetry-bundle-unit.coverage.xml --coverage-html=./var/phpunit/coverage/html/bridge-symfony-telemetry-bundle-unit", + "tools/phpunit/vendor/bin/phpunit --testsuite=bridge-symfony-telemetry-bundle-integration --log-junit ./var/phpunit/logs/bridge-symfony-telemetry-bundle-integration.junit.xml --coverage-clover=./var/phpunit/coverage/clover/bridge-symfony-telemetry-bundle-integration.coverage.xml --coverage-html=./var/phpunit/coverage/html/bridge-symfony-telemetry-bundle-integration" + ], "test:bridge:telemetry-otlp": [ "tools/phpunit/vendor/bin/phpunit --testsuite=bridge-telemetry-otlp-unit --log-junit ./var/phpunit/logs/bridge-telemetry-otlp-unit.junit.xml --coverage-clover=./var/phpunit/coverage/clover/bridge-telemetry-otlp-unit.coverage.xml --coverage-html=./var/phpunit/coverage/html/bridge-telemetry-otlp-unit", "tools/phpunit/vendor/bin/phpunit --testsuite=bridge-telemetry-otlp-integration --log-junit ./var/phpunit/logs/bridge-telemetry-otlp-integration.junit.xml --coverage-clover=./var/phpunit/coverage/clover/bridge-telemetry-otlp-integration.coverage.xml --coverage-html=./var/phpunit/coverage/html/bridge-telemetry-otlp-integration" diff --git a/composer.lock b/composer.lock index b03ab7f4e..823015eaf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7f857eaa375ae5daff4cea7bafb09532", + "content-hash": "5176f122d36144e7d5f913e500f8acf7", "packages": [ { "name": "async-aws/core", @@ -2214,6 +2214,56 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-client", "version": "1.0.3", @@ -2519,6 +2569,85 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "symfony/config", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/4275b53b8ab0cf37f48bf273dc2285c8178efdfb", + "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-13T11:36:38+00:00" + }, { "name": "symfony/console", "version": "v7.4.4", @@ -2556,17 +2685,341 @@ "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/event-dispatcher": "^6.4|^7.0|^8.0", "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-13T11:36:38+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/76a02cddca45a5254479ad68f9fa274ead0a7ef2", + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:16:02+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2586,16 +3039,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" }, "funding": [ { @@ -2615,24 +3062,25 @@ "type": "tidelift" } ], - "time": "2026-01-13T11:36:38+00:00" + "time": "2026-01-05T11:45:34+00:00" }, { - "name": "symfony/deprecation-contracts", + "name": "symfony/event-dispatcher-contracts", "version": "v3.6.0", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "psr/event-dispatcher": "^1" }, "type": "library", "extra": { @@ -2645,9 +3093,9 @@ } }, "autoload": { - "files": [ - "function.php" - ] + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2663,10 +3111,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A generic function and convention to trigger deprecation notices", + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -2684,6 +3140,76 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-27T13:27:24+00:00" + }, { "name": "symfony/http-client", "version": "v7.4.5", @@ -2945,6 +3471,125 @@ ], "time": "2026-01-27T16:16:02+00:00" }, + { + "name": "symfony/http-kernel", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-28T10:33:42+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -3370,8 +4015,88 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", - "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -3389,7 +4114,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php82\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -3409,7 +4134,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -3418,7 +4143,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -3438,20 +4163,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { - "name": "symfony/polyfill-php83", + "name": "symfony/polyfill-php85", "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", "shasum": "" }, "require": { @@ -3469,7 +4194,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" + "Symfony\\Polyfill\\Php85\\": "" }, "classmap": [ "Resources/stubs" @@ -3489,7 +4214,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -3498,7 +4223,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" }, "funding": [ { @@ -3518,7 +4243,7 @@ "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2025-06-23T16:12:55+00:00" }, { "name": "symfony/polyfill-uuid", @@ -4041,6 +4766,174 @@ ], "time": "2026-01-03T23:30:35+00:00" }, + { + "name": "symfony/var-dumper", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-01T22:13:48+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:15:23+00:00" + }, { "name": "webmozart/glob", "version": "4.7.0", @@ -5552,87 +6445,6 @@ } ], "time": "2026-01-26T15:07:59+00:00" - }, - { - "name": "symfony/var-exporter", - "version": "v7.4.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\VarExporter\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", - "homepage": "https://symfony.com", - "keywords": [ - "clone", - "construct", - "export", - "hydrate", - "instantiate", - "lazy-loading", - "proxy", - "serialize" - ], - "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-09-11T10:15:23+00:00" } ], "aliases": [], @@ -5651,7 +6463,7 @@ "ext-xmlreader": "*", "ext-xmlwriter": "*", "ext-zlib": "*", - "composer-runtime-api": "^2.1" + "composer-runtime-api": "^2.0" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/documentation/components/bridges/symfony-telemetry-bundle.md b/documentation/components/bridges/symfony-telemetry-bundle.md new file mode 100644 index 000000000..7e2bd9603 --- /dev/null +++ b/documentation/components/bridges/symfony-telemetry-bundle.md @@ -0,0 +1,63 @@ +# Symfony Telemetry Bundle + +Flow Symfony Telemetry Bundle provides automatic telemetry integration for Symfony applications, including HTTP request/response tracing, console command instrumentation, and configurable exporters. + +- [⬅️️ Back](/documentation/introduction.md) +- [📦Packagist](https://packagist.org/packages/flow-php/symfony-telemetry-bundle) +- [🐙GitHub](https://github.com/flow-php/symfony-telemetry-bundle) + +[TOC] + +## Installation + +``` +composer require flow-php/symfony-telemetry-bundle:~--FLOW_PHP_VERSION-- +``` + +## Overview + +This bundle integrates Flow PHP's Telemetry library with Symfony applications, providing: + +- Automatic HTTP request/response span creation +- Console command tracing +- Context propagation via W3C Trace Context and Baggage +- Configurable exporters (console, memory, void, OTLP) +- Full configuration through Symfony's config system + +## Requirements + +- PHP 8.3+ +- flow-php/telemetry +- flow-php/symfony-http-foundation-telemetry-bridge +- symfony/http-kernel ^6.4 || ^7.3 || ^8.0 +- symfony/dependency-injection ^6.4 || ^7.3 || ^8.0 +- symfony/config ^6.4 || ^7.3 || ^8.0 +- symfony/console ^6.4 || ^7.3 || ^8.0 + +## Configuration + +```yaml +# config/packages/flow_telemetry.yaml +flow_telemetry: + service_name: 'my-app' + service_version: '1.0.0' + environment: '%kernel.environment%' + + exporter: 'console' # console|void|memory|otlp + + tracing: + enabled: true + sampler: 'always_on' + + metrics: + enabled: true + + logging: + enabled: true + + http: + enabled: true + + console: + enabled: true +``` diff --git a/infection.json b/infection.json index b32db142f..b9ef96eb6 100644 --- a/infection.json +++ b/infection.json @@ -10,6 +10,7 @@ "src/bridge/monolog/telemetry/src", "src/bridge/psr7/telemetry/src", "src/bridge/symfony/http-foundation-telemetry/src", + "src/bridge/symfony/telemetry-bundle/src", "src/bridge/telemetry/otlp/src" ], "excludes": [ @@ -30,7 +31,8 @@ "Flow/Bridge/Telemetry/OTLP/Exception", "Flow/Bridge/Monolog/Telemetry/Exception", "Flow/Bridge/Psr7/Telemetry/Exception", - "Flow/Bridge/Symfony/HttpFoundationTelemetry/Exception" + " "Flow/Bridge/Symfony/HttpFoundationTelemetry/Exception", + "Flow/Bridge/Symfony/TelemetryBundle/Exception"" ] }, "logs": { diff --git a/phpdoc/bridge.symfony.telemetry.xml b/phpdoc/bridge.symfony.telemetry.xml new file mode 100644 index 000000000..40b79436f --- /dev/null +++ b/phpdoc/bridge.symfony.telemetry.xml @@ -0,0 +1,24 @@ + + + Flow PHP + + ./../web/landing/build/documentation/api/bridge/symfony/telemetry + ./../var/phpdocumentor/cache/bridge/symfony/telemetry + + + + + src/bridge/symfony/telemetry-bundle/src + + telemetry + Symfony Telemetry Bundle + public + false + + + + diff --git a/phpstan.neon b/phpstan.neon index d2500326f..02951ef1b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -31,6 +31,7 @@ parameters: - src/bridge/psr7/telemetry/src - src/bridge/symfony/http-foundation/src - src/bridge/symfony/http-foundation-telemetry/src + - src/bridge/symfony/telemetry-bundle/src - src/bridge/telemetry/otlp/src - src/lib/array-dot/src - src/lib/azure-sdk/src @@ -68,6 +69,7 @@ parameters: - src/bridge/psr7/telemetry/tests - src/bridge/symfony/http-foundation/tests - src/bridge/symfony/http-foundation-telemetry/tests + - src/bridge/symfony/telemetry-bundle/tests - src/bridge/telemetry/otlp/tests - src/lib/telemetry/tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c44adeaeb..83448ea6e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -136,6 +136,12 @@ src/bridge/symfony/http-foundation-telemetry/tests/Flow/Bridge/Symfony/HttpFoundationTelemetry/Tests/Integration + + src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit + + + src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration + src/bridge/telemetry/otlp/tests/Flow/Bridge/Telemetry/OTLP/Tests/Unit diff --git a/src/bridge/symfony/telemetry-bundle/.gitattributes b/src/bridge/symfony/telemetry-bundle/.gitattributes new file mode 100644 index 000000000..e02097205 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/.gitattributes @@ -0,0 +1,9 @@ +*.php text eol=lf + +/.github export-ignore +/tests export-ignore + +/README.md export-ignore + +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/bridge/symfony/telemetry-bundle/.github/workflows/readonly.yaml b/src/bridge/symfony/telemetry-bundle/.github/workflows/readonly.yaml new file mode 100644 index 000000000..24255888e --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/.github/workflows/readonly.yaml @@ -0,0 +1,17 @@ +name: Readonly + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Hi, thank you for your contribution. + Unfortunately, this repository is read-only. It's a split from our main monorepo repository. + In order to proceed with this PR please open it against https://github.com/flow-php/flow repository. + Thank you. diff --git a/src/bridge/symfony/telemetry-bundle/CONTRIBUTING.md b/src/bridge/symfony/telemetry-bundle/CONTRIBUTING.md new file mode 100644 index 000000000..f035b534a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/CONTRIBUTING.md @@ -0,0 +1,6 @@ +## Contributing + +This repo is **READ ONLY**, in order to contribute to Flow PHP project, please +open PR against [flow](https://github.com/flow-php/flow) monorepo. + +Changes merged to monorepo are automatically propagated into sub repositories. diff --git a/src/bridge/symfony/telemetry-bundle/LICENSE b/src/bridge/symfony/telemetry-bundle/LICENSE new file mode 100644 index 000000000..bc3cc4d08 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Flow PHP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/bridge/symfony/telemetry-bundle/README.md b/src/bridge/symfony/telemetry-bundle/README.md new file mode 100644 index 000000000..18c6b80a6 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/README.md @@ -0,0 +1,20 @@ +# Flow PHP - Symfony Telemetry Bundle + +Bridge connecting Flow PHP Telemetry library with Symfony HttpFoundation for propagating telemetry context via HTTP requests and responses. + +> [!IMPORTANT] +> This repository is a subtree split from our monorepo. If you'd like to contribute, +> please visit our main monorepo [flow-php/flow](https://github.com/flow-php/flow). + +## Installation + +```bash +composer require flow-php/symfony-telemetry-bundle +``` + +## Resources + +- [Documentation](https://flow-php.com/documentation/components/bridges/symfony-telemetry-bundle/) +- [Installation](https://flow-php.com/documentation/installation/) +- [Contributing](https://flow-php.com/documentation/contributing/) +- [Upgrading](https://flow-php.com/documentation/upgrading/) diff --git a/src/bridge/symfony/telemetry-bundle/composer.json b/src/bridge/symfony/telemetry-bundle/composer.json new file mode 100644 index 000000000..69a754fd1 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/composer.json @@ -0,0 +1,51 @@ +{ + "name": "flow-php/symfony-telemetry-bundle", + "type": "symfony-bundle", + "description": "Flow PHP - Symfony Telemetry Bundle", + "keywords": [ + "flow-php", + "symfony", + "telemetry", + "opentelemetry", + "tracing", + "metrics", + "logging", + "bundle" + ], + "homepage": "https://github.com/flow-php/flow", + "license": "MIT", + "require": { + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "flow-php/telemetry": "self.version", + "flow-php/symfony-http-foundation-telemetry-bridge": "self.version", + "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" + }, + "suggest": { + "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support" + }, + "autoload": { + "psr-4": { + "Flow\\": [ + "src/Flow" + ] + }, + "files": [ + "src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "optimize-autoloader": true, + "sort-packages": true + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php new file mode 100644 index 000000000..f73493252 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php @@ -0,0 +1,5 @@ +../../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/web/landing/src/Flow/Website/Model/Documentation/Module.php b/web/landing/src/Flow/Website/Model/Documentation/Module.php index 6e47845c1..82681253c 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, }; } From 0f36c17243e437362d0c769d756032cc31257f22 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Thu, 5 Feb 2026 14:09:27 +0100 Subject: [PATCH 2/7] feature: initialize symfony telemetry bundle - created configuration for multiple instances of telemetry - created robust test case with dedicated TestKernel --- phpstan.neon | 19 + rector.src.php | 5 + .../Compiler/OTLPAvailabilityPass.php | 30 + .../DependencyInjection/Configuration.php | 267 ++++++ .../FlowTelemetryExtension.php | 767 ++++++++++++++++++ .../TelemetryBundle/FlowTelemetryBundle.php | 12 +- .../Tests/Context/SymfonyContext.php | 75 ++ .../Tests/Fixtures/TestKernel.php | 122 +++ .../Tests/Integration/.gitkeep | 0 .../FlowTelemetryExtensionTest.php | 715 ++++++++++++++++ .../Tests/Integration/KernelTestCase.php | 59 ++ .../TelemetryBundle/Tests/Unit/.gitkeep | 0 .../DependencyInjection/ConfigurationTest.php | 491 +++++++++++ 13 files changed, 2560 insertions(+), 2 deletions(-) create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php delete mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/.gitkeep create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php delete mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/.gitkeep create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php diff --git a/phpstan.neon b/phpstan.neon index 02951ef1b..bad4ee47e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -87,6 +87,7 @@ parameters: - src/lib/parquet/src/Flow/Parquet/BinaryReader/* - src/lib/parquet/src/Flow/Parquet/Dremel/ColumnData/DefinitionConverter.php - src/lib/postgresql/src/Flow/PostgreSql/Protobuf/* + - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php tmpDir: var/phpstan/cache @@ -106,6 +107,24 @@ parameters: - message: '#Dom\\(CharacterData|HTMLDocument|HTMLElement|Element)#i' identifier: class.notFound + - + path: src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php + identifier: argument.type + - + path: src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php + identifier: offsetAccess.nonOffsetAccessible + - + path: src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php + identifier: cast.string + - + path: src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php + identifier: foreach.nonIterable + - + path: src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php + identifier: notIdentical.alwaysTrue + - + path: src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php + identifier: binaryOp.invalid includes: - tools/phpstan/vendor/spaze/phpstan-disallowed-calls/extension.neon diff --git a/rector.src.php b/rector.src.php index 0525ba79a..cb26857e2 100644 --- a/rector.src.php +++ b/rector.src.php @@ -1,5 +1,6 @@ [ + __DIR__ . '/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php', + ], ]) ->withCache(__DIR__ . '/var/rector/src') ->withImportNames(importShortClasses: false, removeUnusedImports: true) diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php new file mode 100644 index 000000000..bb7d48a0a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php @@ -0,0 +1,30 @@ +setParameter('flow.telemetry.otlp_available', $otlpAvailable); + + $otlpConfigured = $container->hasParameter('flow.telemetry.otlp_configured') + && $container->getParameter('flow.telemetry.otlp_configured') === true; + + if ($otlpConfigured && !$otlpAvailable) { + throw new RuntimeException( + 'OTLP exporter is configured but the flow-php/telemetry-otlp-bridge package is not installed. ' + . 'Please install it with: composer require flow-php/telemetry-otlp-bridge' + ); + } + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php new file mode 100644 index 000000000..10ff5a76a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -0,0 +1,267 @@ +getRootNode(); + + $rootNode + ->children() + ->arrayNode('service') + ->info('Service resource configuration') + ->isRequired() + ->children() + ->scalarNode('name') + ->info('Service name for Resource (e.g., "MyApp")') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('version') + ->info('Service version (e.g., "1.0.0")') + ->defaultNull() + ->end() + ->arrayNode('attributes') + ->info('Additional resource attributes') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->arrayNode('instances') + ->info('Telemetry instances configuration. If omitted, a "default" instance with void processors is created.') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->arrayNode('tracer_provider') + ->info('TracerProvider configuration. Defaults to void if omitted.') + ->children() + ->arrayNode('sampler') + ->info('Trace sampler configuration') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values(['always_on', 'always_off', 'trace_id_ratio', 'parent_based', 'service']) + ->defaultValue('always_on') + ->end() + ->floatNode('ratio') + ->info('Sampling ratio for trace_id_ratio type (0.0 to 1.0)') + ->defaultValue(1.0) + ->min(0.0) + ->max(1.0) + ->end() + ->scalarNode('service_id') + ->info('Custom sampler service ID (only for type: service)') + ->defaultNull() + ->end() + ->end() + ->end() + ->append($this->processorNode('span')) + ->end() + ->end() + ->arrayNode('meter_provider') + ->info('MeterProvider configuration. Defaults to void if omitted.') + ->children() + ->enumNode('temporality') + ->info('Aggregation temporality') + ->values(['cumulative', 'delta']) + ->defaultValue('cumulative') + ->end() + ->append($this->processorNode('metric')) + ->end() + ->end() + ->arrayNode('logger_provider') + ->info('LoggerProvider configuration. Defaults to void if omitted.') + ->children() + ->append($this->processorNode('log')) + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } + + private function exporterNode(string $signalType) : ArrayNodeDefinition + { + $builder = new TreeBuilder('exporter'); + /** @var ArrayNodeDefinition $node */ + $node = $builder->getRootNode(); + + $node + ->info(\ucfirst($signalType) . ' exporter configuration') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values(['memory', 'console', 'void', 'otlp', 'service']) + ->defaultValue('void') + ->end() + ->scalarNode('service_id') + ->info('Custom exporter service ID (only for type: service)') + ->defaultNull() + ->end() + ->arrayNode('otlp') + ->info('OTLP exporter configuration (only for type: otlp)') + ->children() + ->arrayNode('transport') + ->info('OTLP transport configuration') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values(['curl', 'http', 'grpc', 'service']) + ->defaultValue('curl') + ->end() + ->scalarNode('endpoint') + ->info('OTLP endpoint URL') + ->defaultValue('http://localhost:4318') + ->end() + ->integerNode('timeout') + ->info('Request timeout in seconds') + ->defaultValue(30) + ->min(1) + ->end() + ->arrayNode('headers') + ->info('Additional HTTP headers') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('scalar')->end() + ->end() + ->booleanNode('insecure') + ->info('Allow insecure connections (only for grpc)') + ->defaultTrue() + ->end() + ->scalarNode('service_id') + ->info('Custom transport service ID (only for type: service)') + ->defaultNull() + ->end() + ->arrayNode('serializer') + ->info('Serializer configuration') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values(['json', 'protobuf', 'service']) + ->defaultValue('json') + ->end() + ->scalarNode('service_id') + ->info('Custom serializer service ID (only for type: service)') + ->defaultNull() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $node; + } + + private function innerProcessorNode(string $signalType) : ArrayNodeDefinition + { + $builder = new TreeBuilder('inner_processor'); + /** @var ArrayNodeDefinition $node */ + $node = $builder->getRootNode(); + + $childProcessorTypes = ['memory', 'batching', 'passthrough', 'void', 'service']; + + $node + ->info('Inner processor configuration for severity_filtering (only for type: severity_filtering)') + ->children() + ->enumNode('type') + ->values($childProcessorTypes) + ->isRequired() + ->end() + ->integerNode('batch_size') + ->info('Batch size for batching processor') + ->defaultValue(512) + ->min(1) + ->end() + ->scalarNode('service_id') + ->info('Custom processor service ID (only for type: service)') + ->defaultNull() + ->end() + ->append($this->exporterNode($signalType)) + ->end(); + + return $node; + } + + private function processorNode(string $signalType) : ArrayNodeDefinition + { + $builder = new TreeBuilder('processor'); + /** @var ArrayNodeDefinition $node */ + $node = $builder->getRootNode(); + + $processorTypes = ['composite', 'memory', 'batching', 'passthrough', 'void', 'service']; + $childProcessorTypes = ['memory', 'batching', 'passthrough', 'void', 'service']; + + if ($signalType === 'log') { + $processorTypes[] = 'severity_filtering'; + $childProcessorTypes[] = 'severity_filtering'; + } + + $node + ->info(\ucfirst($signalType) . ' processor configuration') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values($processorTypes) + ->defaultValue('void') + ->end() + ->integerNode('batch_size') + ->info('Batch size for batching processor') + ->defaultValue(512) + ->min(1) + ->end() + ->scalarNode('service_id') + ->info('Custom processor service ID (only for type: service)') + ->defaultNull() + ->end() + ->enumNode('minimum_severity') + ->info('Minimum severity level for severity_filtering processor (only for log processors)') + ->values(['trace', 'debug', 'info', 'warn', 'error', 'fatal']) + ->defaultValue('info') + ->end() + ->arrayNode('processors') + ->info('Array of processor configurations (only for type: composite)') + ->arrayPrototype() + ->children() + ->enumNode('type') + ->values($childProcessorTypes) + ->isRequired() + ->end() + ->integerNode('batch_size') + ->defaultValue(512) + ->min(1) + ->end() + ->scalarNode('service_id') + ->defaultNull() + ->end() + ->enumNode('minimum_severity') + ->values(['trace', 'debug', 'info', 'warn', 'error', 'fatal']) + ->defaultValue('info') + ->end() + ->append($this->exporterNode($signalType)) + ->append($this->innerProcessorNode($signalType)) + ->end() + ->end() + ->end() + ->append($this->exporterNode($signalType)) + ->append($this->innerProcessorNode($signalType)) + ->end(); + + return $node; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php new file mode 100644 index 000000000..459741a9a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -0,0 +1,767 @@ + $configs + */ + public function load(array $configs, ContainerBuilder $container) : void + { + $configuration = new Configuration(); + /** @var array{service: array, instances?: array>} $config */ + $config = $this->processConfiguration($configuration, $configs); + + $this->registerGlobalServices($container); + $this->registerResource($config['service'], $container); + $this->registerInstances($config['instances'] ?? [], $container); + } + + /** + * @param array $config + */ + private function buildInnerLogProcessor(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $processorServiceId = $serviceIdPrefix . '.processor'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when processor type is "service"'); + } + $container->setAlias($processorServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($processorServiceId, new Definition(VoidLogProcessor::class)); + + break; + + case 'memory': + $exporterServiceId = $this->buildLogExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(MemoryLogProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'batching': + $exporterServiceId = $this->buildLogExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(BatchingLogProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $definition->setArgument(1, $config['batch_size'] ?? 512); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'passthrough': + $exporterServiceId = $this->buildLogExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(PassThroughLogProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown inner log processor type: %s', (string) $type)); + } + + return $processorServiceId; + } + + /** + * @param array $config + */ + private function buildLogExporter(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $exporterServiceId = $serviceIdPrefix . '.exporter'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when exporter type is "service"'); + } + $container->setAlias($exporterServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($exporterServiceId, new Definition(VoidLogExporter::class)); + + break; + + case 'memory': + $container->setDefinition($exporterServiceId, new Definition(MemoryLogExporter::class)); + + break; + + case 'console': + $container->setDefinition($exporterServiceId, new Definition(ConsoleLogExporter::class)); + + break; + + case 'otlp': + $container->setParameter('flow.telemetry.otlp_configured', true); + $transportServiceId = $this->buildOTLPTransport($config['otlp']['transport'] ?? [], $exporterServiceId, $container); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\LogExporter\\OTLPLogExporter'); + $definition->setArgument(0, new Reference($transportServiceId)); + $container->setDefinition($exporterServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown log exporter type: %s', (string) $type)); + } + + return $exporterServiceId; + } + + /** + * @param array $config + */ + private function buildLoggerProvider(array $config, string $instanceName, ContainerBuilder $container) : string + { + $providerServiceId = 'flow.telemetry.' . $instanceName . '.logger_provider'; + + $processorServiceId = $this->buildLogProcessor($config['processor'] ?? [], $providerServiceId, $container); + + $definition = new Definition(LoggerProvider::class); + $definition->setArgument(0, new Reference($processorServiceId)); + $definition->setArgument(1, new Reference('flow.telemetry.clock')); + $definition->setArgument(2, new Reference('flow.telemetry.context_storage')); + $container->setDefinition($providerServiceId, $definition); + + return $providerServiceId; + } + + /** + * @param array $config + */ + private function buildLogProcessor(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $processorServiceId = $serviceIdPrefix . '.processor'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when processor type is "service"'); + } + $container->setAlias($processorServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($processorServiceId, new Definition(VoidLogProcessor::class)); + + break; + + case 'memory': + $exporterServiceId = $this->buildLogExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(MemoryLogProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'batching': + $exporterServiceId = $this->buildLogExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(BatchingLogProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $definition->setArgument(1, $config['batch_size'] ?? 512); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'passthrough': + $exporterServiceId = $this->buildLogExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(PassThroughLogProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'composite': + $processors = $config['processors'] ?? []; + $processorRefs = []; + + foreach ($processors as $idx => $processorConfig) { + /** @var array $processorConfig */ + $subProcessorId = $this->buildLogProcessor($processorConfig, $processorServiceId . '.' . $idx, $container); + $processorRefs[] = new Reference($subProcessorId); + } + $definition = new Definition(CompositeLogProcessor::class); + $definition->setArgument(0, $processorRefs); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'severity_filtering': + $innerProcessorConfig = $config['inner_processor'] ?? []; + $innerProcessorServiceId = $this->buildInnerLogProcessor($innerProcessorConfig, $processorServiceId . '.inner', $container); + $minimumSeverity = $this->mapSeverity($config['minimum_severity'] ?? 'info'); + $definition = new Definition(SeverityFilteringLogProcessor::class); + $definition->setArgument(0, new Reference($innerProcessorServiceId)); + $definition->setArgument(1, $minimumSeverity); + $container->setDefinition($processorServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown log processor type: %s', (string) $type)); + } + + return $processorServiceId; + } + + /** + * @param array $config + */ + private function buildMeterProvider(array $config, string $instanceName, ContainerBuilder $container) : string + { + $providerServiceId = 'flow.telemetry.' . $instanceName . '.meter_provider'; + + $processorServiceId = $this->buildMetricProcessor($config['processor'] ?? [], $providerServiceId, $container); + + $temporality = ($config['temporality'] ?? 'cumulative') === 'delta' + ? AggregationTemporality::DELTA + : AggregationTemporality::CUMULATIVE; + + $definition = new Definition(MeterProvider::class); + $definition->setArgument(0, new Reference($processorServiceId)); + $definition->setArgument(1, new Reference('flow.telemetry.clock')); + $definition->setArgument(2, $temporality); + $container->setDefinition($providerServiceId, $definition); + + return $providerServiceId; + } + + /** + * @param array $config + */ + private function buildMetricExporter(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $exporterServiceId = $serviceIdPrefix . '.exporter'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when exporter type is "service"'); + } + $container->setAlias($exporterServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($exporterServiceId, new Definition(VoidMetricExporter::class)); + + break; + + case 'memory': + $container->setDefinition($exporterServiceId, new Definition(MemoryMetricExporter::class)); + + break; + + case 'console': + $container->setDefinition($exporterServiceId, new Definition(ConsoleMetricExporter::class)); + + break; + + case 'otlp': + $container->setParameter('flow.telemetry.otlp_configured', true); + $transportServiceId = $this->buildOTLPTransport($config['otlp']['transport'] ?? [], $exporterServiceId, $container); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\MetricExporter\\OTLPMetricExporter'); + $definition->setArgument(0, new Reference($transportServiceId)); + $container->setDefinition($exporterServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown metric exporter type: %s', (string) $type)); + } + + return $exporterServiceId; + } + + /** + * @param array $config + */ + private function buildMetricProcessor(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $processorServiceId = $serviceIdPrefix . '.processor'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when processor type is "service"'); + } + $container->setAlias($processorServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($processorServiceId, new Definition(VoidMetricProcessor::class)); + + break; + + case 'memory': + $exporterServiceId = $this->buildMetricExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(MemoryMetricProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'batching': + $exporterServiceId = $this->buildMetricExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(BatchingMetricProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $definition->setArgument(1, $config['batch_size'] ?? 512); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'passthrough': + $exporterServiceId = $this->buildMetricExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(PassThroughMetricProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'composite': + $processors = $config['processors'] ?? []; + $processorRefs = []; + + foreach ($processors as $idx => $processorConfig) { + /** @var array $processorConfig */ + $subProcessorId = $this->buildMetricProcessor($processorConfig, $processorServiceId . '.' . $idx, $container); + $processorRefs[] = new Reference($subProcessorId); + } + $definition = new Definition(CompositeMetricProcessor::class); + $definition->setArgument(0, $processorRefs); + $container->setDefinition($processorServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown metric processor type: %s', (string) $type)); + } + + return $processorServiceId; + } + + /** + * @param array{type?: string, service_id?: string} $config + */ + private function buildOTLPSerializer(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $serializerServiceId = $serviceIdPrefix . '.serializer'; + $type = $config['type'] ?? 'json'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when serializer type is "service"'); + } + $container->setAlias($serializerServiceId, $customServiceId); + + break; + + case 'json': + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Serializer\\JsonSerializer'); + $container->setDefinition($serializerServiceId, $definition); + + break; + + case 'protobuf': + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Serializer\\ProtobufSerializer'); + $container->setDefinition($serializerServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown OTLP serializer type: %s', (string) $type)); + } + + return $serializerServiceId; + } + + /** + * @param array $config + */ + private function buildOTLPTransport(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $transportServiceId = $serviceIdPrefix . '.transport'; + $type = $config['type'] ?? 'curl'; + + if ($type === 'service') { + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when transport type is "service"'); + } + $container->setAlias($transportServiceId, $customServiceId); + + return $transportServiceId; + } + + $endpoint = $config['endpoint'] ?? 'http://localhost:4318'; + $timeout = $config['timeout'] ?? 30; + $headers = $config['headers'] ?? []; + + $serializerServiceId = $this->buildOTLPSerializer($config['serializer'] ?? [], $transportServiceId, $container); + + switch ($type) { + case 'curl': + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\CurlTransport'); + $definition->setArgument(0, $endpoint); + $definition->setArgument(1, new Reference($serializerServiceId)); + $definition->setArgument(2, $timeout); + $definition->setArgument(3, $headers); + $container->setDefinition($transportServiceId, $definition); + + break; + + case 'http': + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\HttpTransport'); + $definition->setArgument(0, $endpoint); + $definition->setArgument(1, new Reference($serializerServiceId)); + $definition->setArgument(2, $timeout); + $definition->setArgument(3, $headers); + $container->setDefinition($transportServiceId, $definition); + + break; + + case 'grpc': + $insecure = $config['insecure'] ?? true; + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\GrpcTransport'); + $definition->setArgument(0, $endpoint); + $definition->setArgument(1, new Reference($serializerServiceId)); + $definition->setArgument(2, $timeout); + $definition->setArgument(3, $headers); + $definition->setArgument(4, $insecure); + $container->setDefinition($transportServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown OTLP transport type: %s', (string) $type)); + } + + return $transportServiceId; + } + + /** + * @param array $config + */ + private function buildSampler(array $config, string $instanceName, ContainerBuilder $container) : string + { + $samplerServiceId = 'flow.telemetry.' . $instanceName . '.tracer_provider.sampler'; + $type = $config['type'] ?? 'always_on'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when sampler type is "service"'); + } + $container->setAlias($samplerServiceId, $customServiceId); + + break; + + case 'always_on': + $container->setDefinition($samplerServiceId, new Definition(AlwaysOnSampler::class)); + + break; + + case 'always_off': + $container->setDefinition($samplerServiceId, new Definition(AlwaysOffSampler::class)); + + break; + + case 'trace_id_ratio': + $definition = new Definition(TraceIdRatioBasedSampler::class); + $definition->setArgument(0, $config['ratio'] ?? 1.0); + $container->setDefinition($samplerServiceId, $definition); + + break; + + case 'parent_based': + $rootSamplerServiceId = $samplerServiceId . '.root'; + $rootSamplerDefinition = new Definition(AlwaysOnSampler::class); + $container->setDefinition($rootSamplerServiceId, $rootSamplerDefinition); + + $definition = new Definition(ParentBasedSampler::class); + $definition->setArgument(0, new Reference($rootSamplerServiceId)); + $container->setDefinition($samplerServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown sampler type: %s', (string) $type)); + } + + return $samplerServiceId; + } + + /** + * @param array $config + */ + private function buildSpanExporter(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $exporterServiceId = $serviceIdPrefix . '.exporter'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when exporter type is "service"'); + } + $container->setAlias($exporterServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($exporterServiceId, new Definition(VoidSpanExporter::class)); + + break; + + case 'memory': + $container->setDefinition($exporterServiceId, new Definition(MemorySpanExporter::class)); + + break; + + case 'console': + $container->setDefinition($exporterServiceId, new Definition(ConsoleSpanExporter::class)); + + break; + + case 'otlp': + $container->setParameter('flow.telemetry.otlp_configured', true); + $transportServiceId = $this->buildOTLPTransport($config['otlp']['transport'] ?? [], $exporterServiceId, $container); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\SpanExporter\\OTLPSpanExporter'); + $definition->setArgument(0, new Reference($transportServiceId)); + $container->setDefinition($exporterServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown span exporter type: %s', (string) $type)); + } + + return $exporterServiceId; + } + + /** + * @param array $config + */ + private function buildSpanProcessor(array $config, string $serviceIdPrefix, ContainerBuilder $container) : string + { + $processorServiceId = $serviceIdPrefix . '.processor'; + $type = $config['type'] ?? 'void'; + + switch ($type) { + case 'service': + $customServiceId = $config['service_id'] ?? null; + + if ($customServiceId === null) { + throw new RuntimeException('service_id is required when processor type is "service"'); + } + $container->setAlias($processorServiceId, $customServiceId); + + break; + + case 'void': + $container->setDefinition($processorServiceId, new Definition(VoidSpanProcessor::class)); + + break; + + case 'memory': + $exporterServiceId = $this->buildSpanExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(MemorySpanProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'batching': + $exporterServiceId = $this->buildSpanExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(BatchingSpanProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $definition->setArgument(1, $config['batch_size'] ?? 512); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'passthrough': + $exporterServiceId = $this->buildSpanExporter($config['exporter'] ?? [], $processorServiceId, $container); + $definition = new Definition(PassThroughSpanProcessor::class); + $definition->setArgument(0, new Reference($exporterServiceId)); + $container->setDefinition($processorServiceId, $definition); + + break; + + case 'composite': + $processors = $config['processors'] ?? []; + $processorRefs = []; + + foreach ($processors as $idx => $processorConfig) { + /** @var array $processorConfig */ + $subProcessorId = $this->buildSpanProcessor($processorConfig, $processorServiceId . '.' . $idx, $container); + $processorRefs[] = new Reference($subProcessorId); + } + $definition = new Definition(CompositeSpanProcessor::class); + $definition->setArgument(0, $processorRefs); + $container->setDefinition($processorServiceId, $definition); + + break; + + default: + throw new RuntimeException(\sprintf('Unknown span processor type: %s', (string) $type)); + } + + return $processorServiceId; + } + + /** + * @param array $config + */ + private function buildTracerProvider(array $config, string $instanceName, ContainerBuilder $container) : string + { + $providerServiceId = 'flow.telemetry.' . $instanceName . '.tracer_provider'; + + $processorServiceId = $this->buildSpanProcessor($config['processor'] ?? [], $providerServiceId, $container); + $samplerServiceId = $this->buildSampler($config['sampler'] ?? [], $instanceName, $container); + + $definition = new Definition(TracerProvider::class); + $definition->setArgument(0, new Reference($processorServiceId)); + $definition->setArgument(1, new Reference('flow.telemetry.clock')); + $definition->setArgument(2, new Reference('flow.telemetry.context_storage')); + $definition->setArgument(3, new Reference($samplerServiceId)); + $container->setDefinition($providerServiceId, $definition); + + return $providerServiceId; + } + + private function mapSeverity(string $severity) : Severity + { + return match ($severity) { + 'trace' => Severity::TRACE, + 'debug' => Severity::DEBUG, + 'info' => Severity::INFO, + 'warn' => Severity::WARN, + 'error' => Severity::ERROR, + 'fatal' => Severity::FATAL, + default => throw new RuntimeException(\sprintf('Unknown severity level: %s', $severity)), + }; + } + + private function registerGlobalServices(ContainerBuilder $container) : void + { + $container->setDefinition('flow.telemetry.clock', new Definition(SystemClock::class)); + $container->setDefinition('flow.telemetry.context_storage', new Definition(MemoryContextStorage::class)); + } + + /** + * @param array> $instances + */ + private function registerInstances(array $instances, ContainerBuilder $container) : void + { + if (\count($instances) === 0) { + $instances = ['default' => []]; + } + + foreach ($instances as $name => $config) { + $this->registerTelemetryInstance($name, $config, $container); + } + } + + /** + * @param array $serviceConfig + */ + private function registerResource(array $serviceConfig, ContainerBuilder $container) : void + { + $attributes = [ + 'service.name' => $serviceConfig['name'], + ]; + + if (isset($serviceConfig['version']) && $serviceConfig['version'] !== null) { + $attributes['service.version'] = $serviceConfig['version']; + } + + $additionalAttributes = $serviceConfig['attributes'] ?? []; + + foreach ($additionalAttributes as $key => $value) { + $attributes[(string) $key] = $value; + } + + $definition = new Definition(Resource::class); + $definition->setFactory([Resource::class, 'create']); + $definition->setArgument(0, $attributes); + $container->setDefinition('flow.telemetry.resource', $definition); + } + + /** + * @param array $config + */ + private function registerTelemetryInstance(string $name, array $config, ContainerBuilder $container) : void + { + $tracerProviderServiceId = $this->buildTracerProvider($config['tracer_provider'] ?? [], $name, $container); + $meterProviderServiceId = $this->buildMeterProvider($config['meter_provider'] ?? [], $name, $container); + $loggerProviderServiceId = $this->buildLoggerProvider($config['logger_provider'] ?? [], $name, $container); + + $telemetryServiceId = 'flow.telemetry.' . $name; + $definition = new Definition(Telemetry::class); + $definition->setArgument(0, new Reference('flow.telemetry.resource')); + $definition->setArgument(1, new Reference($tracerProviderServiceId)); + $definition->setArgument(2, new Reference($meterProviderServiceId)); + $definition->setArgument(3, new Reference($loggerProviderServiceId)); + $definition->setPublic(true); + $container->setDefinition($telemetryServiceId, $definition); + + if ($name === 'default') { + $container->setAlias(Telemetry::class, $telemetryServiceId)->setPublic(true); + } + } +} 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 8b67e3c36..3081edf78 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 @@ -4,8 +4,16 @@ namespace Flow\Bridge\Symfony\TelemetryBundle; -use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection\Compiler\OTLPAvailabilityPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; -final class FlowTelemetryBundle extends AbstractBundle +final class FlowTelemetryBundle extends Bundle { + public function build(ContainerBuilder $container) : void + { + parent::build($container); + + $container->addCompilerPass(new OTLPAvailabilityPass()); + } } diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php new file mode 100644 index 000000000..ad79b2fc5 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php @@ -0,0 +1,75 @@ +kernel !== null) { + $this->shutdown(); + } + + $this->kernel = new TestKernel('test', true); + + if (isset($options['config']) && \is_callable($options['config'])) { + $options['config']($this->kernel); + } + + $this->kernel->boot(); + + return $this->kernel; + } + + public function getContainer() : ContainerInterface + { + if ($this->kernel === null) { + throw new \LogicException('Kernel has not been booted. Call bootKernel() first.'); + } + + return $this->kernel->getContainer(); + } + + public function getKernel() : TestKernel + { + if ($this->kernel === null) { + throw new \LogicException('Kernel has not been booted. Call bootKernel() first.'); + } + + return $this->kernel; + } + + public function shutdown() : void + { + if ($this->kernel === null) { + return; + } + + $cacheDir = $this->kernel->getCacheDir(); + $logDir = $this->kernel->getLogDir(); + + $this->kernel->shutdown(); + $this->kernel = null; + + $filesystem = new Filesystem(); + + if ($filesystem->exists($cacheDir)) { + $filesystem->remove($cacheDir); + } + + if ($filesystem->exists($logDir)) { + $filesystem->remove($logDir); + } + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php new file mode 100644 index 000000000..8dd5c4259 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php @@ -0,0 +1,122 @@ +> */ + private array $testBundles = []; + + /** @var array */ + private array $testConfigs = []; + + /** @var array> */ + private array $testExtensionConfigs = []; + + private readonly string $testId; + + public function __construct(string $environment = 'test', bool $debug = true) + { + $this->testId = \bin2hex(\random_bytes(8)); + + parent::__construct($environment, $debug); + } + + /** + * @param class-string $bundleClass + */ + public function addTestBundle(string $bundleClass) : void + { + $this->testBundles[] = $bundleClass; + } + + public function addTestConfig(string $configPath) : void + { + $this->testConfigs[] = $configPath; + } + + /** + * @param array $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/Integration/.gitkeep b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php new file mode 100644 index 000000000..0833465a5 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php @@ -0,0 +1,715 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + '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.default.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'], + 'instances' => [ + 'default' => [ + '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.default.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'], + 'instances' => [ + 'default' => [ + '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.default.tracer_provider.processor')); + self::assertInstanceOf(MemorySpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor.0.processor')); + self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.default.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'], + 'instances' => [ + 'default' => [ + '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.default.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'], + 'instances' => [ + 'default' => [ + '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.default.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'], + 'instances' => [ + 'default' => [ + '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.default.tracer_provider.sampler') + ); + } + + public function test_default_instance_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.default'), $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', + ], + ], + 'instances' => [ + 'default' => [ + '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.default')); + + self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.default.tracer_provider')); + self::assertInstanceOf(TraceIdRatioBasedSampler::class, $container->get('flow.telemetry.default.tracer_provider.sampler')); + self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor')); + self::assertInstanceOf(ConsoleSpanExporter::class, $container->get('flow.telemetry.default.tracer_provider.processor.exporter')); + + self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.default.meter_provider')); + self::assertInstanceOf(PassThroughMetricProcessor::class, $container->get('flow.telemetry.default.meter_provider.processor')); + self::assertInstanceOf(MemoryMetricExporter::class, $container->get('flow.telemetry.default.meter_provider.processor.exporter')); + + self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.default.logger_provider')); + self::assertInstanceOf(MemoryLogProcessor::class, $container->get('flow.telemetry.default.logger_provider.processor')); + self::assertInstanceOf(ConsoleLogExporter::class, $container->get('flow.telemetry.default.logger_provider.processor.exporter')); + } + + public function test_log_exporter_types() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'with_void' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], + 'with_memory' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]]], + 'with_console' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]]], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(VoidLogExporter::class, $container->get('flow.telemetry.with_void.logger_provider.processor.exporter')); + self::assertInstanceOf(MemoryLogExporter::class, $container->get('flow.telemetry.with_memory.logger_provider.processor.exporter')); + self::assertInstanceOf(ConsoleLogExporter::class, $container->get('flow.telemetry.with_console.logger_provider.processor.exporter')); + } + + public function test_log_processor_types() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'with_void' => ['logger_provider' => ['processor' => ['type' => 'void']]], + 'with_memory' => ['logger_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]]], + 'with_batching' => ['logger_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 256, 'exporter' => ['type' => 'void']]]], + 'with_passthrough' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(VoidLogProcessor::class, $container->get('flow.telemetry.with_void.logger_provider.processor')); + self::assertInstanceOf(MemoryLogProcessor::class, $container->get('flow.telemetry.with_memory.logger_provider.processor')); + self::assertInstanceOf(BatchingLogProcessor::class, $container->get('flow.telemetry.with_batching.logger_provider.processor')); + self::assertInstanceOf(PassThroughLogProcessor::class, $container->get('flow.telemetry.with_passthrough.logger_provider.processor')); + } + + public function test_metric_exporter_types() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'with_void' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], + 'with_memory' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]]], + 'with_console' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]]], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(VoidMetricExporter::class, $container->get('flow.telemetry.with_void.meter_provider.processor.exporter')); + self::assertInstanceOf(MemoryMetricExporter::class, $container->get('flow.telemetry.with_memory.meter_provider.processor.exporter')); + self::assertInstanceOf(ConsoleMetricExporter::class, $container->get('flow.telemetry.with_console.meter_provider.processor.exporter')); + } + + public function test_metric_processor_types() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'with_void' => ['meter_provider' => ['processor' => ['type' => 'void']]], + 'with_memory' => ['meter_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]]], + 'with_batching' => ['meter_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 200, 'exporter' => ['type' => 'void']]]], + 'with_passthrough' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(VoidMetricProcessor::class, $container->get('flow.telemetry.with_void.meter_provider.processor')); + self::assertInstanceOf(MemoryMetricProcessor::class, $container->get('flow.telemetry.with_memory.meter_provider.processor')); + self::assertInstanceOf(BatchingMetricProcessor::class, $container->get('flow.telemetry.with_batching.meter_provider.processor')); + self::assertInstanceOf(PassThroughMetricProcessor::class, $container->get('flow.telemetry.with_passthrough.meter_provider.processor')); + } + + public function test_minimal_configuration_creates_default_instance_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.default')); + + 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.default')); + + self::assertTrue($container->has('flow.telemetry.default.tracer_provider')); + self::assertTrue($container->has('flow.telemetry.default.meter_provider')); + self::assertTrue($container->has('flow.telemetry.default.logger_provider')); + + self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.default.tracer_provider')); + self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.default.meter_provider')); + self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.default.logger_provider')); + + self::assertTrue($container->has('flow.telemetry.default.tracer_provider.processor')); + self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor')); + + self::assertTrue($container->has('flow.telemetry.default.meter_provider.processor')); + self::assertInstanceOf(VoidMetricProcessor::class, $container->get('flow.telemetry.default.meter_provider.processor')); + + self::assertTrue($container->has('flow.telemetry.default.logger_provider.processor')); + self::assertInstanceOf(VoidLogProcessor::class, $container->get('flow.telemetry.default.logger_provider.processor')); + } + + public function test_multiple_telemetry_instances() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'main' => [ + 'tracer_provider' => [ + 'processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']], + ], + ], + 'secondary' => [ + 'tracer_provider' => [ + 'processor' => ['type' => 'batching', 'exporter' => ['type' => 'memory']], + ], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.main')); + self::assertTrue($container->has('flow.telemetry.secondary')); + + self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry.main')); + self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry.secondary')); + + self::assertNotSame($container->get('flow.telemetry.main'), $container->get('flow.telemetry.secondary')); + + self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.main.tracer_provider.processor')); + self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.secondary.tracer_provider.processor')); + } + + 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_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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'sampler' => ['type' => 'service'], + ], + ], + ], + ]); + }, + ]); + } + + public function test_span_exporter_types() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'with_void' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], + 'with_memory' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]]], + 'with_console' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]]], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(VoidSpanExporter::class, $container->get('flow.telemetry.with_void.tracer_provider.processor.exporter')); + self::assertInstanceOf(MemorySpanExporter::class, $container->get('flow.telemetry.with_memory.tracer_provider.processor.exporter')); + self::assertInstanceOf(ConsoleSpanExporter::class, $container->get('flow.telemetry.with_console.tracer_provider.processor.exporter')); + } + + public function test_span_processor_types() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'with_void' => ['tracer_provider' => ['processor' => ['type' => 'void']]], + 'with_memory' => ['tracer_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]]], + 'with_batching' => ['tracer_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 100, 'exporter' => ['type' => 'void']]]], + 'with_passthrough' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.with_void.tracer_provider.processor')); + self::assertInstanceOf(MemorySpanProcessor::class, $container->get('flow.telemetry.with_memory.tracer_provider.processor')); + self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.with_batching.tracer_provider.processor')); + self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.with_passthrough.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_off'], + ], + ], + ], + ]); + }, + ]); + + self::assertInstanceOf(AlwaysOffSampler::class, $this->getContainer()->get('flow.telemetry.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_on'], + ], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.default.tracer_provider.sampler')); + self::assertInstanceOf(AlwaysOnSampler::class, $container->get('flow.telemetry.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'sampler' => ['type' => 'parent_based'], + ], + ], + ], + ]); + }, + ]); + + self::assertInstanceOf(ParentBasedSampler::class, $this->getContainer()->get('flow.telemetry.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.5, + ], + ], + ], + ], + ]); + }, + ]); + + self::assertInstanceOf(TraceIdRatioBasedSampler::class, $this->getContainer()->get('flow.telemetry.default.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 000000000..fb84da639 --- /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/Unit/.gitkeep b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/.gitkeep deleted file mode 100644 index e69de29bb..000000000 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 000000000..e5c5888d4 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,491 @@ +processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + [ + 'type' => 'memory', + ], + [ + 'type' => 'batching', + 'batch_size' => 100, + ], + ], + ], + ], + ], + ], + ]]); + + $processors = $config['instances']['default']['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_exporter_defaults_to_void() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + ], + ], + ], + ], + ]]); + + self::assertSame('void', $config['instances']['default']['tracer_provider']['processor']['exporter']['type']); + } + + public function test_instances_key_is_present_when_omitted() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + ]]); + + self::assertArrayHasKey('instances', $config); + self::assertSame([], $config['instances']); + } + + public function test_invalid_exporter_type_is_rejected() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'minimum_severity' => 'invalid_level', + 'inner_processor' => [ + 'type' => 'void', + ], + ], + ], + ], + ], + ]]); + } + + public function test_meter_provider_temporality_can_be_delta() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'meter_provider' => [ + 'temporality' => 'delta', + ], + ], + ], + ]]); + + self::assertSame('delta', $config['instances']['default']['meter_provider']['temporality']); + } + + public function test_meter_provider_temporality_defaults_to_cumulative() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'meter_provider' => [], + ], + ], + ]]); + + self::assertSame('cumulative', $config['instances']['default']['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_instances() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [], + 'secondary' => [ + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_off'], + ], + ], + ], + ]]); + + self::assertArrayHasKey('default', $config['instances']); + self::assertArrayHasKey('secondary', $config['instances']); + self::assertSame('always_off', $config['instances']['secondary']['tracer_provider']['sampler']['type']); + } + + public function test_otlp_serializer_defaults_to_json() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'otlp', + 'otlp' => [ + 'transport' => [], + ], + ], + ], + ], + ], + ], + ]]); + + $serializer = $config['instances']['default']['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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'otlp', + 'otlp' => [ + 'transport' => [], + ], + ], + ], + ], + ], + ], + ]]); + + $transport = $config['instances']['default']['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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + ], + ], + ], + ], + ]]); + + self::assertSame(512, $config['instances']['default']['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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [], + ], + ], + ]]); + + self::assertSame('void', $config['instances']['default']['tracer_provider']['processor']['type']); + } + + public function test_sampler_defaults_to_always_on() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [], + ], + ], + ]]); + + self::assertSame('always_on', $config['instances']['default']['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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.5, + ], + ], + ], + ], + ]]); + + self::assertSame(0.5, $config['instances']['default']['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'], + 'instances' => [ + 'default' => [ + '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'], + 'instances' => [ + 'default' => [ + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'inner_processor' => [ + 'type' => 'void', + ], + ], + ], + ], + ], + ]]); + + self::assertSame('info', $config['instances']['default']['logger_provider']['processor']['minimum_severity']); + } + + public function test_severity_filtering_processor_for_logs() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instances' => [ + 'default' => [ + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'minimum_severity' => 'warn', + 'inner_processor' => [ + 'type' => 'batching', + 'exporter' => ['type' => 'console'], + ], + ], + ], + ], + ], + ]]); + + $processor = $config['instances']['default']['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']); + } +} From 9220ecb61aa9d38cc821b62a9d8f49e489d90a28 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Thu, 5 Feb 2026 18:10:31 +0100 Subject: [PATCH 3/7] feature: hook in to symfony events in order to capture telemetry - capture console commands telemetry - capture messenger telemetry - capture http kernel telemetry --- composer.json | 13 +- composer.lock | 429 +++++++++++++++++- phpstan.neon | 1 + .../symfony/telemetry-bundle/composer.json | 9 +- .../DependencyInjection/Configuration.php | 18 + .../FlowTelemetryExtension.php | 33 +- .../Console/ConsoleEventSubscriber.php | 94 ++++ .../HttpKernel/HttpKernelEventSubscriber.php | 162 +++++++ .../Telemetry/Messenger/TracingMiddleware.php | 77 ++++ .../Tests/Context/SymfonyContext.php | 2 +- .../Tests/Fixtures/Command/FailingCommand.php | 19 + .../Tests/Fixtures/Command/TestCommand.php | 21 + .../Fixtures/Controller/TestController.php | 25 + .../Tests/Fixtures/Message/TestMessage.php | 13 + .../MessageHandler/TestMessageHandler.php | 19 + .../Tests/Fixtures/config/routes.php | 9 + .../Console/ConsoleEventSubscriberTest.php | 210 +++++++++ .../HttpKernelEventSubscriberTest.php | 205 +++++++++ .../Messenger/TracingMiddlewareTest.php | 214 +++++++++ .../DependencyInjection/ConfigurationTest.php | 42 ++ tools/phpunit/composer.lock | 15 +- 21 files changed, 1608 insertions(+), 22 deletions(-) create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/FailingCommand.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/TestCommand.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Controller/TestController.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Message/TestMessage.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/MessageHandler/TestMessageHandler.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/config/routes.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleEventSubscriberTest.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelEventSubscriberTest.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php diff --git a/composer.json b/composer.json index 7e433f95a..9ee392fd0 100644 --- a/composer.json +++ b/composer.json @@ -64,8 +64,11 @@ "symfony/cache": "^6.4 || ^7.3 || ^8.0", "symfony/dotenv": "^6.4 || ^7.3 || ^8.0", "symfony/finder": "^6.4 || ^7.3 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0", "symfony/http-client": "^6.4 || ^7.3 || ^8.0", - "symfony/process": "^7.3 || ^8.0" + "symfony/messenger": "^6.4 || ^7.3 || ^8.0", + "symfony/process": "^7.3 || ^8.0", + "symfony/routing": "^6.4 || ^7.3 || ^8.0" }, "replace": { "flow-php/array-dot": "self.version", @@ -101,11 +104,11 @@ "flow-php/parquet": "self.version", "flow-php/parquet-viewer": "self.version", "flow-php/postgresql": "self.version", + "flow-php/psr7-telemetry-bridge": "self.version", "flow-php/snappy": "self.version", "flow-php/symfony-http-foundation-bridge": "self.version", "flow-php/symfony-http-foundation-telemetry-bridge": "self.version", "flow-php/symfony-telemetry-bundle": "self.version", - "flow-php/psr7-telemetry-bridge": "self.version", "flow-php/telemetry": "self.version", "flow-php/telemetry-otlp-bridge": "self.version", "flow-php/types": "self.version" @@ -136,8 +139,8 @@ "src/bridge/monolog/telemetry/src/Flow", "src/bridge/openapi/specification/src/Flow", "src/bridge/psr7/telemetry/src/Flow", - "src/bridge/symfony/http-foundation/src/Flow", "src/bridge/symfony/http-foundation-telemetry/src/Flow", + "src/bridge/symfony/http-foundation/src/Flow", "src/bridge/symfony/telemetry-bundle/src/Flow", "src/bridge/telemetry/otlp/src/Flow", "src/cli/src/Flow", @@ -183,8 +186,8 @@ "src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/DSL/functions.php", "src/bridge/openapi/specification/src/Flow/Bridge/OpenAPI/Specification/DSL/functions.php", "src/bridge/psr7/telemetry/src/Flow/Bridge/Psr7/Telemetry/DSL/functions.php", - "src/bridge/symfony/http-foundation/src/Flow/Bridge/Symfony/HttpFoundation/functions.php", "src/bridge/symfony/http-foundation-telemetry/src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL/functions.php", + "src/bridge/symfony/http-foundation/src/Flow/Bridge/Symfony/HttpFoundation/functions.php", "src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php", "src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL/functions.php", "src/cli/src/Flow/CLI/DSL/functions.php", @@ -226,8 +229,8 @@ "src/bridge/monolog/telemetry/tests/Flow", "src/bridge/openapi/specification/tests/Flow", "src/bridge/psr7/telemetry/tests/Flow", - "src/bridge/symfony/http-foundation/tests/Flow", "src/bridge/symfony/http-foundation-telemetry/tests/Flow", + "src/bridge/symfony/http-foundation/tests/Flow", "src/bridge/symfony/telemetry-bundle/tests/Flow", "src/bridge/telemetry/otlp/tests/Flow", "src/cli/tests/Flow", diff --git a/composer.lock b/composer.lock index 823015eaf..66cea4ae3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5176f122d36144e7d5f913e500f8acf7", + "content-hash": "776e2d95004d1b4d3d4ceeccc514973e", "packages": [ { "name": "async-aws/core", @@ -141,16 +141,16 @@ }, { "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": { @@ -189,7 +189,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": [ { @@ -197,7 +197,7 @@ "type": "github" } ], - "time": "2026-02-02T16:57:31+00:00" + "time": "2026-02-05T07:59:58+00:00" }, { "name": "coduo/php-humanizer", @@ -6080,6 +6080,84 @@ ], "time": "2025-03-13T15:25:07+00:00" }, + { + "name": "symfony/clock", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+00:00" + }, { "name": "symfony/dotenv", "version": "v7.4.0", @@ -6226,6 +6304,258 @@ ], "time": "2026-01-26T15:07:59+00:00" }, + { + "name": "symfony/framework-bundle", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd", + "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.4|^8.0.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/asset": "<6.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.4", + "symfony/console": "<6.4", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/form": "<7.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<7.4", + "symfony/mime": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.2.5", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<7.3", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/webhook": "<7.2", + "symfony/workflow": "<7.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2", + "seld/jsonlint": "^1.10", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", + "twig/twig": "^3.12" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T08:59:58+00:00" + }, + { + "name": "symfony/messenger", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/messenger.git", + "reference": "0a39e1b256f280762293f2f441e430c8baf74f9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/messenger/zipball/0a39e1b256f280762293f2f441e430c8baf74f9c", + "reference": "0a39e1b256f280762293f2f441e430c8baf74f9c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<7.2", + "symfony/event-dispatcher": "<6.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<7.3", + "symfony/lock": "<7.4", + "symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^7.2|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^7.3|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-08T14:50:10+00:00" + }, { "name": "symfony/options-resolver", "version": "v7.4.0", @@ -6445,6 +6775,91 @@ } ], "time": "2026-01-26T15:07:59+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-12T12:19:02+00:00" } ], "aliases": [], diff --git a/phpstan.neon b/phpstan.neon index bad4ee47e..6549e45f3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -88,6 +88,7 @@ parameters: - src/lib/parquet/src/Flow/Parquet/Dremel/ColumnData/DefinitionConverter.php - src/lib/postgresql/src/Flow/PostgreSql/Protobuf/* - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php + - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php tmpDir: var/phpstan/cache diff --git a/src/bridge/symfony/telemetry-bundle/composer.json b/src/bridge/symfony/telemetry-bundle/composer.json index 69a754fd1..4b8771280 100644 --- a/src/bridge/symfony/telemetry-bundle/composer.json +++ b/src/bridge/symfony/telemetry-bundle/composer.json @@ -24,8 +24,15 @@ "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/messenger": "^6.4 || ^7.3 || ^8.0", + "symfony/framework-bundle": "^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" + "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support", + "symfony/messenger": "Required for Messenger tracing middleware" }, "autoload": { "psr-4": { diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php index 10ff5a76a..782d63fd0 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -88,6 +88,24 @@ public function getConfigTreeBuilder() : TreeBuilder ->end() ->end() ->end() + ->arrayNode('instrumentation') + ->info('Auto-instrumentation configuration') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('http_kernel') + ->info('Enable automatic tracing of HTTP requests') + ->defaultFalse() + ->end() + ->booleanNode('console') + ->info('Enable automatic tracing of console commands') + ->defaultFalse() + ->end() + ->booleanNode('messenger') + ->info('Enable automatic tracing of Messenger messages') + ->defaultFalse() + ->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php index 459741a9a..279f2a9d9 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -5,6 +5,9 @@ namespace Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection; use Flow\Bridge\Symfony\TelemetryBundle\Exception\RuntimeException; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\ConsoleEventSubscriber; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\HttpKernelEventSubscriber; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Messenger\TracingMiddleware; use Flow\Telemetry\Context\MemoryContextStorage; use Flow\Telemetry\Logger\{LoggerProvider, Severity}; use Flow\Telemetry\Logger\Processor\{BatchingLogProcessor, CompositeLogProcessor, PassThroughLogProcessor, SeverityFilteringLogProcessor}; @@ -20,6 +23,7 @@ use Flow\Telemetry\Tracer\TracerProvider; use Symfony\Component\DependencyInjection\{ContainerBuilder, Definition, Reference}; use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\Messenger\Middleware\MiddlewareInterface; final class FlowTelemetryExtension extends Extension { @@ -29,12 +33,13 @@ final class FlowTelemetryExtension extends Extension public function load(array $configs, ContainerBuilder $container) : void { $configuration = new Configuration(); - /** @var array{service: array, instances?: array>} $config */ + /** @var array{service: array, instances?: array>, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool}} $config */ $config = $this->processConfiguration($configuration, $configs); $this->registerGlobalServices($container); $this->registerResource($config['service'], $container); $this->registerInstances($config['instances'] ?? [], $container); + $this->registerInstrumentation($config['instrumentation'] ?? [], $container); } /** @@ -717,6 +722,32 @@ private function registerInstances(array $instances, ContainerBuilder $container } } + /** + * @param array{http_kernel?: bool, console?: bool, messenger?: bool} $config + */ + private function registerInstrumentation(array $config, ContainerBuilder $container) : void + { + if ($config['http_kernel'] ?? true) { + $definition = new Definition(HttpKernelEventSubscriber::class); + $definition->setArgument(0, new Reference(Telemetry::class)); + $definition->addTag('kernel.event_subscriber'); + $container->setDefinition('flow.telemetry.http_kernel.subscriber', $definition); + } + + if ($config['console'] ?? true) { + $definition = new Definition(ConsoleEventSubscriber::class); + $definition->setArgument(0, new Reference(Telemetry::class)); + $definition->addTag('kernel.event_subscriber'); + $container->setDefinition('flow.telemetry.console.subscriber', $definition); + } + + if (($config['messenger'] ?? true) && \interface_exists(MiddlewareInterface::class)) { + $definition = new Definition(TracingMiddleware::class); + $definition->setArgument(0, new Reference(Telemetry::class)); + $container->setDefinition('flow.telemetry.messenger.middleware', $definition); + } + } + /** * @param array $serviceConfig */ diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php new file mode 100644 index 000000000..f0b90b313 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php @@ -0,0 +1,94 @@ + ['onCommand', 10000], + ConsoleEvents::ERROR => ['onError', 0], + ConsoleEvents::TERMINATE => ['onTerminate', -10000], + ConsoleEvents::SIGNAL => ['onSignal', 0], + ]; + } + + public function onCommand(ConsoleCommandEvent $event) : void + { + $command = $event->getCommand(); + $commandName = $command?->getName() ?? 'unknown'; + + $this->tracer = $this->telemetry->tracer('flow.symfony.console'); + + $attributes = [ + 'code.function' => $commandName, + ]; + + if ($command !== null) { + $attributes['code.namespace'] = $command::class; + } + + $this->span = $this->tracer->span( + $commandName, + SpanKind::INTERNAL, + $attributes, + ); + } + + public function onError(ConsoleErrorEvent $event) : void + { + if ($this->span === null) { + return; + } + + $this->span->recordException($event->getError(), new \DateTimeImmutable()); + } + + public function onSignal(ConsoleSignalEvent $event) : void + { + if ($this->span === null) { + return; + } + + $this->span->setAttribute('process.signal', $event->getHandlingSignal()); + } + + public function onTerminate(ConsoleTerminateEvent $event) : void + { + if ($this->span === null || $this->tracer === null) { + return; + } + + $exitCode = $event->getExitCode(); + $this->span->setAttribute('process.exit_code', $exitCode); + + if ($exitCode === 0) { + $this->span->setStatus(SpanStatus::ok()); + } else { + $this->span->setStatus(SpanStatus::error("Exit code: {$exitCode}")); + } + + $this->tracer->complete($this->span); + + $this->span = null; + $this->tracer = null; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php new file mode 100644 index 000000000..e69c58781 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php @@ -0,0 +1,162 @@ + ['onRequest', 10000], + KernelEvents::CONTROLLER => ['onController', 0], + KernelEvents::RESPONSE => ['onResponse', -10000], + KernelEvents::EXCEPTION => ['onException', 0], + KernelEvents::TERMINATE => ['onTerminate', -10000], + ]; + } + + public function onController(ControllerEvent $event) : void + { + $request = $event->getRequest(); + $span = $request->attributes->get(self::SPAN_ATTRIBUTE); + + if (!$span instanceof Span) { + return; + } + + $route = $request->attributes->get('_route'); + + if (\is_string($route)) { + $span->setAttribute('http.route', $route); + $method = $request->getMethod(); + $span->rename("{$method} {$route}"); + } + + $controller = $event->getController(); + $controllerName = $this->resolveControllerName($controller); + + if ($controllerName !== null) { + $span->setAttribute('code.function', $controllerName); + } + } + + public function onException(ExceptionEvent $event) : void + { + $request = $event->getRequest(); + $span = $request->attributes->get(self::SPAN_ATTRIBUTE); + + if (!$span instanceof Span) { + return; + } + + $span->recordException($event->getThrowable(), new \DateTimeImmutable()); + } + + public function onRequest(RequestEvent $event) : void + { + $request = $event->getRequest(); + + $kind = $event->isMainRequest() ? SpanKind::SERVER : SpanKind::INTERNAL; + $method = $request->getMethod(); + $path = $request->getPathInfo(); + + $tracer = $this->telemetry->tracer('flow.symfony.http_kernel'); + $span = $tracer->span( + "{$method} {$path}", + $kind, + [ + 'http.method' => $method, + 'http.url' => $request->getUri(), + 'http.target' => $request->getRequestUri(), + 'http.scheme' => $request->getScheme(), + 'http.host' => $request->getHost(), + ], + ); + + $request->attributes->set(self::SPAN_ATTRIBUTE, $span); + $request->attributes->set(self::TRACER_ATTRIBUTE, $tracer); + } + + public function onResponse(ResponseEvent $event) : void + { + $request = $event->getRequest(); + $span = $request->attributes->get(self::SPAN_ATTRIBUTE); + + if (!$span instanceof Span) { + return; + } + + $response = $event->getResponse(); + $statusCode = $response->getStatusCode(); + + $span->setAttribute('http.status_code', $statusCode); + + if ($statusCode >= 400) { + $span->setStatus(SpanStatus::error("HTTP {$statusCode}")); + } else { + $span->setStatus(SpanStatus::ok()); + } + } + + public function onTerminate(TerminateEvent $event) : void + { + $request = $event->getRequest(); + $span = $request->attributes->get(self::SPAN_ATTRIBUTE); + $tracer = $request->attributes->get(self::TRACER_ATTRIBUTE); + + if (!$span instanceof Span || !$tracer instanceof Tracer) { + return; + } + + $tracer->complete($span); + + $request->attributes->remove(self::SPAN_ATTRIBUTE); + $request->attributes->remove(self::TRACER_ATTRIBUTE); + } + + /** + * @param array|callable|object $controller + */ + private function resolveControllerName(callable|object|array $controller) : ?string + { + if (\is_array($controller) && \count($controller) === 2) { + $firstElement = $controller[0]; + $secondElement = $controller[1]; + $class = \is_object($firstElement) ? $firstElement::class : (\is_string($firstElement) ? $firstElement : ''); + $method = \is_string($secondElement) ? $secondElement : ''; + + return "{$class}::{$method}"; + } + + if (\is_object($controller)) { + if ($controller instanceof \Closure) { + return 'Closure'; + } + + return $controller::class . '::__invoke'; + } + + if (\is_string($controller)) { + return $controller; + } + + return null; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php new file mode 100644 index 000000000..de85426e5 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php @@ -0,0 +1,77 @@ +telemetry->tracer('flow.symfony.messenger'); + + $message = $envelope->getMessage(); + $messageClass = $message::class; + $shortMessageClass = $this->getShortClassName($messageClass); + + $receivedStamp = $envelope->last(ReceivedStamp::class); + $busNameStamp = $envelope->last(BusNameStamp::class); + $transportIdStamp = $envelope->last(TransportMessageIdStamp::class); + + $isReceived = $receivedStamp !== null; + $kind = $isReceived ? SpanKind::CONSUMER : SpanKind::PRODUCER; + $operation = $isReceived ? 'receive' : 'send'; + + $busName = $busNameStamp instanceof BusNameStamp ? $busNameStamp->getBusName() : 'default'; + $spanName = "{$busName} {$shortMessageClass}"; + + $attributes = [ + 'messaging.system' => 'symfony_messenger', + 'messaging.destination' => $busName, + 'messaging.message.class' => $messageClass, + 'messaging.operation' => $operation, + ]; + + if ($receivedStamp instanceof ReceivedStamp) { + $attributes['messaging.transport'] = $receivedStamp->getTransportName(); + } + + if ($transportIdStamp instanceof TransportMessageIdStamp) { + $attributes['messaging.message.id'] = (string) $transportIdStamp->getId(); + } + + $span = $tracer->span($spanName, $kind, $attributes); + + try { + $result = $stack->next()->handle($envelope, $stack); + $span->setStatus(SpanStatus::ok()); + + return $result; + } catch (\Throwable $e) { + $span->recordException($e, new \DateTimeImmutable()); + $span->setStatus(SpanStatus::error($e->getMessage())); + + throw $e; + } finally { + $tracer->complete($span); + } + } + + private function getShortClassName(string $className) : string + { + $parts = \explode('\\', $className); + + return \end($parts); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php index ad79b2fc5..2a53d1c91 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php @@ -21,7 +21,7 @@ public function bootKernel(array $options = []) : TestKernel $this->shutdown(); } - $this->kernel = new TestKernel('test', true); + $this->kernel = new TestKernel('test', false); if (isset($options['config']) && \is_callable($options['config'])) { $options['config']($this->kernel); diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/FailingCommand.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/FailingCommand.php new file mode 100644 index 000000000..c3f4dcf25 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/FailingCommand.php @@ -0,0 +1,19 @@ +writeln('Test command executed'); + + return Command::SUCCESS; + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Controller/TestController.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Controller/TestController.php new file mode 100644 index 000000000..8d18437d7 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Controller/TestController.php @@ -0,0 +1,25 @@ + 'not found'], 404); + } + + public function exception() : Response + { + throw new \RuntimeException('Test exception'); + } + + public function index() : Response + { + return new JsonResponse(['status' => 'ok']); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Message/TestMessage.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Message/TestMessage.php new file mode 100644 index 000000000..40713b14e --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Message/TestMessage.php @@ -0,0 +1,13 @@ +handled = 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 000000000..ecd309f0f --- /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->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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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.default.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(0, $spans); + } + + 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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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.default.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['code.function']); + self::assertSame(TestCommand::class, $attributes['code.namespace']); + 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/HttpKernelEventSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelEventSubscriberTest.php new file mode 100644 index 000000000..aac3b478a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelEventSubscriberTest.php @@ -0,0 +1,205 @@ +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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + 'http_kernel' => true, + 'console' => 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.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + 'http_kernel' => true, + 'console' => 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.default.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['code.function']); + } +} 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 000000000..b6fb60f32 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php @@ -0,0 +1,214 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instrumentation' => [ + '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'], + 'instrumentation' => [ + '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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + '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.default.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'], + 'instances' => [ + 'default' => [ + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + ], + ], + 'instrumentation' => [ + '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.default.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/Unit/DependencyInjection/ConfigurationTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php index e5c5888d4..49e8461dc 100644 --- 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 @@ -79,6 +79,48 @@ public function test_instances_key_is_present_when_omitted() : void self::assertSame([], $config['instances']); } + public function test_instrumentation_can_be_enabled() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instrumentation' => [ + 'http_kernel' => true, + 'console' => true, + 'messenger' => true, + ], + ]]); + + self::assertTrue($config['instrumentation']['http_kernel']); + self::assertTrue($config['instrumentation']['console']); + self::assertTrue($config['instrumentation']['messenger']); + } + + public function test_instrumentation_defaults_to_all_disabled() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + ]]); + + self::assertArrayHasKey('instrumentation', $config); + self::assertFalse($config['instrumentation']['http_kernel']); + self::assertFalse($config['instrumentation']['console']); + self::assertFalse($config['instrumentation']['messenger']); + } + + public function test_instrumentation_partial_config() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'instrumentation' => [ + 'http_kernel' => true, + ], + ]]); + + self::assertTrue($config['instrumentation']['http_kernel']); + self::assertFalse($config['instrumentation']['console']); + self::assertFalse($config['instrumentation']['messenger']); + } + public function test_invalid_exporter_type_is_rejected() : void { $this->expectException(InvalidConfigurationException::class); diff --git a/tools/phpunit/composer.lock b/tools/phpunit/composer.lock index d38d15374..c7026c892 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", From cb51c94ee9c1fb4f3dcf7b496653530c0fcbc71c Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sat, 7 Feb 2026 10:48:58 +0100 Subject: [PATCH 4/7] refactor: allow to register only one instance of telemetry in service container - updated dependencies in website to be links from monorepo --- .../Compiler/OTLPAvailabilityPass.php | 2 +- .../DependencyInjection/Configuration.php | 83 ++- .../FlowTelemetryExtension.php | 108 ++-- .../Console/ConsoleFlushSubscriber.php | 30 + ...bscriber.php => ConsoleSpanSubscriber.php} | 6 +- .../HttpKernel/HttpKernelFlushSubscriber.php | 30 + ...riber.php => HttpKernelSpanSubscriber.php} | 4 +- .../FlowTelemetryExtensionTest.php | 588 ++++++++++-------- .../Console/ConsoleFlushSubscriberTest.php | 127 ++++ ...Test.php => ConsoleSpanSubscriberTest.php} | 53 +- .../HttpKernelFlushSubscriberTest.php | 129 ++++ ...t.php => HttpKernelSpanSubscriberTest.php} | 51 +- .../Messenger/TracingMiddlewareTest.php | 28 +- .../DependencyInjection/ConfigurationTest.php | 313 ++++------ .../assets/codemirror/completions/dsl.js | 181 +++++- web/landing/composer.json | 61 +- web/landing/composer.lock | 504 +++++++++------ web/landing/config/bundles.php | 1 + .../config/packages/flow_telemetry.yaml | 53 ++ web/landing/config/packages/monolog.yaml | 4 + web/landing/config/services.yaml | 12 +- .../templates/documentation/dsl.html.twig | 6 +- .../Tests/Integration/DocumentationTest.php | 1 + 23 files changed, 1531 insertions(+), 844 deletions(-) create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleFlushSubscriber.php rename src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/{ConsoleEventSubscriber.php => ConsoleSpanSubscriber.php} (93%) create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelFlushSubscriber.php rename src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/{HttpKernelEventSubscriber.php => HttpKernelSpanSubscriber.php} (97%) create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php rename src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/{ConsoleEventSubscriberTest.php => ConsoleSpanSubscriberTest.php} (79%) create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php rename src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/{HttpKernelEventSubscriberTest.php => HttpKernelSpanSubscriberTest.php} (80%) create mode 100644 web/landing/config/packages/flow_telemetry.yaml diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php index bb7d48a0a..b2b3fd744 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Compiler/OTLPAvailabilityPass.php @@ -10,7 +10,7 @@ final class OTLPAvailabilityPass implements CompilerPassInterface { - private const string OTLP_BRIDGE_CLASS = 'Flow\\Bridge\\Telemetry\\OTLP\\SpanExporter\\OTLPSpanExporter'; + private const string OTLP_BRIDGE_CLASS = 'Flow\\Bridge\\Telemetry\\OTLP\\Exporter\\OTLPSpanExporter'; public function process(ContainerBuilder $container) : void { diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php index 782d63fd0..00280b3e0 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -37,55 +37,50 @@ public function getConfigTreeBuilder() : TreeBuilder ->end() ->end() ->end() - ->arrayNode('instances') - ->info('Telemetry instances configuration. If omitted, a "default" instance with void processors is created.') - ->useAttributeAsKey('name') - ->arrayPrototype() - ->children() - ->arrayNode('tracer_provider') - ->info('TracerProvider configuration. Defaults to void if omitted.') - ->children() - ->arrayNode('sampler') - ->info('Trace sampler configuration') - ->addDefaultsIfNotSet() - ->children() - ->enumNode('type') - ->values(['always_on', 'always_off', 'trace_id_ratio', 'parent_based', 'service']) - ->defaultValue('always_on') - ->end() - ->floatNode('ratio') - ->info('Sampling ratio for trace_id_ratio type (0.0 to 1.0)') - ->defaultValue(1.0) - ->min(0.0) - ->max(1.0) - ->end() - ->scalarNode('service_id') - ->info('Custom sampler service ID (only for type: service)') - ->defaultNull() - ->end() - ->end() - ->end() - ->append($this->processorNode('span')) + ->arrayNode('tracer_provider') + ->info('TracerProvider configuration. Defaults to void if omitted.') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('sampler') + ->info('Trace sampler configuration') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('type') + ->values(['always_on', 'always_off', 'trace_id_ratio', 'parent_based', 'service']) + ->defaultValue('always_on') ->end() - ->end() - ->arrayNode('meter_provider') - ->info('MeterProvider configuration. Defaults to void if omitted.') - ->children() - ->enumNode('temporality') - ->info('Aggregation temporality') - ->values(['cumulative', 'delta']) - ->defaultValue('cumulative') - ->end() - ->append($this->processorNode('metric')) + ->floatNode('ratio') + ->info('Sampling ratio for trace_id_ratio type (0.0 to 1.0)') + ->defaultValue(1.0) + ->min(0.0) + ->max(1.0) ->end() - ->end() - ->arrayNode('logger_provider') - ->info('LoggerProvider configuration. Defaults to void if omitted.') - ->children() - ->append($this->processorNode('log')) + ->scalarNode('service_id') + ->info('Custom sampler service ID (only for type: service)') + ->defaultNull() ->end() ->end() ->end() + ->append($this->processorNode('span')) + ->end() + ->end() + ->arrayNode('meter_provider') + ->info('MeterProvider configuration. Defaults to void if omitted.') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('temporality') + ->info('Aggregation temporality') + ->values(['cumulative', 'delta']) + ->defaultValue('cumulative') + ->end() + ->append($this->processorNode('metric')) + ->end() + ->end() + ->arrayNode('logger_provider') + ->info('LoggerProvider configuration. Defaults to void if omitted.') + ->addDefaultsIfNotSet() + ->children() + ->append($this->processorNode('log')) ->end() ->end() ->arrayNode('instrumentation') diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php index 279f2a9d9..7813766a6 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -5,8 +5,8 @@ namespace Flow\Bridge\Symfony\TelemetryBundle\DependencyInjection; use Flow\Bridge\Symfony\TelemetryBundle\Exception\RuntimeException; -use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\ConsoleEventSubscriber; -use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\HttpKernelEventSubscriber; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\{ConsoleFlushSubscriber, ConsoleSpanSubscriber}; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\{HttpKernelFlushSubscriber, HttpKernelSpanSubscriber}; use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Messenger\TracingMiddleware; use Flow\Telemetry\Context\MemoryContextStorage; use Flow\Telemetry\Logger\{LoggerProvider, Severity}; @@ -33,12 +33,12 @@ final class FlowTelemetryExtension extends Extension public function load(array $configs, ContainerBuilder $container) : void { $configuration = new Configuration(); - /** @var array{service: array, instances?: array>, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool}} $config */ + /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool}} $config */ $config = $this->processConfiguration($configuration, $configs); $this->registerGlobalServices($container); $this->registerResource($config['service'], $container); - $this->registerInstances($config['instances'] ?? [], $container); + $this->registerTelemetry($config, $container); $this->registerInstrumentation($config['instrumentation'] ?? [], $container); } @@ -135,7 +135,7 @@ private function buildLogExporter(array $config, string $serviceIdPrefix, Contai case 'otlp': $container->setParameter('flow.telemetry.otlp_configured', true); $transportServiceId = $this->buildOTLPTransport($config['otlp']['transport'] ?? [], $exporterServiceId, $container); - $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\LogExporter\\OTLPLogExporter'); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Exporter\\OTLPLogExporter'); $definition->setArgument(0, new Reference($transportServiceId)); $container->setDefinition($exporterServiceId, $definition); @@ -151,9 +151,9 @@ private function buildLogExporter(array $config, string $serviceIdPrefix, Contai /** * @param array $config */ - private function buildLoggerProvider(array $config, string $instanceName, ContainerBuilder $container) : string + private function buildLoggerProvider(array $config, ContainerBuilder $container) : string { - $providerServiceId = 'flow.telemetry.' . $instanceName . '.logger_provider'; + $providerServiceId = 'flow.telemetry.logger_provider'; $processorServiceId = $this->buildLogProcessor($config['processor'] ?? [], $providerServiceId, $container); @@ -251,9 +251,9 @@ private function buildLogProcessor(array $config, string $serviceIdPrefix, Conta /** * @param array $config */ - private function buildMeterProvider(array $config, string $instanceName, ContainerBuilder $container) : string + private function buildMeterProvider(array $config, ContainerBuilder $container) : string { - $providerServiceId = 'flow.telemetry.' . $instanceName . '.meter_provider'; + $providerServiceId = 'flow.telemetry.meter_provider'; $processorServiceId = $this->buildMetricProcessor($config['processor'] ?? [], $providerServiceId, $container); @@ -307,7 +307,7 @@ private function buildMetricExporter(array $config, string $serviceIdPrefix, Con case 'otlp': $container->setParameter('flow.telemetry.otlp_configured', true); $transportServiceId = $this->buildOTLPTransport($config['otlp']['transport'] ?? [], $exporterServiceId, $container); - $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\MetricExporter\\OTLPMetricExporter'); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Exporter\\OTLPMetricExporter'); $definition->setArgument(0, new Reference($transportServiceId)); $container->setDefinition($exporterServiceId, $definition); @@ -456,21 +456,31 @@ private function buildOTLPTransport(array $config, string $serviceIdPrefix, Cont switch ($type) { case 'curl': + $optionsServiceId = $transportServiceId . '.options'; + $optionsDefinition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\CurlTransportOptions'); + $optionsDefinition->addMethodCall('withTimeout', [$timeout]); + + foreach ($headers as $headerName => $headerValue) { + $optionsDefinition->addMethodCall('withHeader', [(string) $headerName, (string) $headerValue]); + } + $container->setDefinition($optionsServiceId, $optionsDefinition); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\CurlTransport'); $definition->setArgument(0, $endpoint); $definition->setArgument(1, new Reference($serializerServiceId)); - $definition->setArgument(2, $timeout); - $definition->setArgument(3, $headers); + $definition->setArgument(2, new Reference($optionsServiceId)); $container->setDefinition($transportServiceId, $definition); break; case 'http': $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\HttpTransport'); - $definition->setArgument(0, $endpoint); - $definition->setArgument(1, new Reference($serializerServiceId)); - $definition->setArgument(2, $timeout); - $definition->setArgument(3, $headers); + $definition->setArgument('$httpClient', new Reference('psr18.http_client')); + $definition->setArgument('$requestFactory', new Reference('psr17.request_factory')); + $definition->setArgument('$streamFactory', new Reference('psr17.stream_factory')); + $definition->setArgument('$endpoint', $endpoint); + $definition->setArgument('$serializer', new Reference($serializerServiceId)); + $definition->setArgument('$headers', $headers); $container->setDefinition($transportServiceId, $definition); break; @@ -497,9 +507,9 @@ private function buildOTLPTransport(array $config, string $serviceIdPrefix, Cont /** * @param array $config */ - private function buildSampler(array $config, string $instanceName, ContainerBuilder $container) : string + private function buildSampler(array $config, ContainerBuilder $container) : string { - $samplerServiceId = 'flow.telemetry.' . $instanceName . '.tracer_provider.sampler'; + $samplerServiceId = 'flow.telemetry.tracer_provider.sampler'; $type = $config['type'] ?? 'always_on'; switch ($type) { @@ -585,7 +595,7 @@ private function buildSpanExporter(array $config, string $serviceIdPrefix, Conta case 'otlp': $container->setParameter('flow.telemetry.otlp_configured', true); $transportServiceId = $this->buildOTLPTransport($config['otlp']['transport'] ?? [], $exporterServiceId, $container); - $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\SpanExporter\\OTLPSpanExporter'); + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Exporter\\OTLPSpanExporter'); $definition->setArgument(0, new Reference($transportServiceId)); $container->setDefinition($exporterServiceId, $definition); @@ -672,12 +682,12 @@ private function buildSpanProcessor(array $config, string $serviceIdPrefix, Cont /** * @param array $config */ - private function buildTracerProvider(array $config, string $instanceName, ContainerBuilder $container) : string + private function buildTracerProvider(array $config, ContainerBuilder $container) : string { - $providerServiceId = 'flow.telemetry.' . $instanceName . '.tracer_provider'; + $providerServiceId = 'flow.telemetry.tracer_provider'; $processorServiceId = $this->buildSpanProcessor($config['processor'] ?? [], $providerServiceId, $container); - $samplerServiceId = $this->buildSampler($config['sampler'] ?? [], $instanceName, $container); + $samplerServiceId = $this->buildSampler($config['sampler'] ?? [], $container); $definition = new Definition(TracerProvider::class); $definition->setArgument(0, new Reference($processorServiceId)); @@ -708,37 +718,33 @@ private function registerGlobalServices(ContainerBuilder $container) : void $container->setDefinition('flow.telemetry.context_storage', new Definition(MemoryContextStorage::class)); } - /** - * @param array> $instances - */ - private function registerInstances(array $instances, ContainerBuilder $container) : void - { - if (\count($instances) === 0) { - $instances = ['default' => []]; - } - - foreach ($instances as $name => $config) { - $this->registerTelemetryInstance($name, $config, $container); - } - } - /** * @param array{http_kernel?: bool, console?: bool, messenger?: bool} $config */ private function registerInstrumentation(array $config, ContainerBuilder $container) : void { if ($config['http_kernel'] ?? true) { - $definition = new Definition(HttpKernelEventSubscriber::class); - $definition->setArgument(0, new Reference(Telemetry::class)); - $definition->addTag('kernel.event_subscriber'); - $container->setDefinition('flow.telemetry.http_kernel.subscriber', $definition); + $spanDefinition = new Definition(HttpKernelSpanSubscriber::class); + $spanDefinition->setArgument(0, new Reference(Telemetry::class)); + $spanDefinition->addTag('kernel.event_subscriber'); + $container->setDefinition('flow.telemetry.http_kernel.span_subscriber', $spanDefinition); + + $flushDefinition = new Definition(HttpKernelFlushSubscriber::class); + $flushDefinition->setArgument(0, new Reference(Telemetry::class)); + $flushDefinition->addTag('kernel.event_subscriber'); + $container->setDefinition('flow.telemetry.http_kernel.flush_subscriber', $flushDefinition); } if ($config['console'] ?? true) { - $definition = new Definition(ConsoleEventSubscriber::class); - $definition->setArgument(0, new Reference(Telemetry::class)); - $definition->addTag('kernel.event_subscriber'); - $container->setDefinition('flow.telemetry.console.subscriber', $definition); + $spanDefinition = new Definition(ConsoleSpanSubscriber::class); + $spanDefinition->setArgument(0, new Reference(Telemetry::class)); + $spanDefinition->addTag('kernel.event_subscriber'); + $container->setDefinition('flow.telemetry.console.span_subscriber', $spanDefinition); + + $flushDefinition = new Definition(ConsoleFlushSubscriber::class); + $flushDefinition->setArgument(0, new Reference(Telemetry::class)); + $flushDefinition->addTag('kernel.event_subscriber'); + $container->setDefinition('flow.telemetry.console.flush_subscriber', $flushDefinition); } if (($config['messenger'] ?? true) && \interface_exists(MiddlewareInterface::class)) { @@ -776,13 +782,13 @@ private function registerResource(array $serviceConfig, ContainerBuilder $contai /** * @param array $config */ - private function registerTelemetryInstance(string $name, array $config, ContainerBuilder $container) : void + private function registerTelemetry(array $config, ContainerBuilder $container) : void { - $tracerProviderServiceId = $this->buildTracerProvider($config['tracer_provider'] ?? [], $name, $container); - $meterProviderServiceId = $this->buildMeterProvider($config['meter_provider'] ?? [], $name, $container); - $loggerProviderServiceId = $this->buildLoggerProvider($config['logger_provider'] ?? [], $name, $container); + $tracerProviderServiceId = $this->buildTracerProvider($config['tracer_provider'] ?? [], $container); + $meterProviderServiceId = $this->buildMeterProvider($config['meter_provider'] ?? [], $container); + $loggerProviderServiceId = $this->buildLoggerProvider($config['logger_provider'] ?? [], $container); - $telemetryServiceId = 'flow.telemetry.' . $name; + $telemetryServiceId = 'flow.telemetry'; $definition = new Definition(Telemetry::class); $definition->setArgument(0, new Reference('flow.telemetry.resource')); $definition->setArgument(1, new Reference($tracerProviderServiceId)); @@ -791,8 +797,6 @@ private function registerTelemetryInstance(string $name, array $config, Containe $definition->setPublic(true); $container->setDefinition($telemetryServiceId, $definition); - if ($name === 'default') { - $container->setAlias(Telemetry::class, $telemetryServiceId)->setPublic(true); - } + $container->setAlias(Telemetry::class, $telemetryServiceId)->setPublic(true); } } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleFlushSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleFlushSubscriber.php new file mode 100644 index 000000000..fd2868b10 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleFlushSubscriber.php @@ -0,0 +1,30 @@ + ['onTerminate', -20000], + ]; + } + + public function onTerminate(ConsoleTerminateEvent $event) : void + { + $this->telemetry->shutdown(); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php similarity index 93% rename from src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php rename to src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php index f0b90b313..16abeed16 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleEventSubscriber.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php @@ -10,7 +10,7 @@ use Symfony\Component\Console\Event\{ConsoleCommandEvent, ConsoleErrorEvent, ConsoleSignalEvent, ConsoleTerminateEvent}; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -final class ConsoleEventSubscriber implements EventSubscriberInterface +final class ConsoleSpanSubscriber implements EventSubscriberInterface { private ?Span $span = null; @@ -39,11 +39,11 @@ public function onCommand(ConsoleCommandEvent $event) : void $this->tracer = $this->telemetry->tracer('flow.symfony.console'); $attributes = [ - 'code.function' => $commandName, + 'command.name' => $commandName, ]; if ($command !== null) { - $attributes['code.namespace'] = $command::class; + $attributes['command.class'] = $command::class; } $this->span = $this->tracer->span( diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelFlushSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelFlushSubscriber.php new file mode 100644 index 000000000..afc53193a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelFlushSubscriber.php @@ -0,0 +1,30 @@ + ['onTerminate', -20000], + ]; + } + + public function onTerminate(TerminateEvent $event) : void + { + $this->telemetry->shutdown(); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php similarity index 97% rename from src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php rename to src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php index e69c58781..5bb41c3f1 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelEventSubscriber.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php @@ -10,7 +10,7 @@ use Symfony\Component\HttpKernel\Event\{ControllerEvent, ExceptionEvent, RequestEvent, ResponseEvent, TerminateEvent}; use Symfony\Component\HttpKernel\KernelEvents; -final readonly class HttpKernelEventSubscriber implements EventSubscriberInterface +final readonly class HttpKernelSpanSubscriber implements EventSubscriberInterface { private const string SPAN_ATTRIBUTE = '_flow_telemetry_span'; @@ -53,7 +53,7 @@ public function onController(ControllerEvent $event) : void $controllerName = $this->resolveControllerName($controller); if ($controllerName !== null) { - $span->setAttribute('code.function', $controllerName); + $span->setAttribute('controller', $controllerName); } } diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php index 0833465a5..a9bad650c 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php @@ -34,16 +34,12 @@ public function test_composite_log_processor() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'logger_provider' => [ - 'processor' => [ - 'type' => 'composite', - 'processors' => [ - ['type' => 'memory', 'exporter' => ['type' => 'memory']], - ['type' => 'passthrough', 'exporter' => ['type' => 'console']], - ], - ], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + ['type' => 'memory', 'exporter' => ['type' => 'memory']], + ['type' => 'passthrough', 'exporter' => ['type' => 'console']], ], ], ], @@ -51,7 +47,7 @@ public function test_composite_log_processor() : void }, ]); - self::assertInstanceOf(CompositeLogProcessor::class, $this->getContainer()->get('flow.telemetry.default.logger_provider.processor')); + self::assertInstanceOf(CompositeLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); } public function test_composite_metric_processor() : void @@ -60,16 +56,12 @@ public function test_composite_metric_processor() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'meter_provider' => [ - 'processor' => [ - 'type' => 'composite', - 'processors' => [ - ['type' => 'memory', 'exporter' => ['type' => 'memory']], - ['type' => 'passthrough', 'exporter' => ['type' => 'console']], - ], - ], + 'meter_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + ['type' => 'memory', 'exporter' => ['type' => 'memory']], + ['type' => 'passthrough', 'exporter' => ['type' => 'console']], ], ], ], @@ -77,7 +69,7 @@ public function test_composite_metric_processor() : void }, ]); - self::assertInstanceOf(CompositeMetricProcessor::class, $this->getContainer()->get('flow.telemetry.default.meter_provider.processor')); + self::assertInstanceOf(CompositeMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); } public function test_composite_span_processor() : void @@ -86,16 +78,12 @@ public function test_composite_span_processor() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'composite', - 'processors' => [ - ['type' => 'memory', 'exporter' => ['type' => 'memory']], - ['type' => 'passthrough', 'exporter' => ['type' => 'console']], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + ['type' => 'memory', 'exporter' => ['type' => 'memory']], + ['type' => 'passthrough', 'exporter' => ['type' => 'console']], ], ], ], @@ -105,9 +93,9 @@ public function test_composite_span_processor() : void $container = $this->getContainer(); - self::assertInstanceOf(CompositeSpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor')); - self::assertInstanceOf(MemorySpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor.0.processor')); - self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor.1.processor')); + 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 @@ -120,16 +108,12 @@ public function test_custom_service_reference_for_exporter() : void $extension->load([ [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'passthrough', - 'exporter' => [ - 'type' => 'service', - 'service_id' => 'my.custom.span_exporter', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'passthrough', + 'exporter' => [ + 'type' => 'service', + 'service_id' => 'my.custom.span_exporter', ], ], ], @@ -141,7 +125,7 @@ public function test_custom_service_reference_for_exporter() : void self::assertSame( $container->get('my.custom.span_exporter'), - $container->get('flow.telemetry.default.tracer_provider.processor.exporter') + $container->get('flow.telemetry.tracer_provider.processor.exporter') ); } @@ -155,14 +139,10 @@ public function test_custom_service_reference_for_processor() : void $extension->load([ [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'service', - 'service_id' => 'my.custom.span_processor', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'service', + 'service_id' => 'my.custom.span_processor', ], ], ], @@ -173,7 +153,7 @@ public function test_custom_service_reference_for_processor() : void self::assertSame( $container->get('my.custom.span_processor'), - $container->get('flow.telemetry.default.tracer_provider.processor') + $container->get('flow.telemetry.tracer_provider.processor') ); } @@ -187,14 +167,10 @@ public function test_custom_service_reference_for_sampler() : void $extension->load([ [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => [ - 'type' => 'service', - 'service_id' => 'my.custom.sampler', - ], - ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'service', + 'service_id' => 'my.custom.sampler', ], ], ], @@ -205,11 +181,11 @@ public function test_custom_service_reference_for_sampler() : void self::assertSame( $container->get('my.custom.sampler'), - $container->get('flow.telemetry.default.tracer_provider.sampler') + $container->get('flow.telemetry.tracer_provider.sampler') ); } - public function test_default_instance_is_aliased_to_telemetry_class() : void + public function test_flow_telemetry_is_aliased_to_telemetry_class() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { @@ -222,7 +198,7 @@ public function test_default_instance_is_aliased_to_telemetry_class() : void $container = $this->getContainer(); self::assertTrue($container->has(Telemetry::class)); - self::assertSame($container->get('flow.telemetry.default'), $container->get(Telemetry::class)); + self::assertSame($container->get('flow.telemetry'), $container->get(Telemetry::class)); } public function test_full_configuration_scenario() : void @@ -237,32 +213,28 @@ public function test_full_configuration_scenario() : void 'deployment.environment' => 'staging', ], ], - 'instances' => [ - 'default' => [ - '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'], - ], - ], + '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'], ], ], ]); @@ -277,188 +249,256 @@ public function test_full_configuration_scenario() : void self::assertSame('3.0.0', $resource->get('service.version')); self::assertSame('staging', $resource->get('deployment.environment')); - self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry.default')); + self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry')); - self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.default.tracer_provider')); - self::assertInstanceOf(TraceIdRatioBasedSampler::class, $container->get('flow.telemetry.default.tracer_provider.sampler')); - self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.default.tracer_provider.processor')); - self::assertInstanceOf(ConsoleSpanExporter::class, $container->get('flow.telemetry.default.tracer_provider.processor.exporter')); + 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.default.meter_provider')); - self::assertInstanceOf(PassThroughMetricProcessor::class, $container->get('flow.telemetry.default.meter_provider.processor')); - self::assertInstanceOf(MemoryMetricExporter::class, $container->get('flow.telemetry.default.meter_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.default.logger_provider')); - self::assertInstanceOf(MemoryLogProcessor::class, $container->get('flow.telemetry.default.logger_provider.processor')); - self::assertInstanceOf(ConsoleLogExporter::class, $container->get('flow.telemetry.default.logger_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_types() : void + public function test_log_exporter_console_type() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'with_void' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], - 'with_memory' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]]], - 'with_console' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]]], - ], + 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]], ]); }, ]); - $container = $this->getContainer(); + 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(VoidLogExporter::class, $container->get('flow.telemetry.with_void.logger_provider.processor.exporter')); - self::assertInstanceOf(MemoryLogExporter::class, $container->get('flow.telemetry.with_memory.logger_provider.processor.exporter')); - self::assertInstanceOf(ConsoleLogExporter::class, $container->get('flow.telemetry.with_console.logger_provider.processor.exporter')); + self::assertInstanceOf(MemoryLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter')); } - public function test_log_processor_types() : void + public function test_log_exporter_void_type() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'with_void' => ['logger_provider' => ['processor' => ['type' => 'void']]], - 'with_memory' => ['logger_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]]], - 'with_batching' => ['logger_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 256, 'exporter' => ['type' => 'void']]]], - 'with_passthrough' => ['logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], - ], + 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], ]); }, ]); - $container = $this->getContainer(); + self::assertInstanceOf(VoidLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter')); + } - self::assertInstanceOf(VoidLogProcessor::class, $container->get('flow.telemetry.with_void.logger_provider.processor')); - self::assertInstanceOf(MemoryLogProcessor::class, $container->get('flow.telemetry.with_memory.logger_provider.processor')); - self::assertInstanceOf(BatchingLogProcessor::class, $container->get('flow.telemetry.with_batching.logger_provider.processor')); - self::assertInstanceOf(PassThroughLogProcessor::class, $container->get('flow.telemetry.with_passthrough.logger_provider.processor')); + 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_metric_exporter_types() : void + public function test_log_processor_memory_type() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'with_void' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], - 'with_memory' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]]], - 'with_console' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]]], - ], + 'logger_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]], ]); }, ]); - $container = $this->getContainer(); + 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(VoidMetricExporter::class, $container->get('flow.telemetry.with_void.meter_provider.processor.exporter')); - self::assertInstanceOf(MemoryMetricExporter::class, $container->get('flow.telemetry.with_memory.meter_provider.processor.exporter')); - self::assertInstanceOf(ConsoleMetricExporter::class, $container->get('flow.telemetry.with_console.meter_provider.processor.exporter')); + self::assertInstanceOf(PassThroughLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); } - public function test_metric_processor_types() : void + public function test_log_processor_void_type() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'with_void' => ['meter_provider' => ['processor' => ['type' => 'void']]], - 'with_memory' => ['meter_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]]], - 'with_batching' => ['meter_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 200, 'exporter' => ['type' => 'void']]]], - 'with_passthrough' => ['meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], - ], + 'logger_provider' => ['processor' => ['type' => 'void']], ]); }, ]); - $container = $this->getContainer(); + self::assertInstanceOf(VoidLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); + } - self::assertInstanceOf(VoidMetricProcessor::class, $container->get('flow.telemetry.with_void.meter_provider.processor')); - self::assertInstanceOf(MemoryMetricProcessor::class, $container->get('flow.telemetry.with_memory.meter_provider.processor')); - self::assertInstanceOf(BatchingMetricProcessor::class, $container->get('flow.telemetry.with_batching.meter_provider.processor')); - self::assertInstanceOf(PassThroughMetricProcessor::class, $container->get('flow.telemetry.with_passthrough.meter_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_minimal_configuration_creates_default_instance_with_void_processors() : void + 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']]], ]); }, ]); - $container = $this->getContainer(); + self::assertInstanceOf(MemoryMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter')); + } - 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.default')); + 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(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.default')); + self::assertInstanceOf(VoidMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter')); + } - self::assertTrue($container->has('flow.telemetry.default.tracer_provider')); - self::assertTrue($container->has('flow.telemetry.default.meter_provider')); - self::assertTrue($container->has('flow.telemetry.default.logger_provider')); + 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(TracerProvider::class, $container->get('flow.telemetry.default.tracer_provider')); - self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.default.meter_provider')); - self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.default.logger_provider')); + self::assertInstanceOf(BatchingMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } - self::assertTrue($container->has('flow.telemetry.default.tracer_provider.processor')); - self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.default.tracer_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::assertTrue($container->has('flow.telemetry.default.meter_provider.processor')); - self::assertInstanceOf(VoidMetricProcessor::class, $container->get('flow.telemetry.default.meter_provider.processor')); + self::assertInstanceOf(MemoryMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } - self::assertTrue($container->has('flow.telemetry.default.logger_provider.processor')); - self::assertInstanceOf(VoidLogProcessor::class, $container->get('flow.telemetry.default.logger_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_multiple_telemetry_instances() : void + 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'], - 'instances' => [ - 'main' => [ - 'tracer_provider' => [ - 'processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']], - ], - ], - 'secondary' => [ - 'tracer_provider' => [ - 'processor' => ['type' => 'batching', 'exporter' => ['type' => 'memory']], - ], - ], - ], ]); }, ]); $container = $this->getContainer(); - self::assertTrue($container->has('flow.telemetry.main')); - self::assertTrue($container->has('flow.telemetry.secondary')); + 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(Telemetry::class, $container->get('flow.telemetry.main')); - self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry.secondary')); + 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::assertNotSame($container->get('flow.telemetry.main'), $container->get('flow.telemetry.secondary')); + self::assertTrue($container->has('flow.telemetry.tracer_provider.processor')); + self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor')); - self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.main.tracer_provider.processor')); - self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.secondary.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_otlp_availability_pass_sets_parameter_when_otlp_not_configured() : void @@ -524,14 +564,10 @@ public function test_service_exporter_without_service_id_throws_exception() : vo 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'passthrough', - 'exporter' => ['type' => 'service'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'passthrough', + 'exporter' => ['type' => 'service'], ], ], ]); @@ -548,12 +584,8 @@ public function test_service_processor_without_service_id_throws_exception() : v 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => ['type' => 'service'], - ], - ], + 'tracer_provider' => [ + 'processor' => ['type' => 'service'], ], ]); }, @@ -569,62 +601,110 @@ public function test_service_sampler_without_service_id_throws_exception() : voi 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => ['type' => 'service'], - ], - ], + 'tracer_provider' => [ + 'sampler' => ['type' => 'service'], ], ]); }, ]); } - public function test_span_exporter_types() : void + public function test_span_exporter_console_type() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'with_void' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], - 'with_memory' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]]], - 'with_console' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]]], - ], + 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]], ]); }, ]); - $container = $this->getContainer(); + self::assertInstanceOf(ConsoleSpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter')); + } - self::assertInstanceOf(VoidSpanExporter::class, $container->get('flow.telemetry.with_void.tracer_provider.processor.exporter')); - self::assertInstanceOf(MemorySpanExporter::class, $container->get('flow.telemetry.with_memory.tracer_provider.processor.exporter')); - self::assertInstanceOf(ConsoleSpanExporter::class, $container->get('flow.telemetry.with_console.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_processor_types() : void + public function test_span_exporter_void_type() : void { $this->bootKernel([ 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'with_void' => ['tracer_provider' => ['processor' => ['type' => 'void']]], - 'with_memory' => ['tracer_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]]], - 'with_batching' => ['tracer_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 100, 'exporter' => ['type' => 'void']]]], - 'with_passthrough' => ['tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]]], - ], + 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], ]); }, ]); - $container = $this->getContainer(); + self::assertInstanceOf(VoidSpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter')); + } - self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.with_void.tracer_provider.processor')); - self::assertInstanceOf(MemorySpanProcessor::class, $container->get('flow.telemetry.with_memory.tracer_provider.processor')); - self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.with_batching.tracer_provider.processor')); - self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.with_passthrough.tracer_provider.processor')); + 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 @@ -633,18 +713,14 @@ public function test_tracer_provider_with_always_off_sampler() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => ['type' => 'always_off'], - ], - ], + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_off'], ], ]); }, ]); - self::assertInstanceOf(AlwaysOffSampler::class, $this->getContainer()->get('flow.telemetry.default.tracer_provider.sampler')); + self::assertInstanceOf(AlwaysOffSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler')); } public function test_tracer_provider_with_always_on_sampler() : void @@ -653,12 +729,8 @@ public function test_tracer_provider_with_always_on_sampler() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => ['type' => 'always_on'], - ], - ], + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_on'], ], ]); }, @@ -666,8 +738,8 @@ public function test_tracer_provider_with_always_on_sampler() : void $container = $this->getContainer(); - self::assertTrue($container->has('flow.telemetry.default.tracer_provider.sampler')); - self::assertInstanceOf(AlwaysOnSampler::class, $container->get('flow.telemetry.default.tracer_provider.sampler')); + 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 @@ -676,18 +748,14 @@ public function test_tracer_provider_with_parent_based_sampler() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => ['type' => 'parent_based'], - ], - ], + 'tracer_provider' => [ + 'sampler' => ['type' => 'parent_based'], ], ]); }, ]); - self::assertInstanceOf(ParentBasedSampler::class, $this->getContainer()->get('flow.telemetry.default.tracer_provider.sampler')); + self::assertInstanceOf(ParentBasedSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler')); } public function test_tracer_provider_with_trace_id_ratio_sampler() : void @@ -696,20 +764,16 @@ public function test_tracer_provider_with_trace_id_ratio_sampler() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => [ - 'type' => 'trace_id_ratio', - 'ratio' => 0.5, - ], - ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.5, ], ], ]); }, ]); - self::assertInstanceOf(TraceIdRatioBasedSampler::class, $this->getContainer()->get('flow.telemetry.default.tracer_provider.sampler')); + 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/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 000000000..3539f6ec9 --- /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'], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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'], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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/ConsoleEventSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php similarity index 79% rename from src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleEventSubscriberTest.php rename to src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php index 5562f59fc..0ed868f4c 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleEventSubscriberTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php @@ -4,7 +4,7 @@ namespace Flow\Bridge\Symfony\TelemetryBundle\Tests\Integration\Telemetry\Console; -use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\ConsoleEventSubscriber; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\ConsoleSpanSubscriber; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\Command\{FailingCommand, TestCommand}; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\TestKernel; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Integration\KernelTestCase; @@ -16,9 +16,10 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; -#[CoversClass(ConsoleEventSubscriber::class)] -final class ConsoleEventSubscriberTest extends KernelTestCase +#[CoversClass(ConsoleSpanSubscriber::class)] +final class ConsoleSpanSubscriberTest extends KernelTestCase { + #[\Override] protected function tearDown() : void { restore_exception_handler(); @@ -40,14 +41,10 @@ public function test_does_not_trace_when_disabled() : void ]); $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -71,7 +68,7 @@ public function test_does_not_trace_when_disabled() : void $container = $this->getContainer(); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(0, $spans); @@ -92,14 +89,10 @@ public function test_traces_failing_console_command() : void ]); $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -125,7 +118,7 @@ public function test_traces_failing_console_command() : void $container = $this->getContainer(); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(1, $spans); @@ -156,14 +149,10 @@ public function test_traces_successful_console_command() : void ]); $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -189,7 +178,7 @@ public function test_traces_successful_console_command() : void $container = $this->getContainer(); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(1, $spans); @@ -199,8 +188,8 @@ public function test_traces_successful_console_command() : void self::assertSame(SpanKind::INTERNAL, $span->kind()); $attributes = $span->attributes(); - self::assertSame('test:command', $attributes['code.function']); - self::assertSame(TestCommand::class, $attributes['code.namespace']); + self::assertSame('test:command', $attributes['command.name']); + self::assertSame(TestCommand::class, $attributes['command.class']); self::assertSame(0, $attributes['process.exit_code']); $status = $span->status(); 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 000000000..1e87f9c36 --- /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'], + ], + ], + 'instrumentation' => [ + 'http_kernel' => true, + 'console' => 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'], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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/HttpKernelEventSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php similarity index 80% rename from src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelEventSubscriberTest.php rename to src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php index aac3b478a..591555d01 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelEventSubscriberTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php @@ -4,7 +4,7 @@ namespace Flow\Bridge\Symfony\TelemetryBundle\Tests\Integration\Telemetry\HttpKernel; -use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\HttpKernelEventSubscriber; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\HttpKernelSpanSubscriber; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\Controller\TestController; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\TestKernel; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Integration\KernelTestCase; @@ -15,9 +15,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\{Route, Router}; -#[CoversClass(HttpKernelEventSubscriber::class)] -final class HttpKernelEventSubscriberTest extends KernelTestCase +#[CoversClass(HttpKernelSpanSubscriber::class)] +final class HttpKernelSpanSubscriberTest extends KernelTestCase { + #[\Override] protected function tearDown() : void { restore_exception_handler(); @@ -39,14 +40,10 @@ public function test_does_not_trace_when_disabled() : void ]); $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -70,7 +67,7 @@ public function test_does_not_trace_when_disabled() : void $kernel->terminate($request, $response); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(0, $spans); @@ -91,14 +88,10 @@ public function test_traces_http_request_with_error_status() : void ]); $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -124,7 +117,7 @@ public function test_traces_http_request_with_error_status() : void self::assertSame(404, $response->getStatusCode()); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(1, $spans); @@ -154,14 +147,10 @@ public function test_traces_successful_http_request() : void ]); $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -187,7 +176,7 @@ public function test_traces_successful_http_request() : void self::assertSame(200, $response->getStatusCode()); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(1, $spans); @@ -200,6 +189,6 @@ public function test_traces_successful_http_request() : void 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['code.function']); + 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 index b6fb60f32..4fa607a97 100644 --- 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 @@ -73,14 +73,10 @@ public function test_traces_message_dispatch() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -119,7 +115,7 @@ public function test_traces_message_dispatch() : void self::assertTrue($handler->handled); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(1, $spans); @@ -145,14 +141,10 @@ public function test_traces_message_with_exception() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'memory', - 'exporter' => ['type' => 'memory'], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], ], ], 'instrumentation' => [ @@ -195,7 +187,7 @@ public function test_traces_message_with_exception() : void self::assertTrue($exceptionThrown, 'Expected exception was not thrown'); /** @var MemorySpanProcessor $processor */ - $processor = $container->get('flow.telemetry.default.tracer_provider.processor'); + $processor = $container->get('flow.telemetry.tracer_provider.processor'); $spans = $processor->endedSpans(); self::assertCount(1, $spans); 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 index 49e8461dc..9f68135d4 100644 --- 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 @@ -15,27 +15,23 @@ public function test_composite_processor_with_multiple_processors() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'composite', - 'processors' => [ - [ - 'type' => 'memory', - ], - [ - 'type' => 'batching', - 'batch_size' => 100, - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + [ + 'type' => 'memory', + ], + [ + 'type' => 'batching', + 'batch_size' => 100, ], ], ], ], ]]); - $processors = $config['instances']['default']['tracer_provider']['processor']['processors']; + $processors = $config['tracer_provider']['processor']['processors']; self::assertCount(2, $processors); self::assertSame('memory', $processors[0]['type']); self::assertSame('batching', $processors[1]['type']); @@ -55,28 +51,14 @@ public function test_exporter_defaults_to_void() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'batching', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', ], ], ]]); - self::assertSame('void', $config['instances']['default']['tracer_provider']['processor']['exporter']['type']); - } - - public function test_instances_key_is_present_when_omitted() : void - { - $config = (new Processor())->processConfiguration(new Configuration(), [[ - 'service' => ['name' => 'test-app'], - ]]); - - self::assertArrayHasKey('instances', $config); - self::assertSame([], $config['instances']); + self::assertSame('void', $config['tracer_provider']['processor']['exporter']['type']); } public function test_instrumentation_can_be_enabled() : void @@ -127,15 +109,11 @@ public function test_invalid_exporter_type_is_rejected() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'batching', - 'exporter' => [ - 'type' => 'invalid_exporter', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'invalid_exporter', ], ], ], @@ -148,13 +126,9 @@ public function test_invalid_processor_type_is_rejected() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'invalid_processor', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'invalid_processor', ], ], ]]); @@ -166,13 +140,9 @@ public function test_invalid_sampler_type_is_rejected() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => [ - 'type' => 'invalid_sampler', - ], - ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'invalid_sampler', ], ], ]]); @@ -184,16 +154,12 @@ public function test_invalid_severity_level_is_rejected() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'logger_provider' => [ - 'processor' => [ - 'type' => 'severity_filtering', - 'minimum_severity' => 'invalid_level', - 'inner_processor' => [ - 'type' => 'void', - ], - ], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'minimum_severity' => 'invalid_level', + 'inner_processor' => [ + 'type' => 'void', ], ], ], @@ -204,30 +170,22 @@ public function test_meter_provider_temporality_can_be_delta() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'meter_provider' => [ - 'temporality' => 'delta', - ], - ], + 'meter_provider' => [ + 'temporality' => 'delta', ], ]]); - self::assertSame('delta', $config['instances']['default']['meter_provider']['temporality']); + 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'], - 'instances' => [ - 'default' => [ - 'meter_provider' => [], - ], - ], + 'meter_provider' => [], ]]); - self::assertSame('cumulative', $config['instances']['default']['meter_provider']['temporality']); + self::assertSame('cumulative', $config['meter_provider']['temporality']); } public function test_minimal_config_requires_service_name() : void @@ -249,47 +207,24 @@ public function test_minimal_config_with_service_name() : void self::assertSame([], $config['service']['attributes']); } - public function test_multiple_instances() : void - { - $config = (new Processor())->processConfiguration(new Configuration(), [[ - 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [], - 'secondary' => [ - 'tracer_provider' => [ - 'sampler' => ['type' => 'always_off'], - ], - ], - ], - ]]); - - self::assertArrayHasKey('default', $config['instances']); - self::assertArrayHasKey('secondary', $config['instances']); - self::assertSame('always_off', $config['instances']['secondary']['tracer_provider']['sampler']['type']); - } - public function test_otlp_serializer_defaults_to_json() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'batching', - 'exporter' => [ - 'type' => 'otlp', - 'otlp' => [ - 'transport' => [], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'otlp', + 'otlp' => [ + 'transport' => [], ], ], ], ], ]]); - $serializer = $config['instances']['default']['tracer_provider']['processor']['exporter']['otlp']['transport']['serializer']; + $serializer = $config['tracer_provider']['processor']['exporter']['otlp']['transport']['serializer']; self::assertSame('json', $serializer['type']); } @@ -297,24 +232,20 @@ public function test_otlp_transport_defaults() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'batching', - 'exporter' => [ - 'type' => 'otlp', - 'otlp' => [ - 'transport' => [], - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'otlp', + 'otlp' => [ + 'transport' => [], ], ], ], ], ]]); - $transport = $config['instances']['default']['tracer_provider']['processor']['exporter']['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']); @@ -326,18 +257,14 @@ public function test_processor_batch_size_default() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'batching', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', ], ], ]]); - self::assertSame(512, $config['instances']['default']['tracer_provider']['processor']['batch_size']); + self::assertSame(512, $config['tracer_provider']['processor']['batch_size']); } public function test_processor_batch_size_minimum_validation() : void @@ -346,14 +273,10 @@ public function test_processor_batch_size_minimum_validation() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'batching', - 'batch_size' => 0, - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 0, ], ], ]]); @@ -363,28 +286,34 @@ public function test_processor_defaults_to_void() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [], - ], - ], + 'tracer_provider' => [], ]]); - self::assertSame('void', $config['instances']['default']['tracer_provider']['processor']['type']); + 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'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [], - ], - ], + 'tracer_provider' => [], ]]); - self::assertSame('always_on', $config['instances']['default']['tracer_provider']['sampler']['type']); + self::assertSame('always_on', $config['tracer_provider']['sampler']['type']); } public function test_sampler_ratio_maximum_validation() : void @@ -393,14 +322,10 @@ public function test_sampler_ratio_maximum_validation() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => [ - 'type' => 'trace_id_ratio', - 'ratio' => 1.1, - ], - ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 1.1, ], ], ]]); @@ -412,14 +337,10 @@ public function test_sampler_ratio_minimum_validation() : void (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => [ - 'type' => 'trace_id_ratio', - 'ratio' => -0.1, - ], - ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => -0.1, ], ], ]]); @@ -429,19 +350,15 @@ public function test_sampler_ratio_validation() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'sampler' => [ - 'type' => 'trace_id_ratio', - 'ratio' => 0.5, - ], - ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.5, ], ], ]]); - self::assertSame(0.5, $config['instances']['default']['tracer_provider']['sampler']['ratio']); + self::assertSame(0.5, $config['tracer_provider']['sampler']['ratio']); } public function test_service_config_with_version_and_attributes() : void @@ -471,13 +388,9 @@ public function test_severity_filtering_is_only_available_for_log_processors() : (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'tracer_provider' => [ - 'processor' => [ - 'type' => 'severity_filtering', - ], - ], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', ], ], ]]); @@ -487,44 +400,36 @@ public function test_severity_filtering_minimum_severity_default() : void { $config = (new Processor())->processConfiguration(new Configuration(), [[ 'service' => ['name' => 'test-app'], - 'instances' => [ - 'default' => [ - 'logger_provider' => [ - 'processor' => [ - 'type' => 'severity_filtering', - 'inner_processor' => [ - 'type' => 'void', - ], - ], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'inner_processor' => [ + 'type' => 'void', ], ], ], ]]); - self::assertSame('info', $config['instances']['default']['logger_provider']['processor']['minimum_severity']); + 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'], - 'instances' => [ - 'default' => [ - 'logger_provider' => [ - 'processor' => [ - 'type' => 'severity_filtering', - 'minimum_severity' => 'warn', - 'inner_processor' => [ - 'type' => 'batching', - 'exporter' => ['type' => 'console'], - ], - ], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'minimum_severity' => 'warn', + 'inner_processor' => [ + 'type' => 'batching', + 'exporter' => ['type' => 'console'], ], ], ], ]]); - $processor = $config['instances']['default']['logger_provider']['processor']; + $processor = $config['logger_provider']['processor']; self::assertSame('severity_filtering', $processor['type']); self::assertSame('warn', $processor['minimum_severity']); self::assertSame('batching', $processor['inner_processor']['type']); diff --git a/web/landing/assets/codemirror/completions/dsl.js b/web/landing/assets/codemirror/completions/dsl.js index bec45bfc2..43d4807dd 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 afc733aac..3d5806969 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 ef3184284..a255dadc4 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 860aa432f..b81b94cdf 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 000000000..fae67c93a --- /dev/null +++ b/web/landing/config/packages/flow_telemetry.yaml @@ -0,0 +1,53 @@ +flow_telemetry: + service: + name: "flow-website" + version: "1.0.0" + attributes: + deployment.environment: "%kernel.environment%" + + 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 + + instrumentation: + http_kernel: true + console: true diff --git a/web/landing/config/packages/monolog.yaml b/web/landing/config/packages/monolog.yaml index efc4ce8ba..9c340dcd0 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/services.yaml b/web/landing/config/services.yaml index b44b95208..352ea06ab 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/templates/documentation/dsl.html.twig b/web/landing/templates/documentation/dsl.html.twig index ef65a63ad..77954c8d1 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 f065a0506..efe41ab9e 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; From 8478bae241558977016b180d5cd47ca7ae2062b0 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sat, 7 Feb 2026 11:13:38 +0100 Subject: [PATCH 5/7] feature: allow to register traces/meters/loggers in service container --- .../DependencyInjection/Configuration.php | 66 ++++++++ .../FlowTelemetryExtension.php | 90 ++++++++++- .../FlowTelemetryExtensionTest.php | 145 +++++++++++++++++- .../DependencyInjection/ConfigurationTest.php | 120 +++++++++++++++ 4 files changed, 419 insertions(+), 2 deletions(-) diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php index 00280b3e0..7c600e3d3 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -101,6 +101,72 @@ public function getConfigTreeBuilder() : TreeBuilder ->end() ->end() ->end() + ->arrayNode('tracers') + ->info('Named tracer configurations') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('version') + ->info('Instrumentation scope version') + ->defaultValue('unknown') + ->end() + ->scalarNode('schema_url') + ->info('Schema URL for semantic conventions') + ->defaultNull() + ->end() + ->arrayNode('attributes') + ->info('Additional scope attributes') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('meters') + ->info('Named meter configurations') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('version') + ->info('Instrumentation scope version') + ->defaultValue('unknown') + ->end() + ->scalarNode('schema_url') + ->info('Schema URL for semantic conventions') + ->defaultNull() + ->end() + ->arrayNode('attributes') + ->info('Additional scope attributes') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('loggers') + ->info('Named logger configurations') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('version') + ->info('Instrumentation scope version') + ->defaultValue('unknown') + ->end() + ->scalarNode('schema_url') + ->info('Schema URL for semantic conventions') + ->defaultNull() + ->end() + ->arrayNode('attributes') + ->info('Additional scope attributes') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php index 7813766a6..5a0358fb1 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -8,6 +8,7 @@ use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\{ConsoleFlushSubscriber, ConsoleSpanSubscriber}; use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\{HttpKernelFlushSubscriber, HttpKernelSpanSubscriber}; use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Messenger\TracingMiddleware; +use Flow\Telemetry\{Attributes, Logger\Logger, Meter\Meter, Tracer\Tracer}; use Flow\Telemetry\Context\MemoryContextStorage; use Flow\Telemetry\Logger\{LoggerProvider, Severity}; use Flow\Telemetry\Logger\Processor\{BatchingLogProcessor, CompositeLogProcessor, PassThroughLogProcessor, SeverityFilteringLogProcessor}; @@ -33,13 +34,16 @@ final class FlowTelemetryExtension extends Extension public function load(array $configs, ContainerBuilder $container) : void { $configuration = new Configuration(); - /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool}} $config */ + /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool}, tracers?: array}>, meters?: array}>, loggers?: array}>} $config */ $config = $this->processConfiguration($configuration, $configs); $this->registerGlobalServices($container); $this->registerResource($config['service'], $container); $this->registerTelemetry($config, $container); $this->registerInstrumentation($config['instrumentation'] ?? [], $container); + $this->registerTracers($config['tracers'] ?? [], $container); + $this->registerMeters($config['meters'] ?? [], $container); + $this->registerLoggers($config['loggers'] ?? [], $container); } /** @@ -754,6 +758,62 @@ private function registerInstrumentation(array $config, ContainerBuilder $contai } } + /** + * @param array}> $config + */ + private function registerLoggers(array $config, ContainerBuilder $container) : void + { + foreach ($config as $name => $loggerConfig) { + $definition = new Definition(Logger::class); + $definition->setFactory([new Reference('flow.telemetry'), 'logger']); + $definition->setArgument(0, $name); + $definition->setArgument(1, $loggerConfig['version'] ?? 'unknown'); + $definition->setArgument(2, $loggerConfig['schema_url'] ?? null); + + $attributes = $loggerConfig['attributes'] ?? []; + + if (\count($attributes) > 0) { + $attributesDefinition = new Definition(Attributes::class); + $attributesDefinition->setFactory([Attributes::class, 'create']); + $attributesDefinition->setArgument(0, $attributes); + $definition->setArgument(3, $attributesDefinition); + } else { + $definition->setArgument(3, null); + } + + $definition->setPublic(true); + $container->setDefinition('flow.telemetry.' . $name . '.logger', $definition); + } + } + + /** + * @param array}> $config + */ + private function registerMeters(array $config, ContainerBuilder $container) : void + { + foreach ($config as $name => $meterConfig) { + $definition = new Definition(Meter::class); + $definition->setFactory([new Reference('flow.telemetry'), 'meter']); + $definition->setArgument(0, $name); + $definition->setArgument(1, $meterConfig['version'] ?? 'unknown'); + $definition->setArgument(2, $meterConfig['schema_url'] ?? null); + + $attributes = $meterConfig['attributes'] ?? []; + + if (\count($attributes) > 0) { + $attributesDefinition = new Definition(Attributes::class); + $attributesDefinition->setFactory([Attributes::class, 'create']); + $attributesDefinition->setArgument(0, $attributes); + $definition->setArgument(3, $attributesDefinition); + } else { + $definition->setArgument(3, null); + } + + $definition->setPublic(true); + $container->setDefinition('flow.telemetry.' . $name . '.meter', $definition); + } + } + /** * @param array $serviceConfig */ @@ -799,4 +859,32 @@ private function registerTelemetry(array $config, ContainerBuilder $container) : $container->setAlias(Telemetry::class, $telemetryServiceId)->setPublic(true); } + + /** + * @param array}> $config + */ + private function registerTracers(array $config, ContainerBuilder $container) : void + { + foreach ($config as $name => $tracerConfig) { + $definition = new Definition(Tracer::class); + $definition->setFactory([new Reference('flow.telemetry'), 'tracer']); + $definition->setArgument(0, $name); + $definition->setArgument(1, $tracerConfig['version'] ?? 'unknown'); + $definition->setArgument(2, $tracerConfig['schema_url'] ?? null); + + $attributes = $tracerConfig['attributes'] ?? []; + + if (\count($attributes) > 0) { + $attributesDefinition = new Definition(Attributes::class); + $attributesDefinition->setFactory([Attributes::class, 'create']); + $attributesDefinition->setArgument(0, $attributes); + $definition->setArgument(3, $attributesDefinition); + } else { + $definition->setArgument(3, null); + } + + $definition->setPublic(true); + $container->setDefinition('flow.telemetry.' . $name . '.tracer', $definition); + } + } } diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php index a9bad650c..c0f237c36 100644 --- a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/FlowTelemetryExtensionTest.php @@ -9,6 +9,7 @@ use Flow\Bridge\Symfony\TelemetryBundle\Exception\RuntimeException; use Flow\Bridge\Symfony\TelemetryBundle\Tests\Fixtures\TestKernel; use Flow\Telemetry\Context\MemoryContextStorage; +use Flow\Telemetry\{Logger\Logger, Meter\Meter, Resource, Telemetry, Tracer\Tracer}; use Flow\Telemetry\Logger\LoggerProvider; use Flow\Telemetry\Logger\Processor\{BatchingLogProcessor, CompositeLogProcessor, PassThroughLogProcessor}; use Flow\Telemetry\Meter\MeterProvider; @@ -17,7 +18,6 @@ use Flow\Telemetry\Provider\Console\{ConsoleLogExporter, ConsoleMetricExporter, ConsoleSpanExporter}; use Flow\Telemetry\Provider\Memory\{MemoryLogExporter, MemoryLogProcessor, MemoryMetricExporter, MemoryMetricProcessor, MemorySpanExporter, MemorySpanProcessor}; use Flow\Telemetry\Provider\Void\{VoidLogExporter, VoidLogProcessor, VoidMetricExporter, VoidMetricProcessor, VoidSpanExporter, VoidSpanProcessor}; -use Flow\Telemetry\{Resource, Telemetry}; use Flow\Telemetry\Tracer\Processor\{BatchingSpanProcessor, CompositeSpanProcessor, PassThroughSpanProcessor}; use Flow\Telemetry\Tracer\Sampler\{AlwaysOffSampler, AlwaysOnSampler, ParentBasedSampler, TraceIdRatioBasedSampler}; use Flow\Telemetry\Tracer\TracerProvider; @@ -501,6 +501,120 @@ public function test_minimal_configuration_creates_telemetry_with_void_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([ @@ -555,6 +669,35 @@ public function test_resource_contains_service_name_and_version() : void 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); 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 index 9f68135d4..f6a269573 100644 --- 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 @@ -47,6 +47,20 @@ public function test_empty_service_name_is_rejected() : void ]]); } + 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(), [[ @@ -166,6 +180,49 @@ public function test_invalid_severity_level_is_rejected() : 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(), [[ @@ -207,6 +264,27 @@ public function test_minimal_config_with_service_name() : void 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(), [[ @@ -435,4 +513,46 @@ public function test_severity_filtering_processor_for_logs() : void self::assertSame('batching', $processor['inner_processor']['type']); self::assertSame('console', $processor['inner_processor']['exporter']['type']); } + + 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']); + } } From d5ed7372dfadd77bb52a808a335d58c4f1ad5641 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sat, 7 Feb 2026 12:13:16 +0100 Subject: [PATCH 6/7] feature: trace twig templates rendering in telemetry bundle --- composer.json | 3 +- composer.lock | 81 +++++- phpstan.neon | 2 + .../symfony/telemetry-bundle/composer.json | 8 +- .../DependencyInjection/Configuration.php | 18 ++ .../FlowTelemetryExtension.php | 22 +- .../Telemetry/Twig/TracingTwigExtension.php | 111 ++++++++ .../Twig/TracingTwigExtensionTest.php | 236 ++++++++++++++++++ tools/box/composer.lock | 20 +- tools/rector/composer.lock | 14 +- .../config/packages/flow_telemetry.yaml | 5 + .../config/packages/prod/flow_telemetry.yaml | 21 ++ .../config/packages/test/flow_telemetry.yaml | 17 ++ .../src/Flow/Website/Twig/DSLExtension.php | 1 + .../src/Flow/Website/Twig/FlowExtension.php | 1 + .../Flow/Website/Twig/HumanizerExtension.php | 1 + .../Flow/Website/Twig/SlugifyExtension.php | 1 + 17 files changed, 538 insertions(+), 24 deletions(-) create mode 100644 src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php create mode 100644 src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php create mode 100644 web/landing/config/packages/prod/flow_telemetry.yaml create mode 100644 web/landing/config/packages/test/flow_telemetry.yaml diff --git a/composer.json b/composer.json index 9ee392fd0..52e50af43 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,8 @@ "symfony/http-client": "^6.4 || ^7.3 || ^8.0", "symfony/messenger": "^6.4 || ^7.3 || ^8.0", "symfony/process": "^7.3 || ^8.0", - "symfony/routing": "^6.4 || ^7.3 || ^8.0" + "symfony/routing": "^6.4 || ^7.3 || ^8.0", + "twig/twig": "^3.0" }, "replace": { "flow-php/array-dot": "self.version", diff --git a/composer.lock b/composer.lock index 66cea4ae3..35e77eddc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "776e2d95004d1b4d3d4ceeccc514973e", + "content-hash": "72bef3ea32003325ddbe8b2e9cc36dbb", "packages": [ { "name": "async-aws/core", @@ -6860,6 +6860,85 @@ } ], "time": "2026-01-12T12:19:02+00:00" + }, + { + "name": "twig/twig", + "version": "v3.23.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-01-23T21:00:41+00:00" } ], "aliases": [], diff --git a/phpstan.neon b/phpstan.neon index 6549e45f3..bc2fe9303 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -89,6 +89,8 @@ parameters: - src/lib/postgresql/src/Flow/PostgreSql/Protobuf/* - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php + - src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php + - src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php tmpDir: var/phpstan/cache diff --git a/src/bridge/symfony/telemetry-bundle/composer.json b/src/bridge/symfony/telemetry-bundle/composer.json index 4b8771280..f6e5ee676 100644 --- a/src/bridge/symfony/telemetry-bundle/composer.json +++ b/src/bridge/symfony/telemetry-bundle/composer.json @@ -26,13 +26,15 @@ }, "require-dev": { "flow-php/telemetry-otlp-bridge": "self.version", - "symfony/messenger": "^6.4 || ^7.3 || ^8.0", "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0", - "symfony/routing": "^6.4 || ^7.3 || ^8.0" + "symfony/messenger": "^6.4 || ^7.3 || ^8.0", + "symfony/routing": "^6.4 || ^7.3 || ^8.0", + "twig/twig": "^3.0" }, "suggest": { "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support", - "symfony/messenger": "Required for Messenger tracing middleware" + "symfony/messenger": "Required for Messenger tracing middleware", + "twig/twig": "Required for Twig template tracing" }, "autoload": { "psr-4": { diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php index 7c600e3d3..5cc1f3e07 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -99,6 +99,24 @@ public function getConfigTreeBuilder() : TreeBuilder ->info('Enable automatic tracing of Messenger messages') ->defaultFalse() ->end() + ->arrayNode('twig') + ->info('Twig template tracing configuration') + ->canBeEnabled() + ->children() + ->booleanNode('trace_templates') + ->info('Trace template rendering') + ->defaultTrue() + ->end() + ->booleanNode('trace_blocks') + ->info('Trace block rendering') + ->defaultFalse() + ->end() + ->booleanNode('trace_macros') + ->info('Trace macro execution') + ->defaultFalse() + ->end() + ->end() + ->end() ->end() ->end() ->arrayNode('tracers') diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php index 5a0358fb1..170b88c3e 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -8,6 +8,7 @@ use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Console\{ConsoleFlushSubscriber, ConsoleSpanSubscriber}; use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\HttpKernel\{HttpKernelFlushSubscriber, HttpKernelSpanSubscriber}; use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Messenger\TracingMiddleware; +use Flow\Bridge\Symfony\TelemetryBundle\Telemetry\Twig\TracingTwigExtension; use Flow\Telemetry\{Attributes, Logger\Logger, Meter\Meter, Tracer\Tracer}; use Flow\Telemetry\Context\MemoryContextStorage; use Flow\Telemetry\Logger\{LoggerProvider, Severity}; @@ -25,6 +26,7 @@ use Symfony\Component\DependencyInjection\{ContainerBuilder, Definition, Reference}; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Twig\Extension\AbstractExtension; final class FlowTelemetryExtension extends Extension { @@ -34,7 +36,7 @@ final class FlowTelemetryExtension extends Extension public function load(array $configs, ContainerBuilder $container) : void { $configuration = new Configuration(); - /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool}, tracers?: array}>, meters?: array}>, loggers?: array}>} $config */ + /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool}}, tracers?: array}>, meters?: array}>, loggers?: array}>} $config */ $config = $this->processConfiguration($configuration, $configs); $this->registerGlobalServices($container); @@ -723,7 +725,7 @@ private function registerGlobalServices(ContainerBuilder $container) : void } /** - * @param array{http_kernel?: bool, console?: bool, messenger?: bool} $config + * @param array{http_kernel?: bool, console?: bool, messenger?: bool, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool}} $config */ private function registerInstrumentation(array $config, ContainerBuilder $container) : void { @@ -756,6 +758,22 @@ private function registerInstrumentation(array $config, ContainerBuilder $contai $definition->setArgument(0, new Reference(Telemetry::class)); $container->setDefinition('flow.telemetry.messenger.middleware', $definition); } + + $twigConfig = $config['twig'] ?? []; + + if ($twigConfig['enabled'] ?? false) { + if (!\class_exists(AbstractExtension::class)) { + throw new RuntimeException('Twig instrumentation requires twig/twig package. Install it via composer: composer require twig/twig'); + } + + $definition = new Definition(TracingTwigExtension::class); + $definition->setArgument(0, new Reference(Telemetry::class)); + $definition->setArgument(1, $twigConfig['trace_templates'] ?? true); + $definition->setArgument(2, $twigConfig['trace_blocks'] ?? false); + $definition->setArgument(3, $twigConfig['trace_macros'] ?? false); + $definition->addTag('twig.extension'); + $container->setDefinition('flow.telemetry.twig.extension', $definition); + } } /** diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php new file mode 100644 index 000000000..b33a5fb35 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php @@ -0,0 +1,111 @@ + + */ + private \SplObjectStorage $activeSpans; + + public function __construct( + private readonly Telemetry $telemetry, + private readonly bool $traceTemplates = true, + private readonly bool $traceBlocks = false, + private readonly bool $traceMacros = false, + ) { + $this->activeSpans = new \SplObjectStorage(); + } + + public function enter(Profile $profile) : void + { + if (!$this->shouldTrace($profile)) { + return; + } + + $tracer = $this->telemetry->tracer('flow.symfony.twig'); + + $spanName = $this->getSpanName($profile); + $attributes = [ + 'twig.type' => $profile->getType(), + 'twig.template' => $profile->getTemplate(), + ]; + + if (!$profile->isRoot() && !$profile->isTemplate()) { + $attributes['twig.name'] = $profile->getName(); + } + + $span = $tracer->span($spanName, SpanKind::INTERNAL, $attributes); + + $this->activeSpans[$profile] = ['span' => $span, 'tracer' => $tracer]; + } + + #[\Override] + public function getNodeVisitors() : array + { + return [new ProfilerNodeVisitor(self::class)]; + } + + public function leave(Profile $profile) : void + { + if (!$this->activeSpans->contains($profile)) { + return; + } + + /** @var array{span: Span, tracer: Tracer} $spanData */ + $spanData = $this->activeSpans[$profile]; + $spanData['tracer']->complete($spanData['span']); + + $this->activeSpans->detach($profile); + } + + private function getSpanName(Profile $profile) : string + { + if ($profile->isRoot()) { + return $profile->getName(); + } + + if ($profile->isTemplate()) { + return $profile->getTemplate(); + } + + return \sprintf( + '%s::%s(%s)', + $profile->getTemplate(), + $profile->getType(), + $profile->getName() + ); + } + + private function shouldTrace(Profile $profile) : bool + { + if ($profile->isRoot()) { + return true; + } + + if ($profile->isTemplate()) { + return $this->traceTemplates; + } + + $type = $profile->getType(); + + if ($type === Profile::BLOCK) { + return $this->traceBlocks; + } + + if ($type === Profile::MACRO) { + return $this->traceMacros; + } + + return true; + } +} 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 000000000..08db3f4a4 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php @@ -0,0 +1,236 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'instrumentation' => [ + 'http_kernel' => false, + 'console' => 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_extension_not_registered_when_disabled() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'instrumentation' => [ + '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'], + 'instrumentation' => [ + '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'], + ], + ], + 'instrumentation' => [ + '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'], + ], + ], + 'instrumentation' => [ + '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/tools/box/composer.lock b/tools/box/composer.lock index 71669fcd2..eaf006883 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/rector/composer.lock b/tools/rector/composer.lock index e5b40e09b..75930daf2 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/config/packages/flow_telemetry.yaml b/web/landing/config/packages/flow_telemetry.yaml index fae67c93a..9c0a37bdc 100644 --- a/web/landing/config/packages/flow_telemetry.yaml +++ b/web/landing/config/packages/flow_telemetry.yaml @@ -51,3 +51,8 @@ flow_telemetry: instrumentation: http_kernel: true console: true + twig: + enabled: true + trace_templates: true + trace_blocks: true + trace_macros: true 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 000000000..4c3cca94f --- /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 + + instrumentation: + 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 000000000..eb8469730 --- /dev/null +++ b/web/landing/config/packages/test/flow_telemetry.yaml @@ -0,0 +1,17 @@ +flow_telemetry: + tracer_provider: + processor: + type: void + + meter_provider: + processor: + type: void + + logger_provider: + processor: + type: void + + instrumentation: + http_kernel: false + console: false + twig: false diff --git a/web/landing/src/Flow/Website/Twig/DSLExtension.php b/web/landing/src/Flow/Website/Twig/DSLExtension.php index 53d0583e7..0211e65d8 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 6bef99c2f..582203e08 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 baf4c86e4..dfeb80a8a 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 2b9f7f3e4..0b21fec4b 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 [ From c90dc895d68358b9527099beef9cb31e820f1b14 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sat, 7 Feb 2026 13:45:36 +0100 Subject: [PATCH 7/7] refactor: allow to exclude commands/routes/twig templates by regex and full names --- .../DependencyInjection/Configuration.php | 32 ++- .../FlowTelemetryExtension.php | 33 +-- .../Console/ConsoleSpanSubscriber.php | 28 +++ .../HttpKernel/HttpKernelSpanSubscriber.php | 31 +++ .../Telemetry/Twig/TracingTwigExtension.php | 42 ++++ .../Console/ConsoleFlushSubscriberTest.php | 12 +- .../Console/ConsoleSpanSubscriberTest.php | 124 ++++++++++- .../HttpKernelFlushSubscriberTest.php | 12 +- .../HttpKernelSpanSubscriberTest.php | 18 +- .../Messenger/TracingMiddlewareTest.php | 8 +- .../Twig/TracingTwigExtensionTest.php | 199 +++++++++++++++++- .../DependencyInjection/ConfigurationTest.php | 132 ++++++++---- .../config/packages/flow_telemetry.yaml | 28 ++- .../config/packages/prod/flow_telemetry.yaml | 2 +- .../config/packages/test/flow_telemetry.yaml | 11 +- 15 files changed, 594 insertions(+), 118 deletions(-) diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php index 5cc1f3e07..98b43f57f 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -83,17 +83,29 @@ public function getConfigTreeBuilder() : TreeBuilder ->append($this->processorNode('log')) ->end() ->end() - ->arrayNode('instrumentation') - ->info('Auto-instrumentation configuration') + ->arrayNode('telemetry') + ->info('Auto-telemetry configuration') ->addDefaultsIfNotSet() ->children() - ->booleanNode('http_kernel') - ->info('Enable automatic tracing of HTTP requests') - ->defaultFalse() + ->arrayNode('http_kernel') + ->info('HTTP kernel request tracing configuration') + ->canBeEnabled() + ->children() + ->arrayNode('exclude_routes') + ->info('Route names to exclude from tracing (supports regex with / delimiters)') + ->scalarPrototype()->end() + ->end() + ->end() ->end() - ->booleanNode('console') - ->info('Enable automatic tracing of console commands') - ->defaultFalse() + ->arrayNode('console') + ->info('Console command tracing configuration') + ->canBeEnabled() + ->children() + ->arrayNode('exclude_commands') + ->info('Command names to exclude from tracing (supports regex with / delimiters)') + ->scalarPrototype()->end() + ->end() + ->end() ->end() ->booleanNode('messenger') ->info('Enable automatic tracing of Messenger messages') @@ -115,6 +127,10 @@ public function getConfigTreeBuilder() : TreeBuilder ->info('Trace macro execution') ->defaultFalse() ->end() + ->arrayNode('exclude_templates') + ->info('Template paths to exclude from tracing (supports regex with / delimiters)') + ->scalarPrototype()->end() + ->end() ->end() ->end() ->end() diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php index 170b88c3e..0b0385f48 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -36,13 +36,13 @@ final class FlowTelemetryExtension extends Extension public function load(array $configs, ContainerBuilder $container) : void { $configuration = new Configuration(); - /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, instrumentation?: array{http_kernel?: bool, console?: bool, messenger?: bool, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool}}, tracers?: array}>, meters?: array}>, loggers?: array}>} $config */ + /** @var array{service: array, tracer_provider?: array, meter_provider?: array, logger_provider?: array, telemetry?: array{http_kernel?: array{enabled?: bool, exclude_routes?: array}, console?: array{enabled?: bool, exclude_commands?: array}, messenger?: bool, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool, exclude_templates?: array}}, tracers?: array}>, meters?: array}>, loggers?: array}>} $config */ $config = $this->processConfiguration($configuration, $configs); $this->registerGlobalServices($container); $this->registerResource($config['service'], $container); $this->registerTelemetry($config, $container); - $this->registerInstrumentation($config['instrumentation'] ?? [], $container); + $this->registerAutoTelemetry($config['telemetry'] ?? [], $container); $this->registerTracers($config['tracers'] ?? [], $container); $this->registerMeters($config['meters'] ?? [], $container); $this->registerLoggers($config['loggers'] ?? [], $container); @@ -718,20 +718,17 @@ private function mapSeverity(string $severity) : Severity }; } - private function registerGlobalServices(ContainerBuilder $container) : void - { - $container->setDefinition('flow.telemetry.clock', new Definition(SystemClock::class)); - $container->setDefinition('flow.telemetry.context_storage', new Definition(MemoryContextStorage::class)); - } - /** - * @param array{http_kernel?: bool, console?: bool, messenger?: bool, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool}} $config + * @param array{http_kernel?: array{enabled?: bool, exclude_routes?: array}, console?: array{enabled?: bool, exclude_commands?: array}, messenger?: bool, twig?: array{enabled?: bool, trace_templates?: bool, trace_blocks?: bool, trace_macros?: bool, exclude_templates?: array}} $config */ - private function registerInstrumentation(array $config, ContainerBuilder $container) : void + private function registerAutoTelemetry(array $config, ContainerBuilder $container) : void { - if ($config['http_kernel'] ?? true) { + $httpKernelConfig = $config['http_kernel'] ?? []; + + if ($httpKernelConfig['enabled'] ?? false) { $spanDefinition = new Definition(HttpKernelSpanSubscriber::class); $spanDefinition->setArgument(0, new Reference(Telemetry::class)); + $spanDefinition->setArgument(1, $httpKernelConfig['exclude_routes'] ?? []); $spanDefinition->addTag('kernel.event_subscriber'); $container->setDefinition('flow.telemetry.http_kernel.span_subscriber', $spanDefinition); @@ -741,9 +738,12 @@ private function registerInstrumentation(array $config, ContainerBuilder $contai $container->setDefinition('flow.telemetry.http_kernel.flush_subscriber', $flushDefinition); } - if ($config['console'] ?? true) { + $consoleConfig = $config['console'] ?? []; + + if ($consoleConfig['enabled'] ?? false) { $spanDefinition = new Definition(ConsoleSpanSubscriber::class); $spanDefinition->setArgument(0, new Reference(Telemetry::class)); + $spanDefinition->setArgument(1, $consoleConfig['exclude_commands'] ?? []); $spanDefinition->addTag('kernel.event_subscriber'); $container->setDefinition('flow.telemetry.console.span_subscriber', $spanDefinition); @@ -753,7 +753,7 @@ private function registerInstrumentation(array $config, ContainerBuilder $contai $container->setDefinition('flow.telemetry.console.flush_subscriber', $flushDefinition); } - if (($config['messenger'] ?? true) && \interface_exists(MiddlewareInterface::class)) { + if (($config['messenger'] ?? false) && \interface_exists(MiddlewareInterface::class)) { $definition = new Definition(TracingMiddleware::class); $definition->setArgument(0, new Reference(Telemetry::class)); $container->setDefinition('flow.telemetry.messenger.middleware', $definition); @@ -771,11 +771,18 @@ private function registerInstrumentation(array $config, ContainerBuilder $contai $definition->setArgument(1, $twigConfig['trace_templates'] ?? true); $definition->setArgument(2, $twigConfig['trace_blocks'] ?? false); $definition->setArgument(3, $twigConfig['trace_macros'] ?? false); + $definition->setArgument(4, $twigConfig['exclude_templates'] ?? []); $definition->addTag('twig.extension'); $container->setDefinition('flow.telemetry.twig.extension', $definition); } } + private function registerGlobalServices(ContainerBuilder $container) : void + { + $container->setDefinition('flow.telemetry.clock', new Definition(SystemClock::class)); + $container->setDefinition('flow.telemetry.context_storage', new Definition(MemoryContextStorage::class)); + } + /** * @param array}> $config */ diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php index 16abeed16..eb0c949c0 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php @@ -16,8 +16,12 @@ final class ConsoleSpanSubscriber implements EventSubscriberInterface private ?Tracer $tracer = null; + /** + * @param array $excludeCommands + */ public function __construct( private readonly Telemetry $telemetry, + private readonly array $excludeCommands = [], ) { } @@ -36,6 +40,10 @@ public function onCommand(ConsoleCommandEvent $event) : void $command = $event->getCommand(); $commandName = $command?->getName() ?? 'unknown'; + if (!$this->shouldTrace($commandName)) { + return; + } + $this->tracer = $this->telemetry->tracer('flow.symfony.console'); $attributes = [ @@ -91,4 +99,24 @@ public function onTerminate(ConsoleTerminateEvent $event) : void $this->span = null; $this->tracer = null; } + + private function matchesPattern(string $command, string $pattern) : bool + { + if (\str_starts_with($pattern, '/') && \str_ends_with($pattern, '/')) { + return (bool) \preg_match($pattern, $command); + } + + return $command === $pattern; + } + + private function shouldTrace(string $commandName) : bool + { + foreach ($this->excludeCommands as $pattern) { + if ($this->matchesPattern($commandName, $pattern)) { + return false; + } + } + + return true; + } } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php index 5bb41c3f1..c90c4eacb 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php @@ -16,8 +16,12 @@ private const string TRACER_ATTRIBUTE = '_flow_telemetry_tracer'; + /** + * @param array $excludeRoutes + */ public function __construct( private Telemetry $telemetry, + private array $excludeRoutes = [], ) { } @@ -44,6 +48,13 @@ public function onController(ControllerEvent $event) : void $route = $request->attributes->get('_route'); if (\is_string($route)) { + if (!$this->shouldTrace($route)) { + $request->attributes->remove(self::SPAN_ATTRIBUTE); + $request->attributes->remove(self::TRACER_ATTRIBUTE); + + return; + } + $span->setAttribute('http.route', $route); $method = $request->getMethod(); $span->rename("{$method} {$route}"); @@ -131,6 +142,15 @@ public function onTerminate(TerminateEvent $event) : void $request->attributes->remove(self::TRACER_ATTRIBUTE); } + private function matchesPattern(string $route, string $pattern) : bool + { + if (\str_starts_with($pattern, '/') && \str_ends_with($pattern, '/')) { + return (bool) \preg_match($pattern, $route); + } + + return $route === $pattern; + } + /** * @param array|callable|object $controller */ @@ -159,4 +179,15 @@ private function resolveControllerName(callable|object|array $controller) : ?str return null; } + + private function shouldTrace(string $route) : bool + { + foreach ($this->excludeRoutes as $pattern) { + if ($this->matchesPattern($route, $pattern)) { + return false; + } + } + + return true; + } } diff --git a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php index b33a5fb35..fd533c59a 100644 --- a/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php @@ -17,17 +17,33 @@ final class TracingTwigExtension extends AbstractExtension */ private \SplObjectStorage $activeSpans; + private int $excludedDepth = 0; + + /** + * @param array $excludeTemplates + */ public function __construct( private readonly Telemetry $telemetry, private readonly bool $traceTemplates = true, private readonly bool $traceBlocks = false, private readonly bool $traceMacros = false, + private readonly array $excludeTemplates = [], ) { $this->activeSpans = new \SplObjectStorage(); } public function enter(Profile $profile) : void { + if ($profile->isTemplate() && $this->isTemplateExcluded($profile->getTemplate())) { + $this->excludedDepth++; + + return; + } + + if ($this->excludedDepth > 0) { + return; + } + if (!$this->shouldTrace($profile)) { return; } @@ -57,6 +73,12 @@ public function getNodeVisitors() : array public function leave(Profile $profile) : void { + if ($profile->isTemplate() && $this->isTemplateExcluded($profile->getTemplate())) { + $this->excludedDepth--; + + return; + } + if (!$this->activeSpans->contains($profile)) { return; } @@ -86,6 +108,26 @@ private function getSpanName(Profile $profile) : string ); } + private function isTemplateExcluded(string $template) : bool + { + foreach ($this->excludeTemplates as $pattern) { + if ($this->matchesPattern($template, $pattern)) { + return true; + } + } + + return false; + } + + private function matchesPattern(string $template, string $pattern) : bool + { + if (\str_starts_with($pattern, '/') && \str_ends_with($pattern, '/')) { + return (bool) \preg_match($pattern, $template); + } + + return $template === $pattern; + } + private function shouldTrace(Profile $profile) : bool { if ($profile->isRoot()) { 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 index 3539f6ec9..403cfcf67 100644 --- 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 @@ -47,9 +47,9 @@ public function test_flush_is_called_on_terminate() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => true, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => true], 'messenger' => false, ], ]); @@ -98,9 +98,9 @@ public function test_flush_is_not_called_when_console_instrumentation_is_disable 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); 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 index 0ed868f4c..1a9648ba2 100644 --- 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 @@ -47,9 +47,9 @@ public function test_does_not_trace_when_disabled() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); @@ -74,6 +74,112 @@ public function test_does_not_trace_when_disabled() : void 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([ @@ -95,9 +201,9 @@ public function test_traces_failing_console_command() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => true, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => true], 'messenger' => false, ], ]); @@ -155,9 +261,9 @@ public function test_traces_successful_console_command() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => true, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => true], 'messenger' => false, ], ]); 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 index 1e87f9c36..5fad0e3b4 100644 --- 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 @@ -46,9 +46,9 @@ public function test_flush_is_called_on_terminate() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => true, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); @@ -100,9 +100,9 @@ public function test_flush_is_not_called_when_http_kernel_instrumentation_is_dis 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); 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 index 591555d01..e57803585 100644 --- 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 @@ -46,9 +46,9 @@ public function test_does_not_trace_when_disabled() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); @@ -94,9 +94,9 @@ public function test_traces_http_request_with_error_status() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => true, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); @@ -153,9 +153,9 @@ public function test_traces_successful_http_request() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => true, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => false], 'messenger' => false, ], ]); 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 index 4fa607a97..15e2da1ec 100644 --- 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 @@ -36,7 +36,7 @@ public function test_middleware_not_registered_when_disabled() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instrumentation' => [ + 'telemetry' => [ 'messenger' => false, ], ]); @@ -54,7 +54,7 @@ public function test_middleware_service_is_registered() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instrumentation' => [ + 'telemetry' => [ 'messenger' => true, ], ]); @@ -79,7 +79,7 @@ public function test_traces_message_dispatch() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ + 'telemetry' => [ 'http_kernel' => false, 'console' => false, 'messenger' => true, @@ -147,7 +147,7 @@ public function test_traces_message_with_exception() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ + 'telemetry' => [ 'http_kernel' => false, 'console' => false, 'messenger' => true, 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 index 08db3f4a4..d950ce071 100644 --- 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 @@ -37,9 +37,9 @@ public function test_does_not_trace_blocks_when_disabled() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ - 'http_kernel' => false, - 'console' => false, + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], 'messenger' => false, 'twig' => [ 'enabled' => true, @@ -77,13 +77,198 @@ public function test_does_not_trace_blocks_when_disabled() : void } } + 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'], - 'instrumentation' => [ + 'telemetry' => [ 'twig' => false, ], ]); @@ -101,7 +286,7 @@ public function test_extension_service_is_registered() : void 'config' => static function (TestKernel $kernel) : void { $kernel->addTestExtensionConfig('flow_telemetry', [ 'service' => ['name' => 'test-app'], - 'instrumentation' => [ + 'telemetry' => [ 'twig' => true, ], ]); @@ -126,7 +311,7 @@ public function test_traces_blocks_in_templates() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ + 'telemetry' => [ 'http_kernel' => false, 'console' => false, 'messenger' => false, @@ -188,7 +373,7 @@ public function test_traces_template_rendering() : void 'exporter' => ['type' => 'memory'], ], ], - 'instrumentation' => [ + 'telemetry' => [ 'http_kernel' => false, 'console' => false, 'messenger' => false, 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 index f6a269573..24733a8fb 100644 --- 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 @@ -75,48 +75,6 @@ public function test_exporter_defaults_to_void() : void self::assertSame('void', $config['tracer_provider']['processor']['exporter']['type']); } - public function test_instrumentation_can_be_enabled() : void - { - $config = (new Processor())->processConfiguration(new Configuration(), [[ - 'service' => ['name' => 'test-app'], - 'instrumentation' => [ - 'http_kernel' => true, - 'console' => true, - 'messenger' => true, - ], - ]]); - - self::assertTrue($config['instrumentation']['http_kernel']); - self::assertTrue($config['instrumentation']['console']); - self::assertTrue($config['instrumentation']['messenger']); - } - - public function test_instrumentation_defaults_to_all_disabled() : void - { - $config = (new Processor())->processConfiguration(new Configuration(), [[ - 'service' => ['name' => 'test-app'], - ]]); - - self::assertArrayHasKey('instrumentation', $config); - self::assertFalse($config['instrumentation']['http_kernel']); - self::assertFalse($config['instrumentation']['console']); - self::assertFalse($config['instrumentation']['messenger']); - } - - public function test_instrumentation_partial_config() : void - { - $config = (new Processor())->processConfiguration(new Configuration(), [[ - 'service' => ['name' => 'test-app'], - 'instrumentation' => [ - 'http_kernel' => true, - ], - ]]); - - self::assertTrue($config['instrumentation']['http_kernel']); - self::assertFalse($config['instrumentation']['console']); - self::assertFalse($config['instrumentation']['messenger']); - } - public function test_invalid_exporter_type_is_rejected() : void { $this->expectException(InvalidConfigurationException::class); @@ -514,6 +472,96 @@ public function test_severity_filtering_processor_for_logs() : void 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(), [[ diff --git a/web/landing/config/packages/flow_telemetry.yaml b/web/landing/config/packages/flow_telemetry.yaml index 9c0a37bdc..e079fd5de 100644 --- a/web/landing/config/packages/flow_telemetry.yaml +++ b/web/landing/config/packages/flow_telemetry.yaml @@ -4,6 +4,25 @@ flow_telemetry: 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: @@ -47,12 +66,3 @@ flow_telemetry: timeout: 30 serializer: type: json - - instrumentation: - http_kernel: true - console: true - twig: - enabled: true - trace_templates: true - trace_blocks: true - trace_macros: true diff --git a/web/landing/config/packages/prod/flow_telemetry.yaml b/web/landing/config/packages/prod/flow_telemetry.yaml index 4c3cca94f..efbe8ef4f 100644 --- a/web/landing/config/packages/prod/flow_telemetry.yaml +++ b/web/landing/config/packages/prod/flow_telemetry.yaml @@ -15,7 +15,7 @@ flow_telemetry: serializer: type: json - instrumentation: + 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 index eb8469730..24fcef89e 100644 --- a/web/landing/config/packages/test/flow_telemetry.yaml +++ b/web/landing/config/packages/test/flow_telemetry.yaml @@ -11,7 +11,10 @@ flow_telemetry: processor: type: void - instrumentation: - http_kernel: false - console: false - twig: false + telemetry: + http_kernel: + enabled: false + console: + enabled: false + twig: + enabled: false