agent/php/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php (176 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\Util; use Closure; use Elastic\Apm\ElasticApm; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Log\LoggerFactory; use Elastic\Apm\Impl\Span; use Elastic\Apm\Impl\Util\Assert; use Elastic\Apm\Impl\Util\DbgUtil; use Elastic\Apm\Impl\Util\StackTraceUtil; use Elastic\Apm\SpanInterface; use Throwable; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ final class AutoInstrumentationUtil { /** @var Logger */ private $logger; public function __construct(LoggerFactory $loggerFactory) { $this->logger = $loggerFactory->loggerForClass( LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__ ); } public static function buildSpanNameFromCall(?string $className, string $funcName): string { return ($className === null) ? $funcName : ($className . StackTraceUtil::CLASS_AND_METHOD_SEPARATOR . $funcName); } private static function processNewSpan(SpanInterface $span): void { if ($span instanceof Span) { // Mark all spans created by auto-instrumentation as compressible $span->setCompressible(true); } } public static function beginCurrentSpan(string $name, string $type, ?string $subtype = null, ?string $action = null): SpanInterface { $span = ElasticApm::getCurrentTransaction()->beginCurrentSpan($name, $type, $subtype, $action); self::processNewSpan($span); return $span; } /** * @param string $name * @param string $type * @param ?string $subtype * @param ?string $action * @param callable $callback * @param mixed[] $callbackArgs * @param int $numberOfStackFramesToSkip * * @return mixed * * @phpstan-param 0|positive-int $numberOfStackFramesToSkip */ public static function captureCurrentSpan(string $name, string $type, ?string $subtype, ?string $action, callable $callback, array $callbackArgs, int $numberOfStackFramesToSkip) { $span = self::beginCurrentSpan($name, $type, $subtype, $action); try { return call_user_func_array($callback, $callbackArgs); } catch (Throwable $throwable) { $span->createErrorFromThrowable($throwable); throw $throwable; } finally { $span->endSpanEx($numberOfStackFramesToSkip + 1); } } /** * @param int $numberOfStackFramesToSkip * @param SpanInterface $span * @param bool $hasExitedByException * @param mixed|Throwable $returnValueOrThrown * @param ?float $duration * * @phpstan-param 0|positive-int $numberOfStackFramesToSkip */ public static function endSpan(int $numberOfStackFramesToSkip, SpanInterface $span, bool $hasExitedByException, $returnValueOrThrown, ?float $duration = null): void { if ($hasExitedByException && ($returnValueOrThrown instanceof Throwable)) { $span->createErrorFromThrowable($returnValueOrThrown); } // endSpanEx() is a public API so by default it will appear on the stacktrace // because it assumes that it was called by the application and // it is important to know from where in the application. // But in this case endSpanEx() is called by agent's code so there's no point for it // to appear on the stack trace. // That is the reason to +1 to the usual $numberOfStackFramesToSkip + 1 $span->endSpanEx($numberOfStackFramesToSkip + 2, $duration); } /** * @param SpanInterface $span * @param null|Closure(bool, mixed): void $doBeforeSpanEnd * * @return null|callable(int, bool, mixed): void */ public static function createInternalFuncPostHookFromEndSpan(SpanInterface $span, ?Closure $doBeforeSpanEnd = null): ?callable { if ($span->isNoop()) { return null; } /** * @param int $numberOfStackFramesToSkip * @param bool $hasExitedByException * @param mixed $returnValueOrThrown Return value of the intercepted call or thrown object * * @phpstan-param 0|positive-int $numberOfStackFramesToSkip */ return function (int $numberOfStackFramesToSkip, bool $hasExitedByException, $returnValueOrThrown) use ($span, $doBeforeSpanEnd): void { if ($doBeforeSpanEnd !== null) { $doBeforeSpanEnd($hasExitedByException, $returnValueOrThrown); } /** @var 0|positive-int $numberOfStackFramesToSkip */ self::endSpan($numberOfStackFramesToSkip + 1, $span, $hasExitedByException, $returnValueOrThrown); }; } /** * @param bool $hasExitedByException * @param array<string, mixed> $dbgCtx * * @return void */ public static function assertInterceptedCallNotExitedByException( bool $hasExitedByException, array $dbgCtx = [] ): void { ($assertProxy = Assert::ifEnabled()) && $assertProxy->that(!$hasExitedByException) && $assertProxy->withContext('!$hasExitedByException', $dbgCtx); } /** * @param bool $isOfExpectedType * @param string $dbgExpectedType * @param mixed $dbgActualValue * @param ?string $dbgParamName * * @return bool */ public function verifyType(bool $isOfExpectedType, string $dbgExpectedType, $dbgActualValue, ?string $dbgParamName = null): bool { if ($isOfExpectedType) { return true; } $ctx = [ 'expected type' => $dbgExpectedType, 'actual type' => DbgUtil::getType($dbgActualValue), 'actual value' => $this->logger->possiblySecuritySensitive($dbgActualValue), ]; if ($dbgParamName !== null) { $ctx = array_merge(['parameter name' => $dbgParamName], $ctx); } ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->includeStackTrace()->log('Actual type does not match the expected type', $ctx); return false; } /** * @param mixed $actualValue * @param ?string $dbgParamName * * @return bool */ public function verifyIsString($actualValue, ?string $dbgParamName = null): bool { return $this->verifyType(is_string($actualValue), 'string', $actualValue, $dbgParamName); } /** * @param mixed $actualValue * @param ?string $dbgParamName * * @return bool */ public function verifyIsInt($actualValue, ?string $dbgParamName = null): bool { return $this->verifyType(is_int($actualValue), 'int', $actualValue, $dbgParamName); } /** * @param mixed $actualValue * @param ?string $dbgParamName * * @return bool */ public function verifyIsBool($actualValue, ?string $dbgParamName = null): bool { return $this->verifyType(is_bool($actualValue), 'bool', $actualValue, $dbgParamName); } /** * @param mixed $actualValue * @param ?string $dbgParamName * * @return bool */ public function verifyIsArray($actualValue, ?string $dbgParamName = null): bool { return $this->verifyType(is_array($actualValue), 'array', $actualValue, $dbgParamName); } /** * @param mixed $actualValue * @param ?string $dbgParamName * * @return bool */ public function verifyIsObject($actualValue, ?string $dbgParamName = null): bool { return $this->verifyType(is_object($actualValue), 'object', $actualValue, $dbgParamName); } /** * @param class-string $expectedClass * @param mixed $actualValue * @param ?string $dbgParamName * * @return bool */ public function verifyInstanceOf(string $expectedClass, $actualValue, ?string $dbgParamName = null): bool { if (!$this->verifyIsObject($actualValue, $dbgParamName)) { return false; } if (!($actualValue instanceof $expectedClass)) { $ctx = [ 'expected class' => $expectedClass, 'actual type' => DbgUtil::getType($actualValue), 'actual value' => $this->logger->possiblySecuritySensitive($actualValue), ]; if ($dbgParamName !== null) { $ctx = array_merge(['parameter name' => $dbgParamName], $ctx); } ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Actual value is not an instance the expected class', $ctx); return false; } return true; } /** * @param mixed $actualValue * @param bool $shouldCheckSyntaxOnly * @param ?string $dbgParamName * * @return bool */ public function verifyIsCallable($actualValue, bool $shouldCheckSyntaxOnly, ?string $dbgParamName = null): bool { $isCallable = is_callable($actualValue, $shouldCheckSyntaxOnly); return $this->verifyType($isCallable, 'callable', $actualValue, $dbgParamName); } /** * @param int $expectedMinArgsCount * @param mixed[] $interceptedCallArgs * * @return bool */ public function verifyMinArgsCount(int $expectedMinArgsCount, array $interceptedCallArgs): bool { if (count($interceptedCallArgs) >= $expectedMinArgsCount) { return true; } ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Actual number of arguments is less than expected', [ 'expected minimal number of arguments' => $expectedMinArgsCount, 'actual number of arguments' => count($interceptedCallArgs), 'actual arguments' => $this->logger->possiblySecuritySensitive($interceptedCallArgs), ] ); return false; } /** * @param int $expectedArgsCount * @param mixed[] $interceptedCallArgs * * @return bool */ public function verifyExactArgsCount(int $expectedArgsCount, array $interceptedCallArgs): bool { if (count($interceptedCallArgs) === $expectedArgsCount) { return true; } ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Actual number of arguments does not equal the expected number', [ 'expected number of arguments' => $expectedArgsCount, 'actual number of arguments' => count($interceptedCallArgs), 'actual arguments' => $this->logger->possiblySecuritySensitive($interceptedCallArgs), ] ); return false; } }