├── Context.php ├── ContextInterface.php ├── ContextKey.php ├── ContextKeyInterface.php ├── ContextKeys.php ├── ContextStorage.php ├── ContextStorageHead.php ├── ContextStorageHeadAware.php ├── ContextStorageInterface.php ├── ContextStorageNode.php ├── ContextStorageScopeInterface.php ├── DebugScope.php ├── ExecutionContextAwareInterface.php ├── FiberBoundContextStorage.php ├── FiberBoundContextStorageExecutionAwareBC.php ├── ImplicitContextKeyedInterface.php ├── Propagation ├── ArrayAccessGetterSetter.php ├── ExtendedPropagationGetterInterface.php ├── MultiTextMapPropagator.php ├── NoopTextMapPropagator.php ├── PropagationGetterInterface.php ├── PropagationSetterInterface.php ├── SanitizeCombinedHeadersPropagationGetter.php └── TextMapPropagatorInterface.php ├── README.md ├── ScopeInterface.php ├── ZendObserverFiber.php ├── composer.json └── fiber ├── initialize_fiber_handler.php └── zend_observer_fiber.h /Context.php: -------------------------------------------------------------------------------- 1 | */ 25 | private array $context = []; 26 | /** @var array */ 27 | private array $contextKeys = []; 28 | 29 | private function __construct() 30 | { 31 | self::$spanContextKey = ContextKeys::span(); 32 | } 33 | 34 | public static function createKey(string $key): ContextKeyInterface 35 | { 36 | return new ContextKey($key); 37 | } 38 | 39 | public static function setStorage(ContextStorageInterface&ExecutionContextAwareInterface $storage): void 40 | { 41 | self::$storage = $storage; 42 | } 43 | 44 | public static function storage(): ContextStorageInterface&ExecutionContextAwareInterface 45 | { 46 | /** @psalm-suppress RedundantPropertyInitializationCheck */ 47 | return self::$storage ??= new FiberBoundContextStorageExecutionAwareBC(); 48 | } 49 | 50 | /** 51 | * @internal OpenTelemetry 52 | */ 53 | public static function resolve(ContextInterface|false|null $context, ?ContextStorageInterface $contextStorage = null): ContextInterface 54 | { 55 | return $context 56 | ?? ($contextStorage ?? self::storage())->current() 57 | ?: self::getRoot(); 58 | } 59 | 60 | /** 61 | * @internal 62 | */ 63 | public static function getRoot(): ContextInterface 64 | { 65 | static $empty; 66 | 67 | return $empty ??= new self(); 68 | } 69 | 70 | public static function getCurrent(): ContextInterface 71 | { 72 | return self::storage()->current(); 73 | } 74 | 75 | public function activate(): ScopeInterface 76 | { 77 | $scope = self::storage()->attach($this); 78 | /** @psalm-suppress RedundantCondition @phpstan-ignore-next-line */ 79 | assert(self::debugScopesDisabled() || $scope = new DebugScope($scope)); 80 | 81 | return $scope; 82 | } 83 | 84 | private static function debugScopesDisabled(): bool 85 | { 86 | return filter_var( 87 | $_SERVER[self::OTEL_PHP_DEBUG_SCOPES_DISABLED] ?? \getenv(self::OTEL_PHP_DEBUG_SCOPES_DISABLED) ?: \ini_get(self::OTEL_PHP_DEBUG_SCOPES_DISABLED), 88 | FILTER_VALIDATE_BOOLEAN 89 | ); 90 | } 91 | 92 | public function withContextValue(ImplicitContextKeyedInterface $value): ContextInterface 93 | { 94 | return $value->storeInContext($this); 95 | } 96 | 97 | public function with(ContextKeyInterface $key, $value): self 98 | { 99 | if ($this->get($key) === $value) { 100 | return $this; 101 | } 102 | 103 | $self = clone $this; 104 | 105 | if ($key === self::$spanContextKey) { 106 | $self->span = $value; // @phan-suppress-current-line PhanTypeMismatchPropertyReal 107 | 108 | return $self; 109 | } 110 | 111 | $id = spl_object_id($key); 112 | if ($value !== null) { 113 | $self->context[$id] = $value; 114 | $self->contextKeys[$id] ??= $key; 115 | } else { 116 | unset( 117 | $self->context[$id], 118 | $self->contextKeys[$id], 119 | ); 120 | } 121 | 122 | return $self; 123 | } 124 | 125 | public function get(ContextKeyInterface $key) 126 | { 127 | if ($key === self::$spanContextKey) { 128 | /** @psalm-suppress InvalidReturnStatement */ 129 | return $this->span; 130 | } 131 | 132 | return $this->context[spl_object_id($key)] ?? null; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /ContextInterface.php: -------------------------------------------------------------------------------- 1 | activate(); 40 | * try { 41 | * // ... 42 | * } finally { 43 | * $scope->detach(); 44 | * } 45 | * ``` 46 | * 47 | * @return ScopeInterface scope to detach the context and restore the previous 48 | * context 49 | * 50 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md#attach-context 51 | */ 52 | public function activate(): ScopeInterface; 53 | 54 | /** 55 | * Returns a context with the given key set to the given value. 56 | * 57 | * @template T 58 | * @param ContextKeyInterface $key key to set 59 | * @param T|null $value value to set 60 | * @return ContextInterface a context with the given key set to `$value` 61 | * 62 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md#set-value 63 | */ 64 | public function with(ContextKeyInterface $key, $value): ContextInterface; 65 | 66 | /** 67 | * Returns a context with the given value set. 68 | * 69 | * @param ImplicitContextKeyedInterface $value value to set 70 | * @return ContextInterface a context with the given `$value` 71 | * 72 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md#set-value 73 | */ 74 | public function withContextValue(ImplicitContextKeyedInterface $value): ContextInterface; 75 | 76 | /** 77 | * Returns the value assigned to the given key. 78 | * 79 | * @template T 80 | * @param ContextKeyInterface $key key to get 81 | * @return T|null value assigned to `$key`, or null if no such value exists 82 | * 83 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md#get-value 84 | */ 85 | public function get(ContextKeyInterface $key); 86 | } 87 | -------------------------------------------------------------------------------- /ContextKey.php: -------------------------------------------------------------------------------- 1 | name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ContextKeyInterface.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $forks = []; 16 | 17 | public function __construct() 18 | { 19 | $this->current = $this->main = new ContextStorageHead($this); 20 | } 21 | 22 | public function fork(int|string $id): void 23 | { 24 | $this->forks[$id] = clone $this->current; 25 | } 26 | 27 | public function switch(int|string $id): void 28 | { 29 | $this->current = $this->forks[$id] ?? $this->main; 30 | } 31 | 32 | public function destroy(int|string $id): void 33 | { 34 | unset($this->forks[$id]); 35 | } 36 | 37 | public function head(): ContextStorageHead 38 | { 39 | return $this->current; 40 | } 41 | 42 | public function scope(): ?ContextStorageScopeInterface 43 | { 44 | return ($this->current->node->head ?? null) === $this->current 45 | ? $this->current->node 46 | : null; 47 | } 48 | 49 | public function current(): ContextInterface 50 | { 51 | return $this->current->node->context ?? Context::getRoot(); 52 | } 53 | 54 | public function attach(ContextInterface $context): ContextStorageScopeInterface 55 | { 56 | return $this->current->node = new ContextStorageNode($context, $this->current, $this->current->node); 57 | } 58 | 59 | private function __clone() 60 | { 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ContextStorageHead.php: -------------------------------------------------------------------------------- 1 | localStorage[$offset]); 26 | } 27 | 28 | public function offsetGet(mixed $offset): mixed 29 | { 30 | return $this->localStorage[$offset]; 31 | } 32 | 33 | public function offsetSet(mixed $offset, mixed $value): void 34 | { 35 | $this->localStorage[$offset] = $value; 36 | } 37 | 38 | public function offsetUnset(mixed $offset): void 39 | { 40 | unset($this->localStorage[$offset]); 41 | } 42 | 43 | public function context(): ContextInterface 44 | { 45 | return $this->context; 46 | } 47 | 48 | public function detach(): int 49 | { 50 | $flags = 0; 51 | if ($this->head !== $this->head->storage->head()) { 52 | $flags |= ScopeInterface::INACTIVE; 53 | } 54 | 55 | static $detached; 56 | $detached ??= (new \ReflectionClass(self::class))->newInstanceWithoutConstructor(); 57 | 58 | if ($this === $this->head->node) { 59 | assert($this->previous !== $detached); 60 | $this->head->node = $this->previous; 61 | $this->previous = $detached; 62 | 63 | return $flags; 64 | } 65 | 66 | if ($this->previous === $detached) { 67 | return $flags | ScopeInterface::DETACHED; 68 | } 69 | 70 | assert($this->head->node !== null); 71 | for ($n = $this->head->node, $depth = 1; 72 | $n->previous !== $this; 73 | $n = $n->previous, $depth++) { 74 | assert($n->previous !== null); 75 | } 76 | $n->previous = $this->previous; 77 | $this->previous = $detached; 78 | 79 | return $flags | ScopeInterface::MISMATCH | $depth; 80 | } 81 | 82 | private function __clone() 83 | { 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ContextStorageScopeInterface.php: -------------------------------------------------------------------------------- 1 | fiberId = self::currentFiberId(); 34 | $this->createdAt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 35 | 36 | if (!self::$shutdownHandlerInitialized) { 37 | self::$shutdownHandlerInitialized = true; 38 | register_shutdown_function('register_shutdown_function', static fn () => self::$finalShutdownPhase = true); 39 | } 40 | } 41 | 42 | public function detach(): int 43 | { 44 | $this->detachedAt ??= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 45 | 46 | $flags = $this->scope->detach(); 47 | 48 | if (($flags & ScopeInterface::DETACHED) !== 0) { 49 | trigger_error(sprintf( 50 | 'Scope: unexpected call to Scope::detach() for scope #%d, scope was already detached %s', 51 | spl_object_id($this), 52 | self::formatBacktrace($this->detachedAt), 53 | )); 54 | } elseif (($flags & ScopeInterface::MISMATCH) !== 0) { 55 | trigger_error(sprintf( 56 | 'Scope: unexpected call to Scope::detach() for scope #%d, scope successfully detached but another scope should have been detached first', 57 | spl_object_id($this), 58 | )); 59 | } elseif (($flags & ScopeInterface::INACTIVE) !== 0) { 60 | trigger_error(sprintf( 61 | 'Scope: unexpected call to Scope::detach() for scope #%d, scope successfully detached from different execution context', 62 | spl_object_id($this), 63 | )); 64 | } 65 | 66 | return $flags; 67 | } 68 | 69 | public function __destruct() 70 | { 71 | if (!$this->detachedAt) { 72 | // Handle destructors invoked during final shutdown 73 | // DebugScope::__destruct() might be called before fiber finally blocks run 74 | if (self::$finalShutdownPhase && $this->fiberId !== self::currentFiberId()) { 75 | return; 76 | } 77 | 78 | trigger_error(sprintf( 79 | 'Scope: missing call to Scope::detach() for scope #%d, created %s', 80 | spl_object_id($this->scope), 81 | self::formatBacktrace($this->createdAt), 82 | )); 83 | } 84 | } 85 | 86 | /** 87 | * @phan-suppress PhanUndeclaredClassReference 88 | * @phan-suppress PhanUndeclaredClassMethod 89 | */ 90 | private static function currentFiberId(): ?int 91 | { 92 | if (PHP_VERSION_ID < 80100) { 93 | return null; 94 | } 95 | 96 | assert(class_exists(Fiber::class, false)); 97 | if (!$fiber = Fiber::getCurrent()) { 98 | return null; 99 | } 100 | 101 | return spl_object_id($fiber); 102 | } 103 | 104 | private static function formatBacktrace(array $trace): string 105 | { 106 | $s = ''; 107 | for ($i = 0, $n = count($trace) + 1; ++$i < $n;) { 108 | $s .= "\n\t"; 109 | $s .= 'at '; 110 | if (isset($trace[$i]['class'])) { 111 | $s .= strtr($trace[$i]['class'], ['\\' => '.']); 112 | $s .= '.'; 113 | } 114 | $s .= strtr($trace[$i]['function'] ?? '{main}', ['\\' => '.']); 115 | $s .= '('; 116 | if (isset($trace[$i - 1]['file'])) { 117 | $s .= basename((string) $trace[$i - 1]['file']); 118 | if (isset($trace[$i - 1]['line'])) { 119 | $s .= ':'; 120 | $s .= $trace[$i - 1]['line']; 121 | } 122 | } else { 123 | $s .= 'Unknown Source'; 124 | } 125 | $s .= ')'; 126 | } 127 | 128 | return $s . "\n"; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ExecutionContextAwareInterface.php: -------------------------------------------------------------------------------- 1 | */ 21 | private WeakMap $heads; 22 | 23 | public function __construct() 24 | { 25 | $this->heads = new WeakMap(); 26 | $this->heads[$this] = new ContextStorageHead($this); 27 | } 28 | 29 | public function head(): ?ContextStorageHead 30 | { 31 | return $this->heads[Fiber::getCurrent() ?? $this] ?? null; 32 | } 33 | 34 | public function scope(): ?ContextStorageScopeInterface 35 | { 36 | $head = $this->heads[Fiber::getCurrent() ?? $this] ?? null; 37 | 38 | if (!$head?->node && Fiber::getCurrent()) { 39 | self::triggerNotInitializedFiberContextWarning(); 40 | 41 | return null; 42 | } 43 | 44 | // Starts with empty head instead of cloned parent -> no need to check for head mismatch 45 | return $head->node; 46 | } 47 | 48 | public function current(): ContextInterface 49 | { 50 | $head = $this->heads[Fiber::getCurrent() ?? $this] ?? null; 51 | 52 | if (!$head?->node && Fiber::getCurrent()) { 53 | self::triggerNotInitializedFiberContextWarning(); 54 | 55 | // Fallback to {main} to preserve BC 56 | $head = $this->heads[$this]; 57 | } 58 | 59 | return $head->node->context ?? Context::getRoot(); 60 | } 61 | 62 | public function attach(ContextInterface $context): ContextStorageScopeInterface 63 | { 64 | $head = $this->heads[Fiber::getCurrent() ?? $this] ??= new ContextStorageHead($this); 65 | 66 | return $head->node = new ContextStorageNode($context, $head, $head->node); 67 | } 68 | 69 | private static function triggerNotInitializedFiberContextWarning(): void 70 | { 71 | $fiber = Fiber::getCurrent(); 72 | assert($fiber !== null); 73 | 74 | trigger_error(sprintf( 75 | 'Access to not initialized OpenTelemetry context in fiber (id: %d), automatic forking not supported, must attach initial fiber context manually', 76 | spl_object_id($fiber), 77 | ), E_USER_WARNING); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /FiberBoundContextStorageExecutionAwareBC.php: -------------------------------------------------------------------------------- 1 | storage = new FiberBoundContextStorage(); 18 | } 19 | 20 | public function fork(int|string $id): void 21 | { 22 | $this->bcStorage()->fork($id); 23 | } 24 | 25 | public function switch(int|string $id): void 26 | { 27 | $this->bcStorage()->switch($id); 28 | } 29 | 30 | public function destroy(int|string $id): void 31 | { 32 | $this->bcStorage()->destroy($id); 33 | } 34 | 35 | private function bcStorage(): ContextStorage 36 | { 37 | if ($this->bc === null) { 38 | $this->bc = new ContextStorage(); 39 | 40 | // Copy head into $this->bc storage to preserve already attached scopes 41 | /** @psalm-suppress PossiblyNullFunctionCall */ 42 | $head = (static fn ($storage) => $storage->heads[$storage]) 43 | ->bindTo(null, FiberBoundContextStorage::class)($this->storage); 44 | $head->storage = $this->bc; 45 | 46 | /** @psalm-suppress PossiblyNullFunctionCall */ 47 | (static fn ($storage) => $storage->current = $storage->main = $head) 48 | ->bindTo(null, ContextStorage::class)($this->bc); 49 | } 50 | 51 | return $this->bc; 52 | } 53 | 54 | public function scope(): ?ContextStorageScopeInterface 55 | { 56 | return $this->bc 57 | ? $this->bc->scope() 58 | : $this->storage->scope(); 59 | } 60 | 61 | public function current(): ContextInterface 62 | { 63 | return $this->bc 64 | ? $this->bc->current() 65 | : $this->storage->current(); 66 | } 67 | 68 | public function attach(ContextInterface $context): ContextStorageScopeInterface 69 | { 70 | return $this->bc 71 | ? $this->bc->attach($context) 72 | : $this->storage->attach($context); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ImplicitContextKeyedInterface.php: -------------------------------------------------------------------------------- 1 | isSupportedCarrier($carrier)) { 42 | $keys = []; 43 | foreach ($carrier as $key => $_) { 44 | $keys[] = (string) $key; 45 | } 46 | 47 | return $keys; 48 | } 49 | 50 | throw new InvalidArgumentException( 51 | sprintf( 52 | 'Unsupported carrier type: %s.', 53 | get_debug_type($carrier), 54 | ) 55 | ); 56 | } 57 | 58 | /** {@inheritdoc} */ 59 | public function get($carrier, string $key): ?string 60 | { 61 | if ($this->isSupportedCarrier($carrier)) { 62 | $value = $carrier[$this->resolveKey($carrier, $key)] ?? null; 63 | if (is_array($value) && $value) { 64 | $value = $value[array_key_first($value)]; 65 | } 66 | 67 | return is_string($value) 68 | ? $value 69 | : null; 70 | } 71 | 72 | throw new InvalidArgumentException( 73 | sprintf( 74 | 'Unsupported carrier type: %s. Unable to get value associated with key:%s', 75 | get_debug_type($carrier), 76 | $key 77 | ) 78 | ); 79 | } 80 | 81 | /** {@inheritdoc} */ 82 | public function getAll($carrier, string $key): array 83 | { 84 | if ($this->isSupportedCarrier($carrier)) { 85 | $value = $carrier[$this->resolveKey($carrier, $key)] ?? null; 86 | if (is_array($value) && $value) { 87 | return array_values(array_filter($value, 'is_string')); 88 | } 89 | 90 | return is_string($value) 91 | ? [$value] 92 | : []; 93 | } 94 | 95 | throw new InvalidArgumentException( 96 | sprintf( 97 | 'Unsupported carrier type: %s. Unable to get value associated with key:%s', 98 | get_debug_type($carrier), 99 | $key 100 | ) 101 | ); 102 | } 103 | 104 | /** {@inheritdoc} */ 105 | public function set(&$carrier, string $key, string $value): void 106 | { 107 | if ($key === '') { 108 | throw new InvalidArgumentException('Unable to set value with an empty key'); 109 | } 110 | if ($this->isSupportedCarrier($carrier)) { 111 | if (($r = $this->resolveKey($carrier, $key)) !== $key) { 112 | unset($carrier[$r]); 113 | } 114 | 115 | $carrier[$key] = $value; 116 | 117 | return; 118 | } 119 | 120 | throw new InvalidArgumentException( 121 | sprintf( 122 | 'Unsupported carrier type: %s. Unable to set value associated with key:%s', 123 | get_debug_type($carrier), 124 | $key 125 | ) 126 | ); 127 | } 128 | 129 | private function isSupportedCarrier($carrier): bool 130 | { 131 | return is_array($carrier) || $carrier instanceof ArrayAccess && $carrier instanceof Traversable; 132 | } 133 | 134 | private function resolveKey($carrier, string $key): string 135 | { 136 | if (isset($carrier[$key])) { 137 | return $key; 138 | } 139 | 140 | foreach ($carrier as $k => $_) { 141 | $k = (string) $k; 142 | if (strcasecmp($k, $key) === 0) { 143 | return $k; 144 | } 145 | } 146 | 147 | return $key; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Propagation/ExtendedPropagationGetterInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function getAll($carrier, string $key): array; 21 | } 22 | -------------------------------------------------------------------------------- /Propagation/MultiTextMapPropagator.php: -------------------------------------------------------------------------------- 1 | */ 17 | private readonly array $fields; 18 | 19 | /** 20 | * @no-named-arguments 21 | * 22 | * @param list $propagators 23 | */ 24 | public function __construct( 25 | private readonly array $propagators, 26 | ) { 27 | $this->fields = $this->extractFields($this->propagators); 28 | } 29 | 30 | public function fields(): array 31 | { 32 | return $this->fields; 33 | } 34 | 35 | public function inject(&$carrier, ?PropagationSetterInterface $setter = null, ?ContextInterface $context = null): void 36 | { 37 | foreach ($this->propagators as $propagator) { 38 | $propagator->inject($carrier, $setter, $context); 39 | } 40 | } 41 | 42 | public function extract($carrier, ?PropagationGetterInterface $getter = null, ?ContextInterface $context = null): ContextInterface 43 | { 44 | $context ??= Context::getCurrent(); 45 | 46 | foreach ($this->propagators as $propagator) { 47 | $context = $propagator->extract($carrier, $getter, $context); 48 | } 49 | 50 | return $context; 51 | } 52 | 53 | /** 54 | * @param list $propagators 55 | * @return list 56 | */ 57 | private function extractFields(array $propagators): array 58 | { 59 | return array_values( 60 | array_unique( 61 | // Phan seems to struggle here with the variadic argument 62 | // @phan-suppress-next-line PhanParamTooFewInternalUnpack 63 | array_merge( 64 | ...array_map( 65 | static fn (TextMapPropagatorInterface $propagator) => $propagator->fields(), 66 | $propagators 67 | ) 68 | ) 69 | ) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Propagation/NoopTextMapPropagator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function keys($carrier): array; 20 | 21 | /** 22 | * Gets the value of a given key from a carrier. 23 | */ 24 | public function get($carrier, string $key) : ?string; 25 | } 26 | -------------------------------------------------------------------------------- /Propagation/PropagationSetterInterface.php: -------------------------------------------------------------------------------- 1 | getter->keys($carrier); 28 | } 29 | 30 | public function get($carrier, string $key): ?string 31 | { 32 | $value = $this->getter->get($carrier, $key); 33 | if ($value === null) { 34 | return null; 35 | } 36 | 37 | return preg_replace( 38 | [self::SERVER_CONCAT_HEADERS_REGEX, self::TRAILING_LEADING_SEPARATOR_REGEX], 39 | [self::LIST_MEMBERS_SEPARATOR], 40 | $value, 41 | ); 42 | } 43 | 44 | public function getAll($carrier, string $key): array 45 | { 46 | $value = $this->getter instanceof ExtendedPropagationGetterInterface 47 | ? $this->getter->getAll($carrier, $key) 48 | : (array) $this->getter->get($carrier, $key); 49 | 50 | if ($value === []) { 51 | return []; 52 | } 53 | 54 | $value = preg_replace( 55 | [self::SERVER_CONCAT_HEADERS_REGEX, self::TRAILING_LEADING_SEPARATOR_REGEX], 56 | [self::LIST_MEMBERS_SEPARATOR], 57 | $value, 58 | ); 59 | 60 | return array_values($value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Propagation/TextMapPropagatorInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function fields() : array; 22 | 23 | /** 24 | * Injects specific values from the provided {@see ContextInterface} into the provided carrier 25 | * via an {@see PropagationSetterInterface}. 26 | * 27 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.6.1/specification/context/api-propagators.md#textmap-inject 28 | */ 29 | public function inject(mixed &$carrier, ?PropagationSetterInterface $setter = null, ?ContextInterface $context = null): void; 30 | 31 | /** 32 | * Extracts specific values from the provided carrier into the provided {@see ContextInterface} 33 | * via an {@see PropagationGetterInterface}. 34 | * 35 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.6.1/specification/context/api-propagators.md#textmap-extract 36 | */ 37 | public function extract($carrier, ?PropagationGetterInterface $getter = null, ?ContextInterface $context = null): ContextInterface; 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/context/releases) 2 | [![Source](https://img.shields.io/badge/source-context-green)](https://github.com/open-telemetry/opentelemetry-php/tree/main/src/Context) 3 | [![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php:context-blue)](https://github.com/opentelemetry-php/context) 4 | [![Latest Version](http://poser.pugx.org/open-telemetry/context/v/unstable)](https://packagist.org/packages/open-telemetry/context/) 5 | [![Stable](http://poser.pugx.org/open-telemetry/context/v/stable)](https://packagist.org/packages/open-telemetry/context/) 6 | 7 | # OpenTelemetry Context 8 | 9 | Immutable execution scoped propagation mechanism, for further details see [opentelemetry-specification][1]. 10 | 11 | ## Installation 12 | 13 | ```shell 14 | composer require open-telemetry/context 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### Implicit propagation 20 | 21 | ```php 22 | $context = Context::getCurrent(); 23 | // modify context 24 | $scope = $context->activate(); 25 | try { 26 | // run within new context 27 | } finally { 28 | $scope->detach(); 29 | } 30 | ``` 31 | 32 | It is recommended to use a `try-finally` statement after `::activate()` to ensure that the created scope is properly `::detach()`ed. 33 | 34 | ### Debug scopes 35 | 36 | By default, scopes created by `::activate()` warn on invalid and missing calls to `::detach()` in non-production 37 | environments. This feature can be disabled by setting the environment variable `OTEL_PHP_DEBUG_SCOPES_DISABLED` to a 38 | truthy value. Disabling is only recommended for applications using `exit` / `die` to prevent unavoidable notices. 39 | 40 | ## Async applications 41 | 42 | ### Fiber support - automatic context propagation to newly created fibers 43 | 44 | Requires an NTS build, `ext-ffi`, and setting the environment variable `OTEL_PHP_FIBERS_ENABLED` to a truthy value. Additionally `vendor/autoload.php` has to be preloaded for non-CLI SAPIs if [`ffi.enable`](https://www.php.net/manual/en/ffi.configuration.php#ini.ffi.enable) is set to `preload`. 45 | 46 | ### Event loops 47 | 48 | Event loops have to restore the original context on callback execution. A basic implementation could look like the following, though implementations should avoid keeping unnecessary references to arguments if possible: 49 | 50 | ```php 51 | function bindContext(Closure $closure): Closure { 52 | $context = Context::getCurrent(); 53 | return static function (mixed ...$args) use ($closure, $context): mixed { 54 | $scope = $context->activate(); 55 | try { 56 | return $closure(...$args); 57 | } finally { 58 | $scope->detach(); 59 | } 60 | }; 61 | } 62 | ``` 63 | 64 | ## Contributing 65 | 66 | This repository is a read-only git subtree split. 67 | To contribute, please see the main [OpenTelemetry PHP monorepo](https://github.com/open-telemetry/opentelemetry-php). 68 | 69 | [1]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md#context 70 | -------------------------------------------------------------------------------- /ScopeInterface.php: -------------------------------------------------------------------------------- 1 | = 8.1, an NTS build, and the FFI extension'); 45 | 46 | return false; 47 | } 48 | 49 | try { 50 | $fibers = FFI::scope('OTEL_ZEND_OBSERVER_FIBER'); 51 | } catch (FFI\Exception) { 52 | try { 53 | $fibers = FFI::load(__DIR__ . '/fiber/zend_observer_fiber.h'); 54 | } catch (FFI\Exception $e) { 55 | trigger_error(sprintf('Context: Fiber context switching not supported, %s', $e->getMessage())); 56 | 57 | return false; 58 | } 59 | } 60 | 61 | $storage = new ContextStorage(); 62 | $fibers->zend_observer_fiber_init_register(static fn (int $initializing) => $storage->fork($initializing)); //@phpstan-ignore-line 63 | $fibers->zend_observer_fiber_switch_register(static fn (int $from, int $to) => $storage->switch($to)); //@phpstan-ignore-line 64 | $fibers->zend_observer_fiber_destroy_register(static fn (int $destroying) => $storage->destroy($destroying)); //@phpstan-ignore-line 65 | 66 | Context::setStorage($storage); 67 | 68 | return true; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-telemetry/context", 3 | "description": "Context implementation for OpenTelemetry PHP.", 4 | "keywords": ["opentelemetry", "otel", "context"], 5 | "type": "library", 6 | "support": { 7 | "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", 8 | "source": "https://github.com/open-telemetry/opentelemetry-php", 9 | "docs": "https://opentelemetry.io/docs/php", 10 | "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V" 11 | }, 12 | "license": "Apache-2.0", 13 | "authors": [ 14 | { 15 | "name": "opentelemetry-php contributors", 16 | "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "symfony/polyfill-php82": "^1.26" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "OpenTelemetry\\Context\\": "." 26 | }, 27 | "files": [ 28 | "fiber/initialize_fiber_handler.php" 29 | ] 30 | }, 31 | "suggest": { 32 | "ext-ffi": "To allow context switching in Fibers" 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-main": "1.0.x-dev" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fiber/initialize_fiber_handler.php: -------------------------------------------------------------------------------- 1 |