prod/php/ElasticOTel/InstrumentationBridge.php (120 lines of code) (raw):

<?php /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** @noinspection PhpIllegalPsrClassPathInspection */ declare(strict_types=1); namespace Elastic\OTel; use Closure; use Elastic\OTel\Log\LogLevel; use Throwable; use Elastic\OTel\Util\SingletonInstanceTrait; use function elastic_otel_get_config_option_by_name; use function elastic_otel_hook; use function elastic_otel_log_feature; /** * Code in this file is part of implementation internals, and thus it is not covered by the backward compatibility. * * @internal * * @phpstan-type PreHook Closure(?object $thisObj, array<mixed> $params, string $class, string $function, ?string $filename, ?int $lineno): (void|array<mixed>) * return value is modified parameters * * @phpstan-type PostHook Closure(?object $thisObj, array<mixed> $params, mixed $returnValue, ?Throwable $throwable): mixed * return value is modified return value */ final class InstrumentationBridge { /** * Constructor is hidden because instance() should be used instead */ use SingletonInstanceTrait; /** * @var array<array{string, string, ?Closure, ?Closure}> */ public array $delayedHooks = []; private bool $enableDebugHooks; public function bootstrap(): void { self::elasticOTelHook(null, 'spl_autoload_register', null, $this->retryDelayedHooks(...)); require ProdPhpDir::$fullPath . DIRECTORY_SEPARATOR . 'OpenTelemetry' . DIRECTORY_SEPARATOR . 'Instrumentation' . DIRECTORY_SEPARATOR . 'hook.php'; $this->enableDebugHooks = (bool)elastic_otel_get_config_option_by_name('debug_php_hooks_enabled'); BootstrapStageLogger::logDebug('Finished successfully', __FILE__, __LINE__, __CLASS__, __FUNCTION__); } /** * @phpstan-param PreHook $pre * @phpstan-param PostHook $post */ public function hook(?string $class, string $function, ?Closure $pre = null, ?Closure $post = null): bool { BootstrapStageLogger::logTrace('Entered. class: ' . $class . ' function: ' . $function, __FILE__, __LINE__, __CLASS__, __FUNCTION__); if ($class !== null && !self::classOrInterfaceExists($class)) { $this->addToDelayedHooks($class, $function, $pre, $post); return true; } $success = self::elasticOTelHookNoThrow($class, $function, $pre, $post); if ($this->enableDebugHooks) { self::placeDebugHooks($class, $function); } return $success; } /** * @phpstan-param PreHook $pre * @phpstan-param PostHook $post */ private function addToDelayedHooks(string $class, string $function, ?Closure $pre = null, ?Closure $post = null): void { BootstrapStageLogger::logTrace('Adding to delayed hooks. class: ' . $class . ', function: ' . $function, __FILE__, __LINE__, __CLASS__, __FUNCTION__); $this->delayedHooks[] = [$class, $function, $pre, $post]; } /** * @phpstan-param PreHook $pre * @phpstan-param PostHook $post */ private static function elasticOTelHook(?string $class, string $function, ?Closure $pre = null, ?Closure $post = null): void { $dbgClassAsString = BootstrapStageLogger::nullableToLog($class); BootstrapStageLogger::logTrace('Entered. class: ' . $dbgClassAsString . ', function: ' . $function, __FILE__, __LINE__, __CLASS__, __FUNCTION__); // elastic_otel_hook function is provided by the extension $retVal = elastic_otel_hook($class, $function, $pre, $post); if ($retVal) { BootstrapStageLogger::logTrace('Successfully hooked. class: ' . $dbgClassAsString . ', function: ' . $function, __FILE__, __LINE__, __CLASS__, __FUNCTION__); return; } BootstrapStageLogger::logDebug('elastic_otel_hook returned false: ' . $dbgClassAsString . ', function: ' . $function, __FILE__, __LINE__, __CLASS__, __FUNCTION__); } /** * @phpstan-param PreHook $pre * @phpstan-param PostHook $post */ private static function elasticOTelHookNoThrow(?string $class, string $function, ?Closure $pre = null, ?Closure $post = null): bool { try { self::elasticOTelHook($class, $function, $pre, $post); return true; } catch (Throwable $throwable) { BootstrapStageLogger::logCriticalThrowable($throwable, 'Call to elasticOTelHook has thrown', __FILE__, __LINE__, __CLASS__, __FUNCTION__); return false; } } private function retryDelayedHooks(): void { $delayedHooksCount = count($this->delayedHooks); BootstrapStageLogger::logTrace('Entered. delayedHooks count: ' . $delayedHooksCount, __FILE__, __LINE__, __CLASS__, __FUNCTION__); if (count($this->delayedHooks) === 0) { return; } $delayedHooksToKeep = []; foreach ($this->delayedHooks as $delayedHookTuple) { $class = $delayedHookTuple[0]; if (!self::classOrInterfaceExists($class)) { BootstrapStageLogger::logTrace('Class/Interface still does not exist - keeping delayed hook. class: ' . $class, __FILE__, __LINE__, __CLASS__, __FUNCTION__); $delayedHooksToKeep[] = $delayedHookTuple; continue; } self::elasticOTelHook(...$delayedHookTuple); } $this->delayedHooks = $delayedHooksToKeep; BootstrapStageLogger::logTrace('Exiting... delayedHooks count: ' . count($this->delayedHooks), __FILE__, __LINE__, __CLASS__, __FUNCTION__); } private static function classOrInterfaceExists(string $classOrInterface): bool { return class_exists($classOrInterface) || interface_exists($classOrInterface); } private static function placeDebugHooks(?string $class, string $function): void { $func = '\''; if ($class) { $func = $class . '::'; } $func .= $function . '\''; self::elasticOTelHookNoThrow( $class, $function, function () use ($func) { elastic_otel_log_feature( 0 /* <- isForced */, LogLevel::debug->value, Log\LogFeature::INSTRUMENTATION, '' /* <- file */, null /* <- line */, $func, ('pre-hook data: ' . var_export(func_get_args(), true)) ); }, function () use ($func) { elastic_otel_log_feature( 0 /* <- isForced */, LogLevel::debug->value, Log\LogFeature::INSTRUMENTATION, '' /* <- file */, null /* <- line */, $func, ('post-hook data: ' . var_export(func_get_args(), true)) ); } ); } }