agent/php/ElasticApm/Impl/Transaction.php (479 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\ExecutionSegmentInterface; use Elastic\Apm\Impl\BackendComm\SerializationUtil; use Elastic\Apm\Impl\BreakdownMetrics\PerTransaction as BreakdownMetricsPerTransaction; use Elastic\Apm\Impl\Config\OptionNames; use Elastic\Apm\Impl\Config\Snapshot as ConfigSnapshot; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Log\LogStreamInterface; use Elastic\Apm\Impl\Util\IdGenerator; use Elastic\Apm\Impl\Util\ObserverSet; use Elastic\Apm\Impl\Util\RandomUtil; use Elastic\Apm\Impl\Util\StackTraceUtil; use Elastic\Apm\SpanInterface; use Elastic\Apm\TransactionContextInterface; use Elastic\Apm\TransactionInterface; use Throwable; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ final class Transaction extends ExecutionSegment implements TransactionInterface { /** @var ?string */ private $parentId = null; /** @var int */ public $startedSpansCount = 0; /** @var int */ private $droppedSpansCount = 0; /** @var ?string */ private $result = null; /** @var bool */ private $isSampled; /** @var ?TransactionContext */ private $context = null; /** @var Tracer */ private $tracer; /** @var ConfigSnapshot */ private $config; /** @var ?Span */ private $currentSpan = null; /** @var Logger */ private $logger; /** @var int */ public $numberOfErrorsSent = 0; /** @var ?BreakdownMetricsPerTransaction */ private $breakdownMetricsPerTransaction = null; /** @var ?string */ private $outgoingTraceState; /** @var ObserverSet<Transaction> */ public $onAboutToEnd; /** @var ObserverSet<?Span> */ public $onCurrentSpanChanged; /** @var ?bool */ private $cachedIsSpanCompressionEnabled = null; /** @var ?int */ private $cachedStackTraceLimitConfig = null; /** @var ?float */ private $cachedSpanStackTraceMinDurationConfig = null; public function __construct(TransactionBuilder $builder) { $this->tracer = $builder->tracer; $this->config = $builder->tracer->getConfig(); if ($this->config->breakdownMetrics()) { $this->breakdownMetricsPerTransaction = new BreakdownMetricsPerTransaction($this); } $distributedTracingData = self::extractDistributedTracingData($builder); if ($distributedTracingData === null) { $traceId = IdGenerator::generateId(Constants::TRACE_ID_SIZE_IN_BYTES); $sampleRate = $this->tracer->getConfig()->transactionSampleRate(); $isSampled = self::makeSamplingDecision($sampleRate); /** * @link https://github.com/elastic/apm/blob/main/specs/agents/tracing-sampling.md#non-sampled-transactions * For non-sampled transactions set the transaction attributes sampled: false and sample_rate: 0 */ $sampleRateToMarkTransaction = $isSampled ? $sampleRate : 0.0; $this->outgoingTraceState = $this->tracer->httpDistributedTracing()->buildOutgoingTraceStateForRootTransaction($sampleRate); } else { $traceId = $distributedTracingData->traceId; $this->parentId = $distributedTracingData->parentId; $isSampled = $distributedTracingData->isSampled; $sampleRateToMarkTransaction = $distributedTracingData->sampleRate; $this->outgoingTraceState = $distributedTracingData->outgoingTraceState; } parent::__construct( $builder->tracer, null /* <- parentExecutionSegment */, $traceId, $builder->name, $builder->type, $sampleRateToMarkTransaction, $builder->timestamp ); $this->logger = $this->tracer->loggerFactory() ->loggerForClass(LogCategory::PUBLIC_API, __NAMESPACE__, __CLASS__, __FILE__) ->addContext('this', $this); $this->isSampled = $isSampled; $this->onCurrentSpanChanged = new ObserverSet(); $this->onAboutToEnd = new ObserverSet(); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Transaction created'); } private static function extractDistributedTracingData(TransactionBuilder $builder): ?DistributedTracingDataInternal { /** @var string[] $traceParentHeaderValues */ $traceParentHeaderValues = []; /** @var string[] $traceStateHeaderValues */ $traceStateHeaderValues = []; self::extractDistributedTracingHeaders( $builder, $traceParentHeaderValues /* <- ref */, $traceStateHeaderValues /* <- ref */ ); return $builder->tracer->httpDistributedTracing()->parseHeaders( $traceParentHeaderValues, $traceStateHeaderValues ); } /** * @param TransactionBuilder $builder * @param string[] $traceParentHeaderValues * @param string[] $traceStateHeaderValues */ private static function extractDistributedTracingHeaders( TransactionBuilder $builder, array &$traceParentHeaderValues, array &$traceStateHeaderValues ): void { if ($builder->serializedDistTracingData !== null) { $traceParentHeaderValues[] = $builder->serializedDistTracingData; return; } $headersExtractor = $builder->headersExtractor; if ($headersExtractor === null) { return; } /** * @param null|string|string[] $headersExtractorRetVal * * @return string[] */ $adaptHeadersExtractorRetVal = function ($headersExtractorRetVal): array { return $headersExtractorRetVal === null ? [] : (is_string($headersExtractorRetVal) ? [$headersExtractorRetVal] : $headersExtractorRetVal); }; $traceParentHeaderValues = $adaptHeadersExtractorRetVal( $headersExtractor(HttpDistributedTracing::TRACE_PARENT_HEADER_NAME) ); $traceStateHeaderValues = $adaptHeadersExtractorRetVal( $headersExtractor(HttpDistributedTracing::TRACE_STATE_HEADER_NAME) ); } /** @inheritDoc */ public function getParentId(): ?string { return $this->parentId; } private static function makeSamplingDecision(float $sampleRate): bool { if ($sampleRate === 0.0) { return false; } if ($sampleRate === 1.0) { return true; } return RandomUtil::generate01Float() < $sampleRate; } public function tracer(): Tracer { return $this->tracer; } /** @inheritDoc */ public function isSampled(): bool { return $this->isSampled; } /** @inheritDoc */ public function containingTransaction(): Transaction { return $this; } /** @inheritDoc */ public function parentExecutionSegment(): ?ExecutionSegment { return null; } /** @inheritDoc */ public function context(): TransactionContextInterface { if (!$this->isSampled()) { return NoopTransactionContext::singletonInstance(); } if ($this->context === null) { $this->context = new TransactionContext($this); } return $this->context; } public function cloneContextData(): ?TransactionContext { if ($this->context === null) { return null; } return clone $this->context; } /** @inheritDoc */ public function setResult(?string $result): void { if ($this->beforeMutating()) { return; } $this->result = $this->tracer->limitNullableKeywordString($result); } /** @inheritDoc */ public function getResult(): ?string { return $this->result; } /** @inheritDoc */ public function getCurrentSpan(): SpanInterface { return $this->currentSpan ?? NoopSpan::singletonInstance(); } public function setCurrentSpan(?Span $newCurrentSpan): void { $this->currentSpan = $newCurrentSpan; $this->onCurrentSpanChanged->callCallbacks($this->currentSpan); } public function getCurrentExecutionSegment(): ExecutionSegmentInterface { return $this->currentSpan ?? $this; } public function tryToAllocateStartedSpan(): bool { if ($this->startedSpansCount < $this->config->transactionMaxSpans()) { ++$this->startedSpansCount; return true; } ++$this->droppedSpansCount; if ($this->droppedSpansCount === 1) { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Starting to drop spans because of ' . OptionNames::TRANSACTION_MAX_SPANS . ' config', [OptionNames::TRANSACTION_MAX_SPANS . ' config' => $this->config->transactionMaxSpans()] ); } return false; } public function beginSpan( ExecutionSegment $parentExecutionSegment, string $name, string $type, ?string $subtype, ?string $action, ?float $timestamp ): ?Span { if ($this->beforeMutating() || !$this->tracer->isRecording()) { return null; } $isDropped = false; // Started and dropped spans should be counted only for sampled transactions if ($this->isSampled) { $isDropped = !$this->tryToAllocateStartedSpan(); } return new Span( $this->tracer, $this /* <- containingTransaction */, $parentExecutionSegment, $name, $type, $subtype, $action, $timestamp, $isDropped, $this->sampleRate ); } /** @inheritDoc */ public function beginChildSpan( string $name, string $type, ?string $subtype = null, ?string $action = null, ?float $timestamp = null ): SpanInterface { return $this->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 beginCurrentSpan( string $name, string $type, ?string $subtype = null, ?string $action = null, ?float $timestamp = null ): SpanInterface { $newCurrentSpan = $this->beginSpan( $this->currentSpan ?? $this /* <- parentExecutionSegment */, $name, $type, $subtype, $action, $timestamp ); $this->setCurrentSpan($newCurrentSpan); return $this->getCurrentSpan(); } /** @inheritDoc */ public function captureCurrentSpan( string $name, string $type, Closure $callback, ?string $subtype = null, ?string $action = null, ?float $timestamp = null ) { $newSpan = $this->beginCurrentSpan($name, $type, $subtype, $action, $timestamp); try { return $callback($newSpan); } catch (Throwable $throwable) { $newSpan->createErrorFromThrowable($throwable); throw $throwable; } finally { // Since endSpanEx was not called directly it should not be kept in the stack trace $newSpan->endSpanEx(/* numberOfStackFramesToSkip: */ 1); } } /** @inheritDoc */ public function ensureParentId(): string { if ($this->parentId === null) { $this->parentId = IdGenerator::generateId(Constants::EXECUTION_SEGMENT_ID_SIZE_IN_BYTES); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Setting parent ID for already existing transaction', ['parentId' => $this->parentId] ); } return $this->parentId; } /** @inheritDoc */ public function dispatchCreateError(ErrorExceptionData $errorExceptionData): ?string { if ($this->currentSpan === null) { return $this->tracer->doCreateError($errorExceptionData, /* transaction: */ $this, /* span */ null); } return $this->currentSpan->dispatchCreateError($errorExceptionData); } /** @inheritDoc */ public function getDistributedTracingDataInternal(): ?DistributedTracingDataInternal { if ($this->currentSpan === null) { return $this->doGetDistributedTracingData(/* span */ null); } return $this->currentSpan->getDistributedTracingDataInternal(); } public function doGetDistributedTracingData(?Span $span): ?DistributedTracingDataInternal { if (!$this->tracer->isRecording()) { return null; } $result = new DistributedTracingDataInternal(); $result->traceId = $this->traceId; if ($span === null) { $result->parentId = $this->id; $this->wasPropogatedViaDistributedTracing = true; } else { $result->parentId = $span->getId(); $span->wasPropogatedViaDistributedTracing = true; } $result->isSampled = $this->isSampled; $result->outgoingTraceState = $this->outgoingTraceState; return $result; } public function getConfig(): ConfigSnapshot { return $this->config; } /** @inheritDoc */ public function discard(): void { while ($this->currentSpan !== null) { $spanToDiscard = $this->currentSpan; $this->setCurrentSpan($this->currentSpan->parentIfSpan()); if (!$spanToDiscard->hasEnded()) { $spanToDiscard->discard(); } } parent::discard(); } /** @inheritDoc */ public function end(?float $duration = null): void { if (!$this->endExecutionSegment($duration)) { return; } $this->onAboutToEnd->callCallbacks($this); $this->prepareForSerialization(); $this->tracer->sendTransactionToApmServer($this->breakdownMetricsPerTransaction, $this); if ($this->tracer->getCurrentTransaction() === $this) { $this->tracer->resetCurrentTransaction(); } } public function addSpanSelfTime(string $spanType, ?string $spanSubtype, float $spanSelfTimeInMicroseconds): void { if ($this->beforeMutating() || !$this->tracer->isRecording()) { return; } /** * if addSpanSelfTime is called that means $this->breakdownMetricsPerTransaction is not null * Local variable to workaround PHPStan not having a way to declare that * $this->breakdownMetricsPerTransaction is not null * * @var BreakdownMetricsPerTransaction $breakdownMetricsPerTransaction */ $breakdownMetricsPerTransaction = $this->breakdownMetricsPerTransaction; $breakdownMetricsPerTransaction->addSpanSelfTime( $spanType, $spanSubtype, $spanSelfTimeInMicroseconds ); } /** @inheritDoc */ protected function updateBreakdownMetricsOnEnd(float $monotonicClockNow): void { $this->doUpdateBreakdownMetricsOnEnd( $monotonicClockNow, BreakdownMetricsPerTransaction::TRANSACTION_SPAN_TYPE, /* subtype: */ null ); } public function isSpanCompressionEnabled(): bool { if ($this->cachedIsSpanCompressionEnabled === null) { $this->cachedIsSpanCompressionEnabled = $this->getConfig()->spanCompressionEnabled(); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log( 'Span compression is ' . ($this->cachedIsSpanCompressionEnabled ? 'enabled' : 'DISABLED') . ' via configuration option `' . OptionNames::SPAN_COMPRESSION_ENABLED . '\'' ); } return $this->cachedIsSpanCompressionEnabled; } public function getStackTraceLimitConfig(): int { if ($this->cachedStackTraceLimitConfig === null) { $this->cachedStackTraceLimitConfig = $this->getConfig()->stackTraceLimit(); /** * 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 */ $msgPrefix = $this->cachedStackTraceLimitConfig === 0 ? 'Span stack trace collection is DISABLED' : ($this->cachedStackTraceLimitConfig < 0 ? 'Span stack trace collection will include all the frames' : 'Span stack trace collection will include up to ' . $this->cachedStackTraceLimitConfig . ' frames'); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log($msgPrefix . ' (set by configuration option `' . OptionNames::STACK_TRACE_LIMIT . '\')'); } return $this->cachedStackTraceLimitConfig; } private function getSpanStackTraceMinDurationConfig(): float { if ($this->cachedSpanStackTraceMinDurationConfig === null) { $this->cachedSpanStackTraceMinDurationConfig = $this->getConfig()->spanStackTraceMinDuration(); /** * span_stack_trace_min_duration * 0 - collect stack traces for spans with any duration * any positive value - it limits stack trace collection to spans with duration equal to or greater than * any negative value - it disable stack trace collection for spans completely */ $msgPrefix = $this->cachedSpanStackTraceMinDurationConfig === 0.0 ? 'Span stack trace collection is enabled for spans with any duration' : ($this->cachedSpanStackTraceMinDurationConfig < 0 ? 'Span stack trace collection is DISABLED for spans with any duration' : 'Span stack trace collection is enabled for spans with duration >= ' . $this->cachedSpanStackTraceMinDurationConfig . ' ms'); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log($msgPrefix . ' (set by configuration option `' . OptionNames::SPAN_STACK_TRACE_MIN_DURATION . '\')'); } return $this->cachedSpanStackTraceMinDurationConfig; } public function shouldCollectStackTraceForSpanDuration(float $durationInMilliseconds): bool { /** * span_stack_trace_min_duration * 0 - collect stack traces for spans with any duration * any positive value - it limits stack trace collection to spans with duration equal to or greater than * any negative value - it disable stack trace collection for spans completely */ return (($stackTraceMinDuration = $this->getSpanStackTraceMinDurationConfig()) >= 0) && $durationInMilliseconds >= $stackTraceMinDuration; } /** * @param int $numberOfStackFramesToSkip * * @return null|StackTraceFrame[] * * @phpstan-param 0|positive-int $numberOfStackFramesToSkip */ public function captureApmFormatStackTrace(int $numberOfStackFramesToSkip): ?array { return ($maxNumberOfFrames = StackTraceUtil::convertLimitConfigToMaxNumberOfFrames($this->getStackTraceLimitConfig())) === 0 ? null : $this->tracer->stackTraceUtil()->captureInApmFormat(/* offset */ $numberOfStackFramesToSkip + 1, $maxNumberOfFrames); } private function prepareForSerialization(): void { SerializationUtil::prepareForSerialization(/* ref */ $this->context); } /** @inheritDoc */ public function jsonSerialize() { $result = SerializationUtil::preProcessResult(parent::jsonSerialize()); SerializationUtil::addNameValueIfNotNull('parent_id', $this->parentId, /* ref */ $result); $spanCountSubObject = ['started' => $this->startedSpansCount]; if ($this->droppedSpansCount != 0) { $spanCountSubObject['dropped'] = $this->droppedSpansCount; } SerializationUtil::addNameValue('span_count', $spanCountSubObject, /* ref */ $result); SerializationUtil::addNameValueIfNotNull('result', $this->result, /* ref */ $result); // https://github.com/elastic/apm-server/blob/7.0/docs/spec/transactions/transaction.json#L72 // 'sampled' is optional and defaults to true. if (!$this->isSampled) { SerializationUtil::addNameValue('sampled', $this->isSampled, /* ref */ $result); } SerializationUtil::addNameValueIfNotNull('context', $this->context, /* ref */ $result); return SerializationUtil::postProcessResult($result); } /** @inheritDoc */ protected static function propertiesExcludedFromLog(): array { return array_merge(parent::propertiesExcludedFromLog(), ['config', 'currentSpan']); } /** @inheritDoc */ public function toLog(LogStreamInterface $stream): void { $currentSpanId = $this->currentSpan === null ? null : $this->currentSpan->getId(); parent::toLogLoggableTraitImpl( $stream, /* customPropValues */ ['currentSpanId' => $currentSpanId] ); } }