agent/php/ElasticApm/Impl/Tracer.php (410 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\ElasticApm;
use Elastic\Apm\ExecutionSegmentInterface;
use Elastic\Apm\Impl\AutoInstrument\PhpErrorData;
use Elastic\Apm\Impl\BackendComm\EventSender;
use Elastic\Apm\Impl\BreakdownMetrics\PerTransaction as BreakdownMetricsPerTransaction;
use Elastic\Apm\Impl\Config\DevInternalSubOptionNames;
use Elastic\Apm\Impl\Config\OptionNames;
use Elastic\Apm\Impl\Config\Snapshot as ConfigSnapshot;
use Elastic\Apm\Impl\Log\Backend as LogBackend;
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\Logger;
use Elastic\Apm\Impl\Log\LoggerFactory;
use Elastic\Apm\Impl\Log\LogStreamInterface;
use Elastic\Apm\Impl\Util\ElasticApmExtensionUtil;
use Elastic\Apm\Impl\Util\ObserverSet;
use Elastic\Apm\Impl\Util\PhpErrorUtil;
use Elastic\Apm\Impl\Util\StackTraceUtil;
use Elastic\Apm\Impl\Util\TextUtil;
use Elastic\Apm\TransactionBuilderInterface;
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 Tracer implements TracerInterface, LoggableInterface
{
/** @var TracerDependencies */
private $providedDependencies;
/** @var ClockInterface */
private $clock;
/** @var EventSinkInterface */
private $eventSink;
/** @var ConfigSnapshot */
private $config;
/** @var LogBackend */
private $logBackend;
/** @var LoggerFactory */
private $loggerFactory;
/** @var Logger */
private $logger;
/** @var ?Transaction */
private $currentTransaction = null;
/** @var bool */
private $isRecording = true;
/** @var MetadataDiscoverer */
private $metadataDiscoverer;
/** @var ?Metadata */
private $cachedMetadata = null;
/** @var HttpDistributedTracing */
private $httpDistributedTracing;
/** @var ObserverSet<Transaction> */
public $onNewCurrentTransactionHasBegun;
/** @var StackTraceUtil */
private $stackTraceUtil;
public function __construct(TracerDependencies $providedDependencies, ConfigSnapshot $config)
{
$this->providedDependencies = $providedDependencies;
$this->config = $config;
$this->logBackend = new LogBackend($this->config->effectiveLogLevel(), $providedDependencies->logSink);
$this->loggerFactory = new LoggerFactory($this->logBackend);
$this->logger = $this->loggerFactory->loggerForClass(LogCategory::PUBLIC_API, __NAMESPACE__, __CLASS__, __FILE__)->addContext('this', $this);
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Constructing Tracer...',
[
'Version of agent PHP part' => ElasticApm::VERSION,
'PHP_VERSION' => PHP_VERSION,
'providedDependencies' => $providedDependencies,
'effectiveLogLevel' => LogLevel::intToName($this->config->effectiveLogLevel()),
]
);
$this->clock = $providedDependencies->clock ?? new Clock($this->loggerFactory);
$this->eventSink = $providedDependencies->eventSink ?? (ElasticApmExtensionUtil::isLoaded() ? new EventSender($this->config, $this->loggerFactory) : NoopEventSink::singletonInstance());
$this->metadataDiscoverer = new MetadataDiscoverer($this->config, $this->loggerFactory);
$this->httpDistributedTracing = new HttpDistributedTracing($this->loggerFactory);
$this->stackTraceUtil = new StackTraceUtil($this->loggerFactory);
$this->onNewCurrentTransactionHasBegun = new ObserverSet();
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Constructed Tracer successfully');
}
public function getConfig(): ConfigSnapshot
{
return $this->config;
}
public function getMetadataDiscoverer(): MetadataDiscoverer
{
return $this->metadataDiscoverer;
}
private function newTransactionBuilder(
string $name,
string $type,
?float $timestamp = null,
?string $serializedDistTracingData = null
): TransactionBuilder {
$builder = new TransactionBuilder($this, $name, $type);
$builder->timestamp = $timestamp;
$builder->serializedDistTracingData = $serializedDistTracingData;
return $builder;
}
/** @inheritDoc */
public function beginCurrentTransaction(
string $name,
string $type,
?float $timestamp = null,
?string $serializedDistTracingData = null
): TransactionInterface {
$builder = $this->newTransactionBuilder($name, $type, $timestamp, $serializedDistTracingData);
$builder->asCurrent();
return $this->beginTransactionWithBuilder($builder);
}
/** @inheritDoc */
public function captureCurrentTransaction(
string $name,
string $type,
Closure $callback,
?float $timestamp = null,
?string $serializedDistTracingData = null
) {
$builder = $this->newTransactionBuilder($name, $type, $timestamp, $serializedDistTracingData);
$builder->asCurrent();
return $this->captureTransactionWithBuilder($builder, $callback);
}
/** @inheritDoc */
public function getCurrentTransaction(): TransactionInterface
{
return $this->currentTransaction ?? NoopTransaction::singletonInstance();
}
/** @inheritDoc */
public function getCurrentExecutionSegment(): ExecutionSegmentInterface
{
if ($this->currentTransaction === null) {
return NoopTransaction::singletonInstance();
}
return $this->currentTransaction->getCurrentExecutionSegment();
}
public function resetCurrentTransaction(): void
{
$this->currentTransaction = null;
}
/** @inheritDoc */
public function beginTransaction(
string $name,
string $type,
?float $timestamp = null,
?string $serializedDistTracingData = null
): TransactionInterface {
$builder = $this->newTransactionBuilder($name, $type, $timestamp, $serializedDistTracingData);
return $this->beginTransactionWithBuilder($builder);
}
/** @inheritDoc */
public function captureTransaction(
string $name,
string $type,
Closure $callback,
?float $timestamp = null,
?string $serializedDistTracingData = null
) {
$builder = $this->newTransactionBuilder($name, $type, $timestamp, $serializedDistTracingData);
return $this->captureTransactionWithBuilder($builder, $callback);
}
/** @inheritDoc */
public function newTransaction(string $name, string $type): TransactionBuilderInterface
{
return new TransactionBuilder($this, $name, $type);
}
public function beginTransactionWithBuilder(TransactionBuilder $builder): TransactionInterface
{
if (!$this->isRecording) {
return NoopTransaction::singletonInstance();
}
$newTransaction = new Transaction($builder);
if ($builder->asCurrent) {
if ($this->currentTransaction !== null) {
($loggerProxy = $this->logger->ifWarningLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Received request to begin a new current transaction'
. ' even though there is a current transaction that is still not ended',
['this' => $this]
);
}
$this->currentTransaction = $newTransaction;
$this->onNewCurrentTransactionHasBegun->callCallbacks($this->currentTransaction);
}
return $newTransaction;
}
/**
* @param TransactionBuilder $builder
* @param Closure $callback
*
* @return mixed The return value of $callback
*
* @template T
* @phpstan-param Closure(TransactionInterface): T $callback Callback to execute as the new transaction
* @phpstan-return T The return value of $callback
*/
public function captureTransactionWithBuilder(TransactionBuilder $builder, Closure $callback)
{
$newTransaction = $this->beginTransactionWithBuilder($builder);
try {
return $callback($newTransaction);
} catch (Throwable $throwable) {
$newTransaction->createErrorFromThrowable($throwable);
throw $throwable;
} finally {
$newTransaction->end();
}
}
/**
* @param PhpErrorData $phpErrorData
* @param ?Throwable $relatedThrowable
* @param int $numberOfStackFramesToSkip
*
* @return void
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*/
public function onPhpError(PhpErrorData $phpErrorData, ?Throwable $relatedThrowable, int $numberOfStackFramesToSkip): void
{
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Entered',
[
'phpErrorData' => $phpErrorData,
'relatedThrowable' => $relatedThrowable,
]
);
if ((error_reporting() & $phpErrorData->type) === 0) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Not creating error event because error_reporting() does not include its type',
['type' => $phpErrorData->type, 'error_reporting()' => error_reporting()]
);
return;
}
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Creating error event because error_reporting() includes its type...',
['type' => $phpErrorData->type, 'error_reporting()' => error_reporting()]
);
$customErrorData = new CustomErrorData();
$customErrorData->code = $phpErrorData->type;
if (
($phpErrorData->message === null)
|| ($phpErrorData->fileName === null)
|| TextUtil::contains($phpErrorData->message, $phpErrorData->fileName)
) {
$customErrorData->message = $phpErrorData->message;
} else {
$messageSuffix = ' in ' . $phpErrorData->fileName;
if ($phpErrorData->lineNumber !== null) {
$messageSuffix .= ':' . $phpErrorData->lineNumber;
}
$customErrorData->message = $phpErrorData->message . $messageSuffix;
}
if ($phpErrorData->type !== null) {
$customErrorData->type = PhpErrorUtil::getTypeName($phpErrorData->type);
}
$this->createError($customErrorData, $phpErrorData, $relatedThrowable, $numberOfStackFramesToSkip + 1);
}
/**
* @param ?CustomErrorData $customErrorData
* @param ?PhpErrorData $phpErrorData
* @param ?Throwable $throwable
* @param int $numberOfStackFramesToSkip
*
* @return ?string
*
* @phpstan-param 0|positive-int $numberOfStackFramesToSkip
*/
private function createError(?CustomErrorData $customErrorData, ?PhpErrorData $phpErrorData, ?Throwable $throwable, int $numberOfStackFramesToSkip): ?string
{
return $this->dispatchCreateError(ErrorExceptionData::build($this, $customErrorData, $phpErrorData, $throwable, $numberOfStackFramesToSkip + 1));
}
/** @inheritDoc */
public function createErrorFromThrowable(Throwable $throwable): ?string
{
return $this->createError(/* customErrorData: */ null, /* phpErrorData: */ null, $throwable, /* numberOfStackFramesToSkip */ 1);
}
/** @inheritDoc */
public function createCustomError(CustomErrorData $customErrorData): ?string
{
return $this->createError($customErrorData, /* phpErrorData: */ null, /* throwable: */ null, /* numberOfStackFramesToSkip */ 1);
}
private function dispatchCreateError(ErrorExceptionData $errorExceptionData): ?string
{
if ($this->currentTransaction === null) {
return $this->doCreateError($errorExceptionData, /* transaction */ null, /* span */ null);
}
return $this->currentTransaction->dispatchCreateError($errorExceptionData);
}
public function doCreateError(ErrorExceptionData $errorExceptionData, ?Transaction $transaction, ?Span $span): ?string
{
if (!$this->isRecording) {
return null;
}
if ($transaction !== null && ($transaction->numberOfErrorsSent >= $this->config->transactionMaxSpans())) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Starting to drop errors because of ' . OptionNames::TRANSACTION_MAX_SPANS . ' config',
[
'$transaction->numberOfErrorsSent' => $transaction->numberOfErrorsSent,
OptionNames::TRANSACTION_MAX_SPANS . ' config' => $this->config->transactionMaxSpans(),
]
);
return null;
}
$newError = Error::build(/* tracer: */ $this, $errorExceptionData, $transaction, $span);
$this->sendErrorToApmServer($newError);
if ($transaction !== null) {
++$transaction->numberOfErrorsSent;
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Error event has been sent',
['$transaction->numberOfErrorsSent' => $transaction->numberOfErrorsSent]
);
}
return $newError->id;
}
public function getClock(): ClockInterface
{
return $this->clock;
}
public function isNoop(): bool
{
return false;
}
public function limitString(string $val, bool $enforceKeywordString): string
{
return $enforceKeywordString ? self::limitKeywordString($val) : $this->limitNonKeywordString($val);
}
public static function limitKeywordString(string $keywordString): string
{
return TextUtil::ensureMaxLength($keywordString, Constants::KEYWORD_STRING_MAX_LENGTH);
}
public static function limitNullableKeywordString(?string $keywordString): ?string
{
if ($keywordString === null) {
return null;
}
return self::limitKeywordString($keywordString);
}
public function limitNonKeywordString(string $nonKeywordString): string
{
return TextUtil::ensureMaxLength($nonKeywordString, $this->config->nonKeywordStringMaxLength());
}
public function limitNullableNonKeywordString(?string $nonKeywordString): ?string
{
if ($nonKeywordString === null) {
return null;
}
return $this->limitNonKeywordString($nonKeywordString);
}
public function loggerFactory(): LoggerFactory
{
return $this->loggerFactory;
}
public function httpDistributedTracing(): HttpDistributedTracing
{
return $this->httpDistributedTracing;
}
public function stackTraceUtil(): StackTraceUtil
{
return $this->stackTraceUtil;
}
/** @inheritDoc */
public function pauseRecording(): void
{
$this->isRecording = false;
}
/** @inheritDoc */
public function resumeRecording(): void
{
$this->isRecording = true;
}
/** @inheritDoc */
public function isRecording(): bool
{
return $this->isRecording;
}
/**
* @param SpanToSendInterface[] $spans
* @param Error[] $errors
* @param ?BreakdownMetricsPerTransaction $breakdownMetricsPerTransaction
* @param ?Transaction $transaction
*/
private function sendEventsToApmServer(array $spans, array $errors, ?BreakdownMetricsPerTransaction $breakdownMetricsPerTransaction, ?Transaction $transaction): void
{
if ($this->config->devInternal()->dropEventAfterEnd()) {
($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
&& $loggerProxy->log(
'Dropping span because '
. OptionNames::DEV_INTERNAL . ' sub-option ' . DevInternalSubOptionNames::DROP_EVENT_AFTER_END
. ' is set'
);
return;
}
if ($this->cachedMetadata === null) {
$this->cachedMetadata = $this->metadataDiscoverer->discover();
}
$this->eventSink->consume(
$this->cachedMetadata,
$spans,
$errors,
$breakdownMetricsPerTransaction,
$transaction
);
}
public function sendSpanToApmServer(SpanToSendInterface $span): void
{
self::sendEventsToApmServer(
[$span] /* <- spans */,
[] /* <- errors */,
null /* <- breakdownMetricsPerTransaction */,
null /* <- transaction */
);
}
public function sendErrorToApmServer(Error $error): void
{
self::sendEventsToApmServer(
[] /* <- spans */,
[$error],
null /* <- breakdownMetricsPerTransaction */,
null /* <- transaction */
);
}
public function sendTransactionToApmServer(
?BreakdownMetricsPerTransaction $breakdownMetricsPerTransaction,
Transaction $transaction
): void {
self::sendEventsToApmServer(
[] /* <- spans */,
[] /* <- errors */,
$breakdownMetricsPerTransaction,
$transaction
);
}
/** @inheritDoc */
public function setAgentEphemeralId(?string $ephemeralId): void
{
$this->metadataDiscoverer->setAgentEphemeralId($ephemeralId);
}
/** @inheritDoc */
public function getSerializedCurrentDistributedTracingData(): string
{
/** @noinspection PhpDeprecationInspection */
$distTracingData = $this->currentTransaction !== null ? $this->currentTransaction->getDistributedTracingData() : null;
/** @noinspection PhpDeprecationInspection */
return $distTracingData !== null ? $distTracingData->serializeToString() : NoopDistributedTracingData::serializedToString();
}
/** @inheritDoc */
public function injectDistributedTracingHeaders(Closure $headerInjector): void
{
if ($this->currentTransaction === null) {
return;
}
/** @noinspection PhpDeprecationInspection */
$distTracingData = $this->currentTransaction->getDistributedTracingData();
if ($distTracingData !== null) {
$distTracingData->injectHeaders($headerInjector);
}
}
public function toLog(LogStreamInterface $stream): void
{
$result = [
'isRecording' => $this->isRecording,
'providedDependencies' => $this->providedDependencies,
'config' => $this->config,
];
if ($this->currentTransaction !== null) {
$result['currentTransactionId'] = $this->currentTransaction->getId();
}
$stream->toLogAs($result);
}
}