feat(Runtime): FrankenPHP worker-mode support and Globals provider source#220
Draft
gaelreyrol wants to merge 1 commit into
Draft
Conversation
…r source PHP's shared-nothing model resets the MeterProvider on every request, so cumulative counters collapse at the collector. Under FrankenPHP worker mode the kernel terminates per request but the worker stays alive, which the bundle's kernel.terminate subscriber broke by calling provider->shutdown(). Detect FrankenPHP via Symfony 7.4's $_SERVER[APP_RUNTIME_MODE]=worker=1 (with explicit `runtime` override), switch the subscriber to forceFlush() under worker mode and defer shutdown to register_shutdown_function, and inject process.pid into the resource so N workers produce N distinct series. Also add `provider_source: globals` for setups where the SDK is bootstrapped externally (OTEL_PHP_AUTOLOAD_ENABLED=true) — the bundle's provider services delegate to OpenTelemetry\API\Globals instead of building their own. In the default `di` mode the bundle publishes its DI-built providers back into Globals on first kernel.request so auto-instrumentation contrib packages see the same instances.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #220 +/- ##
============================================
- Coverage 92.11% 91.22% -0.90%
- Complexity 741 783 +42
============================================
Files 121 127 +6
Lines 2969 3099 +130
============================================
+ Hits 2735 2827 +92
- Misses 234 272 +38
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
PHP's shared-nothing request model is a poor fit for OpenTelemetry metrics. Each FPM process gets its own
MeterProvider, so cumulative counters reset between requests and the collector sees the latest value rather than an accumulating series. The original symptom (reproduced against the PHP SDK + otel-collector):The upstream answer from the OpenTelemetry community: shared-nothing runtimes (FPM, Apache, built-in server) can't produce stable cumulative metrics with the same resource identity. The fix is a long-lived runtime — Swoole, RoadRunner, ReactPHP, or FrankenPHP worker mode, which keeps the PHP process resident across requests and gives the SDK the long-lived-process model it was designed for.
This PR adds first-class support for FrankenPHP worker mode in the bundle, plus a second design path where the bundle consumes providers from
OpenTelemetry\API\Globalsinstead of building them via DI.What changed
Runtime detection
RuntimeModeenum +RuntimeDetectorservice.open_telemetry.runtimeconfig key:auto(default) |classic|frankenphp_worker.$_SERVER['APP_RUNTIME_MODE'](set toweb=1&worker=1by Symfony 7.4'sFrankenPhpWorkerRunner— see symfony/symfony#60503), with a pre-7.4 fallback that checksfunction_exists('frankenphp_handle_request')+APP_RUNTIME.Shutdown semantics fix
ObservableHttpKernelEventSubscriberpreviously calledprovider->shutdown()on everykernel.terminate. Under FrankenPHP the kernel terminates per request but the worker keeps running, so the second request received a shut-down provider. The subscriber is now runtime-aware:kernel.terminatebehaviourshutdown()(unchanged)forceFlush();shutdown()deferred toregister_shutdown_functionregistered once at bootMulti-worker resource attributes
process.pidis now unconditionally added to the resource (OTel semconv), so N FrankenPHP workers produce N distinct time series the backend can sum across rather than collapsing into one undefined writer.Provider source:
divsglobalsNew
open_telemetry.provider_sourceconfig key:di(default) — providers built by the bundle. Additionally: aGlobalsInitializersubscriber publishes the DI-built providers intoOpenTelemetry\API\Globalson firstkernel.request, so third-party libraries / auto-instrumentation contrib packages reaching forGlobals::*Provider()see the same instances.globals— for setups where the SDK is bootstrapped externally (OTEL_PHP_AUTOLOAD_ENABLED=trueor manualSdk::builder()->buildAndRegisterGlobal()in a FrankenPHP worker entry script). The bundle's provider services become thin delegates overGlobals::*Provider(). Bundle still owns instrumentation wiring.Three new factories (
GlobalsTracerProviderFactory,GlobalsMeterProviderFactory,GlobalsLoggerProviderFactory) are registered asprovider_factory.globalsservices, exposed via the newglobalscase on each*ProviderEnum. Settingprovider_source: globalsat the root forces all configured providers totype: globalsat compile time.Traces and logs providers are now tagged (
open_telemetry.traces.provider,open_telemetry.logs.provider) to match the existing metrics tagging — needed soGlobalsInitializercan iterate them.Tests
tests/Functional/Runtime/:WorkerModeAccumulationTest— boots the test kernel withruntime: frankenphp_worker, callsKernelBrowser::disableReboot()so the kernel reuses its container across$client->request()calls (the actual FrankenPHP worker semantics —KernelBrowserreboots by default which would mask the fix), issues two/increment/{value}requests and asserts both increments reach the exporter via the same provider.ShutdownSemanticsTest— locks in classic mode: provider is shut down afterkernel.terminate.WorkerShutdownTest— locks in worker mode: provider is NOT shut down afterkernel.terminate.WorkerResourceTest— assertsprocess.pidis present in the resource info.Test fixtures: new
IncrementController+ route +when@worker_mode/when@globals_modeenv blocks in the test app's config.Documentation
New
docs/src/how-to/frankenphp-runtime.md(linked from navbar + sidebar) covering: when to use worker mode, the two provider-source modes with example bootstraps, multi-worker resource attributes, theOTEL_PHP_AUTOLOAD_ENABLEDinteraction, and known limitations (no periodic export without threads, state-leak hygiene, per-signalglobalsoverride behaviour).Deferred
End-to-end FrankenPHP acceptance test with docker-compose + dedicated CI job. The in-process
WebTestCase+disableReboot()simulation covers the same semantics today; the e2e variant catches FrankenPHP-specific issues (autoloader behaviour, env propagation, realfrankenphp_handle_requestloop) and is worth a follow-up PR.Test plan
composer run test— 465 passingcomposer run phpstan— clean (no new baseline entries)composer run php-cs-fixer:lint— clean