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); } }