agent/php/ElasticApm/Impl/AutoInstrument/InterceptionManager.php (168 lines of code) (raw):

<?php /* * 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. */ declare(strict_types=1); namespace Elastic\Apm\Impl\AutoInstrument; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Tracer; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\DbgUtil; use Throwable; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ final class InterceptionManager { /** @var Registration[] */ private $interceptedCallRegistrations; /** @var Logger */ private $logger; /** @var BuiltinPlugin */ private $builtinPlugin; /** @var int|null */ private $interceptedCallInProgressRegistrationId = null; /** @var Registration|null */ private $interceptedCallInProgressRegistration = null; /** * @var null|callable(int, bool, mixed): void */ private $interceptedCallInProgressPreHookRetVal = null; public function __construct(Tracer $tracer) { $this->logger = $tracer->loggerFactory() ->loggerForClass(LogCategory::INTERCEPTION, __NAMESPACE__, __CLASS__, __FILE__); $this->loadPlugins($tracer); } private function loadPlugins(Tracer $tracer): void { $this->builtinPlugin = new BuiltinPlugin($tracer); $registerCtx = new RegistrationContext(); $registerCtx->dbgCurrentPluginIndex = 0; $registerCtx->dbgCurrentPluginDesc = $this->builtinPlugin->getDescription(); $this->builtinPlugin->register($registerCtx); $this->interceptedCallRegistrations = $registerCtx->interceptedCallRegistrations; } /** * @param int $interceptRegistrationId * @param ?object $thisObj * @param mixed[] $interceptedCallArgs * * @return bool */ public function internalFuncCallPreHook( int $interceptRegistrationId, ?object $thisObj, array $interceptedCallArgs ): bool { $localLogger = $this->logger->inherit()->addAllContext( [ 'interceptRegistrationId' => $interceptRegistrationId, 'thisObj type' => DbgUtil::getType($thisObj), 'interceptedCallArgs count' => count($interceptedCallArgs), 'thisObj' => $this->logger->possiblySecuritySensitive($thisObj), 'interceptedCallArgs' => $this->logger->possiblySecuritySensitive($interceptedCallArgs), ] ); $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); $interceptRegistration = ArrayUtil::getValueIfKeyExistsElse($interceptRegistrationId, $this->interceptedCallRegistrations, null); if ($interceptRegistration === null) { ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('There is no registration with the given interceptRegistrationId'); return false; } $localLogger->addContext('interceptRegistration', $interceptRegistration); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling preHook...'); try { $preHookRetVal = ($interceptRegistration->preHook)($thisObj, $interceptedCallArgs); } catch (Throwable $throwable) { ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable( $throwable, 'preHook has let a Throwable to escape' ); return false; } $shouldCallPostHook = ($preHookRetVal !== null); if ($shouldCallPostHook) { $this->interceptedCallInProgressRegistrationId = $interceptRegistrationId; $this->interceptedCallInProgressRegistration = $interceptRegistration; $this->interceptedCallInProgressPreHookRetVal = $preHookRetVal; } $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'preHook completed successfully', ['shouldCallPostHook' => $shouldCallPostHook]); return $shouldCallPostHook; } /** * @param int $numberOfStackFramesToSkip * @param bool $hasExitedByException * @param mixed|Throwable $returnValueOrThrown Return value of the intercepted call * or the object thrown by the intercepted call */ public function internalFuncCallPostHook( int $numberOfStackFramesToSkip, bool $hasExitedByException, $returnValueOrThrown ): void { $localLogger = $this->logger->inherit()->addAllContext( [ 'interceptRegistrationId' => $this->interceptedCallInProgressRegistrationId, 'interceptRegistration' => $this->interceptedCallInProgressRegistration, ] ); $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); if ($this->interceptedCallInProgressRegistrationId === null) { ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('There is no intercepted call in progress'); return; } assert($this->interceptedCallInProgressRegistration !== null); assert($this->interceptedCallInProgressPreHookRetVal !== null); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling postHook...'); try { ($this->interceptedCallInProgressPreHookRetVal)( $numberOfStackFramesToSkip + 1, $hasExitedByException, $returnValueOrThrown ); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'postHook completed without throwing'); } catch (Throwable $throwable) { ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($throwable, 'postHook has thrown'); } $this->interceptedCallInProgressRegistrationId = null; $this->interceptedCallInProgressPreHookRetVal = null; } public function astInstrumentationDirectCall(string $method): void { $localLogger = $this->logger->inherit()->addAllContext(['method' => $method]); $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); $wordPressAutoInstrumIfEnabled = $this->builtinPlugin->getWordPressAutoInstrumentationIfEnabled(); if ($wordPressAutoInstrumIfEnabled === null) { static $loggedOnce = false; if (!$loggedOnce) { $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'WordPress instrumentation is DISABLED'); $loggedOnce = true; } return; } static $dbgImplFuncDesc = null; if ($dbgImplFuncDesc === null) { $dbgImplFuncDesc = ClassNameUtil::fqToShort(WordPressAutoInstrumentation::class) . '->directCall'; } /** @var string $dbgImplFuncDesc */ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling ' . $dbgImplFuncDesc . '...'); try { $wordPressAutoInstrumIfEnabled->directCall($method); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, $dbgImplFuncDesc . ' completed without throwing'); } catch (Throwable $throwable) { ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($throwable, $dbgImplFuncDesc . ' has thrown'); } } /** * @param ?string $instrumentedClassFullName * @param string $instrumentedFunction * @param mixed[] $capturedArgs * * @return null|callable(?Throwable $thrown, mixed $returnValue): void */ public function astInstrumentationPreHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable { $localLogger = $this->logger->inherit()->addAllContext(['instrumentedClassFullName' => $instrumentedClassFullName]); $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); $wordPressAutoInstrumIfEnabled = $this->builtinPlugin->getWordPressAutoInstrumentationIfEnabled(); if ($wordPressAutoInstrumIfEnabled === null) { static $loggedOnce = false; if (!$loggedOnce) { $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'WordPress instrumentation is DISABLED'); $loggedOnce = true; } return null; } static $dbgImplFuncDesc = null; if ($dbgImplFuncDesc === null) { $dbgImplFuncDesc = ClassNameUtil::fqToShort(WordPressAutoInstrumentation::class) . '->preHook'; } /** @var string $dbgImplFuncDesc */ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling ' . $dbgImplFuncDesc . '...'); try { $retVal = $wordPressAutoInstrumIfEnabled->preHook($instrumentedClassFullName, $instrumentedFunction, $capturedArgs); $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, $dbgImplFuncDesc . ' completed without throwing', ['retVal == null' => ($retVal == null)]); return $retVal; } catch (Throwable $throwable) { ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($throwable, $dbgImplFuncDesc . ' has thrown'); return null; } } }