Skip to content

feat(Runtime): FrankenPHP worker-mode support and Globals provider source#220

Draft
gaelreyrol wants to merge 1 commit into
FriendsOfOpenTelemetry:mainfrom
gaelreyrol:feat/otel-sdk-frankenphp-runtime
Draft

feat(Runtime): FrankenPHP worker-mode support and Globals provider source#220
gaelreyrol wants to merge 1 commit into
FriendsOfOpenTelemetry:mainfrom
gaelreyrol:feat/otel-sdk-frankenphp-runtime

Conversation

@gaelreyrol

Copy link
Copy Markdown
Contributor

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):

Request 1 adds 2 → index_total{} 2
Request 2 adds 6 → index_total{} 6 (expected 8)

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\Globals instead of building them via DI.

What changed

Runtime detection

  • New RuntimeMode enum + RuntimeDetector service.
  • New open_telemetry.runtime config key: auto (default) | classic | frankenphp_worker.
  • Auto-detection reads $_SERVER['APP_RUNTIME_MODE'] (set to web=1&worker=1 by Symfony 7.4's FrankenPhpWorkerRunner — see symfony/symfony#60503), with a pre-7.4 fallback that checks function_exists('frankenphp_handle_request') + APP_RUNTIME.

Shutdown semantics fix

ObservableHttpKernelEventSubscriber previously called provider->shutdown() on every kernel.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:

Mode kernel.terminate behaviour
Classic shutdown() (unchanged)
Worker (any) forceFlush(); shutdown() deferred to register_shutdown_function registered once at boot

Multi-worker resource attributes

process.pid is 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: di vs globals

New open_telemetry.provider_source config key:

  • di (default) — providers built by the bundle. Additionally: a GlobalsInitializer subscriber publishes the DI-built providers into OpenTelemetry\API\Globals on first kernel.request, so third-party libraries / auto-instrumentation contrib packages reaching for Globals::*Provider() see the same instances.
  • globals — for setups where the SDK is bootstrapped externally (OTEL_PHP_AUTOLOAD_ENABLED=true or manual Sdk::builder()->buildAndRegisterGlobal() in a FrankenPHP worker entry script). The bundle's provider services become thin delegates over Globals::*Provider(). Bundle still owns instrumentation wiring.

Three new factories (GlobalsTracerProviderFactory, GlobalsMeterProviderFactory, GlobalsLoggerProviderFactory) are registered as provider_factory.globals services, exposed via the new globals case on each *ProviderEnum. Setting provider_source: globals at the root forces all configured providers to type: globals at compile time.

Traces and logs providers are now tagged (open_telemetry.traces.provider, open_telemetry.logs.provider) to match the existing metrics tagging — needed so GlobalsInitializer can iterate them.

Tests

tests/Functional/Runtime/:

  • WorkerModeAccumulationTest — boots the test kernel with runtime: frankenphp_worker, calls KernelBrowser::disableReboot() so the kernel reuses its container across $client->request() calls (the actual FrankenPHP worker semantics — KernelBrowser reboots 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 after kernel.terminate.
  • WorkerShutdownTest — locks in worker mode: provider is NOT shut down after kernel.terminate.
  • WorkerResourceTest — asserts process.pid is present in the resource info.

Test fixtures: new IncrementController + route + when@worker_mode / when@globals_mode env 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, the OTEL_PHP_AUTOLOAD_ENABLED interaction, and known limitations (no periodic export without threads, state-leak hygiene, per-signal globals override 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, real frankenphp_handle_request loop) and is worth a follow-up PR.

Test plan

  • composer run test — 465 passing
  • composer run phpstan — clean (no new baseline entries)
  • composer run php-cs-fixer:lint — clean
  • Manual: run a sample app under FrankenPHP worker mode, hit it N times, confirm the collector shows a single accumulating series rather than last-value-wins
  • Follow-up: end-to-end FrankenPHP acceptance test + CI job

…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.
@gaelreyrol gaelreyrol self-assigned this May 19, 2026
@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.22%. Comparing base (bbbf291) to head (f13aa3a).

Files with missing lines Patch % Lines
src/OpenTelemetry/Globals/GlobalsInitializer.php 65.51% 10 Missing ⚠️
src/DependencyInjection/OpenTelemetryExtension.php 61.11% 7 Missing ⚠️
...og/LoggerProvider/GlobalsLoggerProviderFactory.php 0.00% 5 Missing ⚠️
...tric/MeterProvider/GlobalsMeterProviderFactory.php 0.00% 5 Missing ⚠️
...ce/TracerProvider/GlobalsTracerProviderFactory.php 0.00% 5 Missing ⚠️
src/Runtime/RuntimeDetector.php 80.00% 4 Missing ⚠️
...HttpKernel/ObservableHttpKernelEventSubscriber.php 85.71% 2 Missing ⚠️
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     
Flag Coverage Δ
phpunit 91.22% <71.42%> (-0.90%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant