agent/php/ElasticApm/Impl/InferredSpansBuilder.php (267 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;
use Elastic\Apm\Impl\Config\OptionNames;
use Elastic\Apm\Impl\Log\LogCategory;
use Elastic\Apm\Impl\Log\LoggableInterface;
use Elastic\Apm\Impl\Log\LoggableTrait;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Log\LogStreamInterface;
use Elastic\Apm\Impl\Util\Assert;
use Elastic\Apm\Impl\Util\ClassicFormatStackTraceFrame;
use Elastic\Apm\Impl\Util\StackTraceUtil;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class InferredSpansBuilder implements LoggableInterface
{
use LoggableTrait;
/** @var float In milliseconds */
private $minDurationInMilliseconds;
/** @var InferredSpanFrame[] */
private $openFramesReverseOrder = [];
/** @var Tracer */
private $tracer;
/** @var Transaction */
private $transaction;
/** @var Logger */
private $logger;
public function __construct(Tracer $tracer)
{
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that(
$tracer->getCurrentTransaction() instanceof Transaction
&& $tracer->getCurrentTransaction()->isSampled()
&& $tracer->getCurrentExecutionSegment() === $tracer->getCurrentTransaction()
)
&& $assertProxy->withContext(
'$tracer->getCurrentTransaction() instanceof Transaction
&& $tracer->getCurrentTransaction()->isSampled()
&& $tracer->getCurrentExecutionSegment() === $tracer->getCurrentTransaction()',
['$tracer->getCurrentTransaction()' => $tracer->getCurrentTransaction()]
);
$this->tracer = $tracer;
/**
* We asserted that $tracer->getCurrentTransaction() is instance of Transaction above
*
* @phpstan-ignore-next-line
*/
$this->transaction = $tracer->getCurrentTransaction();
$this->minDurationInMilliseconds = $tracer->getConfig()->profilingInferredSpansMinDurationInMilliseconds();
$this->logger = $tracer->loggerFactory()
->loggerForClass(LogCategory::INFERRED_SPANS, __NAMESPACE__, __CLASS__, __FILE__)
->addContext('this', $this);
}
/**
* @param int $offset
*
* @return ClassicFormatStackTraceFrame[]
*
* @phpstan-param 0|positive-int $offset
*/
public function captureStackTrace(int $offset): array
{
return $this->tracer->stackTraceUtil()->captureInClassicFormatExcludeElasticApm($offset + 1);
}
/**
* @param ClassicFormatStackTraceFrame[] $newStackTrace
* @param int $bottomNotExtendedFrameIndex
* @param ?ClassicFormatStackTraceFrame $frameCopy
* @param ?int $frameCopyIndex
*
* @return void
*/
private function extendOpenFrames(
array $newStackTrace,
int &$bottomNotExtendedFrameIndex,
?ClassicFormatStackTraceFrame &$frameCopy,
?int &$frameCopyIndex
): void {
$openFramesCount = count($this->openFramesReverseOrder);
$newStackTraceCount = count($newStackTrace);
/** @var ?ClassicFormatStackTraceFrame $newStackTraceParentFrame */
$newStackTraceParentFrame = null;
/** @var ?InferredSpanFrame $openParentFrame */
$openParentFrame = null;
for ($i = 0; $i != $openFramesCount && $i != $newStackTraceCount; ++$i) {
// If we are inside the same frame but at the different line we should not extend child frame
if (
$newStackTraceParentFrame !== null
&& $openParentFrame !== null
&& $openParentFrame->stackFrame->line !== $newStackTraceParentFrame->line
) {
// We need to capture stack trace we update line below
$frameCopy = clone $openParentFrame->stackFrame;
$frameCopyIndex = $i - 1;
$openParentFrame->stackFrame->line = $newStackTraceParentFrame->line;
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Cannot be extended because parent frames lines do not match',
[
'newStackTraceParentFrame' => $newStackTraceParentFrame,
'openParentFrame' => $openParentFrame,
'i' => $i,
'newStackTrace' => $newStackTrace,
]
);
break;
}
$openFrame = $this->openFramesReverseOrder[$i];
$newFrame = $newStackTrace[$newStackTraceCount - $i - 1];
if (!$openFrame->canBeExtendedWith($newFrame)) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Cannot be extended because frames do not match',
['newStackTrace' => $newStackTrace, 'i' => $i]
);
break;
}
$openParentFrame = $openFrame;
$newStackTraceParentFrame = $newFrame;
}
$bottomNotExtendedFrameIndex = $i;
}
/**
* @param int $forFrameIndex
* @param ?ClassicFormatStackTraceFrame $frameCopy
* @param ?int $frameCopyIndex
*
* @return iterable<ClassicFormatStackTraceFrame>
*/
private function buildStackTrace(int $forFrameIndex, ?ClassicFormatStackTraceFrame $frameCopy, ?int $frameCopyIndex): iterable
{
$openFramesCount = count($this->openFramesReverseOrder);
for ($i = $forFrameIndex; $i >= 0 && $i < $openFramesCount; --$i) {
$useFrameCopy = $frameCopy !== null && $frameCopyIndex === $i; // @phpstan-ignore-line
yield $useFrameCopy ? $frameCopy : $this->openFramesReverseOrder[$i]->stackFrame;
}
}
/**
* @param int $openFrameIndex
* @param string $parentId
* @param ?ClassicFormatStackTraceFrame $frameCopy
* @param ?int $frameCopyIndex
*
* @return void
*/
private function sendFrameAsSpan(int $openFrameIndex, string $parentId, ?ClassicFormatStackTraceFrame $frameCopy, ?int $frameCopyIndex): void
{
$frame = $this->openFramesReverseOrder[$openFrameIndex];
$stackTrace = null;
/** @var float $spanDurationInMilliseconds */
$spanDurationInMilliseconds = $frame->duration;
if (
$this->transaction->shouldCollectStackTraceForSpanDuration($spanDurationInMilliseconds)
&& (($maxNumberOfFrames = StackTraceUtil::convertLimitConfigToMaxNumberOfFrames($this->transaction->getStackTraceLimitConfig())) !== 0)
) {
$stackTrace = $this->tracer->stackTraceUtil()->convertClassicToApmFormat($this->buildStackTrace($openFrameIndex, $frameCopy, $frameCopyIndex), $maxNumberOfFrames);
}
$frame->prepareForSerialization($this->transaction, $parentId, $stackTrace);
$this->tracer->sendSpanToApmServer($frame);
}
private function findParentId(int $childFrameIndex, int $bottomNotExtendedIndex): string
{
$loggerTrace = $this->logger->ifTraceLevelEnabledNoLine(__FUNCTION__);
if ($bottomNotExtendedIndex === 0) {
$loggerTrace && $loggerTrace->log(
__LINE__,
'Using current execution segment ID as parent ID'
. ' because there are no open frames below the one to be sent',
['current execution segment ID' => $this->tracer->getCurrentExecutionSegment()->getId()]
);
return $this->tracer->getCurrentExecutionSegment()->getId();
}
$parentFrame = $this->openFramesReverseOrder[$bottomNotExtendedIndex - 1];
if ($parentFrame->isAllocatedToBeSent() || $this->transaction->tryToAllocateStartedSpan()) {
$parentId = $parentFrame->markAsAllocatedToBeSent();
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
"Using parent frame",
[
'parentFrame' => $parentFrame,
'childFrameIndex' => $childFrameIndex,
'bottomNotExtendedIndex' => $bottomNotExtendedIndex,
]
);
return $parentId;
}
$loggerTrace && $loggerTrace->log(
__LINE__,
'Using current execution segment ID as parent ID'
. ' because the current transaction reached configured started spans limit',
['current execution segment ID' => $this->tracer->getCurrentExecutionSegment()->getId()]
);
return $this->tracer->getCurrentExecutionSegment()->getId();
}
private function tryToAllocateToBeSent(InferredSpanFrame $frame, ?int $toBeSentDescendantIndex): bool
{
$loggerTrace = $this->logger->ifTraceLevelEnabledNoLine(__FUNCTION__);
if ($frame->duration < $this->minDurationInMilliseconds) {
$loggerTrace && $loggerTrace->log(
__LINE__,
"Frame will NOT be sent as inferred span because its duration is below configured minimum",
[
'frame' => $frame,
OptionNames::PROFILING_INFERRED_SPANS_MIN_DURATION . ' (milliseconds)'
=> $this->minDurationInMilliseconds,
]
);
return false;
}
if ($toBeSentDescendantIndex !== null) {
$toBeSentDescendant = $this->openFramesReverseOrder[$toBeSentDescendantIndex];
if ($frame->duration <= $toBeSentDescendant->duration) {
$loggerTrace && $loggerTrace->log(
__LINE__,
"Frame will NOT be sent as inferred span"
. " because it's duration is not greater than nearest sent descendant",
['frame' => $frame, 'toBeSentDescendant' => $toBeSentDescendant]
);
return false;
}
}
if (!$this->transaction->tryToAllocateStartedSpan()) {
$loggerTrace && $loggerTrace->log(
__LINE__,
"Frame will NOT be sent as inferred span"
. " because the current transaction reached configured started spans limit",
['frame' => $frame]
);
return false;
}
$loggerTrace && $loggerTrace->log(
__LINE__,
"Frame will be sent as inferred span because it satisfies all the requirements",
['frame' => $frame]
);
$frame->markAsAllocatedToBeSent();
return true;
}
/**
* @param int $bottomNotExtendedFrameIndex
* @param ?ClassicFormatStackTraceFrame $frameCopy
* @param ?int $frameCopyIndex
* @param float $systemClockNow
* @param float $monotonicClockNow
*
* @return void
*/
private function processNotExtendedOpenFrames(
int $bottomNotExtendedFrameIndex,
?ClassicFormatStackTraceFrame $frameCopy,
?int $frameCopyIndex,
float $systemClockNow,
float $monotonicClockNow
): void {
$openFramesCount = count($this->openFramesReverseOrder);
/** @var ?int $toBeSentDescendantIndex */
$toBeSentDescendantIndex = null;
for ($i = $openFramesCount - 1; $i >= $bottomNotExtendedFrameIndex; --$i) {
$frame = $this->openFramesReverseOrder[$i];
$frame->setEndTime($systemClockNow, $monotonicClockNow, $this->tracer->loggerFactory());
if ($frame->isAllocatedToBeSent() || $this->tryToAllocateToBeSent($frame, $toBeSentDescendantIndex)) {
if ($toBeSentDescendantIndex !== null) {
/** @var string $parentId */
$parentId = $frame->id;
$this->sendFrameAsSpan($toBeSentDescendantIndex, $parentId, $frameCopy, $frameCopyIndex);
}
$toBeSentDescendantIndex = $i;
}
}
if ($toBeSentDescendantIndex !== null) {
$parentId = $this->findParentId($toBeSentDescendantIndex, $bottomNotExtendedFrameIndex);
$this->sendFrameAsSpan($toBeSentDescendantIndex, $parentId, $frameCopy, $frameCopyIndex);
}
$this->openFramesReverseOrder = array_slice($this->openFramesReverseOrder, 0, $bottomNotExtendedFrameIndex);
}
/**
* @param ClassicFormatStackTraceFrame[] $newStackTrace
*/
public function addStackTrace(array $newStackTrace, int $durationMs = 0): void
{
$durationOffsetUs = $durationMs * 1000;
$monotonicClockNow = $this->tracer->getClock()->getMonotonicClockCurrentTime() - $durationOffsetUs;
$systemClockNow = $this->tracer->getClock()->getSystemClockCurrentTime() - $durationOffsetUs;
($loggerTrace = $this->logger->ifTraceLevelEnabledNoLine(__FUNCTION__))
&& $loggerTrace->log(
__LINE__,
'Entered',
['newStackTrace' => $newStackTrace, 'systemClockNow' => $systemClockNow, 'monotonicClockNow' => $monotonicClockNow]
);
$openFramesCount = count($this->openFramesReverseOrder);
$bottomNotExtendedFrameIndex = $openFramesCount;
/** @var ?ClassicFormatStackTraceFrame $frameCopy */
$frameCopy = null;
/** @var ?int $frameCopyIndex */
$frameCopyIndex = null;
$this->extendOpenFrames($newStackTrace, /* out */ $bottomNotExtendedFrameIndex, /* out */ $frameCopy, /* out */ $frameCopyIndex);
if ($bottomNotExtendedFrameIndex === $openFramesCount) {
$loggerTrace && $loggerTrace->log(__LINE__, 'All open frames were extended');
} else {
$loggerTrace && $loggerTrace->log(
__LINE__,
'Not all open frames were extended - some will become inferred spans',
['not extended frames' => array_slice($this->openFramesReverseOrder, $bottomNotExtendedFrameIndex)]
);
$this->processNotExtendedOpenFrames($bottomNotExtendedFrameIndex, $frameCopy, $frameCopyIndex, $systemClockNow, $monotonicClockNow);
}
$newStackTraceCount = count($newStackTrace);
for ($i = $bottomNotExtendedFrameIndex; $i != $newStackTraceCount; ++$i) {
$newFrame = $newStackTrace[$newStackTraceCount - $i - 1];
$this->openFramesReverseOrder[] = new InferredSpanFrame($systemClockNow, $monotonicClockNow, $newFrame);
$loggerTrace && $loggerTrace->log(__LINE__, 'Appended new frame on top of open frames');
}
}
public function close(): void
{
$this->addStackTrace([]);
}
/**
* @return string[]
*/
protected static function propertiesExcludedFromLog(): array
{
return array_merge(self::defaultPropertiesExcludedFromLog(), ['tracer', 'transaction']);
}
/** @inheritDoc */
public function toLog(LogStreamInterface $stream): void
{
$transactionId = $this->transaction === null ? null : $this->transaction->getId();
$this->toLogLoggableTraitImpl(
$stream,
/* customPropValues */
['transactionId' => $transactionId]
);
}
}