open-telemetry/context
OpenTelemetry Context for PHP: immutable, execution-scoped context propagation for tracing/metrics. Activate contexts with scopes (try/finally detach), debug scope leak warnings, and optional async support via fibers and event-loop context binding.
Install the package:
composer require open-telemetry/context
First use case: Propagate a trace context across a synchronous request flow (e.g., middleware → controller → service).
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\Scope;
// In middleware or service entry point:
$context = Context::getCurrent()->withBaggageItem('user_id', auth()->id());
$scope = $context->activate();
try {
// Code here runs with the new context (e.g., HTTP client calls, DB queries).
$response = Http::get('https://api.example.com/data');
} finally {
$scope->detach(); // Restore previous context.
}
Key files to reference:
src/Context.php (core API).src/Propagation/TextMapPropagator.php (for HTTP/baggage extraction).Use TextMapPropagator to inject/extract trace IDs from HTTP headers (e.g., traceparent).
use OpenTelemetry\Context\Propagation\TextMapPropagator;
// Extract context from incoming request (e.g., in Laravel middleware):
$propagator = new TextMapPropagator();
$carrier = new ArrayCarrier($_SERVER); // or $_GET, $_POST for query params
$context = $propagator->extract($carrier);
$scope = $context->activate();
try {
// Request processing...
} finally {
$scope->detach();
}
// Inject context into outgoing responses:
$carrier = new ArrayCarrier(['headers' => []]);
$propagator->inject($carrier, $context);
Pass context explicitly between services to correlate logs/traces:
class UserService {
public function getUserData(int $userId, Context $context = null) {
$context = $context ?? Context::getCurrent();
$context = $context->withBaggageItem('user_id', $userId);
$scope = $context->activate();
try {
// Business logic...
return $this->repository->find($userId);
} finally {
$scope->detach();
}
}
}
Fibers: Enable automatic propagation with:
export OTEL_PHP_FIBERS_ENABLED=true
// In a fiber-aware app:
$fiber = new Fiber(function () {
$context = Context::getCurrent(); // Automatically propagated.
// Fiber logic...
});
$fiber->start();
Event Loops: Use bindContext() to wrap callbacks:
use OpenTelemetry\Context\Context;
function bindContext(Closure $callback): Closure {
return function (...$args) use ($callback) {
$scope = Context::getCurrent()->activate();
try {
return $callback(...$args);
} finally {
$scope->detach();
}
};
}
// Usage with ReactPHP:
$loop->addPeriodicTimer(1, bindContext(function () {
// Context is automatically restored.
}));
Propagate context to async jobs using Context::getCurrent() in job payloads:
// Dispatch job with current context:
$job = new ProcessOrder($orderId);
$job->context = Context::getCurrent()->toBaggage(); // Serialize baggage.
dispatch($job);
// In job handle():
$context = Context::getCurrent()->withBaggageItems($this->context);
$scope = $context->activate();
try {
// Job logic...
} finally {
$scope->detach();
}
Ensure trace IDs persist across Artisan commands:
use OpenTelemetry\Context\Context;
Artisan::command('process:orders', function () {
$scope = Context::getCurrent()->activate();
try {
// Command logic...
} finally {
$scope->detach();
}
});
Forgetting detach():
E_USER_NOTICE).try/finally or leverage Laravel’s ensure() helper:
$scope = Context::getCurrent()->activate();
ensure(function () use ($scope) { $scope->detach(); });
Fiber Context Leaks:
OTEL_PHP_FIBERS_ENABLED lose context.OTEL_PHP_FIBERS_ENABLED=true and preload vendor/autoload.php for non-CLI SAPIs.Async Callback Context Loss:
bindContext() (see Implementation Patterns).Baggage Item Collisions:
user_id) in nested scopes.auth.user_id) or merge contexts explicitly:
$context = Context::getCurrent()->withBaggageItems([
'auth.user_id' => $userId,
'request.path' => request()->path(),
]);
Debug Scopes in Production:
E_USER_NOTICE warnings from undetached scopes in production.export OTEL_PHP_DEBUG_SCOPES_DISABLED=true
Warning: Only use this if your app avoids exit/die (e.g., Laravel’s graceful shutdown).Inspect Current Context:
$context = Context::getCurrent();
dump($context->getBaggage()->all()); // View all baggage items.
Log Context Boundaries:
$scope = Context::getCurrent()->activate();
try {
logger()->debug('Entering context scope', ['trace_id' => $context->getTraceId()]);
// ...
} finally {
$scope->detach();
logger()->debug('Exiting context scope');
}
Validate Propagation:
TextMapPropagator to verify headers are correctly injected/extracted:
$propagator = new TextMapPropagator();
$carrier = new ArrayCarrier(['headers' => $_SERVER['HTTP_HEADERS']]);
$context = $propagator->extract($carrier);
dump($context->getTraceId()); // Should match incoming request.
Fiber Debugging:
$fiber = new Fiber(function () {
dump(Context::getCurrent()->getTraceId()); // Should match parent.
});
$fiber->start();
Custom Propagators:
Extend TextMapPropagator for non-HTTP carriers (e.g., gRPC metadata):
class GrpcPropagator extends TextMapPropagator {
public function inject(array &$metadata, Context $context): void {
$carrier = new ArrayCarrier($metadata);
parent::inject($carrier, $context);
}
}
Context Storage Backends: Replace the default storage (e.g., for custom async workers):
use OpenTelemetry\Context\ContextStorageInterface;
class RedisContextStorage implements ContextStorageInterface {
// Implement get/set logic using Redis.
}
Baggage Item Validation: Add middleware to validate baggage items (e.g., reject malformed trace IDs):
$context = Context::getCurrent();
if (!$context->getTraceId() || !preg_match('/^[a-f0-9]{32}$/', $context->getTraceId())) {
throw new \RuntimeException('Invalid trace ID');
}
Laravel Service Provider Integration:
Bootstrap context in register():
public function register(): void {
$this->app->singleton(ContextStorageInterface::class, function () {
return new ContextStorage();
});
}
How can I help you explore Laravel packages today?