agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php (332 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\AutoInstrument;
use Closure;
use Elastic\Apm\Impl\GlobalTracerHolder;
use Elastic\Apm\Impl\Log\LoggableToString;
use Elastic\Apm\Impl\Tracer;
use Elastic\Apm\Impl\Util\ArrayUtil;
use Elastic\Apm\Impl\Util\Assert;
use Elastic\Apm\Impl\Util\DbgUtil;
use Elastic\Apm\Impl\Util\ElasticApmExtensionUtil;
use Elastic\Apm\Impl\Util\HiddenConstructorTrait;
use RuntimeException;
use Throwable;
/**
* Code in this file is part of implementation internals and thus it is not covered by the backward compatibility.
*
* @internal
*
* Called by elastic_apm extension
*
* @noinspection PhpUnused
*/
final class PhpPartFacade
{
/**
* Constructor is hidden because instance() should be used instead
*/
use HiddenConstructorTrait;
/** @var self|null */
private static $singletonInstance = null;
/** @var TransactionForExtensionRequest|null */
private $transactionForExtensionRequest = null;
/** @var InterceptionManager|null */
private $interceptionManager = null;
private function __construct(float $requestInitStartTime)
{
if (!ElasticApmExtensionUtil::isLoaded()) {
throw new RuntimeException(ElasticApmExtensionUtil::EXTENSION_NAME . ' extension is not loaded');
}
$tracer = self::buildTracer();
if ($tracer === null) {
BootstrapStageLogger::logDebug(
'Cutting bootstrap sequence short - tracing is disabled',
__LINE__,
__FUNCTION__
);
return;
}
$this->transactionForExtensionRequest = new TransactionForExtensionRequest($tracer, $requestInitStartTime);
$this->interceptionManager = new InterceptionManager($tracer);
}
/**
* Called by elastic_apm extension
*
* @noinspection PhpUnused
*
* @param int $maxEnabledLogLevel
* @param float $requestInitStartTime
*
* @return bool
*/
public static function bootstrap(int $maxEnabledLogLevel, float $requestInitStartTime): bool
{
BootstrapStageLogger::configure($maxEnabledLogLevel);
BootstrapStageLogger::logDebug(
'Starting bootstrap sequence...' . " maxEnabledLogLevel: $maxEnabledLogLevel",
__LINE__,
__FUNCTION__
);
if (self::$singletonInstance !== null) {
BootstrapStageLogger::logCritical(
'bootstrap() is called even though singleton instance is already created'
. ' (probably bootstrap() is called more than once)',
__LINE__,
__FUNCTION__
);
return false;
}
try {
self::$singletonInstance = new self($requestInitStartTime);
} catch (Throwable $throwable) {
BootstrapStageLogger::logCriticalThrowable(
$throwable,
'One of the steps in bootstrap sequence let a throwable escape',
__LINE__,
__FUNCTION__
);
return false;
}
BootstrapStageLogger::logDebug('Successfully completed bootstrap sequence', __LINE__, __FUNCTION__);
return true;
}
private static function singletonInstance(): self
{
if (self::$singletonInstance === null) {
throw new RuntimeException(
'Trying to use singleton instance that is not set'
. ' (probably either before call to bootstrap() or after failed call to bootstrap())'
);
}
return self::$singletonInstance;
}
/**
* Called by elastic_apm extension
*
* @noinspection PhpUnused
*
* @param int $interceptRegistrationId
* @param object|null $thisObj
* @param mixed ...$interceptedCallArgs
*
* @return bool
*/
public static function internalFuncCallPreHook(
int $interceptRegistrationId,
?object $thisObj,
...$interceptedCallArgs
): bool {
$interceptionManager = self::singletonInstance()->interceptionManager;
if ($interceptionManager === null) {
return false;
}
self::ensureHaveLatestDataDeferredByExtension();
return $interceptionManager->internalFuncCallPreHook(
$interceptRegistrationId,
$thisObj,
$interceptedCallArgs
);
}
/**
* Called by elastic_apm extension
*
* @noinspection PhpUnused
*
* @param bool $hasExitedByException
* @param mixed $returnValueOrThrown
*/
public static function internalFuncCallPostHook(bool $hasExitedByException, $returnValueOrThrown): void
{
$interceptionManager = self::singletonInstance()->interceptionManager;
assert($interceptionManager !== null);
self::ensureHaveLatestDataDeferredByExtension();
$interceptionManager->internalFuncCallPostHook(
1 /* <- $numberOfStackFramesToSkip */,
$hasExitedByException,
$returnValueOrThrown
);
}
/**
* @param string $dbgCallDesc
* @param Closure $implFunc
*
* @return void
*
* @phpstan-param Closure(self): void $implFunc
*/
private static function callAndSwallowThrowable(string $dbgCallDesc, Closure $implFunc): void
{
BootstrapStageLogger::logDebug(
'Starting to handle ' . $dbgCallDesc . ' call...',
__LINE__,
__FUNCTION__
);
if (self::$singletonInstance === null) {
BootstrapStageLogger::logWarning(
'Received ' . $dbgCallDesc . ' call but singleton instance is not created'
. ' (probably because bootstrap sequence failed)',
__LINE__,
__FUNCTION__
);
return;
}
try {
$implFunc(self::singletonInstance());
} catch (Throwable $throwable) {
BootstrapStageLogger::logCriticalThrowable(
$throwable,
'Handling ' . $dbgCallDesc
. ' call let a throwable escape - skipping the rest of the steps',
__LINE__,
__FUNCTION__
);
return;
}
BootstrapStageLogger::logDebug(
'Successfully finished handling ' . $dbgCallDesc . ' call...',
__LINE__,
__FUNCTION__
);
}
/**
* @param string $dbgCallDesc
* @param Closure $implFunc
*
* @return void
*
* @phpstan-param Closure(TransactionForExtensionRequest): void $implFunc
*/
private static function callWithTransactionForExtensionRequest(string $dbgCallDesc, Closure $implFunc): void
{
self::callAndSwallowThrowable(
$dbgCallDesc,
function (PhpPartFacade $singletonInstance) use ($implFunc): void {
if ($singletonInstance->transactionForExtensionRequest === null) {
BootstrapStageLogger::logDebug(
'Received call but transactionForExtensionRequest is null'
. ' - just returning...',
__LINE__,
__FUNCTION__
);
return;
}
$implFunc($singletonInstance->transactionForExtensionRequest);
}
);
}
public static function ensureHaveLatestDataDeferredByExtension(): void
{
self::callWithTransactionForExtensionRequest(
__FUNCTION__,
function (TransactionForExtensionRequest $transactionForExtensionRequest): void {
self::ensureHaveLastErrorData($transactionForExtensionRequest);
}
);
}
private static function ensureHaveLastErrorData(
TransactionForExtensionRequest $transactionForExtensionRequest
): void {
if (!$transactionForExtensionRequest->getConfig()->captureErrors()) {
return;
}
/**
* The last thrown should be fetched before last PHP error because if the error is for "Uncaught Exception"
* agent will use the last thrown exception
*/
self::ensureHaveLastThrown($transactionForExtensionRequest);
self::ensureHaveLastPhpError($transactionForExtensionRequest);
}
private static function ensureHaveLastThrown(TransactionForExtensionRequest $transactionForExtensionRequest): void
{
/**
* elastic_apm_* functions are provided by the elastic_apm extension
*
* @var mixed $lastThrown
*
* @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection
* @phpstan-ignore-next-line
*/
$lastThrown = \elastic_apm_get_last_thrown();
if ($lastThrown === null) {
return;
}
$transactionForExtensionRequest->setLastThrown($lastThrown);
}
/**
* @param string $expectedType
* @param mixed $actualValue
*
* @return void
*/
private static function logUnexpectedType(string $expectedType, $actualValue): void
{
BootstrapStageLogger::logCritical(
'Actual type does not match the expected type'
. '; ' . 'expected type: ' . $expectedType
. ', ' . 'actual type: ' . DbgUtil::getType($actualValue)
. ', ' . 'actual value: ' . LoggableToString::convert($actualValue),
__LINE__,
__FUNCTION__
);
}
/**
* @param string $expectedKey
* @param array<array-key, mixed> $actualArray
*
* @return bool
*/
private static function verifyKeyExists(string $expectedKey, array $actualArray): bool
{
if (array_key_exists($expectedKey, $actualArray)) {
return true;
}
BootstrapStageLogger::logCritical(
'Expected key does not exist'
. '; ' . 'expected key: ' . $expectedKey
. ', ' . 'actual array keys: ' . json_encode(array_keys($actualArray)),
__LINE__,
__FUNCTION__
);
return false;
}
/**
* @param array<array-key, mixed> $dataFromExt
* @param string $key
*
* @return ?int
*/
private static function getIntFromPhpErrorData(array $dataFromExt, string $key): ?int
{
if (!self::verifyKeyExists($key, $dataFromExt)) {
return null;
}
$value = $dataFromExt[$key];
if (!is_int($value)) {
self::logUnexpectedType('int', $value);
return null;
}
return $value;
}
/**
* @param array<array-key, mixed> $dataFromExt
* @param string $key
*
* @return ?string
*/
private static function getNullableStringFromPhpErrorData(array $dataFromExt, string $key): ?string
{
if (!self::verifyKeyExists($key, $dataFromExt)) {
return null;
}
$value = $dataFromExt[$key];
if (!($value === null || is_string($value))) {
self::logUnexpectedType('string|null', $value);
return null;
}
return $value;
}
/**
* @param array<array-key, mixed> $dataFromExt
* @param string $key
*
* @return null|array<string, mixed>[]
*/
private static function getStackTraceFromPhpErrorData(array $dataFromExt, string $key): ?array
{
if (!self::verifyKeyExists($key, $dataFromExt)) {
return null;
}
$stackTrace = $dataFromExt[$key];
if (!is_array($stackTrace)) {
self::logUnexpectedType('array', $stackTrace);
return null;
}
if (!ArrayUtil::isList($stackTrace)) {
BootstrapStageLogger::logCritical(
'Stack trace array should be a list but it is not'
. '; ' . 'stackTrace keys: ' . json_encode(array_keys($stackTrace))
. ', ' . 'stackTrace: ' . LoggableToString::convert($stackTrace),
__LINE__,
__FUNCTION__
);
return null;
}
/** @var array<string, mixed>[] $stackTrace */
return $stackTrace;
}
/**
* @param array<array-key, mixed> $dataFromExt
*
* @return PhpErrorData
*/
private static function buildPhpErrorData(array $dataFromExt): PhpErrorData
{
$result = new PhpErrorData();
$result->type = self::getIntFromPhpErrorData($dataFromExt, 'type');
$result->fileName = self::getNullableStringFromPhpErrorData($dataFromExt, 'fileName');
$result->lineNumber = self::getIntFromPhpErrorData($dataFromExt, 'lineNumber');
$result->message = self::getNullableStringFromPhpErrorData($dataFromExt, 'message');
$result->stackTrace = self::getStackTraceFromPhpErrorData($dataFromExt, 'stackTrace');
return $result;
}
private static function ensureHaveLastPhpError(TransactionForExtensionRequest $transactionForExtensionRequest): void
{
/**
* elastic_apm_* functions are provided by the elastic_apm extension
*
* @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection
* @phpstan-ignore-next-line
*/
$lastPhpErrorData = \elastic_apm_get_last_php_error();
if ($lastPhpErrorData === null) {
return;
}
if (is_array($lastPhpErrorData)) {
BootstrapStageLogger::logDebug(
'Type of value returned by elastic_apm_get_last_php_error(): ' . DbgUtil::getType($lastPhpErrorData),
__LINE__,
__FUNCTION__
);
} else {
BootstrapStageLogger::logCritical(
'Value returned by elastic_apm_get_last_php_error() is not an array'
. ', ' . 'returned value type: ' . DbgUtil::getType($lastPhpErrorData)
. ', ' . 'returned value: ' . $lastPhpErrorData,
__LINE__,
__FUNCTION__
);
return;
}
/** @var array<array-key, mixed> $lastPhpErrorData */
$transactionForExtensionRequest->onPhpError(self::buildPhpErrorData($lastPhpErrorData));
}
/**
* Called by elastic_apm extension
*
* @noinspection PhpUnused
*/
public static function shutdown(): void
{
self::callWithTransactionForExtensionRequest(
__FUNCTION__,
function (TransactionForExtensionRequest $transactionForExtensionRequest): void {
$transactionForExtensionRequest->onShutdown();
}
);
self::$singletonInstance = null;
}
/**
* @return Tracer|null
*/
private static function buildTracer(): ?Tracer
{
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that(!GlobalTracerHolder::isValueSet())
&& $assertProxy->withContext(
'!GlobalTracerHolder::isSet()',
['GlobalTracerHolder::get()' => GlobalTracerHolder::getValue()]
);
$tracer = GlobalTracerHolder::getValue();
if ($tracer->isNoop()) {
return null;
}
($assertProxy = Assert::ifEnabled())
&& $assertProxy->that($tracer instanceof Tracer)
&& $assertProxy->withContext('$tracer instanceof Tracer', ['get_class($tracer)' => get_class($tracer)]);
assert($tracer instanceof Tracer);
return $tracer;
}
/**
* Called by elastic_apm extension
*
* @noinspection PhpUnused
*/
public static function emptyMethod(): void
{
}
/**
* Calls to this method are inserted by AST instrumentation.
* See src/ext/WordPress_instrumentation.c
*
* @noinspection PhpUnused
*
* @param ?string $instrumentedClassFullName
* @param string $instrumentedFunction
* @param mixed[] $capturedArgs
*
* @return null|callable(?Throwable $thrown, mixed $returnValue): void
*/
public static function astInstrumentationPreHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable
{
return (($interceptionManager = self::singletonInstance()->interceptionManager) !== null)
? $interceptionManager->astInstrumentationPreHook($instrumentedClassFullName, $instrumentedFunction, $capturedArgs)
: null;
}
/**
* Calls to this method are inserted by AST instrumentation.
* See src/ext/WordPress_instrumentation.c
*
* @noinspection PhpUnused
*/
public static function astInstrumentationDirectCall(string $method): void
{
if (($interceptionManager = self::singletonInstance()->interceptionManager) !== null) {
$interceptionManager->astInstrumentationDirectCall($method);
}
}
}