open-telemetry/context
OpenTelemetry Context for PHP: immutable execution-scoped context propagation. Activate a context to create a scope and safely detach it (try/finally). Includes debug scope warnings, and optional async support for fibers and event loops.
Install the package:
composer require open-telemetry/context
Activate a context (e.g., in a middleware or service):
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\Scope;
$scope = Context::getCurrent()->activate();
try {
// Execute code within the new context
$this->processRequest();
} finally {
$scope->detach(); // Critical: Restore previous context
}
First use case: Instrument a Laravel HTTP request to propagate trace context:
// In App\Http\Middleware\TraceMiddleware
public function handle($request, Closure $next) {
$scope = Context::getCurrent()->activate();
try {
return $next($request);
} finally {
$scope->detach();
}
}
Context::getCurrent(): Retrieve the current execution context.->activate(): Create a scope to run code in a new context.Scope::detach(): Restore the previous context (use try/finally).detach() calls in non-production.composer.json (as above).// app/Services/ObservabilityService.php
namespace App\Services;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\Scope;
class ObservabilityService {
public function withContext(callable $callback): mixed {
$scope = Context::getCurrent()->activate();
try {
return $callback();
} finally {
$scope->detach();
}
}
}
$observability = app(ObservabilityService::class);
return $observability->withContext(function() {
// Context is active here
return response()->json(['data' => '...']);
});
// app/Http/Middleware/TraceContext.php
public function handle($request, Closure $next) {
$context = Context::getCurrent()->withBaggage([
'http.request_id' => $request->header('X-Request-ID'),
'user.id' => auth()->id(),
]);
$scope = $context->activate();
try {
return $next($request);
} finally {
$scope->detach();
}
}
user.id) propagate through the request stack.// app/Jobs/ProcessOrder.php
public function handle() {
$observability = app(ObservabilityService::class);
$observability->withContext(function() {
// Context includes trace ID from the original HTTP request
$this->processOrderLogic();
});
}
dispatchSync with context activation for synchronous job workflows.export OTEL_PHP_FIBERS_ENABLED=true
ffi.enable=preload).$fiber = new Fiber(function() {
// Context is automatically propagated
$traceId = Context::getCurrent()->getBaggage()->get('trace_id');
// ...
});
$fiber->start();
bindContext pattern for custom event loops (e.g., ReactPHP):
$loop = React\EventLoop\Factory::create();
$loop->addPeriodicTimer(1, $observability->bindContext(function() {
// Context is restored for each callback
}));
use OpenTelemetry\Context\Propagation\TextMapPropagator;
$propagator = new TextMapPropagator();
$context = Context::getCurrent();
$headers = $propagator->extract($context, new ArrayAccessHeaderProvider($request->headers));
$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.example.com', [
'headers' => $headers,
]);
ObservabilityService globally:
// config/app.php
'bindings' => [
ObservabilityService::class => function($app) {
return new ObservabilityService();
},
];
// app/Console/Commands/ProcessPayments.php
protected function handle() {
$observability = app(ObservabilityService::class);
$observability->withContext(function() {
// Context includes trace ID from parent process
$this->processPayments();
});
}
// app/Observers/OrderObserver.php
public function saving(Order $order) {
$traceId = Context::getCurrent()->getBaggage()->get('trace_id');
$order->trace_id = $traceId;
}
use OpenTelemetry\API\Trace\TracerInterface;
$tracer = app(TracerInterface::class);
$span = $tracer->spanBuilder('user_action')
->setAttribute('user.id', Context::getCurrent()->getBaggage()->get('user.id'))
->startSpan();
Context::getBaggage() to access metadata across services:
$baggage = Context::getCurrent()->getBaggage();
$tenantId = $baggage->get('tenant.id');
Context::withBaggage() in tests to simulate propagation:
$context = Context::withBaggage(['test.key' => 'test.value']);
$scope = $context->activate();
try {
$this->assertEquals('test.value', Context::getCurrent()->getBaggage()->get('test.key'));
} finally {
$scope->detach();
}
detach() is called in tests:
$this->expectException(InvalidArgumentException::class);
$scope = Context::getCurrent()->activate();
// Intentionally skip detach() to trigger debug warning
Forgetting detach():
detach() is omitted.try/finally:
$scope = Context::getCurrent()->activate();
try {
// ...
} finally {
$scope->detach(); // Non-negotiable
}
detach().Async Context Leaks:
bindContext or ensure detach() is called in finally blocks.// Bad: Missing detach in callback
$loop->addTimer(1, function() {
$scope = Context::getCurrent()->activate();
// ... no detach!
});
// Good: Wrapped in bindContext
$loop->addTimer(1, $observability->bindContext(function() {
// Context is auto-restored
}));
Fiber Support Requirements:
OTEL_PHP_FIBERS_ENABLED is not set.ext-ffi is missing or misconfigured.php -m | grep ffi # Check FFI extension
php -r 'echo getenv("OT
How can I help you explore Laravel packages today?