waffle-commons / telemetry-otel
OpenTelemetry SDK bridge for Waffle Commons: implements the framework TracerInterface with W3C trace-context propagation, keeping the OTel SDK out of the core perimeter.
Requires
- php: ^8.5
- nyholm/psr7: ^1.8
- open-telemetry/api: ^1.0
- open-telemetry/sdk: ^1.0
- symfony/http-client: ^8.1
- waffle-commons/contracts: 0.1.0-beta6
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.7.0
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 20:26:42 UTC
README
Waffle Telemetry — OpenTelemetry Bridge
Release:
0.1.0-beta6|CHANGELOG.mdRFC: RFC-005 (OBS-02) — OpenTelemetry SDK bridge
OpenTelemetry SDK bridge for the Waffle Commons framework. It implements
Waffle\Commons\Contracts\Telemetry\TracerInterface on top of the audited OpenTelemetry PHP SDK and propagates
W3C Trace Context on outbound HTTP calls — isolating the vendor SDK in this adapter so the core packages
stay vendor-free. The SDK-free defaults, the Prometheus endpoint, and the metric collectors live in the
companion waffle-commons/telemetry package.
📦 Installation
composer require waffle-commons/telemetry-otel
Pulls in open-telemetry/api + open-telemetry/sdk as runtime dependencies — this package is the only
place in the Waffle ecosystem where the OTel SDK is allowed.
🧱 Surface
| Class | Role |
|---|---|
Waffle\Commons\TelemetryOtel\Trace\OtelTracer |
final readonly adapter of an OTel TracerInterface to the framework's Contracts\Telemetry\TracerInterface. Activates each started span so nested spans compose through OTel's own context stack. |
Waffle\Commons\TelemetryOtel\Trace\OtelSpan |
final readonly adapter of an OTel span (plus its active ScopeInterface) to Contracts\Telemetry\SpanInterface. end() detaches the scope, then ends the span. |
Waffle\Commons\TelemetryOtel\Trace\OtelSpanContext |
final readonly adapter of an OTel span context to Contracts\Telemetry\SpanContextInterface; renders a W3C traceparent string via toTraceparent(). |
Waffle\Commons\TelemetryOtel\Propagation\W3CTraceContextPropagator |
final readonly TextMapPropagatorInterface — inject() serialises the Waffle span context as traceparent/tracestate; extract() delegates to OTel's audited TraceContextPropagator for parsing. |
Waffle\Commons\TelemetryOtel\Factory\OtelTracerFactory |
Static factory assembling a ready-to-use OtelTracer from any SpanExporterInterface, or console() for a zero-dependency JSON exporter to php://stdout. |
🚀 Usage
use Waffle\Commons\TelemetryOtel\Factory\OtelTracerFactory; use Waffle\Commons\TelemetryOtel\Propagation\W3CTraceContextPropagator; // Local/dev: export spans as JSON to php://stdout (docker logs becomes a zero-dependency // span collector). Swap OtelTracerFactory::create($serviceName, $exporter) for a real // OTLP exporter in production. $tracer = OtelTracerFactory::console(serviceName: 'waffle-app'); $propagator = new W3CTraceContextPropagator(); $container->set(TracerInterface::class, $tracer);
Registered as shared services, $tracer and $propagator are consumed unmodified by
waffle-commons/telemetry's TracingMiddleware (per-request server span + inbound traceparent extraction)
and by waffle-commons/http-client's Client (outbound traceparent propagation) — one end-to-end trace
across both directions of a request, with zero code in either package aware that OpenTelemetry exists.
🔀 Span-kind mapping
OtelTracer::startSpan() translates the framework's Contracts\Telemetry\Enum\SpanKind 1:1 onto the OTel
API's SpanKind constants:
Contracts\Telemetry\Enum\SpanKind |
OTel SpanKind |
|---|---|
Internal |
KIND_INTERNAL |
Server |
KIND_SERVER |
Client |
KIND_CLIENT |
Producer |
KIND_PRODUCER |
Consumer |
KIND_CONSUMER |
When an explicit $parent span context is supplied — typically one extracted from an inbound traceparent
by W3CTraceContextPropagator::extract() — the new span continues that remote trace via
OtelApiSpanContext::createFromRemoteParent(); otherwise it parents off OTel's own active context, so nested
startSpan() calls compose naturally without threading a context object through every call site.
🐘 PHP 8.5 features used
final readonly classon every adapter (OtelTracer,OtelSpan,OtelSpanContext,W3CTraceContextPropagator) — each wraps an injected SDK object and mutates nothing of its own.#[\Override]on every interface implementation.matchexpressions for theSpanKind→ OTel-kind andSpanStatus→ OTel-status translations.- Static factory methods (
OtelTracerFactory::create()/::console()) instead of a constructor, since the class assembles aTracerProviderrather than holding state.
🧭 Perimeter
The only Waffle package permitted to require the OpenTelemetry SDK (open-telemetry/*) — mago guard's
[guard.perimeter] in mago.toml allows the OpenTelemetry\** namespace here and nowhere
else in the ecosystem. Otherwise depends only on waffle-commons/contracts; production code under
Waffle\Commons\TelemetryOtel may depend only on itself, Contracts\**, the OTel SDK, Psr\**, and PHP core
/ Psl\**. Test code under WaffleTests\Commons\TelemetryOtel is unrestricted (@all).
Contract-first, component-agnostic by construction: components compose through waffle-commons/contracts,
never directly through one another.
🧵 Worker safety
Every adapter (OtelTracer, OtelSpan, OtelSpanContext, W3CTraceContextPropagator) is final readonly
and holds no per-request mutable state of its own — the OpenTelemetry SDK owns the active-context stack
internally, and a SimpleSpanProcessor exports each span synchronously on end() so nothing is buffered
across requests waiting on a background flush. There is nothing here for a FrankenPHP worker to leak between
requests (wfl igor 0 KO).
🧪 Testing
docker exec -w /waffle-commons/telemetry-otel waffle-dev composer tests
📚 Documentation
Central framework docs (Diátaxis) for this component:
- Reference:
reference/telemetry-otel.md - Explanation:
explanation/observability-telemetry.md - Full documentation tree: waffle-commons/documentation
📄 License
MIT — see LICENSE.md.