Distributed Tracing and Telemetry with OpenTelemetry
Winter Boot provides first-class integration with OpenTelemetry (OTel), covering all three observability pillars: distributed traces, metrics, and structured logs. The integration is delivered as an optional module (OpenTelemetryModule) so it has zero overhead when disabled. Once enabled, you can instrument web requests automatically via an interceptor, annotate individual methods for fine-grained tracing or counting, and inject telemetry beans anywhere in your application.
Prerequisites
Section titled “Prerequisites”Complete all four setup steps before enabling the module.
-
Install the PHP OpenTelemetry extension
Terminal window pecl install opentelemetryThen enable it in your
php.ini:php.ini extension=opentelemetry.so -
Install the Composer packages
Terminal window composer require open-telemetry/sdk \open-telemetry/exporter-otlp \open-telemetry/transport-grpc -
Enable the module in application.yml
application.yml modules:- module: 'dev\winterframework\telemetry\OpenTelemetryModule'enabled: trueconfigFile: /path/to/opentelemetry.yaml -
Create opentelemetry.yaml
opentelemetry.yaml winter:telemetry:serviceName: 'my-winter-application'exporter:type: 'otlp'endpoint: 'http://localhost:4317'sampler:type: 'parent_based_always_on'ratio: 1.0Key configuration fields:
-
serviceNamestring(required) Identifies this service in your observability backend (Jaeger, Tempo, etc.). -
exporter.typestring(required) Export protocol —otlp(gRPC/HTTP),zipkin,jaeger, orconsole. -
exporter.endpointstring(required) The OTel collector or backend endpoint URL. -
sampler.typestring(default:parent_based_always_on) Sampling strategy —always_on,always_off,parent_based_always_on, ortraceidratio. -
sampler.ratiofloat(default:1.0) Sampling ratio (0.0–1.0) used when thetraceidratiosampler is selected.
-
Web Request Tracing
Section titled “Web Request Tracing”Register OpenTelemetryWebInterceptor in your WebMvcConfigurer to automatically 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\web\config\WebMvcConfigurer;use dev\winterframework\web\config\InterceptorRegistry;
#[Configuration]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 |