agent/php/ElasticApm/Impl/Span.php (392 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\Impl\BackendComm\SerializationUtil;
use Elastic\Apm\Impl\Log\LogCategory;
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Log\LogStreamInterface;
use Elastic\Apm\Impl\Util\ObserverSet;
use Elastic\Apm\Impl\Util\TimeUtil;
use Elastic\Apm\SpanContextInterface;
use Elastic\Apm\SpanInterface;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class Span extends ExecutionSegment implements SpanInterface, SpanToSendInterface
{
/** @var string */
private $parentId;
/** @var string */
private $transactionId;
/** @var ?string */
private $action = null;
/** @var ?string */
private $subtype = null;
/** @var null|StackTraceFrame[] */
private $stackTrace = null;
/** @var ?SpanContext */
private $context = null;
/** @var Logger */
private $logger;
/** @var Transaction */
private $containingTransaction;
/** @var ExecutionSegment */
private $parentExecutionSegment;
/** @var bool */
private $isDropped;
/** @var ObserverSet<Span> */
public $onAboutToEnd;
/** @var bool */
protected $hasChildren = false;
/** @var bool */
private $isCompressible = false;
/** @var ?SpanComposite */
public $composite = null;
public function __construct(
Tracer $tracer,
Transaction $containingTransaction,
ExecutionSegment $parentExecutionSegment,
string $name,
string $type,
?string $subtype,
?string $action,
?float $timestamp,
bool $isDropped,
?float $sampleRate
) {
$this->parentExecutionSegment = $parentExecutionSegment;
$this->containingTransaction = $containingTransaction;
parent::__construct(
$tracer,
$this->parentExecutionSegment,
$containingTransaction->getTraceId(),
$name,
$type,
$sampleRate,
$timestamp
);
$this->setSubtype($subtype);
$this->setAction($action);
$this->transactionId = $containingTransaction->getId();
$this->parentId = $this->parentExecutionSegment->getId();
$this->logger = $this->containingTransaction()->tracer()->loggerFactory()
->loggerForClass(LogCategory::PUBLIC_API, __NAMESPACE__, __CLASS__, __FILE__)
->addContext('this', $this);
$this->isDropped = $isDropped;
$this->onAboutToEnd = new ObserverSet();
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Span created');
$parentExecutionSegment->onChildSpanAboutToStart($this);
}
/** @inheritDoc */
public function containingTransaction(): Transaction
{
return $this->containingTransaction;
}
/** @inheritDoc */
public function parentExecutionSegment(): ?ExecutionSegment
{
return $this->parentExecutionSegment;
}
public function shouldBeSentToApmServer(): bool
{
return $this->containingTransaction->isSampled() && (!$this->isDropped);
}
/** @inheritDoc */
public function getParentId(): string
{
return $this->parentId;
}
/** @inheritDoc */
public function getTransactionId(): string
{
return $this->transactionId;
}
/** @inheritDoc */
public function context(): SpanContextInterface
{
if (!$this->isSampled()) {
return NoopSpanContext::singletonInstance();
}
if ($this->context === null) {
$this->context = new SpanContext($this);
}
return $this->context;
}
/** @inheritDoc */
public function setAction(?string $action): void
{
if ($this->beforeMutating()) {
return;
}
$this->action = $this->containingTransaction->tracer()->limitNullableKeywordString($action);
}
/** @inheritDoc */
public function setSubtype(?string $subtype): void
{
if ($this->beforeMutating()) {
return;
}
$this->subtype = $this->containingTransaction->tracer()->limitNullableKeywordString($subtype);
}
public static function setServiceFor(SpanInterface $span, ?string $targetType, ?string $targetName, string $destinationName, string $destinationResource, string $destinationType): void
{
$span->context()->service()->target()->setType($targetType);
$span->context()->service()->target()->setName($targetName);
// destination.service is deprecated in favor of service.target
$span->context()->destination()->setService($destinationName, $destinationResource, $destinationType);
}
public function setCompressible(bool $isCompressible): void
{
$this->isCompressible = $isCompressible;
}
/** @inheritDoc */
public function getDistributedTracingDataInternal(): ?DistributedTracingDataInternal
{
$spanAsParent = $this->shouldBeSentToApmServer() ? $this : null;
return $this->containingTransaction->doGetDistributedTracingData($spanAsParent);
}
/** @inheritDoc */
public function beginChildSpan(
string $name,
string $type,
?string $subtype = null,
?string $action = null,
?float $timestamp = null
): SpanInterface {
return
$this->containingTransaction->beginSpan(
$this /* <- parentExecutionSegment */,
$name,
$type,
$subtype,
$action,
$timestamp
)
?? NoopSpan::singletonInstance();
}
/** @inheritDoc */
public function captureChildSpan(
string $name,
string $type,
Closure $callback,
?string $subtype = null,
?string $action = null,
?float $timestamp = null
) {
/** @noinspection PhpUnhandledExceptionInspection */
return $this->captureChildSpanImpl(
$name,
$type,
$callback,
$subtype,
$action,
$timestamp,
1 /* numberOfStackFramesToSkip */
);
}
/** @inheritDoc */
public function dispatchCreateError(ErrorExceptionData $errorExceptionData): ?string
{
$spanForError = $this->shouldBeSentToApmServer() ? $this : null;
return $this->containingTransaction->tracer()->doCreateError($errorExceptionData, $this->containingTransaction, $spanForError);
}
public function isCompressionEligible(): bool
{
if (!$this->isCompressible) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('This span is not eligible for compression because it is not marked as compressible');
return false;
}
if ($this->wasPropogatedViaDistributedTracing) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('This span is not eligible for compression because its ID was propogated via distributed tracing');
return false;
}
if ($this->hasChildren) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('This span is not eligible for compression because it has children');
return false;
}
if ($this->outcome !== null && $this->outcome !== Constants::OUTCOME_SUCCESS) {
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('This span is not eligible its outcome is present and it is not success', ['outcome' => $this->outcome]);
return false;
}
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('This span is eligible for compression');
return true;
}
private function getServiceTarget(): ?SpanContextServiceTarget
{
return ($this->context === null || $this->context->service === null) ? null : $this->context->service->target;
}
private function isSameKind(Span $other): bool
{
/**
* @link https://github.com/elastic/apm/blob/4a5e72b3cee430a839c0adda645c71d4eb0a66bb/specs/agents/handling-huge-traces/tracing-spans-compress.md#consecutive-same-kind-compression-strategy
*/
return ($this->type === $other->type)
&& ($this->subtype === $other->subtype)
&& SpanContextServiceTarget::areNullableEqual($this->getServiceTarget(), $other->getServiceTarget());
}
public function tryToAddToCompress(Span $sibling): bool
{
$logTraceProxy = $this->logger->ifTraceLevelEnabledNoLine(__FUNCTION__);
if ($this->logger->isTraceLevelEnabled()) {
$logger = $this->logger->inherit()->addContext('sibling', $sibling);
$logTraceProxy = $logger->ifTraceLevelEnabledNoLine(__FUNCTION__);
}
$logTraceProxy && $logTraceProxy->log(__LINE__, 'Entered');
if ($this->composite === null) {
$compressionStrategy = $this->canCompressFirstPair($sibling);
if ($compressionStrategy === null) {
$logTraceProxy && $logTraceProxy->log(__LINE__, 'Exiting - cannot compress first pair');
return false;
}
if ($compressionStrategy === Constants::COMPRESSION_STRATEGY_SAME_KIND) {
$this->name = (($serviceTarget = $this->getServiceTarget()) === null)
? self::buildSameKindCompressedCompositeName(null, null)
: self::buildSameKindCompressedCompositeName($serviceTarget->type, $serviceTarget->name);
}
$this->composite = new SpanComposite($compressionStrategy, $this->duration);
} else {
if (!$this->canAddToCompositeToCompress($this->composite, $sibling)) {
$logTraceProxy && $logTraceProxy->log(__LINE__, 'Exiting - cannot add sibling');
return false;
}
}
$this->composite->durationsSum += $sibling->duration;
++$this->composite->count;
$this->recalcDurationForComposite($sibling);
/**
* When a span is compressed into a composite, span_count.reported should ONLY count the compressed composite as a single span.
* Spans that have been compressed into the composite should not be counted.
*
* @link https://github.com/elastic/apm/blob/5e1bfbc95fa0358ef195cedba8cb1be281988227/specs/agents/handling-huge-traces/tracing-spans-compress.md#effects-on-span-count
*/
--$this->containingTransaction->startedSpansCount;
$logTraceProxy && $logTraceProxy->log(__LINE__, 'Exiting - added sibling');
return true;
}
private function canCompressFirstPair(Span $sibling): ?string
{
if (($loggerProxyTrace = $this->logger->ifTraceLevelEnabledNoLine(__FUNCTION__)) !== null) {
$localLogger = $this->logger->inherit()->addAllContext(
[
'this' => ['name' => $this->name, 'type' => $this->type, 'duration' => $this->duration],
'sibling' => ['name' => $sibling->name, 'type' => $sibling->type, 'duration' => $sibling->duration],
]
);
$loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__);
}
if (!$this->isSameKind($sibling)) {
$loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Cannot compress because not even same kind');
return null;
}
$config = $this->containingTransaction->tracer()->getConfig();
$exactMatchMaxDuration = $config->spanCompressionExactMatchMaxDuration();
if ($this->name === $sibling->name) {
if ($this->duration <= $exactMatchMaxDuration && $sibling->duration <= $exactMatchMaxDuration) {
$loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Can compress as ' . Constants::COMPRESSION_STRATEGY_EXACT_MATCH);
return Constants::COMPRESSION_STRATEGY_EXACT_MATCH;
} else {
/**
* Note that if the spans are exact match but duration threshold requirement is not satisfied we just stop compression sequence.
* In particular it means that the implementation should not proceed to try same kind strategy.
* Otherwise user would have to lower both span_compression_exact_match_max_duration and span_compression_same_kind_max_duration
* to prevent longer exact match spans from being compressed.
*
* @link https://github.com/elastic/apm/blob/e528576a5b0f3e95fe3c1da493466882fa7d8329/specs/agents/handling-huge-traces/tracing-spans-compress.md?plain=1#L200
*/
$loggerProxyTrace && $loggerProxyTrace->log(
__LINE__,
'Cannot compress as ' . Constants::COMPRESSION_STRATEGY_EXACT_MATCH . ' because one of the durations is above configured threshold',
['exactMatchMaxDuration (ms)' => $exactMatchMaxDuration]
);
return null;
}
}
$sameKindMaxDuration = $config->spanCompressionSameKindMaxDuration();
if ($this->duration <= $sameKindMaxDuration && $sibling->duration <= $sameKindMaxDuration) {
$loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Can compress as ' . Constants::COMPRESSION_STRATEGY_SAME_KIND);
return Constants::COMPRESSION_STRATEGY_SAME_KIND;
} else {
$loggerProxyTrace && $loggerProxyTrace->log(
__LINE__,
'Cannot compress as ' . Constants::COMPRESSION_STRATEGY_SAME_KIND . ' because one of the durations is above configured threshold',
['sameKindMaxDuration (ms)' => $sameKindMaxDuration]
);
return null;
}
}
public static function buildSameKindCompressedCompositeName(?string $serviceTargetType, ?string $serviceTargetName): string
{
/**
* @link https://github.com/elastic/apm/blob/4a5e72b3cee430a839c0adda645c71d4eb0a66bb/specs/agents/handling-huge-traces/tracing-spans-compress.md#consecutive-same-kind-compression-strategy
*/
$prefix = 'Calls to ';
if ($serviceTargetType === null) {
if ($serviceTargetName === null) {
return $prefix . 'unknown';
}
return $prefix . $serviceTargetName;
}
if ($serviceTargetName === null) {
return $prefix . $serviceTargetType;
}
return $prefix . $serviceTargetType . '/' . $serviceTargetName;
}
public function calcEndTimestamp(): float
{
return $this->timestamp + TimeUtil::millisecondsToMicroseconds($this->duration);
}
private function recalcDurationForComposite(Span $sibling): void
{
$beginTimestamp = min($this->timestamp, $sibling->timestamp);
$endTimestamp = max($this->calcEndTimestamp(), $sibling->calcEndTimestamp());
$this->duration = TimeUtil::microsecondsToMilliseconds($endTimestamp - $beginTimestamp);
}
private function canAddToCompositeToCompress(SpanComposite $compositeData, Span $sibling): bool
{
$config = $this->containingTransaction->tracer()->getConfig();
switch ($compositeData->compressionStrategy) {
case Constants::COMPRESSION_STRATEGY_EXACT_MATCH:
return $this->isSameKind($sibling)
&& $this->name === $sibling->name
&& $sibling->duration <= $config->spanCompressionExactMatchMaxDuration();
case Constants::COMPRESSION_STRATEGY_SAME_KIND:
return $this->isSameKind($sibling)
&& $sibling->duration <= $config->spanCompressionSameKindMaxDuration();
default:
($loggerProxy = $this->logger->ifCriticalLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Unexpected value for compression strategy: `' . $compositeData->compressionStrategy . '\''
);
return false;
}
}
protected function onChildSpanAboutToStart(Span $child): void
{
parent::onChildSpanAboutToStart($child);
$this->hasChildren = true;
}
/**
* @inheritDoc
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*/
public function endSpanEx(int $numberOfStackFramesToSkip, ?float $duration = null): void
{
if (!$this->endExecutionSegment($duration)) {
return;
}
$this->onAboutToEnd->callCallbacks($this);
if ($this->shouldBeSentToApmServer()) {
if ($this->containingTransaction->shouldCollectStackTraceForSpanDuration($this->duration)) {
$this->stackTrace = $this->containingTransaction->captureApmFormatStackTrace($numberOfStackFramesToSkip + 1);
}
$this->prepareForSerialization();
$this->parentExecutionSegment->onChildSpanEnded($this);
}
if ($this->containingTransaction->getCurrentSpan() === $this) {
$this->containingTransaction->setCurrentSpan($this->parentIfSpan());
}
}
public function parentIfSpan(): ?Span
{
// parentExecutionSegment is either a parent span or a containing transaction
if ($this->parentExecutionSegment === $this->containingTransaction) {
return null;
}
/** @noinspection PhpIncompatibleReturnTypeInspection */
return $this->parentExecutionSegment; // @phpstan-ignore-line
// It seems there's no way to tell PHPStan that $this->parentExecutionSegment is a Span
}
/** @inheritDoc */
public function end(?float $duration = null): void
{
// Since endSpanEx was not called directly it should not be kept in the stack trace
$this->endSpanEx(/* numberOfStackFramesToSkip: */ 1, $duration);
}
/** @inheritDoc */
protected function updateBreakdownMetricsOnEnd(float $monotonicClockNow): void
{
$this->doUpdateBreakdownMetricsOnEnd($monotonicClockNow, $this->type, $this->subtype);
}
private function prepareForSerialization(): void
{
SerializationUtil::prepareForSerialization(/* ref */ $this->context);
}
/** @inheritDoc */
public function jsonSerialize()
{
$result = SerializationUtil::preProcessResult(parent::jsonSerialize());
SerializationUtil::addNameValue('parent_id', $this->parentId, /* ref */ $result);
SerializationUtil::addNameValue('transaction_id', $this->transactionId, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('action', $this->action, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('subtype', $this->subtype, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('stacktrace', $this->stackTrace, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('context', $this->context, /* ref */ $result);
SerializationUtil::addNameValueIfNotNull('composite', $this->composite, /* ref */ $result);
return SerializationUtil::postProcessResult($result);
}
/** @inheritDoc */
protected static function propertiesExcludedFromLog(): array
{
return array_merge(
parent::propertiesExcludedFromLog(),
['containingTransaction', 'parentExecutionSegment', 'stackTrace', 'context']
);
}
/** @inheritDoc */
public function toLog(LogStreamInterface $stream): void
{
parent::toLogLoggableTraitImpl(
$stream,
/* customPropValues */
[
'containingTransaction ID' => $this->containingTransaction->getId(),
'parentExecutionSegment ID' => $this->parentExecutionSegment->getId(),
'stackTrace count' => $this->stackTrace === null ? null : count($this->stackTrace),
'context === null' => $this->context === null,
]
);
}
}