agent/php/ElasticApm/Impl/ExecutionSegment.php (420 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 Closure;
use Elastic\Apm\CustomErrorData;
use Elastic\Apm\DistributedTracingData;
use Elastic\Apm\ExecutionSegmentInterface;
use Elastic\Apm\Impl\BackendComm\SerializationUtil;
use Elastic\Apm\Impl\BreakdownMetrics\SelfTimeTracker as BreakdownMetricsSelfTimeTracker;
use Elastic\Apm\Impl\Log\Level as LogLevel;
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\LoggerFactory;
use Elastic\Apm\Impl\Util\ClassNameUtil;
use Elastic\Apm\Impl\Util\IdGenerator;
use Elastic\Apm\Impl\Util\TextUtil;
use Elastic\Apm\Impl\Util\TimeUtil;
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
*/
abstract class ExecutionSegment implements ExecutionSegmentInterface, SerializableDataInterface, LoggableInterface
{
use LoggableTrait;
/** @var string */
public $name;
/** @var string */
public $type;
/** @var string */
public $id;
/** @var string */
public $traceId;
/** @var float UTC based and in microseconds since Unix epoch */
public $timestamp;
/** @var float In milliseconds with 3 decimal points */
public $duration;
/** @var ?string */
public $outcome = null;
/** @var ?Span */
private $pendingCompositeChild = null;
/** @var bool */
protected $wasPropogatedViaDistributedTracing = false;
/**
* @var ?float
*
* Sample rate applied to the monitored service at the time where this transaction/span was recorded.
* Allowed values are [0..1].
* A sample rate < 1 indicates that not all spans are recorded.
*
* @link https://github.com/elastic/apm-server/blob/v7.10.0/docs/spec/transactions/transaction.json#L26
* @link https://github.com/elastic/apm-server/blob/v7.10.0/docs/spec/spans/span.json#L45
*/
public $sampleRate = null;
/** @var bool */
protected $isDiscarded = false;
/** @var ?BreakdownMetricsSelfTimeTracker */
protected $breakdownMetricsSelfTimeTracker = null;
/** @var float */
private $diffStartTimeWithSystemClockOnBeginInMicroseconds;
/** @var float */
private $systemClockBeginTime;
/** @var float Monotonic time since some unspecified starting point, in microseconds */
private $monotonicBeginTime;
/** @var Logger */
private $logger;
/** @var bool */
private $isEnded = false;
protected function __construct(
Tracer $tracer,
?ExecutionSegment $parentExecutionSegment,
string $traceId,
string $name,
string $type,
?float $sampleRate,
?float $timestampArg = null
) {
$monotonicClockNow = $tracer->getClock()->getMonotonicClockCurrentTime();
$systemClockNow = $tracer->getClock()->getSystemClockCurrentTime();
$this->id = IdGenerator::generateId(Constants::EXECUTION_SEGMENT_ID_SIZE_IN_BYTES);
$this->systemClockBeginTime = $systemClockNow;
if ($timestampArg === null) {
$this->timestamp = $systemClockNow;
} elseif ($timestampArg <= $systemClockNow) {
$this->timestamp = $timestampArg;
} else {
$this->timestamp = $systemClockNow;
$localLogger = self::createLogger($tracer->loggerFactory());
($loggerProxy = $localLogger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Using systemClockNow for start time instead of timestampArg argument'
. ' because timestampArg argument is later (further into the future) than systemClockNow',
[
'systemClockNow' => $systemClockNow,
'timestampArg' => $timestampArg,
'timestampArg - systemClockNow (seconds)'
=> TimeUtil::microsecondsToSeconds($timestampArg - $systemClockNow),
'id' => $this->id,
'name' => $name,
'type' => $type,
]
);
}
$this->diffStartTimeWithSystemClockOnBeginInMicroseconds = TimeUtil::calcDurationInMicrosecondsClampNegativeToZero($this->timestamp, $systemClockNow);
$this->monotonicBeginTime = $monotonicClockNow;
$this->traceId = $traceId;
$this->setName($name);
$this->setType($type);
$this->sampleRate = $sampleRate;
if ($this->containingTransaction()->getConfig()->breakdownMetrics()) {
$this->breakdownMetricsSelfTimeTracker = new BreakdownMetricsSelfTimeTracker($monotonicClockNow);
if ($parentExecutionSegment !== null) {
/**
* If breakdownMetrics config is true then all transaction's spans
* breakdownMetricsSelfTimeTracker should not be null
*
* Local variable to workaround PHPStan not having a way to declare that
* $parentExecutionSegment->breakdownMetricsSelfTimeTracker is not null
*
* @var BreakdownMetricsSelfTimeTracker $parentBreakdownMetricsSelfTimeTracker
*/
$parentBreakdownMetricsSelfTimeTracker = $parentExecutionSegment->breakdownMetricsSelfTimeTracker;
$parentBreakdownMetricsSelfTimeTracker->onChildBegin($monotonicClockNow);
}
}
$this->logger = self::createLogger($tracer->loggerFactory())->addContext('this', $this);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Exiting...',
[
'systemClockNow' => $systemClockNow,
'timestampArg' => $timestampArg,
'systemClockNow - timestampArg (seconds)'
=> TimeUtil::microsecondsToSeconds($systemClockNow - $timestampArg),
]
);
}
protected static function createLogger(LoggerFactory $loggerFactory): Logger
{
return $loggerFactory->loggerForClass(LogCategory::PUBLIC_API, __NAMESPACE__, __CLASS__, __FILE__);
}
public function isSampled(): bool
{
return $this->containingTransaction()->isSampled();
}
/**
* @return Transaction
*/
abstract public function containingTransaction(): Transaction;
/**
* @return ExecutionSegment|null
*/
abstract public function parentExecutionSegment(): ?ExecutionSegment;
public function getName(): string
{
return $this->name;
}
public function getType(): string
{
return $this->type;
}
/**
* Begins a new span with this execution segment as the new span's parent,
* runs the provided callback as the new span and automatically ends the new span.
*
* @param string $name New span's name
* @param string $type New span's type
* @param Closure $callback Callback to execute as the new span
* @param string|null $subtype New span's subtype
* @param string|null $action New span's action
* @param float|null $timestamp Start time of the new span
* @param int $numberOfStackFramesToSkip
*
* @return mixed The return value of $callback
*
* @template T
* @phpstan-param Closure(SpanInterface $newSpan): T $callback
* @phpstan-return T
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*
* @see SpanInterface
*
* @noinspection PhpDocMissingThrowsInspection
*/
protected function captureChildSpanImpl(
string $name,
string $type,
Closure $callback,
?string $subtype,
?string $action,
?float $timestamp,
int $numberOfStackFramesToSkip
) {
$newSpan = $this->beginChildSpan(
$name,
$type,
$subtype,
$action,
$timestamp
);
try {
return $callback($newSpan);
} catch (Throwable $throwable) {
$newSpan->createErrorFromThrowable($throwable);
/** @noinspection PhpUnhandledExceptionInspection */
throw $throwable;
} finally {
// Since endSpanEx was not called directly it should not be kept in the stack trace
$newSpan->endSpanEx(/* numberOfStackFramesToSkip: */ $numberOfStackFramesToSkip + 1);
}
}
/**
* @param ErrorExceptionData $errorExceptionData
*
* @return ?string
*/
abstract public function dispatchCreateError(ErrorExceptionData $errorExceptionData): ?string;
/**
* @param ?CustomErrorData $customErrorData
* @param ?Throwable $throwable
* @param int $numberOfStackFramesToSkip
*
* @return ?string
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*/
private function createError(?CustomErrorData $customErrorData, ?Throwable $throwable, int $numberOfStackFramesToSkip): ?string
{
return $this->dispatchCreateError(ErrorExceptionData::build($this->containingTransaction()->tracer(), $customErrorData, /* phpErrorData */ null, $throwable, $numberOfStackFramesToSkip + 1));
}
/** @inheritDoc */
public function createErrorFromThrowable(Throwable $throwable): ?string
{
return $this->createError(/* customErrorData: */ null, $throwable, /* numberOfStackFramesToSkip */ 1);
}
/** @inheritDoc */
public function createCustomError(CustomErrorData $customErrorData): ?string
{
return $this->createError($customErrorData, /* throwable: */ null, /* numberOfStackFramesToSkip */ 1);
}
public function beforeMutating(): bool
{
if (!$this->isEnded) {
return false;
}
if ($this->isDiscarded) {
return true;
}
($loggerProxy = $this->logger->ifWarningLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->includeStackTrace()->log('A mutating method has been called on already ended event');
return true;
}
/** @inheritDoc */
public function isNoop(): bool
{
return false;
}
/** @inheritDoc */
public function getId(): string
{
return $this->id;
}
/** @inheritDoc */
public function getTimestamp(): float
{
return $this->timestamp;
}
/** @inheritDoc */
public function getTraceId(): string
{
return $this->traceId;
}
/** @inheritDoc */
public function setName(string $name): void
{
if ($this->beforeMutating()) {
return;
}
$this->name = Tracer::limitKeywordString($name);
}
/** @inheritDoc */
public function setType(string $type): void
{
if ($this->beforeMutating()) {
return;
}
$this->type = TextUtil::isEmptyString($type)
? Constants::EXECUTION_SEGMENT_TYPE_DEFAULT
: Tracer::limitKeywordString($type);
}
/** @inheritDoc */
public function getDistributedTracingData(): ?DistributedTracingData
{
return $this->getDistributedTracingDataInternal();
}
/**
* Returns distributed tracing data
*/
abstract public function getDistributedTracingDataInternal(): ?DistributedTracingDataInternal;
/** @inheritDoc */
public function injectDistributedTracingHeaders(Closure $headerInjector): void
{
/** @noinspection PhpDeprecationInspection */
$distTracingData = $this->getDistributedTracingData();
if ($distTracingData !== null) {
$distTracingData->injectHeaders($headerInjector);
}
}
public static function isValidOutcome(?string $outcome): bool
{
return $outcome === null
|| $outcome === Constants::OUTCOME_SUCCESS
|| $outcome === Constants::OUTCOME_FAILURE
|| $outcome === Constants::OUTCOME_UNKNOWN;
}
/** @inheritDoc */
public function setOutcome(?string $outcome): void
{
if (!self::isValidOutcome($outcome)) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Given outcome value is invalid', ['outcome' => $outcome]);
return;
}
$this->outcome = $outcome;
}
/** @inheritDoc */
public function getOutcome(): ?string
{
return $this->outcome;
}
/** @inheritDoc */
public function discard(): void
{
if ($this->beforeMutating()) {
return;
}
$this->isDiscarded = true;
$this->end();
}
public static function calcDurationInMicroseconds(
float $systemClockBeginTime,
float $monotonicBeginTime,
float $systemClockEndTime,
float $monotonicEndTime,
LoggerFactory $loggerFactory
): float {
$monotonicDurationInMicroseconds = TimeUtil::calcDurationInMicrosecondsClampNegativeToZero(
$monotonicBeginTime,
$monotonicEndTime
);
$systemClockDurationInMicroseconds = TimeUtil::calcDurationInMicrosecondsClampNegativeToZero(
$systemClockBeginTime,
$systemClockEndTime
);
/** @var ?Logger $logger */
$logger = null;
$logLevel = LogLevel::TRACE;
if ($loggerFactory->isEnabledForLevel($logLevel)) {
$logger = self::createLogger($loggerFactory);
$monotonicMinusSystemDurationInSeconds = TimeUtil::microsecondsToSeconds(
$systemClockDurationInMicroseconds - $monotonicDurationInMicroseconds
);
$logger->addAllContext(
[
'systemClockDurationInMicroseconds' => $systemClockDurationInMicroseconds,
'monotonicDurationInMicroseconds' => $monotonicDurationInMicroseconds,
'monotonicMinusSystemDurationInSeconds' => $monotonicMinusSystemDurationInSeconds,
'systemClockBeginTime' => $systemClockBeginTime,
'monotonicBeginTime' => $monotonicBeginTime,
'systemClockEndTime' => $systemClockEndTime,
'monotonicEndTime' => $monotonicEndTime,
]
);
}
if ($monotonicDurationInMicroseconds >= $systemClockDurationInMicroseconds) {
$durationInMicroseconds = $monotonicDurationInMicroseconds;
$logger && ($loggerProxy = $logger->ifLevelEnabled($logLevel, __LINE__, __FUNCTION__))
&& $loggerProxy->log('Using monotonic clock duration');
} else {
$durationInMicroseconds = $systemClockDurationInMicroseconds;
$logger && ($loggerProxy = $logger->ifLevelEnabled($logLevel, __LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Using system clock duration instead of monotonic clock duration'
. ' because system clock duration is larger'
);
}
return $durationInMicroseconds;
}
protected function endExecutionSegment(?float $durationArg = null): bool
{
if ($this->isDiscarded) {
$this->isEnded = true;
return false;
}
if ($this->beforeMutating()) {
return false;
}
$this->flushPendingCompositeChild();
$clock = $this->containingTransaction()->tracer()->getClock();
$monotonicEndTime = $clock->getMonotonicClockCurrentTime();
$systemClockEndTime = $clock->getSystemClockCurrentTime();
if ($durationArg === null) {
$durationAfterBeginInMicroseconds = self::calcDurationInMicroseconds(
$this->systemClockBeginTime,
$this->monotonicBeginTime,
$systemClockEndTime,
$monotonicEndTime,
$this->containingTransaction()->tracer()->loggerFactory()
);
$this->duration = TimeUtil::microsecondsToMilliseconds(
$this->diffStartTimeWithSystemClockOnBeginInMicroseconds + $durationAfterBeginInMicroseconds
);
} else {
$this->duration = $durationArg;
}
if ($this->breakdownMetricsSelfTimeTracker !== null && !$this->containingTransaction()->hasEnded()) {
$this->updateBreakdownMetricsOnEnd($monotonicEndTime);
}
$this->isEnded = true;
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(ClassNameUtil::fqToShort(get_class($this)) . ' ended', ['durationArg' => $durationArg]);
return true;
}
/**
* @param float $monotonicClockNow
*
* @return void
*/
abstract protected function updateBreakdownMetricsOnEnd(float $monotonicClockNow): void;
protected function doUpdateBreakdownMetricsOnEnd(
float $monotonicClockNow,
string $spanType,
?string $spanSubtype
): void {
/**
* @var BreakdownMetricsSelfTimeTracker $breakdownMetricsSelfTimeTracker
*
* doUpdateBreakdownMetricsOnEnd is called only if breakdownMetricsSelfTimeTracker is not null
*/
$breakdownMetricsSelfTimeTracker = $this->breakdownMetricsSelfTimeTracker;
$breakdownMetricsSelfTimeTracker->end($monotonicClockNow);
$this->containingTransaction()->addSpanSelfTime(
$spanType,
$spanSubtype,
$breakdownMetricsSelfTimeTracker->accumulatedSelfTimeInMicroseconds()
);
$parentExecutionSegment = $this->parentExecutionSegment();
if ($parentExecutionSegment !== null) {
/**
* doUpdateBreakdownMetricsOnEnd is called only if breakdownMetricsSelfTimeTracker is not null
*
* Local variable to workaround PHPStan not having a way to declare that
* $parentExecutionSegment->breakdownMetricsSelfTimeTracker is not null
*
* @var BreakdownMetricsSelfTimeTracker $parentBreakdownMetricsSelfTimeTracker
*/
$parentBreakdownMetricsSelfTimeTracker = $parentExecutionSegment->breakdownMetricsSelfTimeTracker;
$parentBreakdownMetricsSelfTimeTracker->onChildEnd($monotonicClockNow);
}
}
protected function onChildSpanAboutToStart(Span $child): void
{
}
protected function onChildSpanEnded(Span $child): void
{
$shouldSendEndedSpanImmediately = false;
if ($this->containingTransaction()->isSpanCompressionEnabled()) {
if (!$this->tryToCompressChild($child)) {
$this->flushPendingCompositeChild();
$shouldSendEndedSpanImmediately = true;
}
} else {
$shouldSendEndedSpanImmediately = true;
}
if ($shouldSendEndedSpanImmediately) {
$this->containingTransaction()->tracer()->sendSpanToApmServer($child);
}
}
private function tryToCompressChild(Span $child): bool
{
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered', ['child' => $child]);
if ($this->hasEnded()) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Exiting - not going to compress because this execution segment already ended');
return false;
}
if (!$child->isCompressionEligible()) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Exiting - not going to compress because this execution segment already ended');
return false;
}
if ($this->pendingCompositeChild === null) {
$this->pendingCompositeChild = $child;
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Exiting - set pendingCompositeChild to child', ['child' => $child]);
return true;
}
if ($this->pendingCompositeChild->tryToAddToCompress($child)) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Exiting - added to pendingCompositeChild', ['child' => $child]);
return true;
}
/**
* Flush and re-try from the given child
*/
$this->flushPendingCompositeChild();
return $this->tryToCompressChild($child);
}
private function flushPendingCompositeChild(): void
{
if ($this->pendingCompositeChild === null) {
return;
}
$this->containingTransaction()->tracer()->sendSpanToApmServer($this->pendingCompositeChild);
$this->pendingCompositeChild = null;
}
/** @inheritDoc */
public function hasEnded(): bool
{
return $this->isEnded;
}
/** @inheritDoc */
public function jsonSerialize()
{
$result = [];
SerializationUtil::addNameValue('name', $this->name, /* ref */ $result);
SerializationUtil::addNameValue('type', $this->type, /* ref */ $result);
SerializationUtil::addNameValue('id', $this->id, /* ref */ $result);
SerializationUtil::addNameValue('trace_id', $this->traceId, /* ref */ $result);
$timestamp = SerializationUtil::adaptTimestamp($this->timestamp);
SerializationUtil::addNameValue('timestamp', $timestamp, /* ref */ $result);
SerializationUtil::addNameValue('duration', $this->duration, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('outcome', $this->outcome, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('sample_rate', $this->sampleRate, /* ref */ $result);
return SerializationUtil::postProcessResult($result);
}
/**
* @return array<string>
*/
protected static function propertiesExcludedFromLog(): array
{
return ['tracer'];
}
}