Distributed Tracing and Telemetry with OpenTelemetry
Winter Boot integrates with OpenTelemetry (OTel) for distributed traces, metrics, and logs. The integration is an optional module (OpenTelemetryModule), so it costs nothing when disabled. Once enabled, you can trace every web request with an interceptor, trace or count individual methods with attributes, and inject telemetry beans anywhere.
-
Install the PHP OpenTelemetry extension
Terminal window pecl install opentelemetryphp.ini extension=opentelemetry.so -
Install the OpenTelemetry SDK and an exporter
Terminal window composer require open-telemetry/sdk open-telemetry/exporter-otlp -
Enable the module in application.yml
application.yml modules:- module: 'dev\winterframework\telemetry\OpenTelemetryModule'enabled: trueconfigFile: opentelemetry.yml # required; looked up in the config directoryconfig/opentelemetry.yml winter:telemetry:serviceName: my-winter-serviceTo switch telemetry off without removing the module (for example in tests), set
winter.telemetry.enabled: false. -
Configure the exporter with OTel environment variables
Winter Boot uses the SDK’s global providers, so the service name, exporter, endpoint, and sampler are set with the standard OpenTelemetry environment variables:
Terminal window export OTEL_PHP_AUTOLOAD_ENABLED=trueexport OTEL_SERVICE_NAME=my-winter-serviceexport OTEL_TRACES_EXPORTER=otlpexport OTEL_METRICS_EXPORTER=otlpexport OTEL_LOGS_EXPORTER=otlpexport OTEL_EXPORTER_OTLP_PROTOCOL=http/protobufexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318export OTEL_TRACES_SAMPLER=parentbased_always_onWithout
OTEL_PHP_AUTOLOAD_ENABLED=true, the SDK stays in no-op mode and nothing is exported.
Web Request Tracing
Section titled “Web Request Tracing”Register OpenTelemetryWebInterceptor in your WebMvcConfigurer (see Interceptors) to create a span for every incoming HTTP request. The interceptor extracts the W3C Trace Context from request headers (enabling distributed trace propagation from upstream services), records the HTTP method, URL, and response status code, and closes the span — including exception recording — after the response is sent.
use dev\winterframework\telemetry\web\OpenTelemetryWebInterceptor;use dev\winterframework\stereotype\Configuration;use dev\winterframework\core\web\config\WebMvcConfigurer;use dev\winterframework\core\web\config\InterceptorRegistry;
#[Configuration(name: 'webMvcConfigurer')]class MyWebConfigurer implements WebMvcConfigurer{ public function addInterceptors(InterceptorRegistry $registry): void { // Trace all incoming HTTP requests $registry->addInterceptor(new OpenTelemetryWebInterceptor(), '.*'); }}Traces, Metrics, and Logs
Section titled “Traces, Metrics, and Logs”Annotate any public bean method with #[Traceable] to have Winter Boot automatically create an OTel span around that method’s execution. The span is named after the method by default; override the name via the name argument. Exceptions are recorded on the span and the span status is set to ERROR automatically.
use dev\winterframework\telemetry\stereotype\Traceable;use dev\winterframework\stereotype\Service;
#[Service]class OrderService{ #[Traceable] public function processOrder(int $orderId): void { // Execution is wrapped in an OTel span named after this method. }
#[Traceable(name: 'charge-customer')] public function chargeCustomer(int $orderId, float $amount): void { // Span will be reported as "charge-customer". }}Use #[Countable] to increment an OTel counter metric every time a method is invoked. For arbitrary numeric measurements such as latency or queue depth, inject OpenTelemetryMetrics directly.
use dev\winterframework\telemetry\metrics\Countable;use dev\winterframework\telemetry\metrics\OpenTelemetryMetrics;use dev\winterframework\stereotype\Autowired;use dev\winterframework\stereotype\Service;
#[Service]class PaymentService{ #[Autowired] private OpenTelemetryMetrics $metrics;
// Automatically increments the "payments.processed" counter by 1 on each call. #[Countable(name: 'payments.processed', value: 1)] public function processPayment(float $amount): void { // Payment processing logic… }
public function recordLatency(float $durationMs): void { // Record an arbitrary measurement with tags. $this->metrics->record( 'payment.latency', $durationMs, ['currency' => 'USD'] ); }}#[Countable] options:
-
namestring(default:"method.invocations") The OTel counter metric name. -
valueint|float(default:1) The amount added to the counter on each invocation.
OpenTelemetryMetrics methods:
| Method | Description |
|---|---|
add(string $name, int|float $value, array $attributes = []) |
Increments a named counter by $value. |
record(string $name, int|float $value, array $attributes = []) |
Records a value on a named histogram (e.g. latency, queue depth). |
counter(string $name, string $description = '', string $unit = '') |
Returns the raw CounterInterface for the named counter. |
histogram(string $name, string $description = '', string $unit = '') |
Returns the raw HistogramInterface for the named histogram. |
OpenTelemetryLogs is automatically registered as a bean by OpenTelemetryModule. Inject it into any service to emit structured log records through the OTel Logs API, which can be correlated with active trace spans in your observability backend.
use dev\winterframework\telemetry\logs\OpenTelemetryLogs;use dev\winterframework\stereotype\Autowired;use dev\winterframework\stereotype\Service;
#[Service]class AuditService{ #[Autowired] private OpenTelemetryLogs $logs;
public function auditAction(string $userId, string $action): void { $this->logs->info('User action performed', [ 'userId' => $userId, 'action' => $action, ]); }}Available methods:
| Method | Severity |
|---|---|
info(string $message, array $attributes = []) |
INFO |
error(string $message, array $attributes = []) |
ERROR |
warn(string $message, array $attributes = []) |
WARN |
debug(string $message, array $attributes = []) |
DEBUG |
emit(string $message, Severity $severity, array $attributes = []) |
Custom — use any Severity constant |