diff --git a/.github/workflows/monorepo-split.yml b/.github/workflows/monorepo-split.yml index a24f907321..f0a84ed707 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 62942d7449..467feeafaf 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 c1830de37f..52e50af43b 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" @@ -60,8 +64,12 @@ "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", + "twig/twig": "^3.0" }, "replace": { "flow-php/array-dot": "self.version", @@ -97,10 +105,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/psr7-telemetry-bridge": "self.version", + "flow-php/symfony-telemetry-bundle": "self.version", "flow-php/telemetry": "self.version", "flow-php/telemetry-otlp-bridge": "self.version", "flow-php/types": "self.version" @@ -131,8 +140,9 @@ "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", "src/core/etl/src/Flow", @@ -177,8 +187,9 @@ "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", "src/core/etl/src/Flow/ETL/DSL/functions.php", @@ -219,8 +230,9 @@ "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", "src/core/etl/tests/Flow", @@ -292,6 +304,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 +394,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 b03ab7f4e5..35e77eddc6 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": "72bef3ea32003325ddbe8b2e9cc36dbb", "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", @@ -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", @@ -2520,53 +2570,40 @@ "time": "2019-03-08T08:55:37+00:00" }, { - "name": "symfony/console", + "name": "symfony/config", "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" + "url": "https://github.com/symfony/config.git", + "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "url": "https://api.github.com/repos/symfony/config/zipball/4275b53b8ab0cf37f48bf273dc2285c8178efdfb", + "reference": "4275b53b8ab0cf37f48bf273dc2285c8178efdfb", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" }, "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/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/finder": "^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/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" + "Symfony\\Component\\Config\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2586,16 +2623,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "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/config/tree/v7.4.4" }, "funding": [ { @@ -2618,35 +2649,56 @@ "time": "2026-01-13T11:36:38+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "name": "symfony/console", + "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "url": "https://github.com/symfony/console.git", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|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/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" }, + "type": "library", "autoload": { - "files": [ - "function.php" + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2655,18 +2707,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "A generic function and convention to trigger deprecation notices", + "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/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/console/tree/v7.4.4" }, "funding": [ { @@ -2677,67 +2735,57 @@ "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-25T14:21:43+00:00" + "time": "2026-01-13T11:36:38+00:00" }, { - "name": "symfony/http-client", + "name": "symfony/dependency-injection", "version": "v7.4.5", "source": { "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", - "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/76a02cddca45a5254479ad68f9fa274ead0a7ef2", + "reference": "76a02cddca45a5254479ad68f9fa274ead0a7ef2", "shasum": "" }, "require": { "php": ">=8.2", - "psr/log": "^1|^2|^3", + "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.4|^3.5.2", - "symfony/polyfill-php83": "^1.29", - "symfony/service-contracts": "^2.5|^3" + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" }, "conflict": { - "amphp/amp": "<2.5", - "amphp/socket": "<1.1", - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.4" + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "amphp/http-client": "^4.2.1|^5.0", - "amphp/http-tunnel": "^1.0|^2.0", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/cache": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0" + "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\\HttpClient\\": "" + "Symfony\\Component\\DependencyInjection\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2749,21 +2797,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", - "keywords": [ - "http" - ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.5" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5" }, "funding": [ { @@ -2786,17 +2831,17 @@ "time": "2026-01-27T16:16:02+00:00" }, { - "name": "symfony/http-client-contracts", + "name": "symfony/deprecation-contracts", "version": "v3.6.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "75d7043853a42837e68111812f4d964b01e5101c" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", - "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -2813,11 +2858,8 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2834,18 +2876,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to HTTP clients", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -2861,46 +2895,46 @@ "type": "tidelift" } ], - "time": "2025-04-29T11:18:49+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/http-foundation", - "version": "v7.4.5", + "name": "symfony/error-handler", + "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "446d0db2b1f21575f1284b74533e425096abdfb6" + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", - "reference": "446d0db2b1f21575f1284b74533e425096abdfb6", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "^1.1" + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { - "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" }, "require-dev": { - "doctrine/dbal": "^3.6|^4", - "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5|^8.0", - "symfony/clock": "^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/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^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\\HttpFoundation\\": "" + "Symfony\\Component\\ErrorHandler\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2920,7 +2954,499 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Defines an object-oriented layer for the HTTP specification", + "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\\EventDispatcher\\": "" + }, + "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 that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/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-05T11:45:34+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "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": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-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/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", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "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 powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/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/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "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": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-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": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^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-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "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": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { "source": "https://github.com/symfony/http-foundation/tree/v7.4.5" @@ -2943,7 +3469,126 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "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", @@ -3520,6 +4165,86 @@ ], "time": "2025-07-08T02:45:35+00:00" }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "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\\Php85\\": "" + }, + "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.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/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": "2025-06-23T16:12:55+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.33.0", @@ -3979,15 +4704,182 @@ }, "require": { "php": ">=8.2", - "symfony/polyfill-uuid": "^1.15" + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/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-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/console": "^6.4|^7.0|^8.0" + "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\\Uid\\": "" + "Symfony\\Component\\VarExporter\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -3998,10 +4890,6 @@ "MIT" ], "authors": [ - { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4011,15 +4899,20 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to generate and represent UIDs", + "description": "Allows exporting any serializable PHP data structure to plain PHP code", "homepage": "https://symfony.com", "keywords": [ - "UID", - "ulid", - "uuid" + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.4" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -4039,7 +4932,7 @@ "type": "tidelift" } ], - "time": "2026-01-03T23:30:35+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "webmozart/glob", @@ -5038,32 +5931,263 @@ "symfony/http-kernel": "<6.4", "symfony/var-dumper": "<6.4" }, - "provide": { - "psr/cache-implementation": "2.0|3.0", - "psr/simple-cache-implementation": "1.0|2.0|3.0", - "symfony/cache-implementation": "1.1|2.0|3.0" + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "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 extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/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/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "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": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-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": "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", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "1658a4d34df028f3d93bcdd8e81f04423925a364" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/1658a4d34df028f3d93bcdd8e81f04423925a364", + "reference": "1658a4d34df028f3d93bcdd8e81f04423925a364", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" }, "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/dbal": "^3.6|^4", - "predis/predis": "^1.1|^2.0", - "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/filesystem": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Cache\\": "" + "Symfony\\Component\\Dotenv\\": "" }, - "classmap": [ - "Traits/ValueWrapper.php" - ], "exclude-from-classmap": [ "/Tests/" ] @@ -5074,22 +6198,23 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "description": "Registers environment variables from a .env file", "homepage": "https://symfony.com", "keywords": [ - "caching", - "psr6" + "dotenv", + "env", + "environment" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.5" + "source": "https://github.com/symfony/dotenv/tree/v7.4.0" }, "funding": [ { @@ -5109,40 +6234,36 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { - "name": "symfony/cache-contracts", - "version": "v3.6.0", + "name": "symfony/finder", + "version": "v7.4.5", "source": { "type": "git", - "url": "https://github.com/symfony/cache-contracts.git", - "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + "url": "https://github.com/symfony/finder.git", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", - "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/cache": "^3.0" + "php": ">=8.2" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Contracts\\Cache\\": "" - } + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5150,26 +6271,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to caching", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/finder/tree/v7.4.5" }, "funding": [ { @@ -5180,42 +6293,131 @@ "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-03-13T15:25:07+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { - "name": "symfony/dotenv", - "version": "v7.4.0", + "name": "symfony/framework-bundle", + "version": "v7.4.5", "source": { "type": "git", - "url": "https://github.com/symfony/dotenv.git", - "reference": "1658a4d34df028f3d93bcdd8e81f04423925a364" + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/1658a4d34df028f3d93bcdd8e81f04423925a364", - "reference": "1658a4d34df028f3d93bcdd8e81f04423925a364", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd", + "reference": "dcf89ca6712d9e1b5d3f14dea0e1c2685a05d1cd", "shasum": "" }, "require": { - "php": ">=8.2" + "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/process": "<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/process": "^6.4|^7.0|^8.0" - }, - "type": "library", + "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\\Component\\Dotenv\\": "" + "Symfony\\Bundle\\FrameworkBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5235,15 +6437,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Registers environment variables from a .env file", + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", - "keywords": [ - "dotenv", - "env", - "environment" - ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v7.4.0" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.5" }, "funding": [ { @@ -5263,32 +6460,58 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { - "name": "symfony/finder", - "version": "v7.4.5", + "name": "symfony/messenger", + "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" + "url": "https://github.com/symfony/messenger.git", + "reference": "0a39e1b256f280762293f2f441e430c8baf74f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", - "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "url": "https://api.github.com/repos/symfony/messenger/zipball/0a39e1b256f280762293f2f441e430c8baf74f9c", + "reference": "0a39e1b256f280762293f2f441e430c8baf74f9c", "shasum": "" }, "require": { - "php": ">=8.2" + "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": { - "symfony/filesystem": "^6.4|^7.0|^8.0" + "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\\Finder\\": "" + "Symfony\\Component\\Messenger\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5300,18 +6523,18 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Finds files and directories via an intuitive fluent interface", + "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/finder/tree/v7.4.5" + "source": "https://github.com/symfony/messenger/tree/v7.4.4" }, "funding": [ { @@ -5331,7 +6554,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-01-08T14:50:10+00:00" }, { "name": "symfony/options-resolver", @@ -5554,32 +6777,40 @@ "time": "2026-01-26T15:07:59+00:00" }, { - "name": "symfony/var-exporter", - "version": "v7.4.0", + "name": "symfony/routing", + "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "url": "https://github.com/symfony/routing.git", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "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": { - "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" + "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\\VarExporter\\": "" + "Symfony\\Component\\Routing\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5591,28 +6822,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "description": "Maps an HTTP request to a set of configuration variables", "homepage": "https://symfony.com", "keywords": [ - "clone", - "construct", - "export", - "hydrate", - "instantiate", - "lazy-loading", - "proxy", - "serialize" + "router", + "routing", + "uri", + "url" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/routing/tree/v7.4.4" }, "funding": [ { @@ -5632,7 +6859,86 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "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": [], @@ -5651,7 +6957,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 0000000000..7e2bd96034 --- /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 b32db142f6..b9ef96eb68 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 0000000000..40b79436ff --- /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 d2500326f6..bc2fe93039 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 @@ -85,6 +87,10 @@ 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 + - 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 @@ -104,6 +110,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/phpunit.xml.dist b/phpunit.xml.dist index c44adeaeb2..83448ea6e2 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/rector.src.php b/rector.src.php index 0525ba79ab..cb26857e2a 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/.gitattributes b/src/bridge/symfony/telemetry-bundle/.gitattributes new file mode 100644 index 0000000000..e020972059 --- /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 0000000000..24255888e1 --- /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 0000000000..f035b534af --- /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 0000000000..bc3cc4d085 --- /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 0000000000..18c6b80a65 --- /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 0000000000..f6e5ee676a --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/composer.json @@ -0,0 +1,60 @@ +{ + "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" + }, + "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", + "twig/twig": "^3.0" + }, + "suggest": { + "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support", + "symfony/messenger": "Required for Messenger tracing middleware", + "twig/twig": "Required for Twig template tracing" + }, + "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 0000000000..f734932521 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php @@ -0,0 +1,5 @@ +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 0000000000..98b43f57fe --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/Configuration.php @@ -0,0 +1,380 @@ +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('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() + ->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.') + ->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('telemetry') + ->info('Auto-telemetry configuration') + ->addDefaultsIfNotSet() + ->children() + ->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() + ->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') + ->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() + ->arrayNode('exclude_templates') + ->info('Template paths to exclude from tracing (supports regex with / delimiters)') + ->scalarPrototype()->end() + ->end() + ->end() + ->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; + } + + 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 0000000000..0b0385f48d --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DependencyInjection/FlowTelemetryExtension.php @@ -0,0 +1,915 @@ + $configs + */ + public function load(array $configs, ContainerBuilder $container) : void + { + $configuration = new Configuration(); + /** @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->registerAutoTelemetry($config['telemetry'] ?? [], $container); + $this->registerTracers($config['tracers'] ?? [], $container); + $this->registerMeters($config['meters'] ?? [], $container); + $this->registerLoggers($config['loggers'] ?? [], $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\\Exporter\\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, ContainerBuilder $container) : string + { + $providerServiceId = 'flow.telemetry.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, ContainerBuilder $container) : string + { + $providerServiceId = 'flow.telemetry.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\\Exporter\\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': + $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, new Reference($optionsServiceId)); + $container->setDefinition($transportServiceId, $definition); + + break; + + case 'http': + $definition = new Definition('Flow\\Bridge\\Telemetry\\OTLP\\Transport\\HttpTransport'); + $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; + + 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, ContainerBuilder $container) : string + { + $samplerServiceId = 'flow.telemetry.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\\Exporter\\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, ContainerBuilder $container) : string + { + $providerServiceId = 'flow.telemetry.tracer_provider'; + + $processorServiceId = $this->buildSpanProcessor($config['processor'] ?? [], $providerServiceId, $container); + $samplerServiceId = $this->buildSampler($config['sampler'] ?? [], $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)), + }; + } + + /** + * @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 registerAutoTelemetry(array $config, ContainerBuilder $container) : void + { + $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); + + $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); + } + + $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); + + $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'] ?? false) && \interface_exists(MiddlewareInterface::class)) { + $definition = new Definition(TracingMiddleware::class); + $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->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 + */ + 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 + */ + 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 registerTelemetry(array $config, ContainerBuilder $container) : void + { + $tracerProviderServiceId = $this->buildTracerProvider($config['tracer_provider'] ?? [], $container); + $meterProviderServiceId = $this->buildMeterProvider($config['meter_provider'] ?? [], $container); + $loggerProviderServiceId = $this->buildLoggerProvider($config['logger_provider'] ?? [], $container); + + $telemetryServiceId = 'flow.telemetry'; + $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); + + $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/src/Flow/Bridge/Symfony/TelemetryBundle/Exception/Exception.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Exception/Exception.php new file mode 100644 index 0000000000..cb8150f8ba --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Exception/Exception.php @@ -0,0 +1,9 @@ +addCompilerPass(new OTLPAvailabilityPass()); + } +} 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 0000000000..fd2868b10d --- /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/ConsoleSpanSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php new file mode 100644 index 0000000000..eb0c949c0d --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Console/ConsoleSpanSubscriber.php @@ -0,0 +1,122 @@ + $excludeCommands + */ + public function __construct( + private readonly Telemetry $telemetry, + private readonly array $excludeCommands = [], + ) { + } + + public static function getSubscribedEvents() : array + { + return [ + ConsoleEvents::COMMAND => ['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'; + + if (!$this->shouldTrace($commandName)) { + return; + } + + $this->tracer = $this->telemetry->tracer('flow.symfony.console'); + + $attributes = [ + 'command.name' => $commandName, + ]; + + if ($command !== null) { + $attributes['command.class'] = $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; + } + + 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/HttpKernelFlushSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelFlushSubscriber.php new file mode 100644 index 0000000000..afc53193a0 --- /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/HttpKernelSpanSubscriber.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php new file mode 100644 index 0000000000..c90c4eacbc --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/HttpKernel/HttpKernelSpanSubscriber.php @@ -0,0 +1,193 @@ + $excludeRoutes + */ + public function __construct( + private Telemetry $telemetry, + private array $excludeRoutes = [], + ) { + } + + public static function getSubscribedEvents() : array + { + return [ + KernelEvents::REQUEST => ['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)) { + 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}"); + } + + $controller = $event->getController(); + $controllerName = $this->resolveControllerName($controller); + + if ($controllerName !== null) { + $span->setAttribute('controller', $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); + } + + 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 + */ + 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; + } + + 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/Messenger/TracingMiddleware.php b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Messenger/TracingMiddleware.php new file mode 100644 index 0000000000..de85426e57 --- /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/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 0000000000..fd533c59a5 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/Telemetry/Twig/TracingTwigExtension.php @@ -0,0 +1,153 @@ + + */ + 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; + } + + $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 ($profile->isTemplate() && $this->isTemplateExcluded($profile->getTemplate())) { + $this->excludedDepth--; + + return; + } + + 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 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()) { + 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/Context/SymfonyContext.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Context/SymfonyContext.php new file mode 100644 index 0000000000..2a53d1c91e --- /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', false); + + 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/Command/FailingCommand.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/Command/FailingCommand.php new file mode 100644 index 0000000000..c3f4dcf25b --- /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 0000000000..8d18437d71 --- /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 0000000000..40713b14e2 --- /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/TestKernel.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/TestKernel.php new file mode 100644 index 0000000000..8dd5c42591 --- /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/Fixtures/config/routes.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/config/routes.php new file mode 100644 index 0000000000..ecd309f0fe --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Fixtures/config/routes.php @@ -0,0 +1,9 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + ['type' => 'memory', 'exporter' => ['type' => 'memory']], + ['type' => 'passthrough', 'exporter' => ['type' => 'console']], + ], + ], + ], + ]); + }, + ]); + + self::assertInstanceOf(CompositeLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); + } + + public function test_composite_metric_processor() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + ['type' => 'memory', 'exporter' => ['type' => 'memory']], + ['type' => 'passthrough', 'exporter' => ['type' => 'console']], + ], + ], + ], + ]); + }, + ]); + + self::assertInstanceOf(CompositeMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } + + public function test_composite_span_processor() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + ['type' => 'memory', 'exporter' => ['type' => 'memory']], + ['type' => 'passthrough', 'exporter' => ['type' => 'console']], + ], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertInstanceOf(CompositeSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor')); + self::assertInstanceOf(MemorySpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor.0.processor')); + self::assertInstanceOf(PassThroughSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor.1.processor')); + } + + public function test_custom_service_reference_for_exporter() : void + { + $container = new ContainerBuilder(); + + $container->register('my.custom.span_exporter', MemorySpanExporter::class)->setPublic(true); + + $extension = new FlowTelemetryExtension(); + $extension->load([ + [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'passthrough', + 'exporter' => [ + 'type' => 'service', + 'service_id' => 'my.custom.span_exporter', + ], + ], + ], + ], + ], $container); + + $this->makeFlowServicesPublic($container); + $container->compile(); + + self::assertSame( + $container->get('my.custom.span_exporter'), + $container->get('flow.telemetry.tracer_provider.processor.exporter') + ); + } + + public function test_custom_service_reference_for_processor() : void + { + $container = new ContainerBuilder(); + + $container->register('my.custom.span_processor', VoidSpanProcessor::class)->setPublic(true); + + $extension = new FlowTelemetryExtension(); + $extension->load([ + [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'service', + 'service_id' => 'my.custom.span_processor', + ], + ], + ], + ], $container); + + $this->makeFlowServicesPublic($container); + $container->compile(); + + self::assertSame( + $container->get('my.custom.span_processor'), + $container->get('flow.telemetry.tracer_provider.processor') + ); + } + + public function test_custom_service_reference_for_sampler() : void + { + $container = new ContainerBuilder(); + + $container->register('my.custom.sampler', AlwaysOffSampler::class)->setPublic(true); + + $extension = new FlowTelemetryExtension(); + $extension->load([ + [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'service', + 'service_id' => 'my.custom.sampler', + ], + ], + ], + ], $container); + + $this->makeFlowServicesPublic($container); + $container->compile(); + + self::assertSame( + $container->get('my.custom.sampler'), + $container->get('flow.telemetry.tracer_provider.sampler') + ); + } + + public function test_flow_telemetry_is_aliased_to_telemetry_class() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has(Telemetry::class)); + self::assertSame($container->get('flow.telemetry'), $container->get(Telemetry::class)); + } + + public function test_full_configuration_scenario() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => [ + 'name' => 'my-application', + 'version' => '3.0.0', + 'attributes' => [ + 'deployment.environment' => 'staging', + ], + ], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.75, + ], + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 1024, + 'exporter' => ['type' => 'console'], + ], + ], + 'meter_provider' => [ + 'temporality' => 'delta', + 'processor' => [ + 'type' => 'passthrough', + 'exporter' => ['type' => 'memory'], + ], + ], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'console'], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var resource $resource */ + $resource = $container->get('flow.telemetry.resource'); + self::assertSame('my-application', $resource->get('service.name')); + self::assertSame('3.0.0', $resource->get('service.version')); + self::assertSame('staging', $resource->get('deployment.environment')); + + self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry')); + + self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.tracer_provider')); + self::assertInstanceOf(TraceIdRatioBasedSampler::class, $container->get('flow.telemetry.tracer_provider.sampler')); + self::assertInstanceOf(BatchingSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor')); + self::assertInstanceOf(ConsoleSpanExporter::class, $container->get('flow.telemetry.tracer_provider.processor.exporter')); + + self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.meter_provider')); + self::assertInstanceOf(PassThroughMetricProcessor::class, $container->get('flow.telemetry.meter_provider.processor')); + self::assertInstanceOf(MemoryMetricExporter::class, $container->get('flow.telemetry.meter_provider.processor.exporter')); + + self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.logger_provider')); + self::assertInstanceOf(MemoryLogProcessor::class, $container->get('flow.telemetry.logger_provider.processor')); + self::assertInstanceOf(ConsoleLogExporter::class, $container->get('flow.telemetry.logger_provider.processor.exporter')); + } + + public function test_log_exporter_console_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]], + ]); + }, + ]); + + self::assertInstanceOf(ConsoleLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter')); + } + + public function test_log_exporter_memory_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]], + ]); + }, + ]); + + self::assertInstanceOf(MemoryLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter')); + } + + public function test_log_exporter_void_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(VoidLogExporter::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor.exporter')); + } + + public function test_log_processor_batching_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 256, 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(BatchingLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); + } + + public function test_log_processor_memory_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(MemoryLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); + } + + public function test_log_processor_passthrough_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(PassThroughLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); + } + + public function test_log_processor_void_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'logger_provider' => ['processor' => ['type' => 'void']], + ]); + }, + ]); + + self::assertInstanceOf(VoidLogProcessor::class, $this->getContainer()->get('flow.telemetry.logger_provider.processor')); + } + + public function test_metric_exporter_console_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]], + ]); + }, + ]); + + self::assertInstanceOf(ConsoleMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter')); + } + + public function test_metric_exporter_memory_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]], + ]); + }, + ]); + + self::assertInstanceOf(MemoryMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter')); + } + + public function test_metric_exporter_void_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(VoidMetricExporter::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor.exporter')); + } + + public function test_metric_processor_batching_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 200, 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(BatchingMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } + + public function test_metric_processor_memory_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(MemoryMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } + + public function test_metric_processor_passthrough_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(PassThroughMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } + + public function test_metric_processor_void_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meter_provider' => ['processor' => ['type' => 'void']], + ]); + }, + ]); + + self::assertInstanceOf(VoidMetricProcessor::class, $this->getContainer()->get('flow.telemetry.meter_provider.processor')); + } + + public function test_minimal_configuration_creates_telemetry_with_void_processors() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.clock')); + self::assertTrue($container->has('flow.telemetry.context_storage')); + self::assertTrue($container->has('flow.telemetry.resource')); + self::assertTrue($container->has('flow.telemetry')); + + self::assertInstanceOf(SystemClock::class, $container->get('flow.telemetry.clock')); + self::assertInstanceOf(MemoryContextStorage::class, $container->get('flow.telemetry.context_storage')); + self::assertInstanceOf(Resource::class, $container->get('flow.telemetry.resource')); + self::assertInstanceOf(Telemetry::class, $container->get('flow.telemetry')); + + self::assertTrue($container->has('flow.telemetry.tracer_provider')); + self::assertTrue($container->has('flow.telemetry.meter_provider')); + self::assertTrue($container->has('flow.telemetry.logger_provider')); + + self::assertInstanceOf(TracerProvider::class, $container->get('flow.telemetry.tracer_provider')); + self::assertInstanceOf(MeterProvider::class, $container->get('flow.telemetry.meter_provider')); + self::assertInstanceOf(LoggerProvider::class, $container->get('flow.telemetry.logger_provider')); + + self::assertTrue($container->has('flow.telemetry.tracer_provider.processor')); + self::assertInstanceOf(VoidSpanProcessor::class, $container->get('flow.telemetry.tracer_provider.processor')); + + self::assertTrue($container->has('flow.telemetry.meter_provider.processor')); + self::assertInstanceOf(VoidMetricProcessor::class, $container->get('flow.telemetry.meter_provider.processor')); + + self::assertTrue($container->has('flow.telemetry.logger_provider.processor')); + self::assertInstanceOf(VoidLogProcessor::class, $container->get('flow.telemetry.logger_provider.processor')); + } + + public function test_multiple_named_services_of_same_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'database' => [ + 'version' => '1.0.0', + ], + 'http_client' => [ + 'version' => '2.0.0', + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.database.tracer')); + self::assertTrue($container->has('flow.telemetry.http_client.tracer')); + self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.database.tracer')); + self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.http_client.tracer')); + } + + public function test_named_logger_is_registered_as_service() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'loggers' => [ + 'audit' => [ + 'version' => '1.0.0', + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.audit.logger')); + self::assertInstanceOf(Logger::class, $container->get('flow.telemetry.audit.logger')); + } + + public function test_named_meter_is_registered_as_service() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'meters' => [ + 'etl_pipeline' => [ + 'version' => '1.0.0', + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.etl_pipeline.meter')); + self::assertInstanceOf(Meter::class, $container->get('flow.telemetry.etl_pipeline.meter')); + } + + public function test_named_tracer_is_registered_as_service() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'database' => [ + 'version' => '2.0.0', + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.database.tracer')); + self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.database.tracer')); + } + + public function test_named_tracer_with_attributes() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'database' => [ + 'version' => '2.0.0', + 'schema_url' => 'https://opentelemetry.io/schemas/1.20.0', + 'attributes' => [ + 'db.system' => 'postgresql', + ], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + $tracer = $container->get('flow.telemetry.database.tracer'); + + self::assertInstanceOf(Tracer::class, $tracer); + } + + public function test_otlp_availability_pass_sets_parameter_when_otlp_not_configured() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + ]); + }, + ]); + + self::assertTrue($this->getContainer()->hasParameter('flow.telemetry.otlp_available')); + } + + public function test_resource_contains_additional_attributes() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => [ + 'name' => 'my-service', + 'attributes' => [ + 'deployment.environment' => 'production', + 'host.name' => 'server-01', + ], + ], + ]); + }, + ]); + + /** @var resource $resource */ + $resource = $this->getContainer()->get('flow.telemetry.resource'); + self::assertSame('production', $resource->get('deployment.environment')); + self::assertSame('server-01', $resource->get('host.name')); + } + + public function test_resource_contains_service_name_and_version() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => [ + 'name' => 'my-service', + 'version' => '2.1.0', + ], + ]); + }, + ]); + + /** @var resource $resource */ + $resource = $this->getContainer()->get('flow.telemetry.resource'); + self::assertSame('my-service', $resource->get('service.name')); + self::assertSame('2.1.0', $resource->get('service.version')); + } + + public function test_same_name_for_different_types_is_allowed() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'database' => ['version' => '1.0.0'], + ], + 'meters' => [ + 'database' => ['version' => '1.0.0'], + ], + 'loggers' => [ + 'database' => ['version' => '1.0.0'], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.database.tracer')); + self::assertTrue($container->has('flow.telemetry.database.meter')); + self::assertTrue($container->has('flow.telemetry.database.logger')); + self::assertInstanceOf(Tracer::class, $container->get('flow.telemetry.database.tracer')); + self::assertInstanceOf(Meter::class, $container->get('flow.telemetry.database.meter')); + self::assertInstanceOf(Logger::class, $container->get('flow.telemetry.database.logger')); + } + + public function test_service_exporter_without_service_id_throws_exception() : void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('service_id is required when exporter type is "service"'); + + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'passthrough', + 'exporter' => ['type' => 'service'], + ], + ], + ]); + }, + ]); + } + + public function test_service_processor_without_service_id_throws_exception() : void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('service_id is required when processor type is "service"'); + + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => ['type' => 'service'], + ], + ]); + }, + ]); + } + + public function test_service_sampler_without_service_id_throws_exception() : void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('service_id is required when sampler type is "service"'); + + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => ['type' => 'service'], + ], + ]); + }, + ]); + } + + public function test_span_exporter_console_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'console']]], + ]); + }, + ]); + + self::assertInstanceOf(ConsoleSpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter')); + } + + public function test_span_exporter_memory_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'memory']]], + ]); + }, + ]); + + self::assertInstanceOf(MemorySpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter')); + } + + public function test_span_exporter_void_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(VoidSpanExporter::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor.exporter')); + } + + public function test_span_processor_batching_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'batching', 'batch_size' => 100, 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(BatchingSpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor')); + } + + public function test_span_processor_memory_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'memory', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(MemorySpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor')); + } + + public function test_span_processor_passthrough_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'passthrough', 'exporter' => ['type' => 'void']]], + ]); + }, + ]); + + self::assertInstanceOf(PassThroughSpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor')); + } + + public function test_span_processor_void_type() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => ['processor' => ['type' => 'void']], + ]); + }, + ]); + + self::assertInstanceOf(VoidSpanProcessor::class, $this->getContainer()->get('flow.telemetry.tracer_provider.processor')); + } + + public function test_tracer_provider_with_always_off_sampler() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_off'], + ], + ]); + }, + ]); + + self::assertInstanceOf(AlwaysOffSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler')); + } + + public function test_tracer_provider_with_always_on_sampler() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => ['type' => 'always_on'], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.tracer_provider.sampler')); + self::assertInstanceOf(AlwaysOnSampler::class, $container->get('flow.telemetry.tracer_provider.sampler')); + } + + public function test_tracer_provider_with_parent_based_sampler() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => ['type' => 'parent_based'], + ], + ]); + }, + ]); + + self::assertInstanceOf(ParentBasedSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler')); + } + + public function test_tracer_provider_with_trace_id_ratio_sampler() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.5, + ], + ], + ]); + }, + ]); + + self::assertInstanceOf(TraceIdRatioBasedSampler::class, $this->getContainer()->get('flow.telemetry.tracer_provider.sampler')); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php new file mode 100644 index 0000000000..fb84da639e --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/KernelTestCase.php @@ -0,0 +1,59 @@ +context = new SymfonyContext(); + } + + protected function tearDown() : void + { + $this->context->shutdown(); + } + + /** + * @param array{config?: callable(TestKernel): void} $options + */ + protected function bootKernel(array $options = []) : TestKernel + { + return $this->context->bootKernel($options); + } + + protected function getContainer() : ContainerInterface + { + return $this->context->getContainer(); + } + + protected function getKernel() : TestKernel + { + return $this->context->getKernel(); + } + + protected function makeFlowServicesPublic(ContainerBuilder $container) : void + { + foreach ($container->getDefinitions() as $id => $definition) { + if (\str_starts_with($id, 'flow.telemetry')) { + $definition->setPublic(true); + } + } + + foreach ($container->getAliases() as $id => $alias) { + if ($id === Telemetry::class || \str_starts_with($id, 'flow.telemetry')) { + $alias->setPublic(true); + } + } + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php new file mode 100644 index 0000000000..403cfcf678 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleFlushSubscriberTest.php @@ -0,0 +1,127 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 100, + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => true], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new TestCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new BufferedOutput(); + + $exitCode = $application->run($input, $output); + + self::assertSame(0, $exitCode); + + $container = $this->getContainer(); + /** @var MemorySpanExporter $exporter */ + $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter'); + $spans = $exporter->spans(); + + self::assertCount(1, $spans, 'Spans should be exported after console terminate when flush is called'); + } + + public function test_flush_is_not_called_when_console_instrumentation_is_disabled() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 100, + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new TestCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new BufferedOutput(); + + $application->run($input, $output); + + $container = $this->getContainer(); + /** @var MemorySpanExporter $exporter */ + $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter'); + $spans = $exporter->spans(); + + self::assertCount(0, $spans, 'No spans should be exported when instrumentation is disabled'); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php new file mode 100644 index 0000000000..1a9648ba23 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Console/ConsoleSpanSubscriberTest.php @@ -0,0 +1,305 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new TestCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new BufferedOutput(); + + $application->run($input, $output); + + $container = $this->getContainer(); + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(0, $spans); + } + + public function test_excludes_command_with_exact_match() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => [ + 'enabled' => true, + 'exclude_commands' => ['test:command'], + ], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new TestCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new BufferedOutput(); + + $application->run($input, $output); + + $container = $this->getContainer(); + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(0, $spans); + } + + public function test_excludes_command_with_regex_pattern() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => [ + 'enabled' => true, + 'exclude_commands' => ['/^test:.*/'], + ], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new TestCommand()); + $application->add(new FailingCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new BufferedOutput(); + + $application->run($input, $output); + + $input = new ArrayInput(['command' => 'test:failing']); + $application->run($input, $output); + + $container = $this->getContainer(); + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(0, $spans, 'Both test:command and test:failing should be excluded by regex'); + } + + public function test_traces_failing_console_command() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => true], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new FailingCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:failing']); + $output = new BufferedOutput(); + + $exitCode = $application->run($input, $output); + + self::assertSame(1, $exitCode); + + $container = $this->getContainer(); + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(1, $spans); + + $span = $spans[0]; + self::assertSame('test:failing', $span->name()); + + $attributes = $span->attributes(); + self::assertSame(1, $attributes['process.exit_code']); + + $status = $span->status(); + self::assertNotNull($status); + self::assertSame('Exit code: 1', $status->description); + } + + public function test_traces_successful_console_command() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => true], + 'messenger' => false, + ], + ]); + }, + ]); + + $application = new Application($kernel); + $application->add(new TestCommand()); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['command' => 'test:command']); + $output = new BufferedOutput(); + + $exitCode = $application->run($input, $output); + + self::assertSame(0, $exitCode); + + $container = $this->getContainer(); + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(1, $spans); + + $span = $spans[0]; + self::assertSame('test:command', $span->name()); + self::assertSame(SpanKind::INTERNAL, $span->kind()); + + $attributes = $span->attributes(); + self::assertSame('test:command', $attributes['command.name']); + self::assertSame(TestCommand::class, $attributes['command.class']); + self::assertSame(0, $attributes['process.exit_code']); + + $status = $span->status(); + self::assertNotNull($status); + self::assertTrue($status->isOk()); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php new file mode 100644 index 0000000000..5fad0e3b48 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelFlushSubscriberTest.php @@ -0,0 +1,129 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 100, + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Router $router */ + $router = $container->get('router'); + $routes = $router->getRouteCollection(); + $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index'])); + + $request = Request::create('/test', 'GET'); + $response = $kernel->handle($request); + + /** @var MemorySpanExporter $exporter */ + $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter'); + $spansBeforeTerminate = $exporter->spans(); + + self::assertCount(0, $spansBeforeTerminate, 'Spans should not be exported before terminate (batching)'); + + $kernel->terminate($request, $response); + + $spansAfterTerminate = $exporter->spans(); + + self::assertCount(1, $spansAfterTerminate, 'Spans should be exported after terminate when flush is called'); + } + + public function test_flush_is_not_called_when_http_kernel_instrumentation_is_disabled() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 100, + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Router $router */ + $router = $container->get('router'); + $routes = $router->getRouteCollection(); + $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index'])); + + $request = Request::create('/test', 'GET'); + $response = $kernel->handle($request); + $kernel->terminate($request, $response); + + /** @var MemorySpanExporter $exporter */ + $exporter = $container->get('flow.telemetry.tracer_provider.processor.exporter'); + $spans = $exporter->spans(); + + self::assertCount(0, $spans, 'No spans should be exported when instrumentation is disabled'); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php new file mode 100644 index 0000000000..e57803585f --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/HttpKernel/HttpKernelSpanSubscriberTest.php @@ -0,0 +1,194 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Router $router */ + $router = $container->get('router'); + $routes = $router->getRouteCollection(); + $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index'])); + + $request = Request::create('/test', 'GET'); + $response = $kernel->handle($request); + $kernel->terminate($request, $response); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(0, $spans); + } + + public function test_traces_http_request_with_error_status() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Router $router */ + $router = $container->get('router'); + $routes = $router->getRouteCollection(); + $routes->add('test_error', new Route('/error', ['_controller' => TestController::class . '::error'])); + + $request = Request::create('/error', 'GET'); + $response = $kernel->handle($request); + $kernel->terminate($request, $response); + + self::assertSame(404, $response->getStatusCode()); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(1, $spans); + + $span = $spans[0]; + $attributes = $span->attributes(); + self::assertSame(404, $attributes['http.status_code']); + + $status = $span->status(); + self::assertNotNull($status); + self::assertTrue($status->isError()); + self::assertSame('HTTP 404', $status->description); + } + + public function test_traces_successful_http_request() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestBundle(FrameworkBundle::class); + $kernel->addTestExtensionConfig('framework', [ + 'router' => [ + 'utf8' => true, + 'resource' => __DIR__ . '/../../../Fixtures/config/routes.php', + ], + 'http_method_override' => false, + 'handle_all_throwables' => true, + ]); + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => false], + 'messenger' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Router $router */ + $router = $container->get('router'); + $routes = $router->getRouteCollection(); + $routes->add('test_index', new Route('/test', ['_controller' => TestController::class . '::index'])); + + $request = Request::create('/test', 'GET'); + $response = $kernel->handle($request); + $kernel->terminate($request, $response); + + self::assertSame(200, $response->getStatusCode()); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(1, $spans); + + $span = $spans[0]; + self::assertSame('GET test_index', $span->name()); + self::assertSame(SpanKind::SERVER, $span->kind()); + + $attributes = $span->attributes(); + self::assertSame('GET', $attributes['http.method']); + self::assertSame(200, $attributes['http.status_code']); + self::assertSame('test_index', $attributes['http.route']); + self::assertSame(TestController::class . '::index', $attributes['controller']); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php new file mode 100644 index 0000000000..15e2da1eca --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Messenger/TracingMiddlewareTest.php @@ -0,0 +1,206 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'messenger' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertFalse($container->has('flow.telemetry.messenger.middleware')); + } + + public function test_middleware_service_is_registered() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'messenger' => true, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.messenger.middleware')); + self::assertInstanceOf(TracingMiddleware::class, $container->get('flow.telemetry.messenger.middleware')); + } + + public function test_traces_message_dispatch() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => true, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Telemetry $telemetry */ + $telemetry = $container->get(Telemetry::class); + + $handler = new TestMessageHandler(); + + $bus = new MessageBus([ + new TracingMiddleware($telemetry), + new HandleMessageMiddleware( + new HandlersLocator([ + TestMessage::class => [$handler], + ]) + ), + ]); + + $message = new TestMessage('test content'); + $envelope = new Envelope( + $message, + [new BusNameStamp('command.bus')] + ); + + $bus->dispatch($envelope); + + self::assertTrue($handler->handled); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(1, $spans); + + $span = $spans[0]; + self::assertSame('command.bus TestMessage', $span->name()); + self::assertSame(SpanKind::PRODUCER, $span->kind()); + + $attributes = $span->attributes(); + self::assertSame('symfony_messenger', $attributes['messaging.system']); + self::assertSame('command.bus', $attributes['messaging.destination']); + self::assertSame(TestMessage::class, $attributes['messaging.message.class']); + self::assertSame('send', $attributes['messaging.operation']); + + $status = $span->status(); + self::assertNotNull($status); + self::assertTrue($status->isOk()); + } + + public function test_traces_message_with_exception() : void + { + $kernel = $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => true, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var Telemetry $telemetry */ + $telemetry = $container->get(Telemetry::class); + + $failingHandler = static function (TestMessage $message) : void { + throw new \RuntimeException('Handler failed'); + }; + + $bus = new MessageBus([ + new TracingMiddleware($telemetry), + new HandleMessageMiddleware( + new HandlersLocator([ + TestMessage::class => [$failingHandler], + ]) + ), + ]); + + $message = new TestMessage('test content'); + + $exceptionThrown = false; + + try { + $bus->dispatch($message); + } catch (\Throwable) { + $exceptionThrown = true; + } + + self::assertTrue($exceptionThrown, 'Expected exception was not thrown'); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertCount(1, $spans); + + $span = $spans[0]; + + $status = $span->status(); + self::assertNotNull($status); + self::assertTrue($status->isError()); + self::assertStringContainsString('Handler failed', $status->description ?? ''); + + $events = $span->events(); + self::assertCount(1, $events); + self::assertSame('exception', $events[0]->name()); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php new file mode 100644 index 0000000000..d950ce071d --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Integration/Telemetry/Twig/TracingTwigExtensionTest.php @@ -0,0 +1,421 @@ +bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => ['enabled' => false], + 'console' => ['enabled' => false], + 'messenger' => false, + 'twig' => [ + 'enabled' => true, + 'trace_blocks' => false, + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var TracingTwigExtension $extension */ + $extension = $container->get('flow.telemetry.twig.extension'); + + $loader = new ArrayLoader([ + 'base.html.twig' => '{% block content %}Default content{% endblock %}', + 'child.html.twig' => '{% extends "base.html.twig" %}{% block content %}Child content{% endblock %}', + ]); + + $twig = new Environment($loader); + $twig->addExtension($extension); + + $result = $twig->render('child.html.twig'); + + self::assertSame('Child content', $result); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + foreach ($spans as $span) { + $attributes = $span->attributes(); + self::assertNotSame('block', $attributes['twig.type'] ?? '', 'Block span should not be traced'); + } + } + + public function test_does_not_trace_excluded_templates() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => false, + 'twig' => [ + 'enabled' => true, + 'exclude_templates' => ['excluded.html.twig'], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var TracingTwigExtension $extension */ + $extension = $container->get('flow.telemetry.twig.extension'); + + $loader = new ArrayLoader([ + 'included.html.twig' => 'Included template', + 'excluded.html.twig' => 'Excluded template', + ]); + + $twig = new Environment($loader); + $twig->addExtension($extension); + + $twig->render('included.html.twig'); + $twig->render('excluded.html.twig'); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + $templateNames = []; + + foreach ($spans as $span) { + $attributes = $span->attributes(); + + if (($attributes['twig.type'] ?? '') === 'template') { + $templateNames[] = $attributes['twig.template']; + } + } + + self::assertContains('included.html.twig', $templateNames, 'Included template should be traced'); + self::assertNotContains('excluded.html.twig', $templateNames, 'Excluded template should not be traced'); + } + + public function test_does_not_trace_excluded_templates_with_regex() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => false, + 'twig' => [ + 'enabled' => true, + 'exclude_templates' => ['/^@Profiler.*/'], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var TracingTwigExtension $extension */ + $extension = $container->get('flow.telemetry.twig.extension'); + + $loader = new ArrayLoader([ + 'included.html.twig' => 'Included template', + '@Profiler/toolbar.html.twig' => 'Profiler toolbar', + '@Profiler/panel.html.twig' => 'Profiler panel', + ]); + + $twig = new Environment($loader); + $twig->addExtension($extension); + + $twig->render('included.html.twig'); + $twig->render('@Profiler/toolbar.html.twig'); + $twig->render('@Profiler/panel.html.twig'); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + $templateNames = []; + + foreach ($spans as $span) { + $attributes = $span->attributes(); + + if (($attributes['twig.type'] ?? '') === 'template') { + $templateNames[] = $attributes['twig.template']; + } + } + + self::assertContains('included.html.twig', $templateNames, 'Included template should be traced'); + self::assertNotContains('@Profiler/toolbar.html.twig', $templateNames, 'Profiler toolbar should not be traced'); + self::assertNotContains('@Profiler/panel.html.twig', $templateNames, 'Profiler panel should not be traced'); + } + + public function test_excluded_template_cascades_to_children() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => false, + 'twig' => [ + 'enabled' => true, + 'trace_blocks' => true, + 'exclude_templates' => ['excluded.html.twig'], + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var TracingTwigExtension $extension */ + $extension = $container->get('flow.telemetry.twig.extension'); + + $loader = new ArrayLoader([ + 'child.html.twig' => 'Child content', + 'excluded.html.twig' => '{% block content %}Block content{% endblock %}{% include "child.html.twig" %}', + ]); + + $twig = new Environment($loader); + $twig->addExtension($extension); + + $twig->render('excluded.html.twig'); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + $templateNames = []; + $blockNames = []; + + foreach ($spans as $span) { + $attributes = $span->attributes(); + $type = $attributes['twig.type'] ?? ''; + + if ($type === 'template') { + $templateNames[] = $attributes['twig.template']; + } elseif ($type === 'block') { + $blockNames[] = $attributes['twig.name']; + } + } + + self::assertNotContains('excluded.html.twig', $templateNames, 'Excluded template should not be traced'); + self::assertNotContains('child.html.twig', $templateNames, 'Child template should not be traced when parent is excluded'); + self::assertNotContains('content', $blockNames, 'Block in excluded template should not be traced'); + } + + public function test_extension_not_registered_when_disabled() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'twig' => false, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertFalse($container->has('flow.telemetry.twig.extension')); + } + + public function test_extension_service_is_registered() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'twig' => true, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + self::assertTrue($container->has('flow.telemetry.twig.extension')); + self::assertInstanceOf(TracingTwigExtension::class, $container->get('flow.telemetry.twig.extension')); + } + + public function test_traces_blocks_in_templates() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => false, + 'twig' => [ + 'enabled' => true, + 'trace_blocks' => true, + ], + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var TracingTwigExtension $extension */ + $extension = $container->get('flow.telemetry.twig.extension'); + + $loader = new ArrayLoader([ + 'base.html.twig' => '{% block content %}Default content{% endblock %}', + 'child.html.twig' => '{% extends "base.html.twig" %}{% block content %}Child content{% endblock %}', + ]); + + $twig = new Environment($loader); + $twig->addExtension($extension); + + $result = $twig->render('child.html.twig'); + + self::assertSame('Child content', $result); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertGreaterThanOrEqual(1, \count($spans)); + + $blockSpanFound = false; + + foreach ($spans as $span) { + $attributes = $span->attributes(); + + if (($attributes['twig.type'] ?? '') === 'block') { + $blockSpanFound = true; + self::assertSame('content', $attributes['twig.name']); + } + } + + self::assertTrue($blockSpanFound, 'Expected block span was not found'); + } + + public function test_traces_template_rendering() : void + { + $this->bootKernel([ + 'config' => static function (TestKernel $kernel) : void { + $kernel->addTestExtensionConfig('flow_telemetry', [ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'memory', + 'exporter' => ['type' => 'memory'], + ], + ], + 'telemetry' => [ + 'http_kernel' => false, + 'console' => false, + 'messenger' => false, + 'twig' => true, + ], + ]); + }, + ]); + + $container = $this->getContainer(); + + /** @var TracingTwigExtension $extension */ + $extension = $container->get('flow.telemetry.twig.extension'); + + $loader = new ArrayLoader([ + 'test.html.twig' => 'Hello {{ name }}!', + ]); + + $twig = new Environment($loader); + $twig->addExtension($extension); + + $result = $twig->render('test.html.twig', ['name' => 'World']); + + self::assertSame('Hello World!', $result); + + /** @var MemorySpanProcessor $processor */ + $processor = $container->get('flow.telemetry.tracer_provider.processor'); + $spans = $processor->endedSpans(); + + self::assertGreaterThanOrEqual(1, \count($spans)); + + $templateSpanFound = false; + + foreach ($spans as $span) { + if ($span->name() === 'test.html.twig') { + $templateSpanFound = true; + $attributes = $span->attributes(); + self::assertSame('template', $attributes['twig.type']); + self::assertSame('test.html.twig', $attributes['twig.template']); + } + } + + self::assertTrue($templateSpanFound, 'Expected template span was not found'); + } +} diff --git a/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000000..24733a8fb1 --- /dev/null +++ b/src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,606 @@ +processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'composite', + 'processors' => [ + [ + 'type' => 'memory', + ], + [ + 'type' => 'batching', + 'batch_size' => 100, + ], + ], + ], + ], + ]]); + + $processors = $config['tracer_provider']['processor']['processors']; + self::assertCount(2, $processors); + self::assertSame('memory', $processors[0]['type']); + self::assertSame('batching', $processors[1]['type']); + self::assertSame(100, $processors[1]['batch_size']); + } + + public function test_empty_service_name_is_rejected() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => ''], + ]]); + } + + public function test_empty_tracers_meters_loggers_config() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracers' => [], + 'meters' => [], + 'loggers' => [], + ]]); + + self::assertSame([], $config['tracers']); + self::assertSame([], $config['meters']); + self::assertSame([], $config['loggers']); + } + + public function test_exporter_defaults_to_void() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + ], + ], + ]]); + + self::assertSame('void', $config['tracer_provider']['processor']['exporter']['type']); + } + + public function test_invalid_exporter_type_is_rejected() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'invalid_exporter', + ], + ], + ], + ]]); + } + + public function test_invalid_processor_type_is_rejected() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'invalid_processor', + ], + ], + ]]); + } + + public function test_invalid_sampler_type_is_rejected() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'invalid_sampler', + ], + ], + ]]); + } + + public function test_invalid_severity_level_is_rejected() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'minimum_severity' => 'invalid_level', + 'inner_processor' => [ + 'type' => 'void', + ], + ], + ], + ]]); + } + + public function test_logger_configuration() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'loggers' => [ + 'audit' => [ + 'version' => '1.0.0', + 'schema_url' => 'https://example.com/audit-schema/1.0', + 'attributes' => [ + 'log.category' => 'audit', + ], + ], + ], + ]]); + + self::assertArrayHasKey('loggers', $config); + self::assertArrayHasKey('audit', $config['loggers']); + self::assertSame('1.0.0', $config['loggers']['audit']['version']); + self::assertSame('https://example.com/audit-schema/1.0', $config['loggers']['audit']['schema_url']); + self::assertSame(['log.category' => 'audit'], $config['loggers']['audit']['attributes']); + } + + public function test_meter_configuration() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'meters' => [ + 'etl_pipeline' => [ + 'version' => '1.0.0', + 'attributes' => [ + 'flow.pipeline' => 'daily_import', + ], + ], + ], + ]]); + + self::assertArrayHasKey('meters', $config); + self::assertArrayHasKey('etl_pipeline', $config['meters']); + self::assertSame('1.0.0', $config['meters']['etl_pipeline']['version']); + self::assertNull($config['meters']['etl_pipeline']['schema_url']); + self::assertSame(['flow.pipeline' => 'daily_import'], $config['meters']['etl_pipeline']['attributes']); + } + + public function test_meter_provider_temporality_can_be_delta() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'meter_provider' => [ + 'temporality' => 'delta', + ], + ]]); + + self::assertSame('delta', $config['meter_provider']['temporality']); + } + + public function test_meter_provider_temporality_defaults_to_cumulative() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'meter_provider' => [], + ]]); + + self::assertSame('cumulative', $config['meter_provider']['temporality']); + } + + public function test_minimal_config_requires_service_name() : void + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('service'); + + (new Processor())->processConfiguration(new Configuration(), [[]]); + } + + public function test_minimal_config_with_service_name() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + ]]); + + self::assertSame('test-app', $config['service']['name']); + self::assertNull($config['service']['version']); + self::assertSame([], $config['service']['attributes']); + } + + public function test_multiple_named_items_of_same_type() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'database' => [ + 'version' => '1.0.0', + ], + 'http_client' => [ + 'version' => '2.0.0', + ], + 'cache' => [], + ], + ]]); + + self::assertCount(3, $config['tracers']); + self::assertSame('1.0.0', $config['tracers']['database']['version']); + self::assertSame('2.0.0', $config['tracers']['http_client']['version']); + self::assertSame('unknown', $config['tracers']['cache']['version']); + } + + public function test_otlp_serializer_defaults_to_json() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'otlp', + 'otlp' => [ + 'transport' => [], + ], + ], + ], + ], + ]]); + + $serializer = $config['tracer_provider']['processor']['exporter']['otlp']['transport']['serializer']; + self::assertSame('json', $serializer['type']); + } + + public function test_otlp_transport_defaults() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'exporter' => [ + 'type' => 'otlp', + 'otlp' => [ + 'transport' => [], + ], + ], + ], + ], + ]]); + + $transport = $config['tracer_provider']['processor']['exporter']['otlp']['transport']; + self::assertSame('curl', $transport['type']); + self::assertSame('http://localhost:4318', $transport['endpoint']); + self::assertSame(30, $transport['timeout']); + self::assertSame([], $transport['headers']); + self::assertTrue($transport['insecure']); + } + + public function test_processor_batch_size_default() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + ], + ], + ]]); + + self::assertSame(512, $config['tracer_provider']['processor']['batch_size']); + } + + public function test_processor_batch_size_minimum_validation() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'batching', + 'batch_size' => 0, + ], + ], + ]]); + } + + public function test_processor_defaults_to_void() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [], + ]]); + + self::assertSame('void', $config['tracer_provider']['processor']['type']); + } + + public function test_providers_have_defaults() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + ]]); + + self::assertArrayHasKey('tracer_provider', $config); + self::assertArrayHasKey('meter_provider', $config); + self::assertArrayHasKey('logger_provider', $config); + self::assertSame('void', $config['tracer_provider']['processor']['type']); + self::assertSame('void', $config['meter_provider']['processor']['type']); + self::assertSame('void', $config['logger_provider']['processor']['type']); + } + + public function test_sampler_defaults_to_always_on() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [], + ]]); + + self::assertSame('always_on', $config['tracer_provider']['sampler']['type']); + } + + public function test_sampler_ratio_maximum_validation() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 1.1, + ], + ], + ]]); + } + + public function test_sampler_ratio_minimum_validation() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => -0.1, + ], + ], + ]]); + } + + public function test_sampler_ratio_validation() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'sampler' => [ + 'type' => 'trace_id_ratio', + 'ratio' => 0.5, + ], + ], + ]]); + + self::assertSame(0.5, $config['tracer_provider']['sampler']['ratio']); + } + + public function test_service_config_with_version_and_attributes() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => [ + 'name' => 'test-app', + 'version' => '1.2.3', + 'attributes' => [ + 'environment' => 'production', + 'region' => 'us-east-1', + ], + ], + ]]); + + self::assertSame('test-app', $config['service']['name']); + self::assertSame('1.2.3', $config['service']['version']); + self::assertSame([ + 'environment' => 'production', + 'region' => 'us-east-1', + ], $config['service']['attributes']); + } + + public function test_severity_filtering_is_only_available_for_log_processors() : void + { + $this->expectException(InvalidConfigurationException::class); + + (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracer_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + ], + ], + ]]); + } + + public function test_severity_filtering_minimum_severity_default() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'inner_processor' => [ + 'type' => 'void', + ], + ], + ], + ]]); + + self::assertSame('info', $config['logger_provider']['processor']['minimum_severity']); + } + + public function test_severity_filtering_processor_for_logs() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'logger_provider' => [ + 'processor' => [ + 'type' => 'severity_filtering', + 'minimum_severity' => 'warn', + 'inner_processor' => [ + 'type' => 'batching', + 'exporter' => ['type' => 'console'], + ], + ], + ], + ]]); + + $processor = $config['logger_provider']['processor']; + self::assertSame('severity_filtering', $processor['type']); + self::assertSame('warn', $processor['minimum_severity']); + self::assertSame('batching', $processor['inner_processor']['type']); + self::assertSame('console', $processor['inner_processor']['exporter']['type']); + } + + public function test_telemetry_can_be_enabled() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + 'console' => ['enabled' => true], + 'messenger' => true, + ], + ]]); + + self::assertTrue($config['telemetry']['http_kernel']['enabled']); + self::assertTrue($config['telemetry']['console']['enabled']); + self::assertTrue($config['telemetry']['messenger']); + } + + public function test_telemetry_console_exclude_commands() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'console' => [ + 'enabled' => true, + 'exclude_commands' => ['cache:clear', 'debug:router'], + ], + ], + ]]); + + self::assertTrue($config['telemetry']['console']['enabled']); + self::assertSame(['cache:clear', 'debug:router'], $config['telemetry']['console']['exclude_commands']); + } + + public function test_telemetry_defaults_to_all_disabled() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + ]]); + + self::assertArrayHasKey('telemetry', $config); + self::assertFalse($config['telemetry']['http_kernel']['enabled']); + self::assertFalse($config['telemetry']['console']['enabled']); + self::assertFalse($config['telemetry']['messenger']); + } + + public function test_telemetry_http_kernel_exclude_routes() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'http_kernel' => [ + 'enabled' => true, + 'exclude_routes' => ['_wdt', '_profiler', '/_profiler.*/'], + ], + ], + ]]); + + self::assertTrue($config['telemetry']['http_kernel']['enabled']); + self::assertSame(['_wdt', '_profiler', '/_profiler.*/'], $config['telemetry']['http_kernel']['exclude_routes']); + } + + public function test_telemetry_partial_config() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'http_kernel' => ['enabled' => true], + ], + ]]); + + self::assertTrue($config['telemetry']['http_kernel']['enabled']); + self::assertFalse($config['telemetry']['console']['enabled']); + self::assertFalse($config['telemetry']['messenger']); + } + + public function test_telemetry_twig_exclude_templates() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'telemetry' => [ + 'twig' => [ + 'enabled' => true, + 'exclude_templates' => ['@WebProfiler/Collector/time.html.twig', 'debug/exception.html.twig'], + ], + ], + ]]); + + self::assertTrue($config['telemetry']['twig']['enabled']); + self::assertSame(['@WebProfiler/Collector/time.html.twig', 'debug/exception.html.twig'], $config['telemetry']['twig']['exclude_templates']); + } + + public function test_tracer_configuration_with_all_options() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'database' => [ + 'version' => '2.0.0', + 'schema_url' => 'https://opentelemetry.io/schemas/1.20.0', + 'attributes' => [ + 'db.system' => 'postgresql', + 'db.pool_size' => 10, + ], + ], + ], + ]]); + + self::assertArrayHasKey('tracers', $config); + self::assertArrayHasKey('database', $config['tracers']); + self::assertSame('2.0.0', $config['tracers']['database']['version']); + self::assertSame('https://opentelemetry.io/schemas/1.20.0', $config['tracers']['database']['schema_url']); + self::assertSame([ + 'db.system' => 'postgresql', + 'db.pool_size' => 10, + ], $config['tracers']['database']['attributes']); + } + + public function test_tracer_configuration_with_defaults() : void + { + $config = (new Processor())->processConfiguration(new Configuration(), [[ + 'service' => ['name' => 'test-app'], + 'tracers' => [ + 'http_client' => [], + ], + ]]); + + self::assertArrayHasKey('tracers', $config); + self::assertArrayHasKey('http_client', $config['tracers']); + self::assertSame('unknown', $config['tracers']['http_client']['version']); + self::assertNull($config['tracers']['http_client']['schema_url']); + self::assertSame([], $config['tracers']['http_client']['attributes']); + } +} diff --git a/src/core/etl/src/Flow/ETL/Attribute/Module.php b/src/core/etl/src/Flow/ETL/Attribute/Module.php index fc86d5a705..9c521dc250 100644 --- a/src/core/etl/src/Flow/ETL/Attribute/Module.php +++ b/src/core/etl/src/Flow/ETL/Attribute/Module.php @@ -28,6 +28,7 @@ enum Module : string case PSR7_TELEMETRY_BRIDGE = 'PSR7_TELEMETRY_BRIDGE'; case S3_FILESYSTEM = 'S3_FILESYSTEM'; case SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE = 'SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE'; + case SYMFONY_TELEMETRY_BUNDLE = 'SYMFONY_TELEMETRY_BUNDLE'; case TELEMETRY = 'TELEMETRY'; case TELEMETRY_OTLP = 'TELEMETRY_OTLP'; case TEXT = 'TEXT'; diff --git a/tools/box/composer.lock b/tools/box/composer.lock index 71669fcd25..eaf0068830 100644 --- a/tools/box/composer.lock +++ b/tools/box/composer.lock @@ -1083,29 +1083,29 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.5", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -1125,9 +1125,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "fidry/console", diff --git a/tools/infection/phpunit.xml b/tools/infection/phpunit.xml index 63fe5c1d13..b335a11b44 100644 --- a/tools/infection/phpunit.xml +++ b/tools/infection/phpunit.xml @@ -20,6 +20,7 @@ ../../src/lib/telemetry/tests/Flow/Telemetry/Tests/Unit ../../src/bridge/monolog/telemetry/tests/Flow/Bridge/Monolog/Telemetry/Tests/Unit ../../src/bridge/symfony/http-foundation-telemetry/tests/Flow/Bridge/Symfony/HttpFoundationTelemetry/Tests/Unit + ../../src/bridge/symfony/telemetry-bundle/tests/Flow/Bridge/Symfony/TelemetryBundle/Tests/Unit ../../src/bridge/psr7/telemetry/tests/Flow/Bridge/Psr7/Telemetry/Tests/Unit ../../src/bridge/telemetry/otlp/tests/Flow/Bridge/Telemetry/OTLP/Tests/Unit @@ -36,6 +37,7 @@ ../../src/lib/telemetry/src ../../src/bridge/monolog/telemetry/src ../../src/bridge/symfony/http-foundation-telemetry/src + ../../src/bridge/symfony/telemetry-bundle/src ../../src/bridge/psr7/telemetry/src ../../src/bridge/telemetry/otlp/src @@ -51,6 +53,7 @@ ../../src/lib/telemetry/src/Flow/Telemetry/DSL ../../src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/DSL ../../src/bridge/symfony/http-foundation-telemetry/src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL + ../../src/bridge/symfony/telemetry-bundle/src/Flow/Bridge/Symfony/TelemetryBundle/DSL ../../src/bridge/psr7/telemetry/src/Flow/Bridge/Psr7/Telemetry/DSL ../../src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL diff --git a/tools/phpunit/composer.lock b/tools/phpunit/composer.lock index d38d153743..c7026c892b 100644 --- a/tools/phpunit/composer.lock +++ b/tools/phpunit/composer.lock @@ -592,16 +592,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.50", + "version": "11.5.51", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" + "reference": "ad14159f92910b0f0e3928c13e9b2077529de091" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091", + "reference": "ad14159f92910b0f0e3928c13e9b2077529de091", "shasum": "" }, "require": { @@ -616,7 +616,7 @@ "phar-io/version": "^3.2.1", "php": ">=8.2", "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", @@ -628,6 +628,7 @@ "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -673,7 +674,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51" }, "funding": [ { @@ -697,7 +698,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:59:18+00:00" + "time": "2026-02-05T07:59:30+00:00" }, { "name": "sebastian/cli-parser", diff --git a/tools/rector/composer.lock b/tools/rector/composer.lock index e5b40e09b9..75930daf24 100644 --- a/tools/rector/composer.lock +++ b/tools/rector/composer.lock @@ -62,21 +62,21 @@ }, { "name": "rector/rector", - "version": "2.3.5", + "version": "2.3.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070" + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070", - "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b", + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.36" + "phpstan/phpstan": "^2.1.38" }, "conflict": { "rector/rector-doctrine": "*", @@ -110,7 +110,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.5" + "source": "https://github.com/rectorphp/rector/tree/2.3.6" }, "funding": [ { @@ -118,7 +118,7 @@ "type": "github" } ], - "time": "2026-01-28T15:22:48+00:00" + "time": "2026-02-06T14:25:06+00:00" } ], "aliases": [], diff --git a/web/landing/assets/codemirror/completions/dsl.js b/web/landing/assets/codemirror/completions/dsl.js index bec45bfc2e..43d4807ddb 100644 --- a/web/landing/assets/codemirror/completions/dsl.js +++ b/web/landing/assets/codemirror/completions/dsl.js @@ -1,7 +1,7 @@ /** * CodeMirror Completer for Flow PHP DSL Functions * - * Total functions: 674 + * Total functions: 684 * * This completer provides autocompletion for all Flow PHP DSL functions: * - Extractors (flow-extractors) @@ -5387,6 +5387,24 @@ const dslFunctions = [ }, apply: snippet("\\Flow\\Telemetry\\DSL\\logger_provider(" + "$" + "{" + "1:processor" + "}" + ", " + "$" + "{" + "2:clock" + "}" + ", " + "$" + "{" + "3:contextStorage" + "}" + ")"), boost: 10 + }, { + label: "log_record_converter", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ log_record_converter(SeverityMapper $severityMapper = null, ValueNormalizer $valueNormalizer = null) : LogRecordConverter +
+
+ Create a LogRecordConverter for converting Monolog LogRecord to Telemetry LogRecord.
The converter handles:
- Severity mapping from Monolog Level to Telemetry Severity
- Message body conversion
- Channel and level name as monolog.* attributes
- Context values as context.* attributes (Throwables use setException())
- Extra values as extra.* attributes
@param null|SeverityMapper $severityMapper Custom severity mapper (defaults to standard mapping)
@param null|ValueNormalizer $valueNormalizer Custom value normalizer (defaults to standard normalizer)
Example usage:
\`\`\`php
$converter = log_record_converter();
$telemetryRecord = $converter->convert($monologRecord);
\`\`\`
Example with custom mapper:
\`\`\`php
$converter = log_record_converter(
severityMapper: severity_mapper([
Level::Debug->value => Severity::TRACE,
])
);
\`\`\` +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\log_record_converter(" + "$" + "{" + "1:severityMapper" + "}" + ", " + "$" + "{" + "2:valueNormalizer" + "}" + ")"), + boost: 10 }, { label: "lower", type: "function", @@ -7406,6 +7424,36 @@ const dslFunctions = [ }, apply: snippet("\\Flow\\Filesystem\\DSL\\protocol(" + "$" + "{" + "1:protocol" + "}" + ")"), boost: 10 + }, { + label: "psr7_request_carrier", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ psr7_request_carrier(ServerRequestInterface $request) : RequestCarrier +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Psr7\\Telemetry\\DSL\\psr7_request_carrier(" + "$" + "{" + "1:request" + "}" + ")"), + boost: 10 + }, { + label: "psr7_response_carrier", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ psr7_response_carrier(ResponseInterface $response) : ResponseCarrier +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Psr7\\Telemetry\\DSL\\psr7_response_carrier(" + "$" + "{" + "1:response" + "}" + ")"), + boost: 10 }, { label: "random_string", type: "function", @@ -7888,10 +7936,10 @@ const dslFunctions = [ const div = document.createElement("div") div.innerHTML = `
- resource(array $attributes = []) : Resource + resource(Attributes|array $attributes = []) : Resource
- Create a Resource.
@param array|bool|float|int|string> $attributes Resource attributes + Create a Resource.
@param array|bool|float|int|string>|Attributes $attributes Resource attributes
` return div @@ -8519,6 +8567,42 @@ const dslFunctions = [ }, apply: snippet("\\Flow\\PostgreSql\\DSL\\set_transaction()"), boost: 10 + }, { + label: "severity_filtering_log_processor", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ severity_filtering_log_processor(LogProcessor $processor, Severity $minimumSeverity = Flow\\Telemetry\\Logger\\Severity::...) : SeverityFilteringLogProcessor +
+
+ Create a SeverityFilteringLogProcessor.
Filters log entries based on minimum severity level. Only entries at or above
the configured threshold are passed to the wrapped processor.
@param LogProcessor $processor The processor to wrap
@param Severity $minimumSeverity Minimum severity level (default: INFO) +
+ ` + return div + }, + apply: snippet("\\Flow\\Telemetry\\DSL\\severity_filtering_log_processor(" + "$" + "{" + "1:processor" + "}" + ", " + "$" + "{" + "2:minimumSeverity" + "}" + ")"), + boost: 10 + }, { + label: "severity_mapper", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ severity_mapper(array $customMapping = null) : SeverityMapper +
+
+ Create a SeverityMapper for mapping Monolog levels to Telemetry severities.
@param null|array $customMapping Optional custom mapping (Monolog Level value => Telemetry Severity)
Example with default mapping:
\`\`\`php
$mapper = severity_mapper();
\`\`\`
Example with custom mapping:
\`\`\`php
use Monolog\\Level;
use Flow\\Telemetry\\Logger\\Severity;
$mapper = severity_mapper([
Level::Debug->value => Severity::DEBUG,
Level::Info->value => Severity::INFO,
Level::Notice->value => Severity::WARN, // Custom: NOTICE → WARN instead of INFO
Level::Warning->value => Severity::WARN,
Level::Error->value => Severity::ERROR,
Level::Critical->value => Severity::FATAL,
Level::Alert->value => Severity::FATAL,
Level::Emergency->value => Severity::FATAL,
]);
\`\`\` +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\severity_mapper(" + "$" + "{" + "1:customMapping" + "}" + ")"), + boost: 10 }, { label: "similar_to", type: "function", @@ -8593,15 +8677,15 @@ const dslFunctions = [ const div = document.createElement("div") div.innerHTML = `
- span_event(string $name, array $attributes = []) : GenericEvent + span_event(string $name, DateTimeImmutable $timestamp, Attributes|array $attributes = []) : GenericEvent
- Create a SpanEvent (GenericEvent) with the current timestamp.
@param string $name Event name
@param array|bool|float|int|string> $attributes Event attributes + Create a SpanEvent (GenericEvent) with an explicit timestamp.
@param string $name Event name
@param \\DateTimeImmutable $timestamp Event timestamp
@param array|bool|float|int|string>|Attributes $attributes Event attributes
` return div }, - apply: snippet("\\Flow\\Telemetry\\DSL\\span_event(" + "$" + "{" + "1:name" + "}" + ", " + "$" + "{" + "2:attributes" + "}" + ")"), + apply: snippet("\\Flow\\Telemetry\\DSL\\span_event(" + "$" + "{" + "1:name" + "}" + ", " + "$" + "{" + "2:timestamp" + "}" + ", " + "$" + "{" + "3:attributes" + "}" + ")"), boost: 10 }, { label: "span_id", @@ -8629,10 +8713,10 @@ const dslFunctions = [ const div = document.createElement("div") div.innerHTML = `
- span_link(SpanContext $context, array $attributes = []) : SpanLink + span_link(SpanContext $context, Attributes|array $attributes = []) : SpanLink
- Create a SpanLink.
@param SpanContext $context The linked span context
@param array|bool|float|int|string> $attributes Link attributes + Create a SpanLink.
@param SpanContext $context The linked span context
@param array|bool|float|int|string>|Attributes $attributes Link attributes
` return div @@ -9389,6 +9473,36 @@ const dslFunctions = [ }, apply: snippet("\\Flow\\Telemetry\\DSL\\superglobal_carrier()"), boost: 10 + }, { + label: "symfony_request_carrier", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ symfony_request_carrier(Request $request) : RequestCarrier +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Symfony\\HttpFoundationTelemetry\\DSL\\symfony_request_carrier(" + "$" + "{" + "1:request" + "}" + ")"), + boost: 10 + }, { + label: "symfony_response_carrier", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ symfony_response_carrier(Response $response) : ResponseCarrier +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Symfony\\HttpFoundationTelemetry\\DSL\\symfony_response_carrier(" + "$" + "{" + "1:response" + "}" + ")"), + boost: 10 }, { label: "table", type: "function", @@ -9461,6 +9575,39 @@ const dslFunctions = [ }, apply: snippet("\\Flow\\Telemetry\\DSL\\telemetry(" + "$" + "{" + "1:resource" + "}" + ", " + "$" + "{" + "2:tracerProvider" + "}" + ", " + "$" + "{" + "3:meterProvider" + "}" + ", " + "$" + "{" + "4:loggerProvider" + "}" + ")"), boost: 10 + }, { + label: "telemetry_handler", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ telemetry_handler(Logger $logger, LogRecordConverter $converter = Flow\\Bridge\\Monolog\\Telemetry\\LogRecordConverter::..., Level $level = Monolog\\Level::..., bool $bubble = true) : TelemetryHandler +
+
+ Create a TelemetryHandler for forwarding Monolog logs to Flow Telemetry.
@param Logger $logger The Flow Telemetry logger to forward logs to
@param LogRecordConverter $converter Converter to transform Monolog LogRecord to Telemetry LogRecord
@param Level $level The minimum logging level at which this handler will be triggered
@param bool $bubble Whether messages handled by this handler should bubble up to other handlers
Example usage:
\`\`\`php
use Monolog\\Logger as MonologLogger;
use function Flow\\Bridge\\Monolog\\Telemetry\\DSL\\telemetry_handler;
use function Flow\\Telemetry\\DSL\\telemetry;
$telemetry = telemetry();
$logger = $telemetry->logger(\'my-app\');
$monolog = new MonologLogger(\'channel\');
$monolog->pushHandler(telemetry_handler($logger));
$monolog->info(\'User logged in\', [\'user_id\' => 123]);
// → Forwarded to Flow Telemetry with INFO severity
\`\`\`
Example with custom converter:
\`\`\`php
$converter = log_record_converter(
severityMapper: severity_mapper([
Level::Debug->value => Severity::TRACE,
])
);
$monolog->pushHandler(telemetry_handler($logger, $converter));
\`\`\` +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\telemetry_handler(" + "$" + "{" + "1:logger" + "}" + ", " + "$" + "{" + "2:converter" + "}" + ", " + "$" + "{" + "3:level" + "}" + ", " + "$" + "{" + "4:bubble" + "}" + ")"), + boost: 10 + }, { + label: "telemetry_options", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ telemetry_options(bool $trace_loading = false, bool $trace_transformations = false, bool $collect_metrics = false) : TelemetryOptions +
+ ` + return div + }, + apply: snippet("\\Flow\\ETL\\DSL\\telemetry_options(" + "$" + "{" + "1:trace_loading" + "}" + ", " + "$" + "{" + "2:trace_transformations" + "}" + ", " + "$" + "{" + "3:collect_metrics" + "}" + ")"), + boost: 10 }, { label: "text_search_match", type: "function", @@ -11126,6 +11273,24 @@ const dslFunctions = [ }, apply: snippet("\\Flow\\PostgreSql\\DSL\\values_table(" + "$" + "{" + "1:rows" + "}" + ")"), boost: 10 + }, { + label: "value_normalizer", + type: "function", + detail: "flow\u002Ddsl\u002Dhelpers", + info: () => { + const div = document.createElement("div") + div.innerHTML = ` +
+ value_normalizer() : ValueNormalizer +
+
+ Create a ValueNormalizer for converting arbitrary PHP values to Telemetry attribute types.
The normalizer handles:
- null → \'null\' string
- scalars (string, int, float, bool) → unchanged
- DateTimeInterface → unchanged
- Throwable → unchanged
- arrays → recursively normalized
- objects with __toString() → string cast
- objects without __toString() → class name
- other types → get_debug_type() result
Example usage:
\`\`\`php
$normalizer = value_normalizer();
$normalized = $normalizer->normalize($value);
\`\`\` +
+ ` + return div + }, + apply: snippet("\\Flow\\Bridge\\Monolog\\Telemetry\\DSL\\value_normalizer()"), + boost: 10 }, { label: "void_log_exporter", type: "function", diff --git a/web/landing/composer.json b/web/landing/composer.json index afc733aac0..3d58069692 100644 --- a/web/landing/composer.json +++ b/web/landing/composer.json @@ -2,10 +2,67 @@ "name": "flow-php/web", "description": "Flow PHP ETL - Web", "type": "project", + "minimum-stability": "dev", + "prefer-stable": true, + "repositories": [ + { + "type": "path", + "url": "../../src/core/etl", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/adapter/etl-adapter-http", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/lib/telemetry", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/lib/types", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/lib/array-dot", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/lib/filesystem", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/bridge/symfony/telemetry-bundle", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/bridge/symfony/http-foundation-telemetry", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/bridge/telemetry/otlp", + "options": { "symlink": true } + }, + { + "type": "path", + "url": "../../src/bridge/monolog/telemetry", + "options": { "symlink": true } + } + ], "require": { "php": "8.3.*", - "flow-php/etl": ">=0.23.0", - "flow-php/etl-adapter-http": ">=0.23.0", + "flow-php/etl": "1.x-dev", + "flow-php/etl-adapter-http": "1.x-dev", + "flow-php/symfony-telemetry-bundle": "1.x-dev", + "flow-php/telemetry-otlp-bridge": "*", + "flow-php/monolog-telemetry-bridge": "*", "nyholm/psr7": "^1.8", "php-http/curl-client": "^2.3", "psr/http-client": "^1.0", diff --git a/web/landing/composer.lock b/web/landing/composer.lock index ef3184284a..a255dadc46 100644 --- a/web/landing/composer.lock +++ b/web/landing/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eba577c63444051a6ecdac007f116e5f", + "content-hash": "c4f6c4d36749461dfdcbe46e566327d3", "packages": [ { "name": "brick/math", - "version": "0.14.4", + "version": "0.14.6", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "a8b53e6cc4d3a336543f042a4dfa0e3f2f2356a4" + "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/a8b53e6cc4d3a336543f042a4dfa0e3f2f2356a4", - "reference": "a8b53e6cc4d3a336543f042a4dfa0e3f2f2356a4", + "url": "https://api.github.com/repos/brick/math/zipball/32498d5e1897e7642c0b961ace2df6d7dc9a3bc3", + "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.4" + "source": "https://github.com/brick/math/tree/0.14.6" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2026-02-02T16:57:31+00:00" + "time": "2026-02-05T07:59:58+00:00" }, { "name": "clue/stream-filter", @@ -353,33 +353,31 @@ }, { "name": "flow-php/array-dot", - "version": "0.31.0", - "source": { - "type": "git", - "url": "https://github.com/flow-php/array-dot.git", - "reference": "7c0b7f16b12b6e5239ecf487908bfe78673d1f22" - }, + "version": "1.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flow-php/array-dot/zipball/7c0b7f16b12b6e5239ecf487908bfe78673d1f22", - "reference": "7c0b7f16b12b6e5239ecf487908bfe78673d1f22", - "shasum": "" + "type": "path", + "url": "../../src/lib/array-dot", + "reference": "043d107b6cc0e919d6100a84eb513d4b6b81bf55" }, "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0" }, "type": "library", "autoload": { - "files": [ - "src/Flow/ArrayDot/array_dot.php" - ], "psr-4": { "Flow\\": [ "src/Flow" ] + }, + "files": [ + "src/Flow/ArrayDot/array_dot.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -393,38 +391,22 @@ "load", "transform" ], - "support": { - "issues": "https://github.com/flow-php/array-dot/issues", - "source": "https://github.com/flow-php/array-dot/tree/0.31.0" - }, - "funding": [ - { - "url": "https://flow-php.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/norberttech", - "type": "github" - } - ], - "time": "2026-01-19T09:55:20+00:00" + "transport-options": { + "symlink": true, + "relative": true + } }, { "name": "flow-php/etl", - "version": "0.31.0", - "source": { - "type": "git", - "url": "https://github.com/flow-php/etl.git", - "reference": "34a6e48efe93801f2ac29fefcb2ef2f474f928c9" - }, + "version": "1.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flow-php/etl/zipball/34a6e48efe93801f2ac29fefcb2ef2f474f928c9", - "reference": "34a6e48efe93801f2ac29fefcb2ef2f474f928c9", - "shasum": "" + "type": "path", + "url": "../../src/core/etl", + "reference": "101fcb6bc740b481daec219ceacf87d0a66af0a4" }, "require": { "brick/math": "^0.11 || ^0.12 || ^0.13 || ^0.14", + "composer-runtime-api": "^2.0", "ext-json": "*", "flow-php/array-dot": "self.version", "flow-php/filesystem": "self.version", @@ -454,7 +436,11 @@ ] } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" + } + }, "license": [ "MIT" ], @@ -465,35 +451,18 @@ "load", "transform" ], - "support": { - "issues": "https://github.com/flow-php/etl/issues", - "source": "https://github.com/flow-php/etl/tree/0.31.0" - }, - "funding": [ - { - "url": "https://flow-php.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/norberttech", - "type": "github" - } - ], - "time": "2026-01-19T12:58:02+00:00" + "transport-options": { + "symlink": true, + "relative": true + } }, { "name": "flow-php/etl-adapter-http", - "version": "0.31.0", - "source": { - "type": "git", - "url": "https://github.com/flow-php/etl-adapter-http.git", - "reference": "2353aae484d9f4eb07efae8f81c3acbfa006bdea" - }, + "version": "1.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flow-php/etl-adapter-http/zipball/2353aae484d9f4eb07efae8f81c3acbfa006bdea", - "reference": "2353aae484d9f4eb07efae8f81c3acbfa006bdea", - "shasum": "" + "type": "path", + "url": "../../src/adapter/etl-adapter-http", + "reference": "a653aa0bdc76bccd4fcf54f98ddea12af4b5ecf4" }, "require": { "ext-json": "*", @@ -507,16 +476,20 @@ }, "type": "library", "autoload": { - "files": [ - "src/Flow/ETL/Adapter/Http/DSL/functions.php" - ], "psr-4": { "Flow\\": [ "src/Flow" ] + }, + "files": [ + "src/Flow/ETL/Adapter/Http/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -528,35 +501,18 @@ "load", "transform" ], - "support": { - "issues": "https://github.com/flow-php/etl-adapter-http/issues", - "source": "https://github.com/flow-php/etl-adapter-http/tree/0.31.0" - }, - "funding": [ - { - "url": "https://flow-php.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/norberttech", - "type": "github" - } - ], - "time": "2025-12-13T19:30:32+00:00" + "transport-options": { + "symlink": true, + "relative": true + } }, { "name": "flow-php/filesystem", - "version": "0.31.0", - "source": { - "type": "git", - "url": "https://github.com/flow-php/filesystem.git", - "reference": "9a92c442feb3ee3e5e266d7c64d331c7edb4654b" - }, + "version": "1.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flow-php/filesystem/zipball/9a92c442feb3ee3e5e266d7c64d331c7edb4654b", - "reference": "9a92c442feb3ee3e5e266d7c64d331c7edb4654b", - "shasum": "" + "type": "path", + "url": "../../src/lib/filesystem", + "reference": "41a8aae737a5591e07aefde6b83097ec04e8e7d4" }, "require": { "flow-php/types": "self.version", @@ -565,16 +521,20 @@ }, "type": "library", "autoload": { - "files": [ - "src/Flow/Filesystem/DSL/functions.php" - ], "psr-4": { "Flow\\": [ "src/Flow" ] + }, + "files": [ + "src/Flow/Filesystem/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -590,35 +550,174 @@ "remote", "transform" ], - "support": { - "issues": "https://github.com/flow-php/filesystem/issues", - "source": "https://github.com/flow-php/filesystem/tree/0.31.0" + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "flow-php/monolog-telemetry-bridge", + "version": "1.x-dev", + "dist": { + "type": "path", + "url": "../../src/bridge/monolog/telemetry", + "reference": "86126b365e7c90c52d5447928ef1765c622f504e" }, - "funding": [ - { - "url": "https://flow-php.com/sponsor", - "type": "custom" + "require": { + "flow-php/etl": "self.version", + "flow-php/telemetry": "self.version", + "monolog/monolog": "^3.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Flow\\": [ + "src/Flow" + ] }, - { - "url": "https://github.com/norberttech", - "type": "github" + "files": [ + "src/Flow/Bridge/Monolog/Telemetry/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } + }, + "license": [ + "MIT" ], - "time": "2025-12-12T10:55:46+00:00" + "description": "Flow PHP - Monolog Telemetry Bridge", + "homepage": "https://github.com/flow-php/flow", + "keywords": [ + "bridge", + "flow-php", + "monolog", + "telemetry" + ], + "transport-options": { + "symlink": true, + "relative": true + } }, { - "name": "flow-php/telemetry", - "version": "0.31.0", - "source": { - "type": "git", - "url": "https://github.com/flow-php/telemetry.git", - "reference": "32ff43a631896ba6295e674e300541eb65edc9ce" + "name": "flow-php/symfony-http-foundation-telemetry-bridge", + "version": "1.x-dev", + "dist": { + "type": "path", + "url": "../../src/bridge/symfony/http-foundation-telemetry", + "reference": "1f5726349ffadda8d4c1d917cb3d21f5e1de4fd3" + }, + "require": { + "flow-php/telemetry": "self.version", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "symfony/http-foundation": "^6.4 || ^7.3 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Flow\\": [ + "src/Flow" + ] + }, + "files": [ + "src/Flow/Bridge/Symfony/HttpFoundationTelemetry/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" + } }, + "license": [ + "MIT" + ], + "description": "Flow PHP - Symfony Http Foundation Telemetry Bridge", + "homepage": "https://github.com/flow-php/flow", + "keywords": [ + "bridge", + "flow-php", + "http-foundation", + "symfony", + "telemetry" + ], + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "flow-php/symfony-telemetry-bundle", + "version": "1.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flow-php/telemetry/zipball/32ff43a631896ba6295e674e300541eb65edc9ce", - "reference": "32ff43a631896ba6295e674e300541eb65edc9ce", - "shasum": "" + "type": "path", + "url": "../../src/bridge/symfony/telemetry-bundle", + "reference": "d592dbe7011b8a3521cf0b1ccb6236ca9c4e553e" + }, + "require": { + "flow-php/symfony-http-foundation-telemetry-bridge": "self.version", + "flow-php/telemetry": "self.version", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0", + "symfony/config": "^6.4 || ^7.3 || ^8.0", + "symfony/console": "^6.4 || ^7.3 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.3 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.3 || ^8.0" + }, + "require-dev": { + "flow-php/telemetry-otlp-bridge": "self.version", + "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0", + "symfony/messenger": "^6.4 || ^7.3 || ^8.0", + "symfony/routing": "^6.4 || ^7.3 || ^8.0" + }, + "suggest": { + "flow-php/telemetry-otlp-bridge": "Required for OTLP exporter support", + "symfony/messenger": "Required for Messenger tracing middleware" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Flow\\": [ + "src/Flow" + ] + }, + "files": [ + "src/Flow/Bridge/Symfony/TelemetryBundle/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" + } + }, + "license": [ + "MIT" + ], + "description": "Flow PHP - Symfony Telemetry Bundle", + "homepage": "https://github.com/flow-php/flow", + "keywords": [ + "bundle", + "flow-php", + "logging", + "metrics", + "opentelemetry", + "symfony", + "telemetry", + "tracing" + ], + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "flow-php/telemetry", + "version": "1.x-dev", + "dist": { + "type": "path", + "url": "../../src/lib/telemetry", + "reference": "17ec7b5760d93ff7437551d74c73fc06607b5618" }, "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0", @@ -626,71 +725,124 @@ }, "type": "library", "autoload": { - "files": [ - "src/Flow/Telemetry/DSL/functions.php" - ], "psr-4": { "Flow\\": [ "src/Flow" ] + }, + "files": [ + "src/Flow/Telemetry/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Flow PHP - Telemetry library for metrics and tracing", "keywords": [ - "Metrics", + "metrics", "php", "telemetry", "tracing" ], - "support": { - "issues": "https://github.com/flow-php/telemetry/issues", - "source": "https://github.com/flow-php/telemetry/tree/0.31.0" + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "flow-php/telemetry-otlp-bridge", + "version": "1.x-dev", + "dist": { + "type": "path", + "url": "../../src/bridge/telemetry/otlp", + "reference": "27c9acf702f6b246c32a9a838e3d36b1f549e8bb" }, - "funding": [ - { - "url": "https://flow-php.com/sponsor", - "type": "custom" + "require": { + "flow-php/telemetry": "self.version", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "require-dev": { + "google/protobuf": "^4.0", + "grpc/grpc": "^1.74", + "nyholm/psr7": "^1.8", + "open-telemetry/gen-otlp-protobuf": "^1.8", + "symfony/http-client": "^6.4 || ^7.3 || ^8.0" + }, + "suggest": { + "ext-grpc": "Required for gRPC transport", + "google/protobuf": "Required for gRPC transport with binary protobuf encoding", + "open-telemetry/gen-otlp-protobuf": "Generated PHP classes for OTLP protobuf messages (required for gRPC transport)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Flow\\": [ + "src/Flow" + ] }, - { - "url": "https://github.com/norberttech", - "type": "github" + "files": [ + "src/Flow/Bridge/Telemetry/OTLP/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } + }, + "license": [ + "MIT" ], - "time": "2026-01-16T22:44:33+00:00" + "description": "Flow PHP Telemetry - OTLP Exporter Bridge", + "homepage": "https://github.com/flow-php/flow", + "keywords": [ + "flow-php", + "logging", + "metrics", + "observability", + "opentelemetry", + "otlp", + "telemetry", + "tracing" + ], + "transport-options": { + "symlink": true, + "relative": true + } }, { "name": "flow-php/types", - "version": "0.31.0", - "source": { - "type": "git", - "url": "https://github.com/flow-php/types.git", - "reference": "4af1b09a0a379ce33e251fe56c8a90fe39c67522" - }, + "version": "1.x-dev", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flow-php/types/zipball/4af1b09a0a379ce33e251fe56c8a90fe39c67522", - "reference": "4af1b09a0a379ce33e251fe56c8a90fe39c67522", - "shasum": "" + "type": "path", + "url": "../../src/lib/types", + "reference": "08d95eb6cdc6fa8e1af02a3457a34ccbcac8dbb3" }, "require": { "php": "~8.3.0 || ~8.4.0 || ~8.5.0" }, "type": "library", "autoload": { - "files": [ - "src/Flow/Types/DSL/functions.php" - ], "psr-4": { "Flow\\": [ "src/Flow" ] + }, + "files": [ + "src/Flow/Types/DSL/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Flow\\": "tests/Flow" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -699,21 +851,10 @@ "php", "types" ], - "support": { - "issues": "https://github.com/flow-php/types/issues", - "source": "https://github.com/flow-php/types/tree/0.31.0" - }, - "funding": [ - { - "url": "https://flow-php.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/norberttech", - "type": "github" - } - ], - "time": "2025-12-20T17:43:26+00:00" + "transport-options": { + "symlink": true, + "relative": true + } }, { "name": "league/commonmark", @@ -1074,16 +1215,16 @@ }, { "name": "nette/utils", - "version": "v4.1.1", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" + "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", - "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", + "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", + "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", "shasum": "" }, "require": { @@ -1096,7 +1237,7 @@ "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", "nette/tester": "^2.5", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.0@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -1157,9 +1298,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.1" + "source": "https://github.com/nette/utils/tree/v4.1.2" }, - "time": "2025-12-22T12:14:32+00:00" + "time": "2026-02-03T17:21:09+00:00" }, { "name": "nyholm/psr7", @@ -7045,11 +7186,14 @@ } ], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": { + "flow-php/etl": 20, + "flow-php/etl-adapter-http": 20, + "flow-php/symfony-telemetry-bundle": 20, "norberttech/static-content-generator-bundle": 20 }, - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "8.3.*" diff --git a/web/landing/config/bundles.php b/web/landing/config/bundles.php index 860aa432fb..b81b94cdf2 100644 --- a/web/landing/config/bundles.php +++ b/web/landing/config/bundles.php @@ -10,4 +10,5 @@ Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], \Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], \Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true], + Flow\Bridge\Symfony\TelemetryBundle\FlowTelemetryBundle::class => ['all' => true], ]; diff --git a/web/landing/config/packages/flow_telemetry.yaml b/web/landing/config/packages/flow_telemetry.yaml new file mode 100644 index 0000000000..e079fd5de9 --- /dev/null +++ b/web/landing/config/packages/flow_telemetry.yaml @@ -0,0 +1,68 @@ +flow_telemetry: + service: + name: "flow-website" + version: "1.0.0" + attributes: + deployment.environment: "%kernel.environment%" + telemetry: + http_kernel: + enabled: true + exclude_routes: + - '_wdt' + - '_profiler' + - '/_profiler.*/' + console: + enabled: true + exclude_commands: + - 'cache:clear' + - 'debug:router' + twig: + enabled: true + trace_templates: true + trace_blocks: true + trace_macros: true + exclude_templates: + - '@WebProfiler/Profiler/toolbar.html.twig' + + tracer_provider: + sampler: + type: always_on + processor: + type: batching + batch_size: 512 + exporter: + type: otlp + otlp: + transport: + type: curl + endpoint: "http://localhost:4318" + timeout: 30 + serializer: + type: json + + meter_provider: + temporality: cumulative + processor: + type: batching + exporter: + type: otlp + otlp: + transport: + type: curl + endpoint: "http://localhost:4318" + timeout: 30 + serializer: + type: json + + logger_provider: + processor: + type: batching + exporter: + type: otlp + otlp: + transport: + type: curl + endpoint: "http://localhost:4318" + timeout: 30 + serializer: + type: json diff --git a/web/landing/config/packages/monolog.yaml b/web/landing/config/packages/monolog.yaml index efc4ce8ba6..9c340dcd02 100644 --- a/web/landing/config/packages/monolog.yaml +++ b/web/landing/config/packages/monolog.yaml @@ -1,6 +1,10 @@ monolog: channels: - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + handlers: + telemetry: + type: service + id: Flow\Bridge\Monolog\Telemetry\TelemetryHandler when@dev: monolog: diff --git a/web/landing/config/packages/prod/flow_telemetry.yaml b/web/landing/config/packages/prod/flow_telemetry.yaml new file mode 100644 index 0000000000..efbe8ef4fc --- /dev/null +++ b/web/landing/config/packages/prod/flow_telemetry.yaml @@ -0,0 +1,21 @@ +flow_telemetry: + logger_provider: + processor: + type: severity_filtering + minimum_severity: warn + inner_processor: + type: batching + exporter: + type: otlp + otlp: + transport: + type: curl + endpoint: "http://localhost:4318" + timeout: 30 + serializer: + type: json + + telemetry: + twig: + trace_blocks: false + trace_macros: false diff --git a/web/landing/config/packages/test/flow_telemetry.yaml b/web/landing/config/packages/test/flow_telemetry.yaml new file mode 100644 index 0000000000..24fcef89e2 --- /dev/null +++ b/web/landing/config/packages/test/flow_telemetry.yaml @@ -0,0 +1,20 @@ +flow_telemetry: + tracer_provider: + processor: + type: void + + meter_provider: + processor: + type: void + + logger_provider: + processor: + type: void + + telemetry: + http_kernel: + enabled: false + console: + enabled: false + twig: + enabled: false diff --git a/web/landing/config/services.yaml b/web/landing/config/services.yaml index b44b952083..352ea06abc 100644 --- a/web/landing/config/services.yaml +++ b/web/landing/config/services.yaml @@ -71,4 +71,14 @@ services: $projectDir: '%kernel.project_dir%' twig.markdown.league_common_mark_converter_factory: - class: Flow\Website\Service\Markdown\LeagueCommonMarkConverterFactory \ No newline at end of file + class: Flow\Website\Service\Markdown\LeagueCommonMarkConverterFactory + + flow.telemetry.monolog.logger: + class: Flow\Telemetry\Logger\Logger + factory: ['@Flow\Telemetry\Telemetry', 'logger'] + arguments: + - 'monolog' + + Flow\Bridge\Monolog\Telemetry\TelemetryHandler: + arguments: + - '@flow.telemetry.monolog.logger' \ No newline at end of file diff --git a/web/landing/src/Flow/Website/Model/Documentation/Module.php b/web/landing/src/Flow/Website/Model/Documentation/Module.php index 6e47845c1f..82681253c4 100644 --- a/web/landing/src/Flow/Website/Model/Documentation/Module.php +++ b/web/landing/src/Flow/Website/Model/Documentation/Module.php @@ -27,6 +27,7 @@ enum Module : string case PSR7_TELEMETRY_BRIDGE = 'PSR-7 Telemetry Bridge'; case S3_FILESYSTEM = 'S3 Filesystem'; case SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE = 'Symfony HttpFoundation Telemetry Bridge'; + case SYMFONY_TELEMETRY_BUNDLE = 'Symfony Telemetry Bundle'; case TELEMETRY = 'Telemetry'; case TELEMETRY_OTLP = 'Telemetry OTLP'; case TEXT = 'Text'; @@ -66,7 +67,8 @@ public function priority() : int self::TELEMETRY_OTLP => 21, self::MONOLOG_TELEMETRY_BRIDGE => 22, self::SYMFONY_HTTP_FOUNDATION_TELEMETRY_BRIDGE => 23, - self::PSR7_TELEMETRY_BRIDGE => 24, + self::SYMFONY_TELEMETRY_BUNDLE => 24, + self::PSR7_TELEMETRY_BRIDGE => 25, default => 99, }; } diff --git a/web/landing/src/Flow/Website/Twig/DSLExtension.php b/web/landing/src/Flow/Website/Twig/DSLExtension.php index 53d0583e73..0211e65d86 100644 --- a/web/landing/src/Flow/Website/Twig/DSLExtension.php +++ b/web/landing/src/Flow/Website/Twig/DSLExtension.php @@ -18,6 +18,7 @@ public function dsl() : string return file_get_contents($this->dslPath); } + #[\Override] public function getFunctions() { return [ diff --git a/web/landing/src/Flow/Website/Twig/FlowExtension.php b/web/landing/src/Flow/Website/Twig/FlowExtension.php index 6bef99c2fe..582203e087 100644 --- a/web/landing/src/Flow/Website/Twig/FlowExtension.php +++ b/web/landing/src/Flow/Website/Twig/FlowExtension.php @@ -9,6 +9,7 @@ final class FlowExtension extends AbstractExtension { + #[\Override] public function getFilters() : array { return [ diff --git a/web/landing/src/Flow/Website/Twig/HumanizerExtension.php b/web/landing/src/Flow/Website/Twig/HumanizerExtension.php index baf4c86e48..dfeb80a8a4 100644 --- a/web/landing/src/Flow/Website/Twig/HumanizerExtension.php +++ b/web/landing/src/Flow/Website/Twig/HumanizerExtension.php @@ -10,6 +10,7 @@ final class HumanizerExtension extends AbstractExtension { + #[\Override] public function getFilters() { return [ diff --git a/web/landing/src/Flow/Website/Twig/SlugifyExtension.php b/web/landing/src/Flow/Website/Twig/SlugifyExtension.php index 2b9f7f3e43..0b21fec4be 100644 --- a/web/landing/src/Flow/Website/Twig/SlugifyExtension.php +++ b/web/landing/src/Flow/Website/Twig/SlugifyExtension.php @@ -10,6 +10,7 @@ final class SlugifyExtension extends AbstractExtension { + #[\Override] public function getFilters() : array { return [ diff --git a/web/landing/templates/documentation/dsl.html.twig b/web/landing/templates/documentation/dsl.html.twig index ef65a63ade..77954c8d16 100644 --- a/web/landing/templates/documentation/dsl.html.twig +++ b/web/landing/templates/documentation/dsl.html.twig @@ -90,10 +90,8 @@
{% apply spaceless %} -
>
-                                    
+                                
+                                    {{- definition.toString | escape('html') -}}
                                 
{% endapply %}
diff --git a/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php b/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php index f065a05068..efe41ab9e4 100644 --- a/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php +++ b/web/landing/tests/Flow/Website/Tests/Integration/DocumentationTest.php @@ -36,6 +36,7 @@ public function test_documentation_dsl_page() : void self::assertEquals(12, $client->getCrawler()->filter('[data-dsl-type]')->count()); } + #[\Override] protected static function getKernelClass() : string { return Kernel::class;