agent/php/ElasticApm/Impl/InferredSpansManager.php (230 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\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;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*/
final class InferredSpansManager implements LoggableInterface
{
use LoggableTrait;
private const STATE_SHUTDOWN = 'shutdown';
private const STATE_WAITING_FOR_NO_SPANS = 'waiting_for_no_spans';
private const STATE_WAITING_FOR_NEW_TRANSACTION = 'waiting_for_no_spans';
private const STATE_RUNNING = 'running';
/** @var string */
private $state = self::STATE_SHUTDOWN;
/** @var Logger */
private $logger;
/** @var Tracer */
private $tracer;
/** @var ?InferredSpansBuilder */
private $builder = null;
/** @var null|Closure(Transaction): void */
private $onNewCurrentTransactionHasBegunCallback = null;
/** @var ?Transaction */
private $currentTransaction = null;
/** @var null|Closure(Transaction): void */
private $onCurrentTransactionAboutToEndCallback = null;
/** @var null|Closure(?Span): void */
private $onCurrentSpanChangedCallback = null;
public function __construct(Tracer $tracer)
{
$this->tracer = $tracer;
$this->logger = $tracer->loggerFactory()
->loggerForClass(LogCategory::INFERRED_SPANS, __NAMESPACE__, __CLASS__, __FILE__)
->addContext('this', $this);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered');
$currentTransaction = $tracer->getCurrentTransaction();
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($currentTransaction instanceof Transaction && $currentTransaction->isSampled())
&& $assertProxy->withContext(
'$currentTransaction instanceof Transaction && $currentTransaction->isSampled()',
['currentTransaction' => $currentTransaction]
);
/** @var Transaction $currentTransaction */
if (!$this->detectIfEnabled()) {
return;
}
$this->tracer->onNewCurrentTransactionHasBegun->add(
$this->onNewCurrentTransactionHasBegunCallback = function (Transaction $transaction): void {
$this->onNewCurrentTransactionHasBegun($transaction);
}
);
$this->state = self::STATE_WAITING_FOR_NEW_TRANSACTION;
$this->onNewCurrentTransactionHasBegun($currentTransaction);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Inferred spans feature is enabled');
}
private function detectIfEnabled(): bool
{
if (!$this->tracer->getConfig()->profilingInferredSpansEnabled()) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Inferred spans feature is disabled because configuration option '
. OptionNames::PROFILING_INFERRED_SPANS_ENABLED . ' value is false'
);
return false;
}
return true;
}
public function handleAutomaticCapturing(int $duration): void
{
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered');
if (!$this->isShutdown() && $this->builder !== null) {
$stackTrace = $this->builder->captureStackTrace(/* offset */ 1);
if (count($stackTrace) > 0) {
$this->builder->addStackTrace($stackTrace, $duration);
}
}
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Exiting');
}
private function flushAndPause(): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered');
if ($this->isShutdown()) {
return;
}
if ($this->builder !== null) {
$this->builder->close();
$this->builder = null;
}
}
private function onNewCurrentTransactionHasBegun(Transaction $transaction): void
{
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered', ['transaction' => $transaction]);
if ($this->isShutdown()) {
return;
}
if ($this->currentTransaction !== null) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Unexpected: New transaction has begun'
. ' while there is already a transaction in progress'
. ' - shutting down...',
['old transaction' => $this->currentTransaction, 'new transaction' => $transaction]
);
$this->shutdown();
return;
}
if ($transaction !== $this->tracer->getCurrentTransaction()) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
"Unexpected: New transaction has begun but it's not the current transaction"
. ' - shutting down...',
['new transaction' => $transaction, 'current transaction' => $this->tracer->getCurrentTransaction()]
);
$this->shutdown();
return;
}
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->state === self::STATE_WAITING_FOR_NEW_TRANSACTION)
&& $assertProxy->withContext('$this->state === self::STATE_WAITING_FOR_NEW_TRANSACTION', ['this' => $this]);
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->currentTransaction === null)
&& $assertProxy->withContext('$this->currentTransaction === null', ['this' => $this]);
$this->currentTransaction = $transaction;
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->onCurrentTransactionAboutToEndCallback === null)
&& $assertProxy->withContext('$this->onTransactionAboutToEndCallback === null', ['this' => $this]);
$this->currentTransaction->onAboutToEnd->add(
$this->onCurrentTransactionAboutToEndCallback = function (Transaction $transaction): void {
$this->onCurrentTransactionAboutToEnd($transaction);
}
);
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->onCurrentSpanChangedCallback === null)
&& $assertProxy->withContext('$this->onCurrentSpanChangedCallback === null', ['this' => $this]);
if ($this->currentTransaction !== null) {
$this->currentTransaction->onCurrentSpanChanged->add(
$this->onCurrentSpanChangedCallback = function (?Span $span): void {
$this->onCurrentSpanChanged($span);
}
);
}
$this->builder = new InferredSpansBuilder($this->tracer);
$this->state = self::STATE_RUNNING;
}
private function onCurrentTransactionAboutToEnd(Transaction $transaction): void
{
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered', ['transaction' => $transaction]);
if ($this->isShutdown()) {
return;
}
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->currentTransaction === $transaction)
&& $assertProxy->withContext(
'$this->currentTransaction === $transaction',
['this' => $this, 'transaction' => $transaction]
);
$this->flushAndPause();
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->onCurrentTransactionAboutToEndCallback !== null)
&& $assertProxy->withContext('$this->onTransactionAboutToEndCallback !== null', ['this' => $this]);
/* @phpstan-ignore-next-line */
$this->currentTransaction->onAboutToEnd->remove($this->onCurrentTransactionAboutToEndCallback);
$this->onCurrentTransactionAboutToEndCallback = null;
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($this->onCurrentSpanChangedCallback !== null)
&& $assertProxy->withContext('$this->onCurrentSpanChangedCallback !== null', ['this' => $this]);
/* @phpstan-ignore-next-line */
$this->currentTransaction->onCurrentSpanChanged->remove($this->onCurrentSpanChangedCallback);
$this->onCurrentSpanChangedCallback = null;
$this->currentTransaction = null;
$this->state = self::STATE_WAITING_FOR_NEW_TRANSACTION;
}
private function onCurrentSpanChanged(?Span $span): void
{
($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered', ['span' => $span]);
if ($this->isShutdown()) {
return;
}
if ($span === null) {
if ($this->state === self::STATE_RUNNING) {
return;
}
$this->builder = new InferredSpansBuilder($this->tracer);
$this->state = self::STATE_RUNNING;
return;
}
$this->flushAndPause();
$this->state = self::STATE_WAITING_FOR_NO_SPANS;
}
private function isShutdown(): bool
{
return $this->state === self::STATE_SHUTDOWN;
}
public function shutdown(): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log('Entered');
if ($this->isShutdown()) {
return;
}
$this->flushAndPause();
if ($this->onNewCurrentTransactionHasBegunCallback !== null) {
$this->tracer->onNewCurrentTransactionHasBegun->remove($this->onNewCurrentTransactionHasBegunCallback);
$this->onNewCurrentTransactionHasBegunCallback = null;
}
if ($this->currentTransaction !== null) {
if ($this->onCurrentTransactionAboutToEndCallback !== null) {
$this->currentTransaction->onAboutToEnd->remove($this->onCurrentTransactionAboutToEndCallback);
$this->onCurrentTransactionAboutToEndCallback = null;
}
$this->currentTransaction = null;
}
$this->state = self::STATE_SHUTDOWN;
}
/**
* @return string[]
*/
protected static function propertiesExcludedFromLog(): array
{
return array_merge(self::defaultPropertiesExcludedFromLog(), ['tracer', 'currentTransaction']);
}
/** @inheritDoc */
public function toLog(LogStreamInterface $stream): void
{
$currentTransactionId = $this->currentTransaction === null ? null : $this->currentTransaction->getId();
$this->toLogLoggableTraitImpl(
$stream,
/* customPropValues */
['currentTransactionId' => $currentTransactionId]
);
}
}