rasuvaeff / yii3-telemetry
OpenTelemetry-based tracing facade for Yii3 applications
Requires
- php: 8.3 - 8.5
- open-telemetry/api: ^1.5
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^2.0 || ^3.0
- psr/simple-cache: ^2.0 || ^3.0
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.33
- maglnet/composer-require-checker: ^4.17
- nyholm/psr7: ^1.8
- rasuvaeff/property-testing: ^2.3
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.25
- vimeo/psalm: ^6.16
- yiisoft/db: ^2.0
- yiisoft/db-sqlite: ^2.0
- yiisoft/view: ^12.0
Suggests
- rasuvaeff/yii3-telemetry-otel: OpenTelemetry tracing backend (binds TracerProviderInterface)
- yiisoft/db: DB query spans via DbQueryProfiler
- yiisoft/view: View render spans via ViewRenderSpanListener
This package is auto-updated.
Last update: 2026-07-10 18:21:01 UTC
README
Vendor-neutral tracing core for Yii3. One ergonomic call — trace(name, callback)
— opens a span, runs your code, and closes the span, instead of the verbose
OpenTelemetry span-builder. The exporter is a swappable backend.
Using an AI coding assistant? llms.txt has a compact API reference you can pass as context.
Requirements
- PHP 8.3+ (64-bit — epoch nanoseconds exceed
PHP_INT_MAXon 32-bit builds) open-telemetry/api^1.5 (thin: interfaces +NoopTracer, no SDK)- PSR-20 clock, PSR-3 log, PSR-7 http-message interfaces
Installation
composer require rasuvaeff/yii3-telemetry
For real span export, add a backend (Sprint 2): rasuvaeff/yii3-telemetry-otel.
Without one, bind the no-op provider (see Wiring).
Usage
Trace a block of work
use Rasuvaeff\Yii3Telemetry\SpanInterface; use Rasuvaeff\Yii3Telemetry\TraceKind; use Rasuvaeff\Yii3Telemetry\Tracer; /** @var Tracer $tracer (injected) */ $order = $tracer->trace( name: 'checkout.process', callback: static function (SpanInterface $span) use ($cart): Order { $order = $cart->checkout(); $span->setAttribute('order.id', $order->getId()); return $order; }, attributes: ['user.id' => $userId], scoped: true, traceKind: TraceKind::Internal, );
The callback receives the active SpanInterface and its return value becomes the
trace() return value.
trace() contract (frozen at 1.0.0)
| Situation | Behaviour |
|---|---|
| callback returns a value | span ends with its current status; value returned |
| callback throws | recordException(), status Error, span ends, the original exception is re-thrown |
scoped: true (default) |
span is currentSpan() during the callback; the previous span is restored after |
nested trace() |
the child inherits the parent's traceId, gets its own spanId |
| span dropped / tracing disabled | callback still runs; currentSpan() returns a non-recording span, never null |
startNanos: <int> |
backdates the span start (unix-epoch nanoseconds) for work that logically began earlier — a worker receive timestamp, a queue enqueue time; null (default) = now |
end() is idempotent.
Span
SpanInterface is what the callback receives:
| Method | Purpose |
|---|---|
setAttribute(string $key, bool|int|float|string|array|null $value) |
attach a key/value |
updateName(string $name) |
rename the span |
setStatus(SpanStatusCode $code, ?string $description = null) |
set status |
addEvent(string $name, array $attributes = []) |
record a timestamped point-in-time annotation (an OTel span event: retry, cache.miss, …) |
recordException(\Throwable $e) |
record an exception |
end() |
finish (idempotent) |
isRecording() |
false for non-recording spans |
getTraceContext() |
the span's TraceContext |
The concrete Span (recorded by LogTracer) additionally exposes getters:
getName(), getKind(), getStatus(): SpanStatus (a value object pairing a
SpanStatusCode with an optional description), getAttributes(),
getEvents(): list<SpanEvent>, getRecordedExceptions(), getDurationNanos(),
hasEnded().
Tracers
| Class | Use |
|---|---|
Tracer |
DI facade; delegates to the active TracerInterface from the provider |
NullTracer / NullTracerProvider |
no-op; runs the callback with a non-recording span |
LogTracer |
dev tracer; records real spans and logs each finished span via PSR-3 |
use Rasuvaeff\Yii3Telemetry\LogTracer; $tracer = new LogTracer($psrLogger); // logs every finished span, no backend
Context propagation (W3C Trace Context)
use Rasuvaeff\Yii3Telemetry\TraceContextPropagator; $propagator = new TraceContextPropagator(); // Incoming server request → context. $context = $propagator->extract($serverRequest); // Context → outgoing client request (adds the `traceparent` header). $request = $propagator->inject($context, $clientRequest);
extract reads a ServerRequestInterface; inject writes an outgoing
RequestInterface. A missing/malformed header yields TraceContext::invalid().
For non-HTTP transports use the carrier-agnostic pair — a plain header map you can put into any envelope (queue message, AMQP header table, gRPC metadata):
// Producer: attach the current context to the message envelope. $envelope['headers'] = $propagator->toHeaders($tracer->getContext()); // Consumer: restore it and open a Consumer span. $context = $propagator->fromHeaders($message['headers'] ?? []);
fromHeaders matches names case-insensitively; an invalid context yields an
empty map from toHeaders, so the round trip is always safe.
Queue instrumentation roadmap. A ready-made
yiisoft/queuemiddleware (Producer inject + Consumer span) is deferred untilyiisoft/queuehas a stable release — the carrier API above is the supported way to propagate a trace through any queue today.
Clock
ClockInterface extends PSR-20 with a monotonic reading — two clocks that must
not be mixed:
now(): \DateTimeImmutable— the wall clock (span start timestamp);monotonicNanos(): int—hrtime, for measuring durations.
SystemClock is the default (and is a valid PSR-20 clock).
Wiring (yiisoft/config)
The core config/di.php binds only the facade (Tracer, TracerInterface).
It never binds TracerProviderInterface — that swappable key is owned by exactly
one source. With no backend installed, bind the no-op provider in your app:
// config/common/di.php use Rasuvaeff\Yii3Telemetry\NullTracerProvider; use Rasuvaeff\Yii3Telemetry\TracerProviderInterface; return [ TracerProviderInterface::class => NullTracerProvider::class, ];
Installing yii3-telemetry-otel provides the real binding instead — binding it
in two vendor packages is a deliberate yiisoft/config Duplicate key error.
Instrumentation
Backend-agnostic instrumentation that records spans through the facade. Wire it
app-side — never unconditionally in a package di.php, or the container
would fatal when the subsystem isn't installed.
| Class | Wraps / listens to | Spans |
|---|---|---|
HttpClientSpanDecorator |
a PSR-18 client | HTTP <method> (+ traceparent injected) |
TracingCacheDecorator |
a PSR-16 cache | cache.<op> |
DbQueryProfiler |
yiisoft/db profiler |
db.query (parameterized SQL only) |
ViewRenderSpanListener |
yiisoft/view PSR-14 events |
view.render |
TraceContextLogger |
a PSR-3 logger | adds trace_id/span_id to log context |
TraceIdResponseHeaderMiddleware |
PSR-15 response | X-Trace-Id response header (opt-in) |
// HTTP client (PSR-18) — inner client is wrapped $client = new HttpClientSpanDecorator($innerClient, $tracer); // Cache (PSR-16) $cache = new TracingCacheDecorator($innerCache, $tracer); // DB (yiisoft/db) — pass the semconv db.system for your driver (default 'sql') $connection->setProfiler(new DbQueryProfiler($tracer, dbSystem: 'postgresql')); // View (yiisoft/view) — register in config/events.php BeforeRender::class => [[ViewRenderSpanListener::class, 'beforeRender']], AfterRender::class => [[ViewRenderSpanListener::class, 'afterRender']],
DbQueryProfiler and ViewRenderSpanListener bracket a subsystem's split
begin/end hooks with Tracer::startSpan() (a manual span the caller ends).
yiisoft/db and yiisoft/view are optional (suggest); their symbols are
declared in composer-require-checker.json.
Log correlation & exposing the trace id
// Wrap the application logger — every record inside an active trace gets // trace_id / span_id in its context (existing keys are never overwritten): $logger = new TraceContextLogger($innerLogger, $tracer); // Opt-in: return the trace id to the client for support tickets. Place it // AFTER the tracing middleware (inside the root span): $middleware = new TraceIdResponseHeaderMiddleware($tracer); // X-Trace-Id $middleware = new TraceIdResponseHeaderMiddleware($tracer, 'Trace-Ref'); // custom name
Without an active valid trace context both are transparent: the log record and the response pass through unchanged.
Security
- SQL safety:
DbQueryProfilerputs only the parameterized SQL intodb.statement— parameter values are never attached to a span. A debug opt-in for parameter values and a slow-query threshold are deliberately not implemented; if they land later, they will be off by default. TraceContextvalidates ids (hex32 / hex16) and flags (0..255) in its constructor; malformed propagation headers are rejected, not trusted.trace()never swallows exceptions — failures stay visible.
Examples
Runnable, server-independent scripts live in examples/:
01_basic_trace.php, 02_nested_trace.php, 03_propagation.php. See
examples/README.md.
Development
docker run --rm -v "$PWD":/app -w /app composer:2 composer build
Runs validate → normalize → require-checker → cs → psalm → tests (incl.
property tests). make build, make test, make mutation, make release-check
are also available.
License
BSD-3-Clause. See LICENSE.md.