agent/php/ElasticApm/Impl/Util/StackTraceUtil.php (297 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\Util;
use Elastic\Apm\Impl\Log\LogCategory;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Log\LoggerFactory;
use Elastic\Apm\Impl\StackTraceFrame;
use Throwable;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class StackTraceUtil
{
public const FILE_KEY = 'file';
public const LINE_KEY = 'line';
public const FUNCTION_KEY = 'function';
public const CLASS_KEY = 'class';
public const TYPE_KEY = 'type';
public const FUNCTION_IS_STATIC_METHOD_TYPE_VALUE = '::';
public const FUNCTION_IS_METHOD_TYPE_VALUE = '->';
public const CLASS_AND_METHOD_SEPARATOR = '::';
public const THIS_OBJECT_KEY = 'object';
public const ARGS_KEY = 'args';
public const FILE_NAME_NOT_AVAILABLE_SUBSTITUTE = 'FILE NAME N/A';
public const LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE = 0;
private const ELASTIC_APM_FQ_NAME_PREFIX = 'Elastic\\Apm\\';
private const ELASTIC_APM_INTERNAL_FUNCTION_NAME_PREFIX = 'elastic_apm_';
/** @var LoggerFactory */
private $loggerFactory;
/** @var Logger */
private $logger;
/** @var string */
private $namePrefixForFramesToHide;
/** @var string */
private $namePrefixForInternalFramesToHide;
public function __construct(
LoggerFactory $loggerFactory,
string $namePrefixForFramesToHide = self::ELASTIC_APM_FQ_NAME_PREFIX,
string $namePrefixForInternalFramesToHide = self::ELASTIC_APM_INTERNAL_FUNCTION_NAME_PREFIX
) {
$this->loggerFactory = $loggerFactory;
$this->logger = $this->loggerFactory->loggerForClass(LogCategory::INFRASTRUCTURE, __NAMESPACE__, __CLASS__, __FILE__);
$this->namePrefixForFramesToHide = $namePrefixForFramesToHide;
$this->namePrefixForInternalFramesToHide = $namePrefixForInternalFramesToHide;
}
/**
* @param int $offset
* @param ?positive-int $maxNumberOfFrames
*
* @return StackTraceFrame[]
*
* @phpstan-param 0|positive-int $offset
*/
public function captureInApmFormat(int $offset, ?int $maxNumberOfFrames): array
{
$phpFormatFrames = debug_backtrace(/* options */ DEBUG_BACKTRACE_IGNORE_ARGS, /* limit */ $maxNumberOfFrames === null ? 0 : ($offset + $maxNumberOfFrames));
return $this->convertPhpToApmFormat(IterableUtil::arraySuffix($phpFormatFrames, $offset), $maxNumberOfFrames);
}
/**
* @param iterable<array<string, mixed>> $phpFormatFrames
* @param ?positive-int $maxNumberOfFrames
*
* @return StackTraceFrame[]
*/
public function convertPhpToApmFormat(iterable $phpFormatFrames, ?int $maxNumberOfFrames): array
{
$allClassicFormatFrames = $this->convertPhpToClassicFormat(
null /* <- prevPhpFormatFrame */,
$phpFormatFrames,
$maxNumberOfFrames,
false /* keepElasticApmFrames */,
false /* $includeArgs */,
false /* $includeThisObj */
);
return self::convertClassicToApmFormat($allClassicFormatFrames, $maxNumberOfFrames);
}
/**
* @param int $offset
* @param ?positive-int $maxNumberOfFrames
*
* @return ClassicFormatStackTraceFrame[]
*
* @phpstan-param 0|positive-int $offset
*/
public function captureInClassicFormatExcludeElasticApm(int $offset = 0, ?int $maxNumberOfFrames = null): array
{
return $this->captureInClassicFormat($offset + 1, $maxNumberOfFrames, /* keepElasticApmFrames */ false);
}
/**
* @param int $offset
* @param ?positive-int $maxNumberOfFrames
* @param bool $keepElasticApmFrames
* @param bool $includeArgs
* @param bool $includeThisObj
*
* @return ClassicFormatStackTraceFrame[]
*
* @phpstan-param 0|positive-int $offset
*/
public function captureInClassicFormat(int $offset = 0, ?int $maxNumberOfFrames = null, bool $keepElasticApmFrames = true, bool $includeArgs = false, bool $includeThisObj = false): array
{
$options = ($includeArgs ? 0 : DEBUG_BACKTRACE_IGNORE_ARGS) | ($includeThisObj ? DEBUG_BACKTRACE_PROVIDE_OBJECT : 0);
return $this->convertCaptureToClassicFormat(
// If there is non-null $maxNumberOfFrames we need to capture one more frame in PHP format
debug_backtrace($options, /* limit */ $maxNumberOfFrames === null ? 0 : ($offset + $maxNumberOfFrames + 1)),
// $offset + 1 to exclude the frame for the current method (captureInClassicFormat) call
$offset + 1,
$maxNumberOfFrames,
$keepElasticApmFrames,
$includeArgs,
$includeThisObj
);
}
/**
* @param array<array<mixed>> $phpFormatFrames
* @param int $offset
* @param ?positive-int $maxNumberOfFrames
* @param bool $keepElasticApmFrames
* @param bool $includeArgs
* @param bool $includeThisObj
*
* @return ClassicFormatStackTraceFrame[]
*
* @phpstan-param 0|positive-int $offset
*/
public function convertCaptureToClassicFormat(array $phpFormatFrames, int $offset, ?int $maxNumberOfFrames, bool $keepElasticApmFrames, bool $includeArgs, bool $includeThisObj): array
{
if ($offset >= count($phpFormatFrames)) {
return [];
}
return $this->convertPhpToClassicFormat(
$offset === 0 ? null : $phpFormatFrames[$offset - 1] /* <- prevPhpFormatFrame */,
$offset === 0 ? $phpFormatFrames : IterableUtil::arraySuffix($phpFormatFrames, $offset),
$maxNumberOfFrames,
$keepElasticApmFrames,
$includeArgs,
$includeThisObj
);
}
/**
* @param ?array<mixed> $prevPhpFormatFrame
* @param iterable<array<mixed>> $phpFormatFrames
* @param ?positive-int $maxNumberOfFrames
* @param bool $keepElasticApmFrames
* @param bool $includeArgs
* @param bool $includeThisObj
*
* @return ClassicFormatStackTraceFrame[]
*/
public function convertPhpToClassicFormat(
?array $prevPhpFormatFrame,
iterable $phpFormatFrames,
?int $maxNumberOfFrames,
bool $keepElasticApmFrames,
bool $includeArgs,
bool $includeThisObj
): array {
$allClassicFormatFrames = [];
$prevInFrame = $prevPhpFormatFrame;
foreach ($phpFormatFrames as $currentInFrame) {
$outFrame = new ClassicFormatStackTraceFrame();
$isOutFrameEmpty = true;
if ($prevInFrame !== null && $this->hasLocationPropertiesInPhpFormat($prevInFrame)) {
$this->copyLocationPropertiesFromPhpToClassicFormat($prevInFrame, $outFrame);
$isOutFrameEmpty = false;
}
if ($this->hasNonLocationPropertiesInPhpFormat($currentInFrame)) {
$this->copyNonLocationPropertiesFromPhpToClassicFormat($currentInFrame, $includeArgs, $includeThisObj, $outFrame);
$isOutFrameEmpty = false;
}
if (!$isOutFrameEmpty) {
$allClassicFormatFrames[] = $outFrame;
}
$prevInFrame = $currentInFrame;
}
if ($prevInFrame !== null && $this->hasLocationPropertiesInPhpFormat($prevInFrame)) {
$outFrame = new ClassicFormatStackTraceFrame();
$this->copyLocationPropertiesFromPhpToClassicFormat($prevInFrame, $outFrame);
$allClassicFormatFrames[] = $outFrame;
}
return $keepElasticApmFrames
? ($maxNumberOfFrames === null ? $allClassicFormatFrames : array_slice($allClassicFormatFrames, /* offset */ 0, $maxNumberOfFrames))
: $this->excludeCodeToHide($allClassicFormatFrames, $maxNumberOfFrames);
}
/**
* @param ClassicFormatStackTraceFrame[] $inFrames
* @param ?positive-int $maxNumberOfFrames
*
* @return ClassicFormatStackTraceFrame[]
*/
private function excludeCodeToHide(array $inFrames, ?int $maxNumberOfFrames): array
{
$outFrames = [];
/** @var ?int $bufferedFromIndex */
$bufferedFromIndex = null;
foreach (RangeUtil::generateUpTo(count($inFrames)) as $currentInFrameIndex) {
$currentInFrame = $inFrames[$currentInFrameIndex];
if (self::isTrampolineCall($currentInFrame)) {
if ($bufferedFromIndex === null) {
$bufferedFromIndex = $currentInFrameIndex;
}
continue;
}
if ($this->isCallToCodeToHide($currentInFrame)) {
$bufferedFromIndex = null;
continue;
}
for ($index = $bufferedFromIndex ?? $currentInFrameIndex; $index <= $currentInFrameIndex; ++$index) {
self::addToOutputFrames($inFrames[$index], $maxNumberOfFrames, /* ref */ $outFrames);
}
$bufferedFromIndex = null;
}
return $outFrames;
}
public static function buildApmFormatFunctionForClassMethod(?string $classicName, ?bool $isStaticMethod, ?string $methodName): ?string
{
if ($methodName === null) {
return null;
}
if ($classicName === null) {
return $methodName;
}
return $classicName . StackTraceUtil::CLASS_AND_METHOD_SEPARATOR . $methodName;
}
private static function isTrampolineCall(ClassicFormatStackTraceFrame $frame): bool
{
return $frame->class === null && $frame->isStaticMethod === null && ($frame->function === 'call_user_func' || $frame->function === 'call_user_func_array');
}
private function isCallToCodeToHide(ClassicFormatStackTraceFrame $frame): bool
{
return ($frame->class !== null && TextUtil::isPrefixOf($this->namePrefixForFramesToHide, $frame->class))
|| ($frame->function !== null && TextUtil::isPrefixOf($this->namePrefixForFramesToHide, $frame->function))
|| ($frame->function !== null && $frame->file === null && TextUtil::isPrefixOf($this->namePrefixForInternalFramesToHide, $frame->function));
}
/**
* @param array<string, mixed> $frame
*
* @return ?bool
*/
private function isStaticMethodInPhpFormat(array $frame): ?bool
{
if (($funcType = self::getNullableStringValue(StackTraceUtil::TYPE_KEY, $frame)) === null) {
return null;
}
switch ($funcType) {
case StackTraceUtil::FUNCTION_IS_STATIC_METHOD_TYPE_VALUE:
return true;
case StackTraceUtil::FUNCTION_IS_METHOD_TYPE_VALUE:
return false;
default:
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Unexpected `' . StackTraceUtil::TYPE_KEY . '\' value', ['type' => $funcType]);
return null;
}
}
/**
* @param string $key
* @param array<string, mixed> $phpFormatFormatFrame
*
* @return ?string
*/
private function getNullableStringValue(string $key, array $phpFormatFormatFrame): ?string
{
/** @var ?string $value */
$value = $this->getNullableValue($key, 'is_string', 'string', $phpFormatFormatFrame);
return $value;
}
/**
* @param string $key
* @param array<string, mixed> $phpFormatFormatFrame
*
* @return ?int
*
* @noinspection PhpSameParameterValueInspection
*/
private function getNullableIntValue(string $key, array $phpFormatFormatFrame): ?int
{
/** @var ?int $value */
$value = $this->getNullableValue($key, 'is_int', 'int', $phpFormatFormatFrame);
return $value;
}
/**
* @param string $key
* @param array<string, mixed> $phpFormatFormatFrame
*
* @return ?object
*
* @noinspection PhpSameParameterValueInspection
*/
private function getNullableObjectValue(string $key, array $phpFormatFormatFrame): ?object
{
/** @var ?object $value */
$value = $this->getNullableValue($key, 'is_object', 'object', $phpFormatFormatFrame);
return $value;
}
/**
* @param string $key
* @param array<string, mixed> $phpFormatFormatFrame
*
* @return null|mixed[]
*
* @noinspection PhpSameParameterValueInspection
*/
private function getNullableArrayValue(string $key, array $phpFormatFormatFrame): ?array
{
/** @var ?array<mixed> $value */
$value = $this->getNullableValue($key, 'is_array', 'array', $phpFormatFormatFrame);
return $value;
}
/**
* @param string $key
* @param callable(mixed): bool $isValueTypeFunc
* @param string $dbgExpectedType
* @param array<string, mixed> $phpFormatFormatFrame
*
* @return mixed
*/
private function getNullableValue(string $key, callable $isValueTypeFunc, string $dbgExpectedType, array $phpFormatFormatFrame)
{
if (!array_key_exists($key, $phpFormatFormatFrame)) {
return null;
}
$value = $phpFormatFormatFrame[$key];
if ($value === null) {
return null;
}
if (!$isValueTypeFunc($value)) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Unexpected type for value under key (expected ' . $dbgExpectedType . ')',
['$key' => $key, 'value type' => DbgUtil::getType($value), 'value' => $value]
);
return null;
}
return $value;
}
/**
* @param array<string, mixed> $frame
*/
private function hasNonLocationPropertiesInPhpFormat(array $frame): bool
{
return $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $frame) !== null;
}
/**
* @param array<string, mixed> $frame
*/
private function hasLocationPropertiesInPhpFormat(array $frame): bool
{
return $this->getNullableStringValue(StackTraceUtil::FILE_KEY, $frame) !== null;
}
private static function hasLocationPropertiesInClassicFormat(ClassicFormatStackTraceFrame $frame): bool
{
return $frame->file !== null;
}
/**
* @param array<string, mixed> $srcFrame
* @param ClassicFormatStackTraceFrame $dstFrame
*/
private function copyLocationPropertiesFromPhpToClassicFormat(array $srcFrame, ClassicFormatStackTraceFrame $dstFrame): void
{
$dstFrame->file = $this->getNullableStringValue(StackTraceUtil::FILE_KEY, $srcFrame);
$dstFrame->line = $this->getNullableIntValue(StackTraceUtil::LINE_KEY, $srcFrame);
}
/**
* @param array<string, mixed> $srcFrame
* @param bool $includeArgs
* @param bool $includeThisObj
* @param ClassicFormatStackTraceFrame $dstFrame
*/
private function copyNonLocationPropertiesFromPhpToClassicFormat(array $srcFrame, bool $includeArgs, bool $includeThisObj, ClassicFormatStackTraceFrame $dstFrame): void
{
$dstFrame->class = $this->getNullableStringValue(StackTraceUtil::CLASS_KEY, $srcFrame);
$dstFrame->function = $this->getNullableStringValue(StackTraceUtil::FUNCTION_KEY, $srcFrame);
$dstFrame->isStaticMethod = $this->isStaticMethodInPhpFormat($srcFrame);
if ($includeThisObj) {
$dstFrame->thisObj = $this->getNullableObjectValue(StackTraceUtil::THIS_OBJECT_KEY, $srcFrame);
}
if ($includeArgs) {
$dstFrame->args = $this->getNullableArrayValue(StackTraceUtil::ARGS_KEY, $srcFrame);
}
}
/**
* @template TOutputFrame
*
* @param TOutputFrame $frameToAdd
* @param ?int $maxNumberOfFrames
* @param TOutputFrame[] &$outputFrames
*
* @return bool
*
* @phpstan-param null|positive-int $maxNumberOfFrames
*/
private static function addToOutputFrames($frameToAdd, ?int $maxNumberOfFrames, /* ref */ array &$outputFrames): bool
{
$outputFrames[] = $frameToAdd;
return (count($outputFrames) !== $maxNumberOfFrames);
}
/**
* @param Throwable $throwable
* @param ?positive-int $maxNumberOfFrames
*
* @return StackTraceFrame[]
*/
public function convertThrowableTraceToApmFormat(Throwable $throwable, ?int $maxNumberOfFrames): array
{
$frameForThrowLocation = [StackTraceUtil::FILE_KEY => $throwable->getFile(), StackTraceUtil::LINE_KEY => $throwable->getLine()];
return $this->convertPhpToApmFormat(IterableUtil::prepend($frameForThrowLocation, $throwable->getTrace()), $maxNumberOfFrames);
}
// TODO: Sergey Kleyman: REMOVE:
// /**
// * @param iterable<ClassicFormatStackTraceFrame> $inFrames
// * @param ?positive-int $maxNumberOfFrames
// *
// * @return StackTraceFrame[]
// */
// private static function convertClassicToApmFormat(iterable $inFrames, ?int $maxNumberOfFrames): array
// {
// /** @var StackTraceFrame[] $outFrames */
// $outFrames = [];
//
// /** @var ?ClassicFormatStackTraceFrame $prevInFrame */
// $prevInFrame = null;
// foreach ($inFrames as $currentInFrame) {
// if ($currentInFrame->file === null) {
// $isOutFrameEmpty = true;
// $outFrame = new StackTraceFrame(self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE);
// } else {
// $isOutFrameEmpty = false;
// $outFrame = new StackTraceFrame($currentInFrame->file, $currentInFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE);
// }
// if ($prevInFrame !== null && $prevInFrame->function !== null) {
// $isOutFrameEmpty = false;
// $outFrame->function = self::buildApmFormatFunctionForClassMethod($prevInFrame->class, $prevInFrame->isStaticMethod, $prevInFrame->function);
// }
// if (!$isOutFrameEmpty && !self::addToOutputFrames($outFrame, $maxNumberOfFrames, /* ref */ $outFrames)) {
// break;
// }
// $prevInFrame = $currentInFrame;
// }
//
// return $outFrames;
// }
/**
* @param iterable<ClassicFormatStackTraceFrame> $inputFrames
* @param ?positive-int $maxNumberOfFrames
*
* @return StackTraceFrame[]
*/
public static function convertClassicToApmFormat(iterable $inputFrames, ?int $maxNumberOfFrames): array
{
$outputFrames = [];
/** @var ?ClassicFormatStackTraceFrame $prevInputFrame */
$prevInputFrame = null;
$exitedEarly = false;
foreach ($inputFrames as $currentInputFrame) {
if ($prevInputFrame === null) {
if (self::hasLocationPropertiesInClassicFormat($currentInputFrame)) {
$outputFrame = new StackTraceFrame($currentInputFrame->file ?? self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, $currentInputFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE);
if (!self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames)) {
$exitedEarly = true;
break;
}
}
$prevInputFrame = $currentInputFrame;
continue;
}
$outputFrame = new StackTraceFrame($currentInputFrame->file ?? self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, $currentInputFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE);
$outputFrame->function = StackTraceUtil::buildApmFormatFunctionForClassMethod($prevInputFrame->class, $prevInputFrame->isStaticMethod, $prevInputFrame->function);
if (!self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames)) {
$exitedEarly = true;
break;
}
$prevInputFrame = $currentInputFrame;
}
if (!$exitedEarly && $prevInputFrame !== null && $prevInputFrame->function !== null) {
$outputFrame = new StackTraceFrame(
self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE,
self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE,
StackTraceUtil::buildApmFormatFunctionForClassMethod($prevInputFrame->class, $prevInputFrame->isStaticMethod, $prevInputFrame->function)
);
self::addToOutputFrames($outputFrame, $maxNumberOfFrames, /* ref */ $outputFrames);
}
return $outputFrames;
}
/**
* @param int $stackTraceLimit
*
* @return ?int
* @phpstan-return null|0|positive-int
*/
public static function convertLimitConfigToMaxNumberOfFrames(int $stackTraceLimit): ?int
{
/**
* stack_trace_limit
* 0 - stack trace collection should be disabled
* any positive value - the value is the maximum number of frames to collect
* any negative value - all frames should be collected
*/
return $stackTraceLimit < 0 ? null : $stackTraceLimit;
}
}