diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 8b20142f4..4346c8116 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -28,4 +28,7 @@
*/polyfills/Stringable.php
*/polyfills/WeakMap.php
+
+ */tests/ElasticApmTests/ComponentTests/WordPress/mock_src/*.php
+ */tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/*.php
diff --git a/phpstan.neon b/phpstan.neon
index 72356fe70..7d58c1f03 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -10,6 +10,8 @@ parameters:
excludePaths:
- tests/polyfills/Stringable.php
- tests/polyfills/WeakMap.php
+ - tests/ElasticApmTests/ComponentTests/WordPress/mock_src/*.php
+ - tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/*.php
ignoreErrors:
#
diff --git a/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php b/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php
index 54fbf06f9..918bf8c23 100644
--- a/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php
+++ b/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php
@@ -55,6 +55,14 @@ public function isEnabled(?string &$reason = null): bool
return false;
}
+ $isUserlandCodeInstrumentationEnabled = $this->tracer->getConfig()->astProcessEnabled();
+ if ($this->requiresUserlandCodeInstrumentation() && (!$isUserlandCodeInstrumentationEnabled)) {
+ $reason = 'Instrumentation ' . $this->name() . ' needs userland code instrumentation'
+ . ' but AST-process is the only currently supported mechanism to instrument userland code and it is DISABLED'
+ . ' (via ' . OptionNames::AST_PROCESS_ENABLED . ' configuration option)';
+ return false;
+ }
+
$disabledInstrumentationsMatcher = $this->tracer->getConfig()->disableInstrumentations();
if ($disabledInstrumentationsMatcher === null) {
return true;
@@ -85,6 +93,14 @@ public function requiresAttachContextToExternalObjects(): bool
return false;
}
+ /**
+ * @return bool
+ */
+ public function requiresUserlandCodeInstrumentation(): bool
+ {
+ return false;
+ }
+
/**
* @return string[]
*/
diff --git a/src/ElasticApm/Impl/AutoInstrument/Autoloader.php b/src/ElasticApm/Impl/AutoInstrument/Autoloader.php
index 620e60186..0e8d864c0 100644
--- a/src/ElasticApm/Impl/AutoInstrument/Autoloader.php
+++ b/src/ElasticApm/Impl/AutoInstrument/Autoloader.php
@@ -83,7 +83,25 @@ public static function autoloadCodeForClass(string $fqClassName): void
__LINE__,
__FUNCTION__
);
+
+ /**
+ * elastic_apm_* functions are provided by the elastic_apm extension
+ *
+ * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection
+ * @phpstan-ignore-next-line
+ */
+ \elastic_apm_before_loading_agent_php_code();
+
require $classSrcFileAbsolute;
+
+ /**
+ * elastic_apm_* functions are provided by the elastic_apm extension
+ *
+ * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection
+ * @phpstan-ignore-next-line
+ */
+ \elastic_apm_after_loading_agent_php_code();
+
BootstrapStageLogger::logTrace(
"After require `$classSrcFileAbsolute' ...",
__LINE__,
diff --git a/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php b/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php
index a0d3fa45d..6284b7cdc 100644
--- a/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php
+++ b/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php
@@ -32,6 +32,9 @@
*/
final class BuiltinPlugin extends PluginBase
{
+ /** @var ?WordPressAutoInstrumentation */
+ private $wordPressAutoInstrumentationIfEnabled;
+
public function __construct(Tracer $tracer)
{
parent::__construct(
@@ -42,10 +45,21 @@ public function __construct(Tracer $tracer)
new MySQLiAutoInstrumentation($tracer),
]
);
+
+ $wordPressAutoInstrumentation = new WordPressAutoInstrumentation($tracer);
+ $this->wordPressAutoInstrumentationIfEnabled =
+ $this->checkIfInstrumentationEnabled($wordPressAutoInstrumentation)
+ ? $wordPressAutoInstrumentation
+ : null;
}
public function getDescription(): string
{
return 'BUILT-IN';
}
+
+ public function getWordPressAutoInstrumentationIfEnabled(): ?WordPressAutoInstrumentation
+ {
+ return $this->wordPressAutoInstrumentationIfEnabled;
+ }
}
diff --git a/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php b/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php
index 938532455..18dcaf42c 100644
--- a/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php
+++ b/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php
@@ -37,4 +37,5 @@ final class InstrumentationNames
public const CURL = 'curl';
public const PDO = 'pdo';
public const MYSQLI = 'mysqli';
+ public const WORDPRESS = 'wordpress';
}
diff --git a/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php b/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php
index f7a1f95a2..6f795d796 100644
--- a/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php
+++ b/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php
@@ -27,6 +27,7 @@
use Elastic\Apm\Impl\Log\Logger;
use Elastic\Apm\Impl\Tracer;
use Elastic\Apm\Impl\Util\ArrayUtil;
+use Elastic\Apm\Impl\Util\ClassNameUtil;
use Elastic\Apm\Impl\Util\DbgUtil;
use Throwable;
@@ -43,6 +44,9 @@ final class InterceptionManager
/** @var Logger */
private $logger;
+ /** @var BuiltinPlugin */
+ private $builtinPlugin;
+
/** @var int|null */
private $interceptedCallInProgressRegistrationId = null;
@@ -64,20 +68,12 @@ public function __construct(Tracer $tracer)
private function loadPlugins(Tracer $tracer): void
{
+ $this->builtinPlugin = new BuiltinPlugin($tracer);
$registerCtx = new RegistrationContext();
- $this->loadPluginsImpl($tracer, $registerCtx);
-
- $this->interceptedCallRegistrations = $registerCtx->interceptedCallRegistrations;
- }
-
- private function loadPluginsImpl(Tracer $tracer, RegistrationContext $registerCtx): void
- {
- $builtinPlugin = new BuiltinPlugin($tracer);
$registerCtx->dbgCurrentPluginIndex = 0;
- $registerCtx->dbgCurrentPluginDesc = $builtinPlugin->getDescription();
- $builtinPlugin->register($registerCtx);
-
- // self::loadConfiguredPlugins();
+ $registerCtx->dbgCurrentPluginDesc = $this->builtinPlugin->getDescription();
+ $this->builtinPlugin->register($registerCtx);
+ $this->interceptedCallRegistrations = $registerCtx->interceptedCallRegistrations;
}
/**
@@ -101,8 +97,9 @@ public function internalFuncCallPreHook(
'interceptedCallArgs' => $this->logger->possiblySecuritySensitive($interceptedCallArgs),
]
);
- ($loggerProxy = $localLogger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
- && $loggerProxy->log('Entered');
+ $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__);
+
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered');
$interceptRegistration
= ArrayUtil::getValueIfKeyExistsElse($interceptRegistrationId, $this->interceptedCallRegistrations, null);
@@ -113,8 +110,7 @@ public function internalFuncCallPreHook(
}
$localLogger->addContext('interceptRegistration', $interceptRegistration);
- ($loggerProxy = $localLogger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
- && $loggerProxy->log('Calling preHook...');
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling preHook...');
try {
$preHookRetVal = ($interceptRegistration->preHook)($thisObj, $interceptedCallArgs);
} catch (Throwable $throwable) {
@@ -133,8 +129,7 @@ public function internalFuncCallPreHook(
$this->interceptedCallInProgressPreHookRetVal = $preHookRetVal;
}
- ($loggerProxy = $localLogger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
- && $loggerProxy->log('preHook completed successfully', ['shouldCallPostHook' => $shouldCallPostHook]);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'preHook completed successfully', ['shouldCallPostHook' => $shouldCallPostHook]);
return $shouldCallPostHook;
}
@@ -149,8 +144,14 @@ public function internalFuncCallPostHook(
bool $hasExitedByException,
$returnValueOrThrown
): void {
- ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
- && $loggerProxy->log('Entered');
+ $localLogger = $this->logger->inherit()->addAllContext(
+ [
+ 'interceptRegistrationId' => $this->interceptedCallInProgressRegistrationId,
+ 'interceptRegistration' => $this->interceptedCallInProgressRegistration,
+ ]
+ );
+ $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered');
if ($this->interceptedCallInProgressRegistrationId === null) {
($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
@@ -160,28 +161,92 @@ public function internalFuncCallPostHook(
assert($this->interceptedCallInProgressRegistration !== null);
assert($this->interceptedCallInProgressPreHookRetVal !== null);
- $localLogger = $this->logger->inherit()->addAllContext(
- [
- 'interceptRegistrationId' => $this->interceptedCallInProgressRegistrationId,
- 'interceptRegistration' => $this->interceptedCallInProgressRegistration,
- ]
- );
-
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling postHook...');
try {
($this->interceptedCallInProgressPreHookRetVal)(
$numberOfStackFramesToSkip + 1,
$hasExitedByException,
$returnValueOrThrown
);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'postHook completed without throwing');
} catch (Throwable $throwable) {
($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
- && $loggerProxy->logThrowable(
- $throwable,
- 'postHook has let a Throwable to escape'
- );
+ && $loggerProxy->logThrowable($throwable, 'postHook has thrown');
}
$this->interceptedCallInProgressRegistrationId = null;
$this->interceptedCallInProgressPreHookRetVal = null;
}
+
+ public function astInstrumentationDirectCall(string $method): void
+ {
+ $localLogger = $this->logger->inherit()->addAllContext(['method' => $method]);
+
+ $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered');
+
+ $wordPressAutoInstrumIfEnabled = $this->builtinPlugin->getWordPressAutoInstrumentationIfEnabled();
+ if ($wordPressAutoInstrumIfEnabled === null) {
+ static $loggedOnce = false;
+ if (!$loggedOnce) {
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'WordPress instrumentation is DISABLED');
+ $loggedOnce = true;
+ }
+ return;
+ }
+
+ static $dbgImplFuncDesc = null;
+ if ($dbgImplFuncDesc === null) {
+ $dbgImplFuncDesc = ClassNameUtil::fqToShort(WordPressAutoInstrumentation::class) . '->directCall';
+ }
+ /** @var string $dbgImplFuncDesc */
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling ' . $dbgImplFuncDesc . '...');
+ try {
+ $wordPressAutoInstrumIfEnabled->directCall($method);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, $dbgImplFuncDesc . ' completed without throwing');
+ } catch (Throwable $throwable) {
+ ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($throwable, $dbgImplFuncDesc . ' has thrown');
+ }
+ }
+
+ /**
+ * @param ?string $instrumentedClassFullName
+ * @param string $instrumentedFunction
+ * @param mixed[] $capturedArgs
+ *
+ * @return null|callable(?Throwable $thrown, mixed $returnValue): void
+ */
+ public function astInstrumentationPreHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable
+ {
+ $localLogger = $this->logger->inherit()->addAllContext(['instrumentedClassFullName' => $instrumentedClassFullName]);
+
+ $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered');
+
+ $wordPressAutoInstrumIfEnabled = $this->builtinPlugin->getWordPressAutoInstrumentationIfEnabled();
+ if ($wordPressAutoInstrumIfEnabled === null) {
+ static $loggedOnce = false;
+ if (!$loggedOnce) {
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'WordPress instrumentation is DISABLED');
+ $loggedOnce = true;
+ }
+ return null;
+ }
+
+ static $dbgImplFuncDesc = null;
+ if ($dbgImplFuncDesc === null) {
+ $dbgImplFuncDesc = ClassNameUtil::fqToShort(WordPressAutoInstrumentation::class) . '->preHook';
+ }
+ /** @var string $dbgImplFuncDesc */
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling ' . $dbgImplFuncDesc . '...');
+ try {
+ $retVal = $wordPressAutoInstrumIfEnabled->preHook($instrumentedClassFullName, $instrumentedFunction, $capturedArgs);
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, $dbgImplFuncDesc . ' completed without throwing', ['retVal == null' => ($retVal == null)]);
+ return $retVal;
+ } catch (Throwable $throwable) {
+ ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->logThrowable($throwable, $dbgImplFuncDesc . ' has thrown');
+ return null;
+ }
+ }
}
diff --git a/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php b/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php
index ca37610b2..d5043bf3f 100644
--- a/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php
+++ b/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php
@@ -515,4 +515,36 @@ private static function buildTracer(): ?Tracer
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);
+ }
+ }
}
diff --git a/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php b/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php
index 7a50fddeb..fbda2d549 100644
--- a/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php
+++ b/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php
@@ -59,18 +59,55 @@ public static function buildSpanNameFromCall(?string $className, string $funcNam
return ($className === null) ? $funcName : ($className . '->' . $funcName);
}
- public static function beginCurrentSpan(string $name, string $type, ?string $subtype = null, ?string $action = null): SpanInterface
+ private static function processNewSpan(SpanInterface $span): void
{
- $span = ElasticApm::getCurrentTransaction()->beginCurrentSpan($name, $type, $subtype, $action);
-
if ($span instanceof Span) {
// Mark all spans created by auto-instrumentation as compressible
$span->setCompressible(true);
}
+ }
+
+ public static function beginCurrentSpan(string $name, string $type, ?string $subtype = null, ?string $action = null): SpanInterface
+ {
+ $span = ElasticApm::getCurrentTransaction()->beginCurrentSpan($name, $type, $subtype, $action);
+
+ self::processNewSpan($span);
return $span;
}
+ /**
+ * @template T
+ *
+ * @param string $name New span's name
+ * @param string $type New span's type
+ * @param Closure(SpanInterface $newSpan): T $callback
+ * @param string|null $subtype New span's subtype
+ * @param string|null $action New span's action
+ * @param float|null $timestamp Start time of the new span
+ *
+ * @return T The return value of $callback
+ */
+ public static function captureCurrentSpan(string $name, string $type, Closure $callback, ?string $subtype = null, ?string $action = null, ?float $timestamp = null)
+ {
+ return ElasticApm::getCurrentTransaction()->captureCurrentSpan(
+ $name,
+ $type,
+ /**
+ * @param SpanInterface $newSpan
+ *
+ * @return T The return value of $callback
+ */
+ function (SpanInterface $newSpan) use ($callback) {
+ self::processNewSpan($newSpan);
+ return $callback($newSpan);
+ },
+ $subtype,
+ $action,
+ $timestamp
+ );
+ }
+
/**
* @param int $numberOfStackFramesToSkip
* @param SpanInterface $span
@@ -264,6 +301,19 @@ public function verifyInstanceOf(string $expectedClass, $actualValue, ?string $d
return true;
}
+ /**
+ * @param mixed $actualValue
+ * @param bool $shouldCheckSyntaxOnly
+ * @param ?string $dbgParamName
+ *
+ * @return bool
+ */
+ public function verifyIsCallable($actualValue, bool $shouldCheckSyntaxOnly, ?string $dbgParamName = null): bool
+ {
+ $isCallable = is_callable($actualValue, $shouldCheckSyntaxOnly);
+ return $this->verifyType($isCallable, 'callable', $actualValue, $dbgParamName);
+ }
+
/**
* @param int $expectedMinArgsCount
* @param mixed[] $interceptedCallArgs
@@ -287,4 +337,28 @@ public function verifyMinArgsCount(int $expectedMinArgsCount, array $intercepted
);
return false;
}
+
+ /**
+ * @param int $expectedArgsCount
+ * @param mixed[] $interceptedCallArgs
+ *
+ * @return bool
+ */
+ public function verifyExactArgsCount(int $expectedArgsCount, array $interceptedCallArgs): bool
+ {
+ if (count($interceptedCallArgs) === $expectedArgsCount) {
+ return true;
+ }
+
+ ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log(
+ 'Actual number of arguments does not equal the expected number',
+ [
+ 'expected number of arguments' => $expectedArgsCount,
+ 'actual number of arguments' => count($interceptedCallArgs),
+ 'actual arguments' => $this->logger->possiblySecuritySensitive($interceptedCallArgs),
+ ]
+ );
+ return false;
+ }
}
diff --git a/src/ElasticApm/Impl/AutoInstrument/WordPressAutoInstrumentation.php b/src/ElasticApm/Impl/AutoInstrument/WordPressAutoInstrumentation.php
new file mode 100644
index 000000000..a13a2e852
--- /dev/null
+++ b/src/ElasticApm/Impl/AutoInstrument/WordPressAutoInstrumentation.php
@@ -0,0 +1,530 @@
+ self::CALLBACK_GROUP_KIND_PLUGIN,
+ self::WORDPRESS_MU_PLUGINS_SUBDIR_SUBPATH => self::CALLBACK_GROUP_KIND_PLUGIN,
+ self::WORDPRESS_THEMES_SUBDIR_SUBPATH => self::CALLBACK_GROUP_KIND_THEME,
+ ];
+
+ /**
+ * \ELASTIC_APM_* constants are provided by the elastic_apm extension
+ *
+ * @phpstan-ignore-next-line
+ */
+ private const DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS = \ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS;
+
+ /** @var LoggerFactory */
+ private $loggerFactory;
+
+ /** @var Logger */
+ private $logger;
+
+ /** @var AutoInstrumentationUtil */
+ private $util;
+
+ /** @var bool */
+ private $isInFailedMode = false;
+
+ /** @var bool */
+ private $isReadyToWrapFilterCallbacks = false;
+
+ public function __construct(Tracer $tracer)
+ {
+ parent::__construct($tracer);
+
+ $this->loggerFactory = $tracer->loggerFactory();
+ $this->logger = $this->loggerFactory->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)->addContext('this', $this);
+
+ $this->util = new AutoInstrumentationUtil($tracer->loggerFactory());
+ }
+
+ /** @inheritDoc */
+ public function name(): string
+ {
+ return InstrumentationNames::WORDPRESS;
+ }
+
+ /** @inheritDoc */
+ public function keywords(): array
+ {
+ return [];
+ }
+
+ public function register(RegistrationContextInterface $ctx): void
+ {
+ }
+
+ /** @inheritDoc */
+ public function requiresUserlandCodeInstrumentation(): bool
+ {
+ return false;
+ }
+
+ private function switchToFailedMode(): void
+ {
+ if ($this->isInFailedMode) {
+ return;
+ }
+
+ ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->includeStackTrace()->log('Switching to FAILED mode');
+ $this->isInFailedMode = true;
+ }
+
+ private function setReadyToWrapFilterCallbacks(): void
+ {
+ $this->isReadyToWrapFilterCallbacks = true;
+ }
+
+ /**
+ * @param string $filePath
+ * @param LoggerFactory $loggerFactory
+ * @param ?string $groupKind
+ * @param ?string $groupName
+ *
+ * @return void
+ *
+ * @param-out string $groupKind
+ * @param-out ?string $groupName
+ */
+ public static function findAddonInfoFromFilePath(string $filePath, LoggerFactory $loggerFactory, /* out */ ?string &$groupKind, /* out */ ?string &$groupName): void
+ {
+ $logger = null;
+ $loggerProxyTrace = null;
+ if ($loggerFactory->isEnabledForLevel(Level::TRACE)) {
+ $logger = $loggerFactory->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)->addContext('filePath', $filePath);
+ $loggerProxyTrace = $logger->ifTraceLevelEnabledNoLine(__FUNCTION__);
+ }
+
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered');
+
+ $groupKind = self::CALLBACK_GROUP_KIND_CORE;
+ $groupName = null;
+
+ $currentGroupKind = null;
+ /** @var ?int $posAfterAddonsSubDir */
+ $posAfterAddonsSubDir = null;
+ foreach (self::CALLBACK_SUBDIR_SUBPATH_TO_GROUP_KIND as $subDirSubPath => $currentGroupKind) {
+ $pluginsSubDirPos = strpos($filePath, $subDirSubPath);
+ if ($pluginsSubDirPos !== false) {
+ $posAfterAddonsSubDir = $pluginsSubDirPos + strlen($subDirSubPath);
+ break;
+ }
+ }
+ if ($posAfterAddonsSubDir === null) {
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Not found any of the known sub-paths in the given path');
+ return;
+ }
+ $logger && $logger->addContext('posAfterAddonsSubDir', $posAfterAddonsSubDir);
+
+ $dirSeparatorAfterPluginPos = strpos($filePath, DIRECTORY_SEPARATOR, $posAfterAddonsSubDir);
+ if ($dirSeparatorAfterPluginPos !== false && $dirSeparatorAfterPluginPos > $posAfterAddonsSubDir) {
+ $groupKind = $currentGroupKind;
+ $groupName = substr($filePath, $posAfterAddonsSubDir, $dirSeparatorAfterPluginPos - $posAfterAddonsSubDir);
+ return;
+ }
+
+ $fileExtAfterPluginPos = strpos($filePath, '.php', $posAfterAddonsSubDir);
+ if ($fileExtAfterPluginPos !== false && $fileExtAfterPluginPos > $posAfterAddonsSubDir) {
+ $groupKind = $currentGroupKind;
+ $groupName = substr($filePath, $posAfterAddonsSubDir, $fileExtAfterPluginPos - $posAfterAddonsSubDir);
+ return;
+ }
+
+ $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Found one of the known sub-paths but the suffix is not as expected');
+ }
+
+ /**
+ * @param Closure|string $callback
+ * @param Logger $logger
+ *
+ * @return ?string
+ *
+ * @throws ReflectionException
+ */
+ private static function getCallbackSourceFilePathImplForFunc($callback, Logger $logger): ?string
+ {
+ $reflectFunc = new ReflectionFunction($callback);
+ if (($srcFilePath = $reflectFunc->getFileName()) === false) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Failed to get file name from ReflectionFunction of captured callback');
+ return null;
+ }
+ return $srcFilePath;
+ }
+
+ /**
+ * @param object|string $classInstanceOrName
+ * @param Logger $logger
+ *
+ * @return ?string
+ *
+ * @throws ReflectionException
+ */
+ private static function getCallbackSourceFilePathImplForClass($classInstanceOrName, Logger $logger): ?string
+ {
+ /** @var object|class-string $classInstanceOrName */
+ $reflectClass = new ReflectionClass($classInstanceOrName);
+ if (($srcFilePath = $reflectClass->getFileName()) === false) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('Failed to get file name from ReflectionClass of captured callback', ['classInstanceOrName' => $classInstanceOrName]);
+ return null;
+ }
+ return $srcFilePath;
+ }
+
+ /**
+ * @param mixed $callback
+ * @param Logger $logger
+ *
+ * @return ?string
+ *
+ * @throws ReflectionException
+ */
+ private static function getCallbackSourceFilePathImpl($callback, Logger $logger): ?string
+ {
+ // If callback is a Closure or string but not 'Class::method'
+ if ($callback instanceof Closure) {
+ return self::getCallbackSourceFilePathImplForFunc($callback, $logger);
+ }
+
+ // If callback is a string but not 'Class::method'
+ if (is_string($callback)) {
+ if (($afterClassNamePos = strpos($callback, '::')) === false) {
+ return self::getCallbackSourceFilePathImplForFunc($callback, $logger);
+ }
+ $className = substr($callback, /* offset */ 0, /* length */ $afterClassNamePos);
+ return self::getCallbackSourceFilePathImplForClass($className, $logger);
+ }
+
+ if (!is_array($callback)) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('callback of unexpected type');
+ return null;
+ }
+
+ if (ArrayUtil::isEmpty($callback)) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('callback is an empty array');
+ return null;
+ }
+
+ $firstElement = $callback[0];
+
+ if (is_string($firstElement) || is_object($firstElement)) {
+ return self::getCallbackSourceFilePathImplForClass($firstElement, $logger);
+ }
+
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('callback is an array but its first element is of unexpected type', ['firstElement type' => DbgUtil::getType($firstElement), 'firstElement' => $firstElement]);
+ return null;
+ }
+
+ /**
+ * @param mixed $callback
+ * @param LoggerFactory $loggerFactory
+ *
+ * @return ?string
+ */
+ public static function getCallbackSourceFilePath($callback, LoggerFactory $loggerFactory): ?string
+ {
+ $logger = $loggerFactory->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)
+ ->addAllContext(['callback type' => DbgUtil::getType($callback), 'callback' => $callback]);
+
+ try {
+ return self::getCallbackSourceFilePathImpl($callback, $logger);
+ } catch (ReflectionException $e) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($e, 'Failed to reflect captured callback');
+ return null;
+ }
+ }
+
+ /**
+ * @param mixed $callback
+ * @param-out string $groupKind
+ * @param-out ?string $groupName
+ */
+ private function findCallbackInfo($callback, /* out */ ?string &$groupKind, /* out */ ?string &$groupName): void
+ {
+ if (($srcFilePath = self::getCallbackSourceFilePath($callback, $this->loggerFactory)) === null) {
+ $groupKind = self::CALLBACK_GROUP_KIND_CORE;
+ $groupName = null;
+ return;
+ }
+
+ self::findAddonInfoFromFilePath($srcFilePath, $this->loggerFactory, /* out */ $groupKind, /* out */ $groupName);
+ }
+
+ public function directCall(string $method): void
+ {
+ if ($this->isInFailedMode) {
+ return;
+ }
+
+ $logger = $this->logger->inherit()->addAllContext(['method' => $method]);
+
+ switch ($method) {
+ case self::DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS:
+ $this->setReadyToWrapFilterCallbacks();
+ return;
+
+ default:
+ ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected method');
+ $this->switchToFailedMode();
+ }
+ }
+
+ /**
+ * @param ?string $instrumentedClassFullName
+ * @param string $instrumentedFunction
+ * @param mixed[] $capturedArgs
+ *
+ * @return null|callable(?Throwable $thrown, mixed $returnValue): void
+ */
+ public function preHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable
+ {
+ if ($this->isInFailedMode) {
+ return null /* <- null means there is no post-hook */;
+ }
+
+ $logger = $this->logger->inherit()->addAllContext(
+ ['instrumentedClassFullName' => $instrumentedClassFullName, 'instrumentedFunction' => $instrumentedFunction, 'capturedArgs' => $capturedArgs]
+ );
+
+ // We should cover all the function instrumented in src/ext/WordPress_instrumentation.c
+
+ if ($instrumentedClassFullName !== null) {
+ if ($instrumentedClassFullName !== 'WP_Hook') {
+ ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected instrumentedClassFullName');
+ $this->switchToFailedMode();
+ return null /* <- null means there is no post-hook */;
+ }
+ if ($instrumentedFunction !== 'add_filter') {
+ ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected instrumentedFunction');
+ $this->switchToFailedMode();
+ return null /* <- null means there is no post-hook */;
+ }
+
+ $this->preHookAddFilter($capturedArgs);
+ return null /* <- null means there is no post-hook */;
+ }
+
+ switch ($instrumentedFunction) {
+ case '_wp_filter_build_unique_id':
+ $this->preHookWpFilterBuildUniqueId($capturedArgs);
+ return null /* <- null means there is no post-hook */;
+ case 'get_template':
+ /**
+ * @param ?Throwable $thrown
+ * @param mixed $returnValue
+ */
+ return function (?Throwable $thrown, $returnValue): void {
+ $this->postHookGetTemplate($thrown, $returnValue);
+ };
+ default:
+ ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected instrumentedFunction');
+ $this->switchToFailedMode();
+ }
+ return null /* <- null means there is no post-hook */;
+ }
+
+ /**
+ * @param mixed[] $capturedArgs
+ *
+ * @return void
+ */
+ private function preHookAddFilter(array $capturedArgs): void
+ {
+ if (!$this->isReadyToWrapFilterCallbacks) {
+ static $isFirstTime = true;
+ if ($isFirstTime) {
+ ($loggerProxy = $this->logger->ifWarningLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('First attempt to wrap callback but it is not ready yet');
+ $isFirstTime = false;
+ }
+ return;
+ }
+
+ if (!$this->preHookAddFilterImpl($capturedArgs)) {
+ $this->switchToFailedMode();
+ }
+ }
+
+ /**
+ * @param mixed[] $capturedArgs
+ *
+ * @return void
+ */
+ private function preHookWpFilterBuildUniqueId(array $capturedArgs): void
+ {
+ if (!$this->preHookWpFilterBuildUniqueIdImpl($capturedArgs)) {
+ $this->switchToFailedMode();
+ }
+ }
+
+ /**
+ * @param mixed[] $capturedArgs
+ *
+ * @return bool
+ */
+ private function preHookAddFilterImpl(array $capturedArgs): bool
+ {
+ if (!$this->verifyHookNameCallbackArgs($capturedArgs)) {
+ return false;
+ }
+ /** @var string $hookName */
+ $hookName = $capturedArgs[0];
+ $callback =& $capturedArgs[1];
+
+ if ($callback instanceof WordPressFilterCallbackWrapper) {
+ return true;
+ }
+
+ $originalCallback = $callback;
+ $this->findCallbackInfo($originalCallback, /* out */ $callbackGroupKind, /* out */ $callbackGroupName);
+ $wrapper = new WordPressFilterCallbackWrapper($hookName, $originalCallback, $callbackGroupKind, $callbackGroupName);
+ $callback = $wrapper;
+
+ ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('Callback has been wrapped', ['original callback' => $originalCallback, 'wrapper' => $wrapper]);
+ return true;
+ }
+
+ /**
+ * @param mixed[] $capturedArgs
+ *
+ * @return bool
+ */
+ private function preHookWpFilterBuildUniqueIdImpl(array $capturedArgs): bool
+ {
+ if (!$this->verifyHookNameCallbackArgs($capturedArgs)) {
+ return false;
+ }
+ $callback =& $capturedArgs[1];
+
+ if (!($callback instanceof WordPressFilterCallbackWrapper)) {
+ return true;
+ }
+
+ $wrapper = $callback;
+ $originalCallback = $wrapper->getWrappedCallback();
+ $callback = $originalCallback;
+
+ ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('Callback has been unwrapped', ['original callback' => $originalCallback, 'wrapper' => $wrapper]);
+ return true;
+ }
+
+ /**
+ * @param mixed[] $capturedArgs
+ *
+ * @return bool
+ */
+ private function verifyHookNameCallbackArgs(array $capturedArgs): bool
+ {
+ //
+ // We should get (see src/ext/WordPress_instrumentation.c):
+ // [0] $hook_name parameter by value
+ // [1] $callback parameter by reference
+ //
+ // function add_filter($hook_name, $callback, $priority = 10, $accepted_args = 1)
+ // function _wp_filter_build_unique_id($hook_name, $callback, $priority)
+
+ return $this->util->verifyExactArgsCount(2, $capturedArgs)
+ && $this->util->verifyIsString($capturedArgs[0], 'hook_name')
+ && $this->util->verifyIsCallable($capturedArgs[1], /* shouldCheckSyntaxOnly */ true, '$callback');
+ }
+
+ /**
+ * @param ?Throwable $thrown
+ * @param mixed $returnValue
+ */
+ private function postHookGetTemplate(?Throwable $thrown, $returnValue): void
+ {
+ $logger = $this->logger->inherit()->addAllContext(['thrown' => $thrown, 'returnValue type' => DbgUtil::getType($returnValue), 'returnValue' => $returnValue]);
+
+ if ($thrown !== null) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Instrumented function has thrown so there is no return value');
+ return;
+ }
+
+ if ($returnValue === null) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Return value is null');
+ return;
+ }
+
+ if (!is_string($returnValue)) {
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Return value is not a string');
+ return;
+ }
+
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('Recording WordPress theme as a label on transaction', ['theme' => $returnValue, 'label key' => self::LABEL_KEY_FOR_WORDPRESS_THEME]);
+ ElasticApm::getCurrentTransaction()->context()->setLabel(self::LABEL_KEY_FOR_WORDPRESS_THEME, $returnValue);
+ }
+}
diff --git a/src/ElasticApm/Impl/AutoInstrument/WordPressFilterCallbackWrapper.php b/src/ElasticApm/Impl/AutoInstrument/WordPressFilterCallbackWrapper.php
new file mode 100644
index 000000000..56a9a32a6
--- /dev/null
+++ b/src/ElasticApm/Impl/AutoInstrument/WordPressFilterCallbackWrapper.php
@@ -0,0 +1,94 @@
+hookName = $hookName;
+ $this->callback = $callback;
+ $this->callbackGroupKind = $callbackGroupKind;
+ $this->callbackGroupName = $callbackGroupName;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getWrappedCallback()
+ {
+ return $this->callback;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function __invoke()
+ {
+ $args = func_get_args();
+ return AutoInstrumentationUtil::captureCurrentSpan(
+ $this->hookName . ' - ' . ($this->callbackGroupName ?? WordPressAutoInstrumentation::SPAN_NAME_PART_FOR_CORE) /* <- name */,
+ $this->callbackGroupKind /* <- type */,
+ /**
+ * @return mixed
+ */
+ function () use ($args) {
+ return call_user_func_array($this->callback, $args); // @phpstan-ignore-line - $this->callback should have type callable
+ },
+ $this->callbackGroupName /* <- subtype */,
+ $this->hookName /* <- action */
+ );
+ }
+}
diff --git a/src/ElasticApm/Impl/Config/AllOptionsMetadata.php b/src/ElasticApm/Impl/Config/AllOptionsMetadata.php
index 518ff2262..610656bdf 100644
--- a/src/ElasticApm/Impl/Config/AllOptionsMetadata.php
+++ b/src/ElasticApm/Impl/Config/AllOptionsMetadata.php
@@ -77,6 +77,12 @@ public static function get(): array
/** @var array> $value */
$value = [
OptionNames::API_KEY => new NullableStringOptionMetadata(),
+ OptionNames::AST_PROCESS_ENABLED => new BoolOptionMetadata(/* defaultValue: */ true),
+ OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE
+ => new BoolOptionMetadata(/* defaultValue: */ true),
+ OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX
+ => new NullableStringOptionMetadata(),
+ OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR => new NullableStringOptionMetadata(),
OptionNames::ASYNC_BACKEND_COMM => new BoolOptionMetadata(/* default */ true),
OptionNames::BREAKDOWN_METRICS => new BoolOptionMetadata(/* default */ true),
OptionNames::CAPTURE_ERRORS => new BoolOptionMetadata(/* default */ true),
@@ -102,6 +108,7 @@ public static function get(): array
OptionNames::SPAN_COMPRESSION_ENABLED => new BoolOptionMetadata(/* default */ true),
OptionNames::SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 50),
OptionNames::SPAN_COMPRESSION_SAME_KIND_MAX_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 0),
+ OptionNames::STACK_TRACE_LIMIT => new IntOptionMetadata(/* min */ -1, /* max */ null, /* default */ OptionDefaultValues::STACK_TRACE_LIMIT),
OptionNames::TRANSACTION_IGNORE_URLS => new NullableWildcardListOptionMetadata(),
OptionNames::TRANSACTION_MAX_SPANS => self::buildPositiveOrZeroIntMetadata(OptionDefaultValues::TRANSACTION_MAX_SPANS),
OptionNames::TRANSACTION_SAMPLE_RATE => new FloatOptionMetadata(/* min */ 0.0, /* max */ 1.0, /* default */ 1.0),
diff --git a/src/ElasticApm/Impl/Config/OptionDefaultValues.php b/src/ElasticApm/Impl/Config/OptionDefaultValues.php
index 9be02393c..b24ba9436 100644
--- a/src/ElasticApm/Impl/Config/OptionDefaultValues.php
+++ b/src/ElasticApm/Impl/Config/OptionDefaultValues.php
@@ -34,5 +34,6 @@ final class OptionDefaultValues
{
use StaticClassTrait;
+ public const STACK_TRACE_LIMIT = 50;
public const TRANSACTION_MAX_SPANS = 500;
}
diff --git a/src/ElasticApm/Impl/Config/OptionNames.php b/src/ElasticApm/Impl/Config/OptionNames.php
index 8e674ac2a..1e8751928 100644
--- a/src/ElasticApm/Impl/Config/OptionNames.php
+++ b/src/ElasticApm/Impl/Config/OptionNames.php
@@ -35,6 +35,10 @@ final class OptionNames
use StaticClassTrait;
public const API_KEY = 'api_key';
+ public const AST_PROCESS_ENABLED = 'ast_process_enabled';
+ public const AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE = 'ast_process_debug_dump_converted_back_to_source';
+ public const AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX = 'ast_process_debug_dump_for_path_prefix';
+ public const AST_PROCESS_DEBUG_DUMP_OUT_DIR = 'ast_process_debug_dump_out_dir';
public const ASYNC_BACKEND_COMM = 'async_backend_comm';
public const BREAKDOWN_METRICS = 'breakdown_metrics';
public const CAPTURE_ERRORS = 'capture_errors';
@@ -62,6 +66,7 @@ final class OptionNames
public const SPAN_COMPRESSION_ENABLED = 'span_compression_enabled';
public const SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION = 'span_compression_exact_match_max_duration';
public const SPAN_COMPRESSION_SAME_KIND_MAX_DURATION = 'span_compression_same_kind_max_duration';
+ public const STACK_TRACE_LIMIT = 'stack_trace_limit';
public const TRANSACTION_IGNORE_URLS = 'transaction_ignore_urls';
public const TRANSACTION_MAX_SPANS = 'transaction_max_spans';
public const TRANSACTION_SAMPLE_RATE = 'transaction_sample_rate';
diff --git a/src/ElasticApm/Impl/Config/Snapshot.php b/src/ElasticApm/Impl/Config/Snapshot.php
index a0afc2ad8..c24596418 100644
--- a/src/ElasticApm/Impl/Config/Snapshot.php
+++ b/src/ElasticApm/Impl/Config/Snapshot.php
@@ -98,6 +98,18 @@ final class Snapshot implements LoggableInterface
/** @var string */
private $apiKey;
+ /** @var bool */
+ private $astProcessEnabled;
+
+ /** @var bool */
+ private $astProcessDebugDumpConvertedBackToSource;
+
+ /** @var string */
+ private $astProcessDebugDumpForPathPrefix;
+
+ /** @var string */
+ private $astProcessDebugDumpOutDir;
+
/** @var bool */
private $asyncBackendComm;
@@ -176,6 +188,9 @@ final class Snapshot implements LoggableInterface
/** @var float */
private $spanCompressionSameKindMaxDuration;
+ /** @var int */
+ private $stackTraceLimit;
+
/** @var ?WildcardListMatcher */
private $transactionIgnoreUrls;
@@ -246,6 +261,11 @@ public function parsedValueFor(string $optName)
return $this->optNameToParsedValue[$optName];
}
+ public function astProcessEnabled(): bool
+ {
+ return $this->astProcessEnabled;
+ }
+
public function breakdownMetrics(): bool
{
return $this->breakdownMetrics;
@@ -351,6 +371,11 @@ public function spanCompressionSameKindMaxDuration(): float
return $this->spanCompressionSameKindMaxDuration;
}
+ public function stackTraceLimit(): int
+ {
+ return $this->stackTraceLimit;
+ }
+
public function transactionIgnoreUrls(): ?WildcardListMatcher
{
return $this->transactionIgnoreUrls;
diff --git a/src/ElasticApm/Impl/Span.php b/src/ElasticApm/Impl/Span.php
index b9e57d714..8abde9a8d 100644
--- a/src/ElasticApm/Impl/Span.php
+++ b/src/ElasticApm/Impl/Span.php
@@ -479,16 +479,21 @@ public function endSpanEx(int $numberOfStackFramesToSkip, ?float $duration = nul
return;
}
- // This method is part of public API so it should be kept in the stack trace
- // if $numberOfStackFramesToSkip is 0
- $this->stackTrace = StackTraceUtil::captureCurrent(
- $numberOfStackFramesToSkip,
- true /* <- hideElasticApmImpl */
- );
-
$this->onAboutToEnd->callCallbacks($this);
if ($this->shouldBeSentToApmServer()) {
+ /**
+ * stack_trace_limit
+ * 0 - stack trace collection should be disabled
+ * any positive integer value - the value is the maximum number of frames to collect
+ * -1 - all frames should be collected
+ */
+ $stackTraceLimit = $this->containingTransaction->getStackTraceLimitConfig();
+ if ($stackTraceLimit !== 0) {
+ // This method is part of public API so it should be kept in the stack trace
+ // if $numberOfStackFramesToSkip is 0
+ $this->stackTrace = StackTraceUtil::captureCurrent($numberOfStackFramesToSkip, /* hideElasticApmImpl */ true);
+ }
$this->prepareForSerialization();
$this->parentExecutionSegment->onChildSpanEnded($this);
}
diff --git a/src/ElasticApm/Impl/Transaction.php b/src/ElasticApm/Impl/Transaction.php
index 3742803d8..34c7c3ce0 100644
--- a/src/ElasticApm/Impl/Transaction.php
+++ b/src/ElasticApm/Impl/Transaction.php
@@ -95,6 +95,9 @@ final class Transaction extends ExecutionSegment implements TransactionInterface
/** @var ?bool */
private $cachedIsSpanCompressionEnabled = null;
+ /** @var ?int */
+ private $cachedStackTraceLimitConfig = null;
+
public function __construct(TransactionBuilder $builder)
{
$this->tracer = $builder->tracer;
@@ -567,6 +570,28 @@ public function isSpanCompressionEnabled(): bool
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 integer value - the value is the maximum number of frames to collect
+ * -1 - 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 prepareForSerialization(): void
{
SerializationUtil::prepareForSerialization(/* ref */ $this->context);
diff --git a/src/ElasticApm/Impl/Util/ArrayUtil.php b/src/ElasticApm/Impl/Util/ArrayUtil.php
index 73707a095..391e0d2ce 100644
--- a/src/ElasticApm/Impl/Util/ArrayUtil.php
+++ b/src/ElasticApm/Impl/Util/ArrayUtil.php
@@ -224,16 +224,4 @@ public static function removeKeyIfExists(string $key, array &$array): void
unset($array[$key]);
}
}
-
- /**
- * @template TKey of string|int
- * @template TValue
- *
- * @param array $from
- * @param array $to
- */
- public static function append(array $from, /* in,out */ array &$to): void
- {
- $to = array_merge($to, $from);
- }
}
diff --git a/src/ext/AST_debug.c b/src/ext/AST_debug.c
new file mode 100644
index 000000000..5fb72e979
--- /dev/null
+++ b/src/ext/AST_debug.c
@@ -0,0 +1,841 @@
+/*
+ * 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.
+ */
+
+#include "AST_debug.h"
+#include "ConfigSnapshot.h"
+#include "log.h"
+#include
+#include
+#include
+#include
+#include
+#include "util.h"
+#include "util_for_PHP.h"
+#include "elastic_apm_alloc.h"
+#include "AST_util.h"
+
+#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT
+
+static bool g_astProcessDebugDumpIsEnabled = false;
+static bool g_astProcessDebugDumpConvertedBackToSource = false;
+static StringBuffer g_astProcessDebugDumpForPathPrefix;
+static StringBuffer g_astProcessDebugDumpOutDir;
+
+String zendAstMagicConstAttrToString( zend_ast_attr attr )
+{
+ switch ( attr )
+ {
+ case T_DIR: return "__DIR__";
+ case T_FILE: return "__FILE__";
+ case T_LINE: return "__LINE__";
+ case T_NS_C: return "__NAMESPACE__";
+ case T_CLASS_C: return "__CLASS__";
+ case T_TRAIT_C: return "__TRAIT__";
+ case T_METHOD_C: return "__METHOD__";
+ case T_FUNC_C: return "__FUNCTION__";
+ default: return NULL;
+ }
+}
+
+String streamZendAstMagicConstAttr( zend_ast_attr attr, TextOutputStream* txtOutStream )
+{
+ String asString = zendAstMagicConstAttrToString( attr );
+ return asString == NULL ? streamPrintf( txtOutStream, "UNKNOWN (as int: %d)", (int)attr ) : asString;
+}
+
+String zendAstKindToString( zend_ast_kind kind )
+{
+# define ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( enumMember ) \
+ case enumMember: \
+ return (#enumMember) \
+ /**/
+
+ // Up to date with PHP v8.2.3
+ switch ( kind )
+ {
+ /**
+ * zend_ast_kind enum values as of PHP 7.2
+ */
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_AND );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARG_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY_ELEM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_OP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_REF );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BINARY_OP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BREAK );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CALL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CAST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST_DECL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLONE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE_USES );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_COALESCE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONDITIONAL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_DECL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_ELEM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONTINUE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DECLARE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DIM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DO_WHILE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ECHO );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EMPTY );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ENCAPS_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXIT );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXPR_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOR );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOREACH );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FUNC_DECL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GLOBAL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GOTO );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER_EQUAL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GROUP_USE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_HALT_COMPILER );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF_ELEM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INCLUDE_OR_EVAL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INSTANCEOF );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ISSET );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_LABEL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MAGIC_CONST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_CALL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_REFERENCE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAME_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAMESPACE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NEW );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_OR );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_DEC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_INC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_DEC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_INC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRINT );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_DECL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_ELEM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_REF );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_RETURN );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SHELL_EXEC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SILENCE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_CALL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_PROP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STMT_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_CASE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_THROW );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ADAPTATIONS );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ALIAS );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_PRECEDENCE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRY );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_MINUS );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_OP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_PLUS );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNPACK );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNSET );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_ELEM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_TRAIT );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_VAR );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_WHILE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD_FROM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZNODE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZVAL );
+
+ /**
+ * values added in PHP 7.3
+ */
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT_CLASS );
+ #endif
+
+ /**
+ * values added in PHP 7.4
+ */
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 4, 0 ) /* if PHP version from 7.4.0 */
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARROW_FUNC );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_COALESCE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_NAME );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_GROUP );
+ #endif
+
+ /**
+ * values added in PHP 8.0
+ */
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) /* if PHP version from 8.0.0 */
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ATTRIBUTE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ATTRIBUTE_GROUP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ATTRIBUTE_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST_GROUP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MATCH );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MATCH_ARM );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MATCH_ARM_LIST );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAMED_ARG );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NULLSAFE_METHOD_CALL );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NULLSAFE_PROP );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE_UNION );
+ #endif
+
+ /**
+ * values added in PHP 8.1
+ */
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 1, 0 ) /* if PHP version from 8.1.0 */
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CALLABLE_CONVERT );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_ENUM_INIT );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ENUM_CASE );
+ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE_INTERSECTION );
+ #endif
+
+ default:
+ return NULL;
+ }
+# undef ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE
+}
+
+String streamZendAstKind( zend_ast_kind kind, TextOutputStream* txtOutStream )
+{
+ String asString = zendAstKindToString( kind );
+ return asString == NULL ? streamPrintf( txtOutStream, "UNKNOWN (as int: %d)", (int)kind ) : asString;
+}
+
+typedef void (* DebugDumpAstPrintLine )( void* ctx, String text, UInt nestingDepth );
+
+struct DebugDumpAstPrinter
+{
+ DebugDumpAstPrintLine printLine;
+ void* ctx;
+};
+typedef struct DebugDumpAstPrinter DebugDumpAstPrinter;
+
+void debugDumpAst( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth );
+
+void debugDumpAstPrintLineFormattedText( DebugDumpAstPrinter* printer, String text, UInt nestingDepth )
+{
+ printer->printLine( printer->ctx, text, nestingDepth );
+}
+
+UInt getAstLineNumber( zend_ast* ast )
+{
+ return (UInt) zend_ast_get_lineno( ast );
+}
+
+void debugDumpAstPrintLineForNull( DebugDumpAstPrinter* printer, UInt nestingDepth )
+{
+ debugDumpAstPrintLineFormattedText( printer, "NULL", nestingDepth );
+}
+
+void debugDumpAstPrintLineTemplate( DebugDumpAstPrinter* printer, zend_ast_kind kind, UInt lineNumber, String attrAsString, UInt childCount, String additionalInfo, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ String text = ( additionalInfo == NULL )
+ ? streamPrintf( &txtOutStream, "%s (line: %u, attr: %s, childCount: %u)"
+ , streamZendAstKind( kind, &txtOutStream ), lineNumber, attrAsString, childCount )
+ : streamPrintf( &txtOutStream, "%s (line: %u, attr: %s, childCount: %u, %s)"
+ , streamZendAstKind( kind, &txtOutStream ), lineNumber, attrAsString, childCount, additionalInfo );
+
+ debugDumpAstPrintLineFormattedText( printer, text, nestingDepth );
+}
+
+static inline
+String streamAstAttribute( zend_ast_attr attr, TextOutputStream* txtOutStream )
+{
+ return streamPrintf( txtOutStream, "%u", attr );
+}
+
+void debugDumpAstPrintLineDefault( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ String attrAsString = streamAstAttribute( ast->attr, &txtOutStream );
+
+ debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, /* additionalInfo */ NULL, nestingDepth );
+}
+
+size_t calcNumberOfNonWhiteChars( StringView strVw )
+{
+ size_t result = 0;
+ ELASTIC_APM_FOR_EACH_INDEX( i, strVw.length )
+ {
+ if ( ! isWhiteSpace( strVw.begin[ i ] ) )
+ {
+ ++result;
+ }
+ }
+ return result;
+}
+
+void debugDumpAstPrintLineForDecl( DebugDumpAstPrinter* printer, zend_ast_decl* astDecl, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ String attrAsString = streamAstAttribute( astDecl->attr, &txtOutStream );
+ size_t docCommentNumberOfNonWhiteChars = calcNumberOfNonWhiteChars( nullableZStringToStringView( astDecl->doc_comment ) );
+ String additionalInfo = streamPrintf(
+ &txtOutStream
+ , "name: %s, end line: %u, flags: %u, doc_comment: %s"
+ , nullableZStringToString( astDecl->name ), (UInt)( astDecl->start_lineno ), (UInt)( astDecl->flags )
+ , astDecl->doc_comment == NULL ? "NULL" : streamPrintf( &txtOutStream, "[number of non-white chars: %u]", (unsigned)docCommentNumberOfNonWhiteChars )
+ );
+
+ debugDumpAstPrintLineTemplate( printer, astDecl->kind, astDecl->start_lineno, attrAsString, getAstChildren( (zend_ast*)astDecl ).count, additionalInfo, nestingDepth );
+}
+
+void debugDumpAstPrintLineForMagicConst( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ String attrAsString = streamZendAstMagicConstAttr( ast->attr, &txtOutStream );
+
+ debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, /* additionalInfo */ NULL, nestingDepth );
+}
+
+String debugDumpAstZvalStreamVal( zend_ast* ast, TextOutputStream* txtOutStream )
+{
+ zval* zVal = zend_ast_get_zval( ast );
+ if ( zVal == NULL )
+ {
+ return "ast->val is NULL";
+ }
+
+ int zValType = (int)Z_TYPE_P( zVal );
+ switch ( zValType )
+ {
+ case IS_STRING:
+ {
+ StringView strVw = zStringToStringView( Z_STR_P( zVal ) );
+ return streamPrintf( txtOutStream, "type: string, value: %.*s", (int)(strVw.length), strVw.begin );
+ }
+
+ case IS_LONG:
+ return streamPrintf( txtOutStream, "type: long, value: %"PRId64, (Int64)(Z_LVAL_P( zVal )) );
+
+ case IS_DOUBLE:
+ return streamPrintf( txtOutStream, "type: double, value: %f", (double)(Z_DVAL_P( zVal )) );
+
+ case IS_NULL:
+ return streamPrintf( txtOutStream, "type: null" );
+
+ case IS_FALSE:
+ return streamPrintf( txtOutStream, "type: false" );
+ case IS_TRUE:
+ return streamPrintf( txtOutStream, "type: true " );
+
+ default:
+ return streamPrintf( txtOutStream, "type: %s (type ID as int: %d)", zend_get_type_by_const( zValType ), (int)zValType );
+ }
+}
+
+void debugDumpAstPrintLineForZVal( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ String attrAsString = streamAstAttribute( ast->attr, &txtOutStream );
+ String additionalInfo = debugDumpAstZvalStreamVal( ast, &txtOutStream );
+
+ debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, additionalInfo, nestingDepth );
+}
+
+void debugDumpAstPrintLineForBinaryOp( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ String attrAsString = streamPrintf( &txtOutStream, "opcode: %s (ID as int: %d)", zend_get_opcode_name( (zend_uchar)(ast->attr) ), (int)(ast->attr) );
+
+ debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, /* additionalInfo */ NULL, nestingDepth );
+}
+
+void debugDumpAstPrintLineDispatch( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth )
+{
+ if ( isAstDecl( ast->kind ) )
+ {
+ debugDumpAstPrintLineForDecl( printer, (zend_ast_decl*)ast, nestingDepth );
+ return;
+ }
+
+ switch ( ast->kind )
+ {
+ case ZEND_AST_BINARY_OP:
+ debugDumpAstPrintLineForBinaryOp( printer, ast, nestingDepth );
+ return;
+
+ case ZEND_AST_MAGIC_CONST:
+ debugDumpAstPrintLineForMagicConst( printer, ast, nestingDepth );
+ return;
+
+ case ZEND_AST_ZVAL:
+ debugDumpAstPrintLineForZVal( printer, ast, nestingDepth );
+ return;
+ default:
+ debugDumpAstPrintLineDefault( printer, ast, nestingDepth );
+ return;
+ }
+}
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "misc-no-recursion"
+void debugDumpAst( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth )
+{
+ if ( ast == NULL )
+ {
+ debugDumpAstPrintLineForNull( printer, nestingDepth );
+ return;
+ }
+
+ debugDumpAstPrintLineDispatch( printer, ast, nestingDepth );
+
+ ZendAstPtrArrayView children = getAstChildren( ast );
+ ELASTIC_APM_FOR_EACH_INDEX( i, children.count )
+ {
+ debugDumpAst( printer, children.values[ i ], nestingDepth + 1 );
+ }
+}
+#pragma clang diagnostic pop
+
+struct DebugDumpAstPrintToLogCtx
+{
+ LogLevel logLevel;
+};
+typedef struct DebugDumpAstPrintToLogCtx DebugDumpAstPrintToLogCtx;
+
+void debugDumpAstPrintLineToLog( void* ctx, String text, UInt nestingDepth )
+{
+ const DebugDumpAstPrintToLogCtx* const localCtx = (const DebugDumpAstPrintToLogCtx*)ctx;
+
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ ELASTIC_APM_LOG_WITH_LEVEL( localCtx->logLevel, "%s%s", streamIndent( nestingDepth, &txtOutStream ), text );
+}
+
+void debugDumpAstTreeToLog( zend_ast* ast, LogLevel logLevel )
+{
+ if ( maxEnabledLogLevel() < logLevel )
+ {
+ return;
+ }
+
+ DebugDumpAstPrintToLogCtx ctx = (DebugDumpAstPrintToLogCtx){ .logLevel = logLevel };
+ DebugDumpAstPrinter printer = (DebugDumpAstPrinter){ .printLine = &debugDumpAstPrintLineToLog, .ctx = &ctx };
+ debugDumpAst( &printer, ast, /* nestingDepth */ 0 );
+}
+
+
+struct DebugDumpAstPrintToTextOutputStreamCtx
+{
+ TextOutputStream* txtOutStream;
+ String result;
+};
+typedef struct DebugDumpAstPrintToTextOutputStreamCtx DebugDumpAstPrintToTextOutputStreamCtx;
+
+void debugDumpAstPrintLineToTextOutputStream( void* ctx, String text, UInt nestingDepth )
+{
+ ELASTIC_APM_UNUSED( nestingDepth );
+
+ DebugDumpAstPrintToTextOutputStreamCtx* const localCtx = (DebugDumpAstPrintToTextOutputStreamCtx*)ctx;
+
+ localCtx->result = streamPrintf( localCtx->txtOutStream, "%s", text );
+}
+
+String streamZendAstNode( zend_ast* ast, TextOutputStream* txtOutStream )
+{
+ DebugDumpAstPrintToTextOutputStreamCtx ctx = (DebugDumpAstPrintToTextOutputStreamCtx){ .txtOutStream = txtOutStream, .result = NULL };
+ DebugDumpAstPrinter printer = (DebugDumpAstPrinter){ .printLine = &debugDumpAstPrintLineToTextOutputStream, .ctx = &ctx };
+ debugDumpAstPrintLineDispatch( &printer, ast, /* nestingDepth */ 0 );
+ return ctx.result;
+}
+
+struct DebugDumpAstPrintToFileCtx
+{
+ FILE* outFile;
+};
+typedef struct DebugDumpAstPrintToFileCtx DebugDumpAstPrintToFileCtx;
+
+void debugDumpAstPrintLineToFile( void* ctx, String text, UInt nestingDepth )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ const DebugDumpAstPrintToFileCtx* const localCtx = (const DebugDumpAstPrintToFileCtx*)ctx;
+
+ fputs( streamIndent( nestingDepth, &txtOutStream ), localCtx->outFile );
+ fputs( text, localCtx->outFile );
+ fputs( "\n", localCtx->outFile );
+}
+
+bool isFileSystemPathPrefix( StringView path, StringView pathPrefix )
+{
+ return isStringViewPrefix(
+ path,
+ pathPrefix,
+ /* shouldIgnoreCase */
+# ifdef PHP_WIN32
+ true
+# else // #ifdef PHP_WIN32
+ false
+# endif // #ifdef PHP_WIN32
+ );
+}
+
+ResultCode ensureDirectoryExists( String dirFullPath )
+{
+# ifdef PHP_WIN32
+
+ return resultFailure;
+
+# else // #ifdef PHP_WIN32
+
+ int mkdirRetVal = mkdir( dirFullPath, /* mode */ S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH );
+ if ( mkdirRetVal != 0 )
+ {
+ int errnoValue = errno;
+ if (errnoValue == EEXIST)
+ {
+ return resultSuccess;
+ }
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ ELASTIC_APM_LOG_ERROR( "mkdir failed; dirFullPath: `%s', mkdirRetVal: %d, errno: %d (%s)", dirFullPath, mkdirRetVal, errnoValue, streamErrNo( errnoValue, &txtOutStream ) );
+ return resultFailure;
+ }
+ return resultSuccess;
+
+# endif // #ifdef PHP_WIN32
+}
+
+ResultCode ensureDirectoriesExist( StringView fullPath )
+{
+ ELASTIC_APM_ASSERT( ! isEmptyStringView( fullPath ), "fullPath should not be empty" );
+
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "fullPath: %s", fullPath.begin );
+
+ ResultCode resultCode;
+ StringBuffer dirFullPath = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ size_t dirFullPathLen = 0;
+
+ ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ fullPath.length, /* out */ dirFullPath );
+ dirFullPath.begin[ 0 ] = '\0';
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ fullPath, dirFullPath, /* in,out */ &dirFullPathLen ) );
+ ELASTIC_APM_ASSERT_EQ_UINT64( dirFullPathLen, fullPath.length );
+
+ char directorySeparator =
+# ifdef PHP_WIN32
+ '\\';
+# else // #ifdef PHP_WIN32
+ '/';
+# endif // #ifdef PHP_WIN32
+
+ for ( MutableString begin = &( dirFullPath.begin[ 0 ] ), current = begin + 1, end = begin + fullPath.length ; current != end ; )
+ {
+ char* pDirSep = strchr( current, directorySeparator );
+ if ( pDirSep == NULL )
+ {
+ break;
+ }
+
+ char savedDirSep = *pDirSep;
+ *pDirSep = '\0';
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureDirectoryExists( dirFullPath.begin ) );
+ *pDirSep = savedDirSep;
+ current = pDirSep + 1;
+ }
+
+ resultCode = resultSuccess;
+ finally:
+
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ dirFullPath );
+
+ ELASTIC_APM_UNUSED( resultCode );
+
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "fullPath: %s", fullPath.begin );
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+ResultCode buildFileFullPath( StringViewArrayView pathParts, /* out */ StringBuffer* pResult )
+{
+ ResultCode resultCode;
+ StringBuffer result = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ size_t length = 0;
+ size_t contentLength = 0;
+
+ ELASTIC_APM_FOR_EACH_INDEX( i, pathParts.count )
+ {
+ length += pathParts.values[ i ].length;
+ }
+
+ ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ length, /* out */ result );
+ result.begin[ 0 ] = '\0';
+
+ ELASTIC_APM_FOR_EACH_INDEX( i, pathParts.count )
+ {
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ pathParts.values[ i ], result, /* in,out */ &contentLength ) );
+ }
+ ELASTIC_APM_ASSERT_EQ_UINT64( contentLength, length );
+
+ *pResult = result;
+ result = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ result );
+ goto finally;
+}
+
+void debugDumpAstSubTreeConvertedBackToSource( StringView compiledFileFullPath, StringView compiledFileRelativePath, zend_ast* ast, StringView isBeforeProcessSuffix, TextOutputStream* txtOutStream )
+{
+ ResultCode resultCode;
+ FILE* convertedBackToSourceFile = NULL;
+ int errnoValue = 0;
+ StringBuffer convertedBackToSourceFileFullPath = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ zend_string* convertedBackToSourceText = NULL;
+
+ StringView convertedBackToSourceFileExtensionSuffix = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".php" );
+ StringView convertedBackToSourceFileFullPathParts[] = {
+ stringBufferToView( g_astProcessDebugDumpOutDir ),
+ compiledFileRelativePath,
+ isBeforeProcessSuffix,
+ ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".converted_back_to_source" ),
+ convertedBackToSourceFileExtensionSuffix
+ };
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( buildFileFullPath( ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( StringViewArrayView, convertedBackToSourceFileFullPathParts ), /* out */ &convertedBackToSourceFileFullPath ) );
+
+ errnoValue = openFile( convertedBackToSourceFileFullPath.begin, "w", /* out */ &convertedBackToSourceFile );
+ if ( errnoValue != 0 )
+ {
+ ELASTIC_APM_LOG_ERROR( "Failed to open file; convertedBackToSourceFileFullPath: %s; errno: %d (%s)", convertedBackToSourceFileFullPath.begin, errnoValue, streamErrNo( errnoValue, txtOutStream ) );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ ELASTIC_APM_LOG_INFO( "Printing AST converted back to source of %s to %s ...", compiledFileFullPath.begin, convertedBackToSourceFileFullPath.begin );
+ convertedBackToSourceText = zend_ast_export( /* prefix */ "", ast, /* suffix */ "" );
+ String textAsCString = nullableZStringToString( convertedBackToSourceText );
+ if ( textAsCString == NULL )
+ {
+ ELASTIC_APM_LOG_INFO( "Nothing to print for AST of %s converted back to source to %s", compiledFileFullPath.begin, convertedBackToSourceFileFullPath.begin );
+ }
+ else
+ {
+ fputs( textAsCString, convertedBackToSourceFile );
+ ELASTIC_APM_LOG_INFO( "Printed AST converted back to source of %s to %s. Contents:\n%s"
+ , compiledFileFullPath.begin, convertedBackToSourceFileFullPath.begin, nullableZStringToString( convertedBackToSourceText ) );
+ }
+
+ resultCode = resultSuccess;
+ finally:
+ if ( convertedBackToSourceFile != NULL )
+ {
+ fclose( convertedBackToSourceFile );
+ convertedBackToSourceFile = NULL;
+ }
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ convertedBackToSourceFileFullPath );
+ if ( convertedBackToSourceText != NULL )
+ {
+ zend_string_release( convertedBackToSourceText );
+ convertedBackToSourceText = NULL;
+ }
+
+ ELASTIC_APM_UNUSED( resultCode );
+ return;
+
+ failure:
+ goto finally;
+}
+
+void debugDumpAstSubTreeToFile( StringView compiledFileFullPath, zend_ast* ast, bool isBeforeProcess )
+{
+ ResultCode resultCode;
+ StringBuffer debugDumpFileFullPath = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ FILE* debugDumpFile = NULL;
+ int errnoValue = 0;
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin );
+
+ StringView pathPrefix = stringBufferToView( g_astProcessDebugDumpForPathPrefix );
+ if ( ! isFileSystemPathPrefix( compiledFileFullPath, pathPrefix ) )
+ {
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "Skipping this file because it does not have required prefix: %s", pathPrefix.begin );
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+
+ StringView compiledFileRelativePath = subStringView( compiledFileFullPath, pathPrefix.length );
+ StringView isBeforeProcessSuffix = isBeforeProcess ? ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".before_AST_process" ) : ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".after_AST_process" );
+ StringView debugDumpFileExtensionSuffix = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".txt" );
+ StringView debugDumpFileFullPathParts[] = {
+ stringBufferToView( g_astProcessDebugDumpOutDir ),
+ compiledFileRelativePath,
+ isBeforeProcessSuffix,
+ debugDumpFileExtensionSuffix
+ };
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( buildFileFullPath( ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( StringViewArrayView, debugDumpFileFullPathParts ), /* out */ &debugDumpFileFullPath ) );
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureDirectoriesExist( stringBufferToView( debugDumpFileFullPath ) ) );
+
+ errnoValue = openFile( debugDumpFileFullPath.begin, "w", /* out */ &debugDumpFile );
+ if ( errnoValue != 0 )
+ {
+ ELASTIC_APM_LOG_ERROR( "Failed to open file; debugDumpFileFullPath: %s; errno: %d (%s)", debugDumpFileFullPath.begin, errnoValue, streamErrNo( errnoValue, &txtOutStream ) );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ ELASTIC_APM_LOG_INFO( "Printing AST debug dump of %s to %s ...", compiledFileFullPath.begin, debugDumpFileFullPath.begin );
+ DebugDumpAstPrintToFileCtx ctx = (DebugDumpAstPrintToFileCtx){ .outFile = debugDumpFile };
+ DebugDumpAstPrinter printer = (DebugDumpAstPrinter){ .printLine = &debugDumpAstPrintLineToFile, .ctx = &ctx };
+ debugDumpAst( &printer, ast, /* nestingDepth */ 0 );
+ ELASTIC_APM_LOG_INFO( "Printed AST debug dump of %s to %s", compiledFileFullPath.begin, debugDumpFileFullPath.begin );
+
+ if ( g_astProcessDebugDumpConvertedBackToSource )
+ {
+ debugDumpAstSubTreeConvertedBackToSource( compiledFileFullPath, compiledFileRelativePath, ast, isBeforeProcessSuffix, &txtOutStream );
+ }
+
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG();
+ if ( debugDumpFile != NULL )
+ {
+ fclose( debugDumpFile );
+ debugDumpFile = NULL;
+ }
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ debugDumpFileFullPath );
+
+ ELASTIC_APM_UNUSED( resultCode );
+ return;
+
+ failure:
+ goto finally;
+}
+
+void debugDumpAstTree( StringView compiledFileFullPath, zend_ast* ast, bool isBeforeProcess )
+{
+ LogLevel logLevel = g_astProcessDebugDumpIsEnabled ? logLevel_debug : logLevel_trace;
+ ELASTIC_APM_LOG_FUNCTION_ENTRY_MSG_WITH_LEVEL( logLevel, "compiledFileFullPath: %s, isBeforeProcess: %s, g_astProcessDebugDumpIsEnabled: %s"
+ , compiledFileFullPath.begin, boolToString( isBeforeProcess ), boolToString( g_astProcessDebugDumpIsEnabled ) );
+
+ debugDumpAstTreeToLog( ast, g_astProcessDebugDumpIsEnabled ? logLevel_debug : logLevel_trace );
+
+ if ( g_astProcessDebugDumpIsEnabled )
+ {
+ debugDumpAstSubTreeToFile( compiledFileFullPath, ast, isBeforeProcess );
+ }
+
+ ELASTIC_APM_LOG_FUNCTION_EXIT_MSG_WITH_LEVEL( logLevel, "compiledFileFullPath: %s, isBeforeProcess: %s, g_astProcessDebugDumpIsEnabled: %s"
+ , compiledFileFullPath.begin, boolToString( isBeforeProcess ), boolToString( g_astProcessDebugDumpIsEnabled ) );
+}
+
+StringView directorySeparatorAsStringView()
+{
+ return ELASTIC_APM_STRING_LITERAL_TO_VIEW(
+# ifdef PHP_WIN32
+ "\\"
+# else // #ifdef PHP_WIN32
+ "/"
+# endif // #ifdef PHP_WIN32
+ );
+}
+
+ResultCode ensureTrailingDirectorySeparator( StringView inPath, /* out */ StringBuffer* result )
+{
+ ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "inPath: %s", inPath.begin );
+
+ ResultCode resultCode;
+ StringBuffer outPathBuf = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ size_t outPathLen = 0;
+ size_t outPathMaxLen = inPath.length + 1;
+ StringView directorySeparator = directorySeparatorAsStringView();
+
+ ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ outPathMaxLen, /* out */ outPathBuf );
+ outPathBuf.begin[ 0 ] = '\0';
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ inPath, outPathBuf, /* in,out */ &outPathLen ) );
+ if ( ! isStringViewSuffix( inPath, directorySeparator ) )
+ {
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ directorySeparator, outPathBuf, /* in,out */ &outPathLen ) );
+ }
+
+ *result = outPathBuf;
+ outPathBuf = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG( "inPath: %s", inPath.begin );
+ return resultCode;
+
+ failure:
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ outPathBuf );
+ goto finally;
+
+#undef ELASTIC_APM_DIRECTORY_SEPARATOR
+}
+
+void astProcessDebugDumpOnRequestInit( const ConfigSnapshot* config )
+{
+ ResultCode resultCode;
+
+ if ( config->astProcessDebugDumpOutDir == NULL )
+ {
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+
+ StringView pathPrefix = config->astProcessDebugDumpForPathPrefix == NULL ? ELASTIC_APM_EMPTY_STRING_VIEW : stringToView( config->astProcessDebugDumpForPathPrefix );
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureTrailingDirectorySeparator( pathPrefix, /* out */ &g_astProcessDebugDumpForPathPrefix ) );
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureTrailingDirectorySeparator( stringToView( config->astProcessDebugDumpOutDir ), /* out */ &g_astProcessDebugDumpOutDir ) );
+
+ g_astProcessDebugDumpConvertedBackToSource = config->astProcessDebugDumpConvertedBackToSource;
+ g_astProcessDebugDumpIsEnabled = true;
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_UNUSED( resultCode );
+ return;
+
+ failure:
+ goto finally;
+}
+
+void astProcessDebugDumpOnRequestShutdown()
+{
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ g_astProcessDebugDumpOutDir );
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ g_astProcessDebugDumpForPathPrefix );
+ g_astProcessDebugDumpConvertedBackToSource = false;
+ g_astProcessDebugDumpIsEnabled = false;
+}
diff --git a/src/ext/AST_debug.h b/src/ext/AST_debug.h
new file mode 100644
index 000000000..dd0bbf52a
--- /dev/null
+++ b/src/ext/AST_debug.h
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include "TextOutputStream_forward_decl.h"
+#include "ConfigSnapshot_forward_decl.h"
+#include "LogLevel.h"
+#include "basic_types.h"
+#include "StringView.h"
+
+void astProcessDebugDumpOnRequestInit( const ConfigSnapshot* config );
+void astProcessDebugDumpOnRequestShutdown();
+
+String streamZendAstKind( zend_ast_kind kind, TextOutputStream* txtOutStream );
+String streamZendAstNode( zend_ast* ast, TextOutputStream* txtOutStream );
+
+void debugDumpAstTreeToLog( zend_ast* ast, LogLevel logLevel );
+void debugDumpAstTree( StringView compiledFileFullPath, zend_ast* ast, bool isBeforeProcess );
diff --git a/src/ext/AST_instrumentation.c b/src/ext/AST_instrumentation.c
index 210b497fa..6cfbdcc9c 100644
--- a/src/ext/AST_instrumentation.c
+++ b/src/ext/AST_instrumentation.c
@@ -18,459 +18,1626 @@
*/
#include "AST_instrumentation.h"
+#include "ConfigSnapshot.h"
+#include "ConfigManager.h"
#include "log.h"
+#include "AST_debug.h"
+#include
#include
-#include
-#include
-#include
+#include
+#include
+#include
+#include "WordPress_instrumentation.h"
#include "util.h"
+#include "util_for_PHP.h"
+#include "AST_util.h"
+#include "elastic_apm_alloc.h"
#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT
-zend_ast_process_t original_zend_ast_process;
+static bool g_isOriginalZendAstProcessSet = false;
+static zend_ast_process_t g_originalZendAstProcess = NULL;
-#define ZEND_AST_ALLOC( size ) zend_arena_alloc(&CG(ast_arena), size);
+static bool g_isLoadingAgentPhpCode = false;
-String zendAstKindToString( zend_ast_kind kind )
+void elasticApmBeforeLoadingAgentPhpCode()
{
-# define ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( enumMember ) \
- case enumMember: \
- return #enumMember; \
- /**/
+ g_isLoadingAgentPhpCode = true;
+}
+
+void elasticApmAfterLoadingAgentPhpCode()
+{
+ g_isLoadingAgentPhpCode = false;
+}
+
+bool getStringFromAstZVal( zend_ast* astZval, /* out */ StringView* pResult )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
- switch ( kind )
+ if ( astZval == NULL )
{
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZVAL )
- #ifdef ZEND_AST_CONSTANT
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT )
- #endif
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZNODE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FUNC_DECL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS )
- #ifdef ZEND_AST_ARROW_FUNC
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARROW_FUNC )
- #endif
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARG_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ENCAPS_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXPR_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STMT_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE_USES )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_DECL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_DECL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST_DECL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAME_LIST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ADAPTATIONS )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MAGIC_CONST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE )
- #ifdef ZEND_AST_CONSTANT_CLASS
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT_CLASS )
- #endif
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_VAR )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNPACK )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_PLUS )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_MINUS )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CAST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EMPTY )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ISSET )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SILENCE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SHELL_EXEC )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLONE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXIT )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRINT )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INCLUDE_OR_EVAL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_OP )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_INC )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_DEC )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_INC )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_DEC )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD_FROM )
- #ifdef ZEND_AST_CLASS_NAME
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_NAME )
- #endif
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GLOBAL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNSET )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_RETURN )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_LABEL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_REF )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_HALT_COMPILER )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ECHO )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_THROW )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GOTO )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BREAK )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONTINUE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DIM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_PROP )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CALL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_REF )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_OP )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BINARY_OP )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER_EQUAL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_AND )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_OR )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY_ELEM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NEW )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INSTANCEOF )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_COALESCE )
- #ifdef ZEND_AST_ASSIGN_COALESCE
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_COALESCE )
- #endif
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_WHILE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DO_WHILE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF_ELEM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_CASE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DECLARE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_TRAIT )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_PRECEDENCE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_REFERENCE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAMESPACE )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_ELEM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ALIAS )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GROUP_USE )
- #ifdef ZEND_AST_PROP_GROUP
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_GROUP )
- #endif
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_CALL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_CALL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONDITIONAL )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRY )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_ELEM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_ELEM )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOR )
- ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOREACH )
+ ELASTIC_APM_LOG_TRACE( "Returning false - astZval == NULL" );
+ return false;
+ }
- default:
- return "UNKNOWN";
+ if ( astZval->kind != ZEND_AST_ZVAL )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - astZval->kind: %s", streamZendAstKind( astZval->kind, &txtOutStream ) );
+ return false;
+ }
+
+ zval* zVal = zend_ast_get_zval( astZval );
+ if ( zVal == NULL )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - zVal == NULL" );
+ return false;
+ }
+ int zValType = (int)Z_TYPE_P( zVal );
+ if ( zValType != IS_STRING )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - zValType: %s (%d)", zend_get_type_by_const( zValType ), (int)zValType );
+ return false;
+ }
+
+ zend_string* zString = Z_STR_P( zVal );
+ *pResult = zStringToStringView( zString );
+ ELASTIC_APM_LOG_TRACE( "Returning true - with result string [length: %"PRIu64"]: %.*s", (UInt64)(pResult->length), (int)(pResult->length), pResult->begin );
+ return true;
+}
+
+bool getAstDeclName( zend_ast_decl* astDecl, /* out */ StringView* name )
+{
+ if ( astDecl->name == NULL )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - astAsDecl->name == NULL" );
+ return false;
+ }
+
+ *name = zStringToStringView( astDecl->name );
+ ELASTIC_APM_LOG_TRACE( "Returning true - name [length: %"PRIu64"]: %.*s", (UInt64)(name->length), (int)(name->length), name->begin );
+ return true;
+}
+
+bool getAstFunctionParameters( zend_ast_decl* astDecl, /* out */ ZendAstPtrArrayView* paramsAsts )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ ELASTIC_APM_ASSERT( astDecl->kind == ZEND_AST_FUNC_DECL || astDecl->kind == ZEND_AST_METHOD, "astDecl->kind: %s", streamZendAstKind( astDecl->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+ ELASTIC_APM_ASSERT_VALID_PTR( paramsAsts );
+
+ // function list of parameters is always child[ 0 ] - see zend_compile_func_decl
+ zend_ast* astFuncParams = astDecl->child[ 0 ];
+ if ( ! ( ( astFuncParams->kind == ZEND_AST_PARAM_LIST ) && zend_ast_is_list( astFuncParams ) ) )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - zend_ast_is_list( astFuncParams ): %s, astFuncParams->kind: %s"
+ , boolToString( zend_ast_is_list( astFuncParams ) ), streamZendAstKind( astFuncParams->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+ return false;
+ }
+ zend_ast_list* astFuncParamsAsList = zend_ast_get_list( astFuncParams );
+
+ paramsAsts->count = astFuncParamsAsList->children;
+ paramsAsts->values = &( astFuncParamsAsList->child[ 0 ] );
+
+ return true;
+}
+
+bool getAstFunctionParameterName( zend_ast_decl* astDecl, unsigned int parameterIndex, /* out */ StringView* name )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ ZendAstPtrArrayView paramsAsts;
+
+ if ( ! getAstFunctionParameters( astDecl, /* out */ ¶msAsts ) )
+ {
+ return false;
+ }
+
+ if ( parameterIndex >= paramsAsts.count )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - paramsAsts.count: %u, parameterIndex: %u", (unsigned)paramsAsts.count, parameterIndex );
+ return false;
+ }
+
+ zend_ast* param = paramsAsts.values[ parameterIndex ];
+ if ( param == NULL )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - param == NULL" );
+ return false;
+ }
+
+ if ( param->kind != ZEND_AST_PARAM )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - param->kind: %s", streamZendAstKind( param->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+ return false;
+ }
+
+ if ( zend_ast_get_num_children( param ) == 0 )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - zend_ast_get_num_children( param ): %d", (int)zend_ast_get_num_children( param ) );
+ return false;
}
-# undef ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE
+
+ // parameter name is in child[ 1 ]
+ return getStringFromAstZVal( param->child[ 1 ], /* out */ name );
}
-static inline
-size_t calcAstListAllocSize( uint32_t children )
+zend_string* createZStringForAst( StringView inStr )
{
- return sizeof( zend_ast_list ) - sizeof( zend_ast* ) + sizeof( zend_ast* ) * children;
+ return zend_string_init( inStr.begin, inStr.length, /* persistent: */ false );
}
-static
-zend_ast* createZvalAst( zval* zv, uint32_t attr, uint32_t lineno )
+bool isZendAstListKind( zend_ast_kind kind )
{
- #if PHP_VERSION_ID >= 70300 /* if PHP version is 7.3.0 and later */
- zend_ast* ast = zend_ast_create_zval_with_lineno( zv, lineno );
- ast->attr = attr;
+ return ((kind >> ZEND_AST_IS_LIST_SHIFT) & 1) != 0;
+}
+
+/**
+ * zend_ast_create and zend_ast_create_ex allowed up to 4 child* parameters for version before PHP v8
+ * and the limit was increased to 5 in PHP v8
+ *
+ * @see ZEND_AST_SPEC_CALL_EX
+ */
+static size_t g_maxCreateAstChildCount =
+ #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 )
+ 4
#else
- zend_ast* ast = zend_ast_create_zval_with_lineno( zv, attr, lineno );
+ 5
#endif
+;
+
+zend_ast* createAstEx( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ ELASTIC_APM_ASSERT_LE_UINT64( children.count, g_maxCreateAstChildCount );
+ ELASTIC_APM_ASSERT( ! isZendAstListKind( kind ), "kind: %s", streamZendAstKind( kind, &txtOutStream ) );
- return ast;
+ switch( children.count )
+ {
+ case 0:
+ return zend_ast_create_ex( kind, attr );
+ case 1:
+ return zend_ast_create_ex( kind, attr, children.values[ 0 ] );
+ case 2:
+ return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ] );
+ case 3:
+ return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ] );
+ case 4:
+ return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ], children.values[ 3 ] );
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 )
+ case 5:
+ return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ], children.values[ 3 ], children.values[ 4 ] );
+ #endif
+ }
}
-static
-zend_ast* createStringAst( char* str, size_t len, uint32_t attr, uint32_t lineno )
+ResultCode createAstExCheckChildrenCount( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children, /* out */ zend_ast** pResult )
{
- zval zv;
- zend_ast* ast;
- ZVAL_NEW_STR( &zv, zend_string_init( str, len, 0 ) );
- ast = createZvalAst( &zv, attr, /* lineno */ 0 );
- return ast;
+ ResultCode resultCode;
+
+ if ( children.count > g_maxCreateAstChildCount )
+ {
+ ELASTIC_APM_LOG_ERROR( "Number of children is larger than max; children.count: %u, g_maxCreateAstChildCount: %u", (unsigned)children.count, (unsigned)g_maxCreateAstChildCount );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultFailure );
+ }
+
+ *pResult = createAstEx( kind, attr, children );
+
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
}
-static
-zend_ast* createCatchTypeAst( uint32_t lineno )
+zend_ast* createAstWithAttribute( zend_ast_kind kind, zend_ast_attr attr )
{
- zend_ast * name = createStringAst( "Throwable", sizeof( "Throwable" ) - 1, ZEND_NAME_FQ, lineno );
- return zend_ast_create_list( 1, ZEND_AST_NAME_LIST, name );
+ return createAstEx( kind, attr, ELASTIC_APM_MAKE_EMPTY_ARRAY_VIEW( ZendAstPtrArrayView ) );
}
-static
-zend_ast* createCatchAst( uint32_t lineno )
+zend_ast* createAstWithAttributeAndOneChild( zend_ast_kind kind, zend_ast_attr attr, zend_ast* child )
{
- zend_ast * exVarNameAst = createStringAst( "ex", sizeof( "ex" ) - 1, /* attr: */ 0, lineno );
- zend_ast * catchTypeAst = createCatchTypeAst( lineno );
+ return createAstEx( kind, attr, ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, /* count */ 1, &child ) );
+}
- zend_ast * instrumentationPostHookCallAst =
- zend_ast_create( ZEND_AST_CALL
- , createStringAst( "instrumentationPostHookException", sizeof( "instrumentationPostHookException" ) - 1, ZEND_NAME_FQ, lineno )
- , zend_ast_create_list( 1
- , ZEND_AST_ARG_LIST
- , zend_ast_create( ZEND_AST_VAR
- , createStringAst( "ex", sizeof( "ex" ) - 1, /* attr: */ 0, lineno ) )
- ) );
+zend_ast* createAstWithAttributeAndTwoChildren( zend_ast_kind kind, zend_ast_attr attr, zend_ast* child0, zend_ast* child1 )
+{
+ zend_ast* children[] = { child0, child1 };
+ return createAstEx( kind, attr, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ZendAstPtrArrayView, children ) );
+}
- zend_ast * throwAst = zend_ast_create( ZEND_AST_THROW, instrumentationPostHookCallAst );
- throwAst->lineno = lineno;
- zend_ast * throwStmtListAst = zend_ast_create_list( 1, ZEND_AST_STMT_LIST, throwAst );
- throwStmtListAst->lineno = lineno;
- zend_ast * catchAst = zend_ast_create_list( 1
- , ZEND_AST_CATCH_LIST
- , zend_ast_create( ZEND_AST_CATCH
- , catchTypeAst
- , exVarNameAst
- , throwStmtListAst ) );
- catchAst->lineno = lineno;
- return catchAst;
+zend_ast* createAstWithAttributeAndThreeChildren( zend_ast_kind kind, zend_ast_attr attr, zend_ast* child0, zend_ast* child1, zend_ast* child2 )
+{
+ zend_ast* children[] = { child0, child1, child2 };
+ return createAstEx( kind, attr, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ZendAstPtrArrayView, children ) );
}
-struct TransformContext
+zend_ast* createAstWithOneChild( zend_ast_kind kind, zend_ast* child )
{
- bool isInsideFunction;
- bool isFunctionRetByRef;
-};
-typedef struct TransformContext TransformContext;
+ return createAstWithAttributeAndOneChild( kind, /* attr */ 0, child );
+}
-TransformContext g_transformContext = { .isInsideFunction = false, .isFunctionRetByRef = false };
+zend_ast* createAstWithTwoChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1 )
+{
+ return createAstWithAttributeAndTwoChildren( kind, /* attr */ 0, child0, child1 );
+}
-TransformContext makeTransformContext( TransformContext* base, bool isInsideFunction, bool isFunctionRetByRef )
+zend_ast* createAstWithThreeChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1, zend_ast* child2 )
{
- TransformContext transformCtx = *base;
- transformCtx.isInsideFunction = isInsideFunction;
- transformCtx.isFunctionRetByRef = isFunctionRetByRef;
- return transformCtx;
+ return createAstWithAttributeAndThreeChildren( kind, /* attr */ 0, child0, child1, child2 );
}
-static
-zend_ast* transformAst( zend_ast* ast, int nestingDepth );
+zend_ast* createAstMagicConst( zend_ast_attr attr, uint32_t lineNumber )
+{
+ zend_ast* result = createAstWithAttribute( ZEND_AST_MAGIC_CONST, attr );
+ result->lineno = lineNumber;
+ return result;
+}
-static
-zend_ast* transformFunctionAst( zend_ast* originalAst, int nestingDepth )
+zend_ast* createAstMagicConst__FUNCTION__( uint32_t lineNumber )
+{
+ return createAstMagicConst( T_FUNC_C, lineNumber );
+}
+
+zend_ast* createAstMagicConst__CLASS__( uint32_t lineNumber )
+{
+ return createAstMagicConst( T_CLASS_C, lineNumber );
+}
+
+zend_ast* createAstZValWithAttribute( zval* zv, zend_ast_attr attr, uint32_t lineNumber )
+{
+ zend_ast* result = zend_ast_create_zval_with_lineno(
+ zv,
+ #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version before 7.3.0 */
+ attr,
+ #endif
+ lineNumber
+ );
+
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */
+ result->attr = attr;
+ #endif
+
+ return result;
+}
+
+zend_ast* createAstZValStringWithAttribute( StringView inStr, zend_ast_attr attr, uint32_t lineNumber )
+{
+ zend_string* asZString = createZStringForAst( inStr );
+ zval stringAsZVal;
+ ZVAL_NEW_STR( &stringAsZVal, asZString );
+ return createAstZValWithAttribute( &stringAsZVal, attr, lineNumber );
+}
+
+zend_ast* createAstZValString( StringView inStr, uint32_t lineNumber )
+{
+ return createAstZValStringWithAttribute( inStr, /* attr */ 0, lineNumber );
+}
+
+zend_ast* createAstVar( StringView name, uint32_t lineNumber )
+{
+ // ZEND_AST_VAR (256) (line: 121)
+ // ZEND_AST_ZVAL (64) (line: 483) [type: string, value: hook_name]
+
+ return createAstWithOneChild( /* kind */ ZEND_AST_VAR, createAstZValString( name, lineNumber ) );
+}
+
+zend_ast* createAstConst( StringView name, zend_ast_attr nameAstAttr, uint32_t lineNumber )
+{
+ // ZEND_AST_CONST (line: 20, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 20, attr: 1) [type: string, value: null]
+
+ zend_ast* nameAst = createAstZValString( name, lineNumber );
+ nameAst->attr = nameAstAttr;
+ return createAstWithOneChild( /* kind */ ZEND_AST_CONST, /* child0 */ nameAst );
+}
+
+zend_ast* createAstConstNull( uint32_t lineNumber )
+{
+ return createAstConst( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "null" ), ZEND_NAME_NOT_FQ, lineNumber );
+}
+
+zend_ast* createAstGlobalConst( StringView name, uint32_t lineNumber )
+{
+ return createAstConst( name, /* nameAstAttr */ 0, lineNumber );
+}
+
+/**
+ * @see zend_ast_create_list_* in zend_ast.h
+ */
+static size_t g_elasticApmCreateAstListExChildrenCount = 2;
+
+zend_ast* createAstListEx( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children )
{
char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( originalAst->kind ) );
-
- zend_ast * transformedAst;
- zend_ast_decl* funcDeclAst = (zend_ast_decl*) originalAst;
-
- TransformContext savedTransformCtx = makeTransformContext(
- &g_transformContext
- , /* isInsideFunction: */ true
- , /* isFunctionRetByRef */ funcDeclAst->flags & ZEND_ACC_RETURN_REFERENCE );
-
- if ( ! isStringViewPrefixIgnoringCase( stringToView( ZSTR_VAL( funcDeclAst->name ) )
- , ELASTIC_APM_STRING_LITERAL_TO_VIEW( "functionToInstrument" ) ) )
- {
- transformedAst = originalAst;
- goto finally;
- }
-
- zend_ast_list* funcStmtListAst = zend_ast_get_list( funcDeclAst->child[ 2 ] );
-
- // guess at feasible line numbers
- uint32_t funcStmtBeginLineNumber = funcStmtListAst->lineno;
- uint32_t funcStmtEndLineNumber = funcStmtListAst->child[ funcStmtListAst->children - 1 ]->lineno;
-
- zend_ast * callInstrumentationPreHookAst =
- zend_ast_create(
- ZEND_AST_CALL
- , createStringAst( "instrumentationPreHook", sizeof( "instrumentationPreHook" ) - 1, ZEND_NAME_FQ, originalAst->lineno )
- , zend_ast_create_list( 1
- , ZEND_AST_ARG_LIST
- , zend_ast_create( ZEND_AST_CALL
- , createStringAst( "func_get_args", sizeof( "func_get_args" ) - 1, ZEND_NAME_FQ, originalAst->lineno )
- , zend_ast_create_list( 0, ZEND_AST_ARG_LIST ) )
- ) );
-
- zend_ast * callInstrumentationPostHookAst =
- zend_ast_create(
- ZEND_AST_CALL
- , createStringAst( "instrumentationPostHookRetVoid", sizeof( "instrumentationPostHookRetVoid" ) - 1, ZEND_NAME_FQ, originalAst->lineno )
- , zend_ast_create_list( 0, ZEND_AST_ARG_LIST ) );
-
- zend_ast * catchAst = createCatchAst( funcStmtEndLineNumber );
- zend_ast * finallyAst = NULL;
-
- zend_ast * tryCatchAst = zend_ast_create( ZEND_AST_TRY, transformAst( funcDeclAst->child[ 2 ], nestingDepth + 1 ), catchAst, finallyAst );
- tryCatchAst->lineno = funcStmtBeginLineNumber;
- zend_ast_list* newFuncBodyAst = ZEND_AST_ALLOC( calcAstListAllocSize( 3 ) );
- newFuncBodyAst->kind = ZEND_AST_STMT_LIST;
- newFuncBodyAst->lineno = funcStmtBeginLineNumber;
- newFuncBodyAst->children = 3;
- newFuncBodyAst->child[ 0 ] = callInstrumentationPreHookAst;
- newFuncBodyAst->child[ 1 ] = tryCatchAst;
- newFuncBodyAst->child[ 2 ] = callInstrumentationPostHookAst;
- funcDeclAst->child[ 2 ] = (zend_ast*) newFuncBodyAst;
- transformedAst = originalAst;
- finally:
- g_transformContext = savedTransformCtx;
- textOutputStreamRewind( &txtOutStream );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) );
- return transformedAst;
+ ELASTIC_APM_ASSERT_LE_UINT64( children.count, g_elasticApmCreateAstListExChildrenCount );
+ ELASTIC_APM_ASSERT( isZendAstListKind( kind ), "kind: %s", streamZendAstKind( kind, &txtOutStream ) );
+
+ zend_ast* result = NULL;
+
+ switch( children.count )
+ {
+ case 0:
+ result = zend_ast_create_list( children.count, kind );
+ break;
+ case 1:
+ result = zend_ast_create_list( children.count, kind, children.values[ 0 ] );
+ break;
+ case 2:
+ result = zend_ast_create_list( children.count, kind, children.values[ 0 ], children.values[ 1 ] );
+ break;
+ }
+
+ zend_ast_list* resultAsList = (zend_ast_list*)result;
+ resultAsList->attr = attr;
+ return result;
+}
+
+zend_ast* createAstListWithAttribute( zend_ast_kind kind, zend_ast_attr attr, uint32_t lineNumber )
+{
+ zend_ast* result = createAstListEx( kind, attr, ELASTIC_APM_MAKE_EMPTY_ARRAY_VIEW( ZendAstPtrArrayView ) );
+ ((zend_ast_list*)result)->lineno = lineNumber;
+ return result;
+}
+
+zend_ast* createAstList( zend_ast_kind kind, uint32_t lineNumber )
+{
+ return createAstListWithAttribute( kind, /* attr */ 0, lineNumber );
}
-static
-zend_ast* transformReturnAst( zend_ast* originalAst, int nestingDepth )
+void addChildToAstList( zend_ast* child, /* in,out */ zend_ast** pInSrcListOutNewList )
{
+ ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( pInSrcListOutNewList );
+
char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( originalAst->kind ) );
+ ELASTIC_APM_ASSERT( zend_ast_is_list( *pInSrcListOutNewList ), "kind: %s", streamZendAstKind( (*pInSrcListOutNewList)->kind, &txtOutStream ) );
+
+ zend_ast* newList = zend_ast_list_add( /* in */ *pInSrcListOutNewList, child );
+ *pInSrcListOutNewList = newList;
+}
+
+zend_ast* createAstListWithOneChild( zend_ast_kind kind, zend_ast* child )
+{
+ return createAstListEx( kind, /* attr */ 0, ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, /* count */ 1, &child ) );
+}
+
+zend_ast* createAstListWithTwoChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1 )
+{
+ zend_ast* children[] = { child0, child1 };
+ return createAstListEx( kind, /* attr */ 0, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ZendAstPtrArrayView, children ) );
+}
+
+zend_ast* createAstListWithThreeChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1, zend_ast* child2 )
+{
+ zend_ast* result = createAstListWithTwoChildren( kind, child0, child1 );
+ addChildToAstList( child2, /* in,out */ &result );
+ return result;
+}
+
+ResultCode createCapturedArgsAstArray( zend_ast_decl* astDecl, ArgCaptureSpecArrayView argCaptureSpecs, uint32_t lineNumber, /* out */ zend_ast** pResult )
+{
+ // AST for PHP code:
+ //
+ // (..., [$hook_name, &$callback])
+ // ^^^^^^^^^^^^^^^^^^^^^^^^ - captured args AST array
+ //
+ // ZEND_AST_ARRAY (129) (line: 121, attr: 3) <- [$hook_name, /* ref */ &$callback] and 3 == ZEND_ARRAY_SYNTAX_SHORT
+ // ZEND_AST_ARRAY_ELEM (526) (line: 121, attr: 0)
+ // ZEND_AST_VAR (256) (line: 121, attr: 0)
+ // ZEND_AST_ZVAL (64) (line: 121, attr: 0) [type: string, value: hook_name]
+ // NULL
+ // ZEND_AST_ARRAY_ELEM (526) (line: 121, attr: 1) <- attr == 1 because callback variable is taken by reference
+ // ZEND_AST_VAR (256) (line: 121, attr: 0)
+ // ZEND_AST_ZVAL (64) (line: 121, attr: 0) [type: string, value: callback]
+ // NULL
+
+ ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pResult );
- zend_ast * transformedAst;
+ ResultCode resultCode;
+ zend_ast* result = createAstListWithAttribute( ZEND_AST_ARRAY, /* attr */ ZEND_ARRAY_SYNTAX_SHORT, lineNumber );
- // if there isn't an active function then don't wrap it
- // e.g. return at file scope
- if ( ! g_transformContext.isInsideFunction )
+ ELASTIC_APM_FOR_EACH_INDEX( i, argCaptureSpecs.count )
{
- transformedAst = originalAst;
- goto finally;
+ StringView parameterName;
+ ArgCaptureSpec argCaptureSpec = argCaptureSpecs.values[ i ];
+ if ( argCaptureSpec == dontCaptureArg )
+ {
+ continue;
+ }
+ ELASTIC_APM_ASSERT( argCaptureSpec == captureArgByRef || argCaptureSpec == captureArgByValue, "argCaptureSpec: %d, i: %d", argCaptureSpec, (int)i );
+ // ZEND_AST_ARRAY_ELEM attribute should be 1 when passed by reference and 0 when passed by value
+ zend_ast_attr arrayElemAttr = argCaptureSpec == captureArgByRef ? 1 : 0;
+ if ( ! getAstFunctionParameterName( astDecl, /* parameterIndex */ i, /* out */ &( parameterName ) ) )
+ {
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+ zend_ast* varAst = createAstVar( parameterName, lineNumber );
+ // Array element value is the first child (i.e., index 0) and array element key is the second child (i.e., index 1)
+ zend_ast* arrayElement = createAstWithAttributeAndTwoChildren( ZEND_AST_ARRAY_ELEM, arrayElemAttr, /* array element value */ varAst, /* array element key */ NULL );
+ addChildToAstList( arrayElement, /* in,out */ &result );
}
- zend_ast * returnExprAst = originalAst->child[ 0 ];
- // If it's an empty return;
- if ( returnExprAst == NULL )
+ *pResult = result;
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+zend_ast* createAstStandaloneFunctionCall( StringView funcName, bool isFullyQualified, zend_ast* astArgList )
+{
+ // AST for PHP code:
+ //
+ // \elastic_apm_ast_instrumentation_pre_hook(__CLASS__, __FUNCTION__, [$hook_name, &$callback])
+ //
+ // ZEND_AST_CALL (line: 15, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 15, attr: 0) [type: string, value: elastic_apm_ast_instrumentation_pre_hook]
+ // ZEND_AST_ARG_LIST (line: 15, attr: 0, childCount: 3)
+
+ uint32_t lineNumber = zend_ast_get_lineno( astArgList );
+ zend_ast_attr zValAttr = isFullyQualified ? ZEND_NAME_FQ : ZEND_NAME_NOT_FQ;
+ return createAstWithTwoChildren( ZEND_AST_CALL, createAstZValStringWithAttribute( funcName, zValAttr, lineNumber ), astArgList );
+}
+
+zend_ast* createAstStandaloneFqFunctionCall( StringView funcName, zend_ast* astArgList )
+{
+ return createAstStandaloneFunctionCall( funcName, /* isFullyQualified */ true, astArgList );
+}
+
+zend_ast* createAstStandaloneNotFqFunctionCall( StringView funcName, zend_ast* astArgList )
+{
+ return createAstStandaloneFunctionCall( funcName, /* isFullyQualified */ false, astArgList );
+}
+
+ResultCode createPreHookAstArgListByCaptureSpec( zend_ast_decl* astDecl, ArgCaptureSpecArrayView argCaptureSpecs, /* out */ zend_ast** pResult )
+{
+ // AST for PHP code:
+ //
+ // \elastic_apm_ast_instrumentation_pre_hook(, __FUNCTION__, [$hook_name, &$callback])
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ // is __CLASS__ for methods and null for standalone functions
+ //
+ // ZEND_AST_ARG_LIST (line: 15, attr: 0, childCount: 3)
+ // ZEND_AST_MAGIC_CONST (line: 15, attr: __CLASS__)
+ // ZEND_AST_MAGIC_CONST (line: 15, attr: __FUNCTION__)
+ // ZEND_AST_ARRAY (line: 15, attr: 3, childCount: 2)
+
+ ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pResult );
+
+ ResultCode resultCode;
+ uint32_t lineNumber = astDecl->start_lineno;
+ zend_ast* capturedArgsAstArray = NULL;
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( createCapturedArgsAstArray( astDecl, argCaptureSpecs, lineNumber, /* out */ &capturedArgsAstArray ) );
+
+ *pResult = createAstListWithThreeChildren(
+ ZEND_AST_ARG_LIST
+ , astDecl->kind == ZEND_AST_METHOD ? createAstMagicConst__CLASS__( lineNumber ) : createAstConstNull( lineNumber )
+ , createAstMagicConst__FUNCTION__( lineNumber )
+ , capturedArgsAstArray
+ );
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG();
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+static StringView g_elastic_apm_ast_instrumentation_pre_hook_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "elastic_apm_ast_instrumentation_pre_hook" );
+
+/**
+ * function body is always child[ 2 ]
+ *
+ * @see zend_compile_func_decl
+ */
+static const size_t g_funcDeclBodyChildIndex = 2;
+
+ResultCode insertAstForFunctionPreHook( zend_ast_decl* funcAstDecl, ArgCaptureSpecArrayView argCaptureSpecs )
+{
+ // Before:
+ //
+ // function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
+ // ////////////////////////////
+ // // original function body //
+ // ////////////////////////////
+ // }
+ //
+ // ZEND_AST_FUNC_DECL (name: add_filter, line: 7, flags: 0, attr: 0, childCount: 4)
+ // ZEND_AST_PARAM_LIST (line: 7, attr: 0, childCount: 4)
+ // NULL
+ // ZEND_AST_STMT_LIST (line: 7, attr: 0, childCount: 4) <- original function body
+ // NULL
+ //
+ // After:
+ //
+ // function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { /* fold-into-one-line-begin */
+ // \elastic_apm_ast_instrumentation_pre_hook( /* pre-hook args */ );
+ // { /* fold-into-one-line-end */
+ // ////////////////////////////
+ // // original function body //
+ // ////////////////////////////
+ // } }
+ //
+ // ZEND_AST_FUNC_DECL (name: add_filter, line: 24, flags: 0, attr: 0, childCount: 4)
+ // ZEND_AST_PARAM_LIST (line: 24, attr: 0, childCount: 4)
+ // NULL
+ // ZEND_AST_STMT_LIST (line: 24, attr: 0, childCount: 2) <- new function body
+ // ZEND_AST_CALL (line: 24, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 24, attr: 0) [type: string, value: elastic_apm_ast_instrumentation_pre_hook]
+ // ZEND_AST_ARG_LIST (line: 24, attr: 0, childCount: 3) <- pre-hook args
+ // ZEND_AST_STMT_LIST (line: 24, attr: 0, childCount: 4) <- original function body
+ // NULL
+
+ ELASTIC_APM_ASSERT_VALID_PTR( funcAstDecl );
+
+ ResultCode resultCode;
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ String dbgCompiledFileName = stringIfNotNullElse( nullableZStringToStringView( CG(compiled_filename) ).begin, "" );
+
+ ELASTIC_APM_ASSERT( funcAstDecl->kind == ZEND_AST_FUNC_DECL || funcAstDecl->kind == ZEND_AST_METHOD, "funcAstDecl->kind: %s", streamZendAstKind( funcAstDecl->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+
+ StringView dbgFuncName;
+ if ( ! getAstDeclName( funcAstDecl, /* out */ &dbgFuncName ) )
{
- zend_ast * callInstrumentationPostHookAst = zend_ast_create(
- ZEND_AST_CALL
- , createStringAst( "instrumentationPostHookRetVoid", sizeof( "instrumentationPostHookRetVoid" ) - 1, ZEND_NAME_FQ, originalAst->lineno )
- , zend_ast_create_list( 0, ZEND_AST_ARG_LIST ) );
- transformedAst = zend_ast_create_list( 2, ZEND_AST_STMT_LIST, callInstrumentationPostHookAst, originalAst );
- goto finally;
+ ELASTIC_APM_LOG_ERROR( "Failed to get function name - returning failure" );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "dbgFuncName: %s, compiled_filename: %s", dbgFuncName.begin, dbgCompiledFileName );
+ debugDumpAstTreeToLog( (zend_ast*) funcAstDecl, logLevel_debug );
- // Either: return by reference or not
- char* name;
- size_t len;
- if ( g_transformContext.isFunctionRetByRef )
+ zend_ast* originalFuncBodyAst = funcAstDecl->child[ g_funcDeclBodyChildIndex ];
+ if ( originalFuncBodyAst == NULL )
{
- name = "instrumentationPostHookRetByRef";
- len = sizeof( "instrumentationPostHookRetByRef" ) - 1;
+ ELASTIC_APM_LOG_TRACE( "originalFuncBodyAst == NULL" );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
- else
+ if ( originalFuncBodyAst->kind != ZEND_AST_STMT_LIST )
{
- name = "instrumentationPostHookRetNotByRef";
- len = sizeof( "instrumentationPostHookRetNotByRef" ) - 1;
+ ELASTIC_APM_LOG_TRACE( "Expected originalFuncBodyAst->kind to be ZEND_AST_STMT_LIST but it is %s", streamZendAstKind( originalFuncBodyAst->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
- zend_ast * callInstrumentationPostHookAst = zend_ast_create(
- ZEND_AST_CALL
- , createStringAst( name, len, ZEND_NAME_FQ, originalAst->lineno )
- , zend_ast_create_list( 1, ZEND_AST_ARG_LIST, returnExprAst ) );
- originalAst->child[ 0 ] = callInstrumentationPostHookAst;
- transformedAst = originalAst;
+ zend_ast* preHookCallAstArgList = NULL;
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( createPreHookAstArgListByCaptureSpec( funcAstDecl, argCaptureSpecs, /* out */ &preHookCallAstArgList ) );
+
+ funcAstDecl->child[ g_funcDeclBodyChildIndex ] = createAstListWithTwoChildren(
+ ZEND_AST_STMT_LIST
+ , createAstStandaloneFqFunctionCall( g_elastic_apm_ast_instrumentation_pre_hook_funcName, preHookCallAstArgList )
+ , originalFuncBodyAst
+ );
+
+ resultCode = resultSuccess;
finally:
- textOutputStreamRewind( &txtOutStream );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) );
- return transformedAst;
+ ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG();
+ debugDumpAstTreeToLog( (zend_ast*) funcAstDecl, logLevel_debug );
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+zend_ast* createDirectCallAstArgList( uint32_t lineNumber, StringView constNameForMethodName )
+{
+ // PHP code:
+ //
+ // \elastic_apm_ast_instrumentation_direct_call(\ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS);
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ //
+ // AST:
+ //
+ // ZEND_AST_ARG_LIST (line: 63, attr: 0, childCount: 1)
+ // ZEND_AST_CONST (line: 63, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 63, attr: 0) [type: string, value: ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS]
+
+ return createAstListWithOneChild( ZEND_AST_ARG_LIST, createAstGlobalConst( constNameForMethodName, lineNumber ) );
}
-static
-zend_ast* transformChildrenAst( zend_ast* ast, int nestingDepth )
+static StringView g_elastic_apm_ast_instrumentation_direct_call_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "elastic_apm_ast_instrumentation_direct_call" );
+
+ResultCode appendDirectCallToInstrumentation( zend_ast_decl** pAstChildSlot, StringView constNameForMethodName )
{
+ // Before:
+ //
+ // function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) {
+ // // ...
+ // }
+ //
+ // ZEND_AST_FUNC_DECL (name: _wp_filter_build_unique_id, line: 44, flags: 0, attr: 0, childCount: 4) <- original function declaration
+ //
+ // After:
+ //
+ // { function _wp_filter_build_unique_id($hook_name, $callback, $priority ) { markerForElasticApmTestsFoldAstIntoOneLineBegin();
+ // // ...
+ // } } /* fold-into-one-line-begin */
+ // \elastic_apm_ast_instrumentation_direct_call(/* direct call args */);
+ // /* fold-into-one-line-end */ }
+ //
+ // ZEND_AST_STMT_LIST (line: 44, attr: 0, childCount: 2)
+ // ZEND_AST_FUNC_DECL (name: _wp_filter_build_unique_id, line: 44, flags: 0, attr: 0, childCount: 4) <- original function declaration
+ // ZEND_AST_CALL (line: 63, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 63, attr: 0) [type: string, value: elastic_apm_ast_instrumentation_direct_call]
+
+ ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( pAstChildSlot );
+
+ ResultCode resultCode;
char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( ast->kind ) );
+ String dbgCompiledFileName = stringIfNotNullElse( nullableZStringToStringView( CG(compiled_filename) ).begin, "" );
+ zend_ast_decl* appendToAstDecl = *pAstChildSlot;
+ uint32_t lineNumber = appendToAstDecl->end_lineno;
+
+ ELASTIC_APM_ASSERT( appendToAstDecl->kind == ZEND_AST_FUNC_DECL, "appendToAst->kind: %s", streamZendAstKind( appendToAstDecl->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+
+ StringView dbgFuncName;
+ if ( ! getAstDeclName( appendToAstDecl, /* out */ &dbgFuncName ) )
+ {
+ ELASTIC_APM_LOG_ERROR( "Failed to get function name - returning failure" );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "dbgFuncName: %s, compiled_filename: %s", dbgFuncName.begin, dbgCompiledFileName );
+ debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug );
+
+ zend_ast* appendedCallAstArgList = createDirectCallAstArgList( lineNumber, constNameForMethodName );
+ zend_ast* appendedCallAst = createAstStandaloneFqFunctionCall( g_elastic_apm_ast_instrumentation_direct_call_funcName, appendedCallAstArgList );
+
+ *((zend_ast**)pAstChildSlot) = createAstListWithTwoChildren( ZEND_AST_STMT_LIST, (zend_ast*) appendToAstDecl, appendedCallAst );
+
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG( "dbgFuncName: %s, compiled_filename: %s", dbgFuncName.begin, dbgCompiledFileName );
+ debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug );
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+static StringView g_wrappedFunctionNewNameSuffix = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "ElasticApmWrapped" );
+
+ResultCode createWrappedFunctionNewName( StringView originalName, /* out */ StringBuffer* pResult )
+{
+ ResultCode resultCode;
+ StringBuffer result = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ size_t newNameLength = originalName.length + g_wrappedFunctionNewNameSuffix.length;
+ size_t contentLength = 0;
+
+ ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ newNameLength, /* out */ result );
+ result.begin[ 0 ] = '\0';
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( originalName, result, /* in,out */ &contentLength ) );
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( g_wrappedFunctionNewNameSuffix, result, /* in,out */ &contentLength ) );
+ ELASTIC_APM_ASSERT_EQ_UINT64( contentLength, newNameLength );
+
+ *pResult = result;
+ result = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ result );
+ goto finally;
+}
+
+zend_string* cloneZStringForAst( zend_string* src )
+{
+ if ( src == NULL )
+ {
+ return NULL;
+ }
+
+ return createZStringForAst( zStringToStringView( src ) );
+}
+
+zend_ast* cloneAstZVal( zend_ast* ast, uint32_t lineNumber )
+{
+ zval clonedZVal;
+ ZVAL_COPY( /* out */ &clonedZVal, zend_ast_get_zval( ast ) );
+ return createAstZValWithAttribute( &clonedZVal, ast->attr, lineNumber );
+}
- zend_ast * transformedAst = ast;
+// ZEND_AST_CONSTANT was added in PHP 7.3
+#if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */
+zend_ast* cloneAstConstant( zend_ast* ast, uint32_t lineNumber )
+{
+ zend_ast* result = zend_ast_create_constant( zend_ast_get_constant_name( ast ), ast->attr );
+ zval* pResultZVal = zend_ast_get_zval( result );
+ Z_LINENO_P( pResultZVal ) = lineNumber;
+ return result;
+}
+#endif
- uint32_t childrenCount = zend_ast_get_num_children( ast );
- ELASTIC_APM_FOR_EACH_INDEX( i, childrenCount )
+ResultCode cloneAstTree( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult );
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "misc-no-recursion"
+ResultCode cloneAstDecl( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult )
+{
+ ResultCode resultCode;
+ ZendAstPtrArrayView children = getAstChildren( ast );
+ zend_ast_decl* astDecl = (zend_ast_decl*)ast;
+ zend_ast** clonedChildren = NULL;
+
+ if ( children.count != elasticApmZendAstDeclChildrenCount )
{
- if ( ast->child[ i ] != NULL )
+ ELASTIC_APM_LOG_ERROR( "Number of children is not as expected; children.count: %u, elasticApmZendAstDeclChildrenCount: %u"
+ , (unsigned)children.count, (unsigned)elasticApmZendAstDeclChildrenCount );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultFailure );
+ }
+
+ clonedChildren = emalloc( sizeof( zend_ast* ) * children.count );
+ ELASTIC_APM_FOR_EACH_INDEX( i, children.count )
+ {
+ clonedChildren[ i ] = NULL;
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstTree( children.values[ i ], lineNumber, /* out */ &( clonedChildren[ i ] ) ) );
+ }
+
+ *pResult = zend_ast_create_decl(
+ astDecl->kind
+ , astDecl->flags
+ , lineNumber /* <- start_lineno */
+ , cloneZStringForAst( astDecl->doc_comment )
+ , cloneZStringForAst( astDecl->name )
+ , clonedChildren[ 0 ]
+ , clonedChildren[ 1 ]
+ , clonedChildren[ 2 ]
+ , clonedChildren[ 3 ]
+ /**
+ * number of child* parameters accepted by zend_ast_create_decl
+ * 4 before PHP v8.0.0
+ * 5 from PHP v8.0.0
+ */
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 )
+ , clonedChildren[ 4 ]
+ #endif
+ );
+
+ resultCode = resultSuccess;
+ finally:
+ if ( clonedChildren != NULL )
+ {
+ efree( clonedChildren );
+ clonedChildren = NULL;
+ }
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+ResultCode cloneAstList( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult )
+{
+ ResultCode resultCode;
+ zend_ast_list* astList = zend_ast_get_list( ast );
+ zend_ast* result = createAstListWithAttribute( astList->kind, astList->attr, lineNumber );
+ ELASTIC_APM_FOR_EACH_INDEX( i, astList->children )
+ {
+ zend_ast* clonedChildAst = NULL;
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstTree( astList->child[ i ], lineNumber, /* out */ &clonedChildAst ) );
+ addChildToAstList( clonedChildAst, /* in,out */ &result );
+ }
+
+ *pResult = result;
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+ResultCode cloneFallbackAst( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult )
+{
+ ResultCode resultCode;
+ ZendAstPtrArrayView children = getAstChildren( ast );
+ zend_ast** clonedChildren = NULL;
+
+ if ( children.count != 0 )
+ {
+ clonedChildren = emalloc( sizeof( zend_ast* ) * children.count );
+ }
+ ELASTIC_APM_FOR_EACH_INDEX( i, children.count )
+ {
+ clonedChildren[ i ] = NULL;
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstTree( children.values[ i ], lineNumber, /* out */ &( clonedChildren[ i ] ) ) );
+ }
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( createAstExCheckChildrenCount( ast->kind, ast->attr, ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, children.count, clonedChildren ), /* out */ pResult ) );
+ (*pResult)->lineno = lineNumber;
+
+ resultCode = resultSuccess;
+ finally:
+ if ( clonedChildren != NULL )
+ {
+ efree( clonedChildren );
+ clonedChildren = NULL;
+ }
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+ResultCode cloneAstTree( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult )
+{
+ /**
+ * @see zend_ast_copy
+ */
+
+ ResultCode resultCode;
+
+ if ( ast == NULL )
+ {
+ *pResult = NULL;
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+
+ if ( ast->kind == ZEND_AST_ZVAL )
+ {
+ *pResult = cloneAstZVal( ast, lineNumber );
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+
+ // ZEND_AST_CONSTANT was added in PHP 7.3
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */
+ if ( ast->kind == ZEND_AST_CONSTANT )
+ {
+ *pResult = cloneAstConstant( ast, lineNumber );
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+ #endif
+
+ if ( isAstDecl( ast->kind ) )
+ {
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstDecl( ast, lineNumber, /* out */ pResult ) );
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+
+ if ( zend_ast_is_list( ast ) )
+ {
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstList( ast, lineNumber, /* out */ pResult ) );
+ ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY();
+ }
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneFallbackAst( ast, lineNumber, /* out */ pResult ) );
+
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+#pragma clang diagnostic pop
+
+zend_ast* createAstAssign( StringView varName, zend_ast* rhsAst )
+{
+ // PHP code:
+ //
+ // $args = func_get_args();
+ // $postHook = \elastic_apm_ast_instrumentation_pre_hook(/* instrumentedClassFullName */ null, __FUNCTION__, $args);
+ //
+ // AST:
+ //
+ // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args)
+ // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2) <- rhsAst
+
+ return createAstWithTwoChildren( ZEND_AST_ASSIGN, createAstVar( varName, zend_ast_get_lineno( rhsAst ) ), rhsAst );
+}
+
+static StringView g_argsVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "args" );
+static StringView g_postHookVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "postHook" );
+
+zend_ast* createPreHookAstArgList( bool isMethod, uint32_t lineNumber )
+{
+ // PHP code:
+ //
+ // \elastic_apm_ast_instrumentation_pre_hook(, __FUNCTION__, $args);
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ // is __CLASS__ for methods and null for standalone functions
+ //
+ // AST:
+ //
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 3)
+ // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null)
+ // ZEND_AST_MAGIC_CONST (line: 48, attr: __FUNCTION__, childCount: 0)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args)
+
+ return createAstListWithThreeChildren(
+ ZEND_AST_ARG_LIST
+ , isMethod ? createAstMagicConst__CLASS__( lineNumber ) : createAstConstNull( lineNumber )
+ , createAstMagicConst__FUNCTION__( lineNumber )
+ , createAstVar( g_argsVarName, lineNumber )
+ );
+}
+
+void createWrapperFunctionBodyPrologAst( /* in,out */ zend_ast** appendToAstStmtList )
+{
+ // PHP code:
+ //
+ // $args = func_get_args();
+ // $postHook = \elastic_apm_ast_instrumentation_pre_hook(/* instrumentedClassFullName */ null, __FUNCTION__, $args);
+ //
+ // AST:
+ //
+ // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args)
+ // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: func_get_args)
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 0)
+ // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: postHook)
+ // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: elastic_apm_ast_instrumentation_pre_hook)
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 3)
+
+ ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( appendToAstStmtList );
+
+ uint32_t lineNumber = zend_ast_get_lineno( *appendToAstStmtList );
+ zend_ast* func_get_args_astCall = createAstStandaloneFqFunctionCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "func_get_args" ), createAstList( ZEND_AST_ARG_LIST, lineNumber ) );
+ addChildToAstList( createAstAssign( g_argsVarName, func_get_args_astCall ), /* in,out */ appendToAstStmtList );
+ zend_ast* preHookAstCall = createAstStandaloneFqFunctionCall( g_elastic_apm_ast_instrumentation_pre_hook_funcName, createPreHookAstArgList( /* isMethod */ false, lineNumber ) );
+ addChildToAstList( createAstAssign( g_postHookVarName, preHookAstCall ), /* in,out */ appendToAstStmtList );
+}
+
+zend_ast* createCallPostHookIfNotNullAst( zend_ast* thrownAst, zend_ast* retValAst )
+{
+ // PHP code:
+ //
+ // if ($postHook !== null) $postHook(/* thrown */ null, $retVal);
+ //
+ // or
+ //
+ // if ($postHook !== null) $postHook($thrown, /* retVal */ null);
+ //
+ // AST:
+ //
+ // ZEND_AST_IF (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_IF_ELEM (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_BINARY_OP (line: 48, attr: 17, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: postHook)
+ // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null)
+ // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: postHook)
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: retVal)
+ //
+ // or
+ //
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: thrown)
+ // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null)
+
+ ELASTIC_APM_ASSERT_VALID_PTR( thrownAst );
+ ELASTIC_APM_ASSERT_VALID_PTR( retValAst );
+
+ uint32_t lineNumber = zend_ast_get_lineno( thrownAst );
+
+ return createAstListWithOneChild(
+ ZEND_AST_IF
+ , createAstWithTwoChildren(
+ ZEND_AST_IF_ELEM
+ , zend_ast_create_binary_op(
+ ZEND_IS_NOT_IDENTICAL
+ , createAstVar( g_postHookVarName, lineNumber )
+ , createAstConstNull( lineNumber )
+ )
+ , createAstWithTwoChildren(
+ ZEND_AST_CALL
+ , createAstVar( g_postHookVarName, lineNumber )
+ , createAstListWithTwoChildren( ZEND_AST_ARG_LIST, thrownAst, retValAst )
+ )
+ )
+ );
+}
+
+zend_ast* createWrappedFunctionCallAstArgList( uint32_t lineNumber )
+{
+ // PHP code:
+ //
+ // get_templateElasticApmWrapped(...$args);
+ // ^^^^^^^^
+ //
+ // AST:
+ //
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_UNPACK (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args)
+
+ return createAstListWithOneChild(
+ ZEND_AST_ARG_LIST
+ , createAstWithOneChild(
+ ZEND_AST_UNPACK
+ , createAstVar( g_argsVarName, lineNumber )
+ )
+ );
+}
+
+static StringView g_retValVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "retVal" );
+
+zend_ast* createWrapperFunctionBodyTryBlockAst( StringView wrappedFunctionNewName, uint32_t lineNumber )
+{
+ // PHP code:
+ //
+ // $retVal = get_templateElasticApmWrapped(...$args);
+ // if ($postHook !== null) $postHook(/* thrown */ null, $retVal);
+ // return $retVal;
+ //
+ // AST:
+ //
+ // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 3)
+ // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: retVal)
+ // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: get_templateElasticApmWrapped)
+ // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_IF (line: 48, attr: 0, childCount: 1) <- if ($postHook !== null) $postHook(/* thrown */ null, $retVal);
+ // ZEND_AST_RETURN (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: retVal)
+
+// return createAstList( ZEND_AST_STMT_LIST, lineNumber );
+
+ return createAstListWithThreeChildren(
+ ZEND_AST_STMT_LIST
+ , createAstAssign( g_retValVarName, createAstStandaloneNotFqFunctionCall( wrappedFunctionNewName, createWrappedFunctionCallAstArgList( lineNumber ) ) )
+ , createCallPostHookIfNotNullAst( /* thrownAst */ createAstConstNull( lineNumber ), createAstVar( g_retValVarName, lineNumber ) )
+ , createAstWithOneChild( ZEND_AST_RETURN, createAstVar( g_retValVarName, lineNumber ) )
+ );
+}
+
+static StringView g_thrownVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "thrown" );
+
+zend_ast* createWrapperFunctionBodyCatchPartAst( uint32_t lineNumber )
+{
+ // PHP code:
+ //
+ // } catch (\Throwable $thrown) {
+ // if ($postHook !== null) $postHook($thrown, /* retVal */ null);
+ // throw $thrown;
+ // }
+ //
+ // AST:
+ //
+ // ZEND_AST_CATCH_LIST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_CATCH (line: 48, attr: 0, childCount: 3)
+ // ZEND_AST_NAME_LIST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: Throwable)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: thrown)
+ // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 2)
+ // ZEND_AST_IF (line: 48, attr: 0, childCount: 1) <- if ($postHook !== null) $postHook($thrown, /* retVal */ null);
+ // ZEND_AST_THROW (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: thrown)
+
+ /**
+ * @see zend_compile_try
+ */
+
+ return createAstListWithOneChild(
+ ZEND_AST_CATCH_LIST
+ , createAstWithThreeChildren(
+ ZEND_AST_CATCH
+ /* ZEND_AST_CATCH child[ 0 ] - class name(s) */
+ , createAstListWithOneChild( ZEND_AST_NAME_LIST, createAstZValString( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "Throwable" ), lineNumber ) )
+ /* ZEND_AST_CATCH child[ 1 ] - var name */
+ , createAstZValString( g_thrownVarName, lineNumber )
+ /* ZEND_AST_CATCH child[ 2 ] - block */
+ , createAstListWithTwoChildren(
+ ZEND_AST_STMT_LIST
+ , createCallPostHookIfNotNullAst( createAstVar( g_thrownVarName, lineNumber ), /* retValAst */ createAstConstNull( lineNumber ) )
+ , createAstWithOneChild( ZEND_AST_THROW, createAstVar( g_thrownVarName, lineNumber ) )
+ )
+ )
+ );
+}
+
+void createWrapperFunctionBodyTryCatchAst( StringView wrappedFunctionNewName, /* in,out */ zend_ast** appendToAstStmtList )
+{
+ // PHP code:
+ //
+ // try {
+ // // ...
+ // } catch ( ... ) {
+ // // ...
+ // }
+ //
+ // AST:
+ //
+ // ZEND_AST_TRY (line: 48, attr: 0, childCount: 3)
+ // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 3) <- try block
+ // ZEND_AST_CATCH_LIST (line: 48, attr: 0, childCount: 1)
+ // ZEND_AST_CATCH (line: 48, attr: 0, childCount: 3) <- catch block
+ // NULL <- no finally block
+
+ ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( appendToAstStmtList );
+
+ uint32_t lineNumber = zend_ast_get_lineno( *appendToAstStmtList );
+
+ /**
+ * @see zend_compile_try
+ */
+ zend_ast* astTryCatch = createAstWithThreeChildren(
+ ZEND_AST_TRY
+ , createWrapperFunctionBodyTryBlockAst( wrappedFunctionNewName, lineNumber )
+ , createWrapperFunctionBodyCatchPartAst( lineNumber )
+ , NULL /* <- no finally block */
+ );
+
+ addChildToAstList( astTryCatch, /* in,out */ appendToAstStmtList );
+}
+
+zend_ast* createWrapperFunctionBodyAst( StringView wrappedFunctionNewName, uint32_t lineNumber )
+{
+ // PHP code:
+ //
+ // // prolog
+ // try {
+ // // ...
+ // } catch ( ... ) {
+ // // ...
+ // }
+ //
+ // AST:
+ //
+ // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 3)
+ // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) <- // part of prolog
+ // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) <- // part of prolog
+ // ZEND_AST_TRY (line: 48, attr: 0, childCount: 3)
+
+ zend_ast* funcBodyAstStmtList = createAstList( ZEND_AST_STMT_LIST, lineNumber );
+
+ createWrapperFunctionBodyPrologAst( /* in,out */ &funcBodyAstStmtList );
+ createWrapperFunctionBodyTryCatchAst( wrappedFunctionNewName, /* in,out */ &funcBodyAstStmtList );
+
+ return funcBodyAstStmtList;
+}
+
+ResultCode createWrapperFunctionAst( zend_ast_decl* originalFuncAstDecl, StringView wrappedFunctionNewName, /* out */ zend_ast_decl** pResult )
+{
+ ELASTIC_APM_ASSERT_VALID_PTR( originalFuncAstDecl );
+ ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pResult );
+
+ ResultCode resultCode;
+ zend_ast* originalFuncBodyAst = NULL;
+ zend_ast_decl* clonedFuncDecl = NULL;
+ uint32_t lineNumber = originalFuncAstDecl->end_lineno;
+
+ originalFuncBodyAst = originalFuncAstDecl->child[ g_funcDeclBodyChildIndex ];
+ // Temporarily set the body to NULL because we don't want to clone it
+ // We restore it back after clone call
+ originalFuncAstDecl->child[ g_funcDeclBodyChildIndex ] = NULL;
+ resultCode = cloneAstTree( (zend_ast*)originalFuncAstDecl, lineNumber, /* out */ (zend_ast**)&clonedFuncDecl );
+ originalFuncAstDecl->child[ g_funcDeclBodyChildIndex ] = originalFuncBodyAst;
+ if ( resultCode != resultSuccess )
+ {
+ goto failure;
+ }
+ clonedFuncDecl->child[ g_funcDeclBodyChildIndex ] = createWrapperFunctionBodyAst( wrappedFunctionNewName, lineNumber );
+
+ *pResult = clonedFuncDecl;
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+uint32_t findAstDeclStartLineNumber( zend_ast_decl* astDecl )
+{
+ ZendAstPtrArrayView children = getAstDeclChildren( astDecl );
+ uint32_t result = astDecl->start_lineno;
+ ELASTIC_APM_FOR_EACH_INDEX( i, children.count )
+ {
+ zend_ast* child = children.values[ i ];
+ if ( ( child != NULL ) && ( zend_ast_get_lineno( child ) < result ) )
{
- ast->child[ i ] = transformAst( ast->child[ i ], nestingDepth + 1 );
+ result = zend_ast_get_lineno( child );
}
}
+ return result;
+}
+
+ResultCode wrapStandaloneFunctionAstWithPrePostHooks( /* in,out */ zend_ast_decl** pAstChildSlot )
+{
+ // Before:
+ //
+ // function get_template() {
+ // // ...
+ // }
+ //
+ // ZEND_AST_FUNC_DECL (name: get_template, line: 16, flags: 0, attr: 0, childCount: 4) <- original function under original name
+ //
+ // After:
+ //
+ // { function get_templateElasticApmWrapped() {
+ // // ...
+ // } // fold-AST-into-one-line-begin
+ //
+ // function get_template()
+ // {
+ // // wrapper function body
+ // }
+ // } // fold-AST-into-one-line-end
+ //
+ // ZEND_AST_STMT_LIST (line: 16, attr: 0, childCount: 2)
+ // ZEND_AST_FUNC_DECL (name: get_templateElasticApmWrapped, line: 16, flags: 0, attr: 0, childCount: 4) <- original function under wrapped name
+ // ZEND_AST_FUNC_DECL (name: get_template, line: 25, flags: 0, attr: 0, childCount: 4) <- wrapper function under original name
+
+ ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( pAstChildSlot );
+
+ ResultCode resultCode;
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ String dbgCompiledFileName = stringIfNotNullElse( nullableZStringToStringView( CG(compiled_filename) ).begin, "" );
+ zend_ast_decl* originalFuncAstDecl = *pAstChildSlot; // it's not created by this function, so we should NOT clean it on failure
+ StringBuffer wrappedFunctionNewName = ELASTIC_APM_EMPTY_STRING_BUFFER;
+ zend_ast_decl* wrapperFuncAst = NULL;
+
+ ELASTIC_APM_ASSERT( originalFuncAstDecl->kind == ZEND_AST_FUNC_DECL, "originalFuncAstDecl->kind: %s", streamZendAstKind( originalFuncAstDecl->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
+
+ StringView originalFuncName;
+ if ( ! getAstDeclName( originalFuncAstDecl, /* out */ &originalFuncName ) )
+ {
+ ELASTIC_APM_LOG_ERROR( "Failed to get function name - returning failure" );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "originalFuncName: %s, compiled_filename: %s", originalFuncName.begin, dbgCompiledFileName );
+ debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug );
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( createWrappedFunctionNewName( originalFuncName, /* out */ &wrappedFunctionNewName ) );
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( createWrapperFunctionAst( originalFuncAstDecl, stringBufferToView( wrappedFunctionNewName ), /* out */ &wrapperFuncAst ) );
+ zend_ast* newCombinedAst = createAstListWithTwoChildren( ZEND_AST_STMT_LIST, (zend_ast*)originalFuncAstDecl, (zend_ast*)wrapperFuncAst );
+ newCombinedAst->lineno = findAstDeclStartLineNumber( originalFuncAstDecl );
+ originalFuncAstDecl->name = createZStringForAst( stringBufferToView( wrappedFunctionNewName ) );
+ *((zend_ast**)pAstChildSlot) = newCombinedAst;
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ wrappedFunctionNewName );
+ ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG( "originalFuncName: %s, compiled_filename: %s", originalFuncName.begin, dbgCompiledFileName );
+ debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug );
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+bool getAstName( zend_ast* ast, /* out */ StringView* name )
+{
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ switch ( ast->kind )
+ {
+ case ZEND_AST_CLASS:
+ case ZEND_AST_FUNC_DECL:
+ case ZEND_AST_METHOD:
+ return getAstDeclName( (zend_ast_decl*)ast, /* out */ name );
+
+ default:
+ ELASTIC_APM_ASSERT( false, "Unexpected ast->kind: %s", streamZendAstKind( ast->kind, &txtOutStream ) );
+ return false;
+ }
+}
+
+bool parseAstNamespace( zend_ast* astNamespace, /* out */ StringView* pName, /* out */ zend_ast** pEnclosedScope )
+{
+ // PHP code:
+ //
+ // namespace // global
+ // {
+ // }
+ //
+ // AST:
+ //
+ // ZEND_AST_NAMESPACE (line: 4, attr: 0, childCount: 2)
+ // NULL
+ // ZEND_AST_STMT_LIST (line: 4, attr: 0, childCount: 5)
+
+ // PHP code:
+ //
+ // namespace MyNamespace;
+ //
+ // AST:
+ //
+ // ZEND_AST_NAMESPACE (line: 3, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 3, attr: 0) [type: string, value: MyNamespace]
+ // NULL
+ //
+ // PHP code:
+ //
+ // namespace MyNamespace
+ // {
+ // }
+ //
+ // AST:
+ //
+ // ZEND_AST_NAMESPACE (line: 3, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 3, attr: 0) [type: string, value: MyNamespace]
+ // ZEND_AST_STMT_LIST (line: 4, attr: 0, childCount: 5)
+
+
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ zend_ast* nameAstZval = NULL;
+ zend_ast* enclosedScopeAst = NULL;
+ StringView name;
+
+ ELASTIC_APM_ASSERT( astNamespace->kind == ZEND_AST_NAMESPACE, "ast->kind: %s", streamZendAstKind( astNamespace->kind, &txtOutStream ) );
textOutputStreamRewind( &txtOutStream );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) );
- return transformedAst;
+ ELASTIC_APM_ASSERT_VALID_PTR( pName );
+ ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pEnclosedScope );
+
+ uint32_t childrenCountAstNamespace = zend_ast_get_num_children( astNamespace );
+ if ( childrenCountAstNamespace < 2 )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - childrenCountAstNamespace: %u", (unsigned)childrenCountAstNamespace );
+ return false;
+ }
+
+ nameAstZval = astNamespace->child[ 0 ];
+ if ( nameAstZval == NULL )
+ {
+ name = ELASTIC_APM_EMPTY_STRING_VIEW;
+ }
+ else if ( ! getStringFromAstZVal( nameAstZval, /* out */ &name ) )
+ {
+ return false;
+ }
+
+ enclosedScopeAst = astNamespace->child[ 1 ];
+ if ( ( enclosedScopeAst != NULL ) && ( enclosedScopeAst->kind != ZEND_AST_STMT_LIST ) )
+ {
+ ELASTIC_APM_LOG_TRACE( "Returning false - enclosedScopeAst->kind: %s", streamZendAstKind( enclosedScopeAst->kind, &txtOutStream ) );
+ return false;
+ }
+
+ *pName = name;
+ *pEnclosedScope = enclosedScopeAst;
+ ELASTIC_APM_LOG_TRACE( "Returning true - name [length: %"PRIu64"]: %.*s", (UInt64)(pName->length), (int)(pName->length), pName->begin );
+ return true;
}
-static
-zend_ast* transformAst( zend_ast* ast, int nestingDepth )
+typedef bool (* CheckFindAstReqs)( zend_ast* ast, void* ctx );
+
+bool checkFunctionReqs( zend_ast* ast, void* ctx )
{
char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( ast->kind ) );
+ ELASTIC_APM_ASSERT( ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_METHOD, "ast->kind: %s", streamZendAstKind( ast->kind, &txtOutStream ) );
+ textOutputStreamRewind( &txtOutStream );
- zend_ast * transformedAst;
+ size_t minParamsCount = *(size_t*)ctx;
- if ( zend_ast_is_list( ast ) )
+ ZendAstPtrArrayView paramsAsts;
+ if ( ! getAstFunctionParameters( (zend_ast_decl*)ast, /* out */ ¶msAsts ) )
+ {
+ return false;
+ }
+
+ return paramsAsts.count >= minParamsCount;
+}
+
+bool findAstOfKindCheckNode( zend_ast* ast, zend_ast_kind kindToFind, StringView name, CheckFindAstReqs checkFindAstReqs, void* checkFindAstReqsCtx )
+{
+ if ( ast->kind != kindToFind )
{
- zend_ast_list* list = zend_ast_get_list( ast );
- uint32_t i;
- for ( i = 0 ; i < list->children ; i ++ )
+ return false;
+ }
+
+ if ( name.begin != NULL )
+ {
+ StringView astName;
+ if ( ! ( getAstName( ast, /* out */ &astName ) && areStringViewsEqual( astName, name ) ) )
{
- if ( list->child[ i ] )
- {
- list->child[ i ] = transformAst( list->child[ i ], nestingDepth + 1 );
- }
+ return false;
}
- transformedAst = ast;
- goto finally;
}
- switch ( ast->kind )
+ return ( checkFindAstReqs == NULL ) || checkFindAstReqs( ast, checkFindAstReqsCtx );
+}
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "misc-no-recursion"
+zend_ast** findChildSlotAstByKind( zend_ast* ast, zend_ast_kind kindToFind, StringView namespace, StringView name, CheckFindAstReqs checkFindAstReqs, void* checkFindAstReqsCtx )
+{
+ if ( ! zend_ast_is_list( ast ) )
{
- case ZEND_AST_ZVAL:
- #ifdef ZEND_AST_CONSTANT
- case ZEND_AST_CONSTANT:
- #endif
- transformedAst = ast;
- goto finally;
+ return NULL;
+ }
- case ZEND_AST_FUNC_DECL:
- case ZEND_AST_METHOD:
- transformedAst = transformFunctionAst( ast, nestingDepth + 1 );
- goto finally;
+ zend_ast_list* astAsList = zend_ast_get_list( ast );
+ ELASTIC_APM_FOR_EACH_INDEX( i, astAsList->children )
+ {
+ zend_ast* child = astAsList->child[ i ];
+ if ( zend_ast_is_list( child ) )
+ {
+ zend_ast** foundAst = findChildSlotAstByKind( child, kindToFind, namespace, name, checkFindAstReqs, checkFindAstReqsCtx );
+ if ( foundAst != NULL )
+ {
+ return foundAst;
+ }
+ continue;
+ }
- case ZEND_AST_RETURN:
- transformedAst = transformReturnAst( ast, nestingDepth + 1 );
- goto finally;
+ if ( child->kind == ZEND_AST_NAMESPACE )
+ {
+ StringView foundNamespaceName;
+ zend_ast* namespaceEnclosedScope = NULL;
+ if ( ! parseAstNamespace( child, /* out */ &foundNamespaceName, /* out */ &namespaceEnclosedScope ) )
+ {
+ continue;
+ }
- default:
- transformedAst = transformChildrenAst( ast, nestingDepth + 1 );
- goto finally;
+ if ( ! areStringViewsEqual( foundNamespaceName, namespace ) )
+ {
+ continue;
+ }
+
+ if ( namespaceEnclosedScope != NULL )
+ {
+ zend_ast** foundAst = findChildSlotAstByKind( namespaceEnclosedScope, kindToFind, namespace, name, checkFindAstReqs, checkFindAstReqsCtx );
+ if ( foundAst != NULL )
+ {
+ return foundAst;
+ }
+ continue;
+ }
+ }
+
+ if ( findAstOfKindCheckNode( astAsList->child[ i ], kindToFind, name, checkFindAstReqs, checkFindAstReqsCtx ) )
+ {
+ // It's not &child on purpose since child is a local variable
+ return &( astAsList->child[ i ] );
+ }
}
- finally:
+ return NULL;
+}
+#pragma clang diagnostic pop
+
+zend_ast_decl** findChildSlotForStandaloneFunctionAst( zend_ast* rootAst, StringView namespace, StringView funcName, size_t minParamsCount )
+{
+ return (zend_ast_decl**) findChildSlotAstByKind( rootAst, ZEND_AST_FUNC_DECL, namespace, funcName, checkFunctionReqs, &minParamsCount );
+}
+
+zend_ast_decl* findClassAst( zend_ast* rootAst, StringView namespace, StringView className )
+{
+ zend_ast** result = findChildSlotAstByKind( rootAst, ZEND_AST_CLASS, namespace, className, /* checkFuncDeclReqs */ NULL, /* checkFindAstReqsCtx */ NULL );
+ return (zend_ast_decl*)(*result);
+}
+
+zend_ast_decl** findChildSlotForMethodAst( zend_ast_decl* astClass, StringView methodName, size_t minParamsCount )
+{
+ // ZEND_AST_CLASS (name: WP_Hook, line: 6, flags: 32, attr: 0, childCount: 3)
+ // NULL
+ // ZEND_AST_NAME_LIST (line: 6, attr: 0, childCount: 2)
+ // ZEND_AST_ZVAL (line: 6, attr: 1) [type: string, value: Iterator]
+ // ZEND_AST_ZVAL (line: 6, attr: 1) [type: string, value: ArrayAccess]
+ // ZEND_AST_STMT_LIST (line: 6, attr: 0, childCount: 1)
+ // ZEND_AST_METHOD (name: add_filter, line: 7, flags: 1, attr: 0, childCount: 4)
+
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ ELASTIC_APM_ASSERT( astClass->kind == ZEND_AST_CLASS, "ast->kind: %s", streamZendAstKind( astClass->kind, &txtOutStream ) );
textOutputStreamRewind( &txtOutStream );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) );
- return transformedAst;
+
+ zend_ast_decl* astAsDecl = (zend_ast_decl*)astClass;
+
+ // class body is always child[ 2 ] - see zend_compile_class_decl
+ return (zend_ast_decl**) findChildSlotAstByKind( astAsDecl->child[ 2 ], ZEND_AST_METHOD, /* namespace */ ELASTIC_APM_EMPTY_STRING_VIEW, methodName, checkFunctionReqs, &minParamsCount );
}
-static
-void elasticApmProcessAstRoot( zend_ast* ast )
+void elasticApmTransformAstImpl( zend_ast* ast )
{
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "ast->kind: %s", zendAstKindToString( ast->kind ) );
+ StringView compiledFileFullPath = nullableZStringToStringView( CG( compiled_filename) );
+ if ( compiledFileFullPath.begin == NULL )
+ {
+ return;
+ }
+
+ size_t fileIndex;
+ if ( ! wordPressInstrumentationShouldTransformAstInFile( compiledFileFullPath, /* out */ &fileIndex ) )
+ {
+ return;
+ }
+
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin );
+ debugDumpAstTree( compiledFileFullPath, ast, /* isBeforeProcess */ true );
- zend_ast * transformedAst = transformAst( ast, 0 );
- if ( original_zend_ast_process != NULL ) original_zend_ast_process( transformedAst );
+ wordPressInstrumentationTransformAst( fileIndex, compiledFileFullPath, ast );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin );
+ debugDumpAstTree( compiledFileFullPath, ast, /* isBeforeProcess */ false );
}
-void astInstrumentationInit()
+void elasticApmTransformAst( zend_ast* ast )
{
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
+ ELASTIC_APM_ASSERT( g_isOriginalZendAstProcessSet, "g_originalZendAstProcess: %p", g_originalZendAstProcess );
- original_zend_ast_process = zend_ast_process;
- zend_ast_process = elasticApmProcessAstRoot;
+ if ( ( ! g_isLoadingAgentPhpCode ) && ast != NULL )
+ {
+ elasticApmTransformAstImpl( ast );
+ }
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
+ if ( g_originalZendAstProcess != NULL )
+ {
+ g_originalZendAstProcess( ast );
+ }
}
-void astInstrumentationShutdown()
+void astInstrumentationOnModuleInit( const ConfigSnapshot* config )
{
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
+ if ( config->astProcessEnabled )
+ {
+ g_originalZendAstProcess = zend_ast_process;
+ g_isOriginalZendAstProcessSet = true;
+ zend_ast_process = elasticApmTransformAst;
+ ELASTIC_APM_LOG_DEBUG( "Changed zend_ast_process: from %p to elasticApmTransformAst (%p)", g_originalZendAstProcess, elasticApmTransformAst );
+ } else {
+ ELASTIC_APM_LOG_DEBUG( "AST processing will be DISABLED because configuration option %s (astProcessEnabled) is set to false", ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED );
+ }
+}
- zend_ast_process = original_zend_ast_process;
+void astInstrumentationOnModuleShutdown()
+{
+ if ( g_isOriginalZendAstProcessSet )
+ {
+ zend_ast_process_t zendAstProcessBeforeRestore = zend_ast_process;
+ zend_ast_process = g_originalZendAstProcess;
+ g_originalZendAstProcess = NULL;
+ g_isOriginalZendAstProcessSet = false;
+ ELASTIC_APM_LOG_DEBUG( "Restored zend_ast_process: from %p (%s elasticApmTransformAst: %p) -> %p"
+ , zendAstProcessBeforeRestore, zendAstProcessBeforeRestore == elasticApmTransformAst ? "==" : "!=", elasticApmTransformAst, g_originalZendAstProcess );
+ }
+}
- ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
+void astInstrumentationOnRequestInit( const ConfigSnapshot* config )
+{
+ astProcessDebugDumpOnRequestInit( config );
+ wordPressInstrumentationOnRequestInit();
+}
+
+void astInstrumentationOnRequestShutdown()
+{
+ wordPressInstrumentationOnRequestShutdown();
+ astProcessDebugDumpOnRequestShutdown();
}
diff --git a/src/ext/AST_instrumentation.h b/src/ext/AST_instrumentation.h
index c024e4295..822d7445a 100644
--- a/src/ext/AST_instrumentation.h
+++ b/src/ext/AST_instrumentation.h
@@ -19,6 +19,34 @@
#pragma once
-void astInstrumentationInit();
+#include
+#include "ConfigSnapshot_forward_decl.h"
+#include "StringView.h"
+#include "TextOutputStream.h"
+#include "ResultCode.h"
+#include "ArrayView.h"
-void astInstrumentationShutdown();
+enum ArgCaptureSpec
+{
+ captureArgByValue,
+ captureArgByRef,
+ dontCaptureArg
+};
+typedef enum ArgCaptureSpec ArgCaptureSpec;
+ELASTIC_APM_DECLARE_ARRAY_VIEW( ArgCaptureSpec, ArgCaptureSpecArrayView );
+
+void astInstrumentationOnModuleInit( const ConfigSnapshot* config );
+void astInstrumentationOnModuleShutdown();
+
+void astInstrumentationOnRequestInit( const ConfigSnapshot* config );
+void astInstrumentationOnRequestShutdown();
+
+zend_ast_decl** findChildSlotForStandaloneFunctionAst( zend_ast* rootAst, StringView namespace, StringView funcName, size_t minParamsCount );
+zend_ast_decl* findClassAst( zend_ast* rootAst, StringView namespace, StringView className );
+zend_ast_decl** findChildSlotForMethodAst( zend_ast_decl* astClass, StringView methodName, size_t minParamsCount );
+
+ResultCode insertAstForFunctionPreHook( zend_ast_decl* funcAstDecl, ArgCaptureSpecArrayView argCaptureSpecs );
+ResultCode appendDirectCallToInstrumentation( zend_ast_decl** pAstChildSlot, StringView constNameForMethodName );
+ResultCode wrapStandaloneFunctionAstWithPrePostHooks( zend_ast_decl** pAstChildSlot );
+
+String streamZendAstKind( zend_ast_kind kind, TextOutputStream* txtOutStream );
diff --git a/src/ext/AST_util.h b/src/ext/AST_util.h
new file mode 100644
index 000000000..62d72d54d
--- /dev/null
+++ b/src/ext/AST_util.h
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include
+#include "ArrayView.h"
+#include "basic_macros.h"
+
+typedef zend_ast* ZendAstPtr;
+ELASTIC_APM_DECLARE_ARRAY_VIEW( ZendAstPtr, ZendAstPtrArrayView );
+
+static inline
+bool isAstDecl( zend_ast_kind kind )
+{
+ switch( kind )
+ {
+ case ZEND_AST_FUNC_DECL:
+ case ZEND_AST_CLOSURE:
+ case ZEND_AST_METHOD:
+ case ZEND_AST_CLASS:
+ // ZEND_AST_ARROW_FUNC was added in PHP 7.4
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 4, 0 )
+ case ZEND_AST_ARROW_FUNC:
+ #endif
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+/**
+ * zend_ast_decl.child is array of size
+ * 4 before PHP v8.0.0
+ * 5 from PHP v8.0.0
+ *
+ * @see zend_ast_decl and zend_ast_create_decl
+ */
+enum
+{
+ elasticApmZendAstDeclChildrenCount =
+ #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) /* if PHP version before 8.0.0 */
+ 4
+ #else
+ 5
+ #endif
+};
+
+static inline
+ZendAstPtrArrayView getAstDeclChildren( zend_ast_decl* astDecl )
+{
+ ELASTIC_APM_STATIC_ASSERT( ELASTIC_APM_STATIC_ARRAY_SIZE( astDecl->child ) == elasticApmZendAstDeclChildrenCount );
+ return ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, elasticApmZendAstDeclChildrenCount, &( astDecl->child[ 0 ] ) );
+}
+
+static inline
+ZendAstPtrArrayView getAstChildren( zend_ast* ast )
+{
+ /** @see enum zend_ast_copy */
+
+ if ( zend_ast_is_list( ast ) )
+ {
+ zend_ast_list* astList = zend_ast_get_list( ast );
+ return ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, (size_t)( astList->children ), &( astList->child[ 0 ] ) );
+ }
+
+ if ( isAstDecl( ast->kind ) )
+ {
+ return getAstDeclChildren( (zend_ast_decl*)ast );
+ }
+
+ switch ( ast->kind )
+ {
+ /**
+ * special nodes
+ *
+ * @see enum _zend_ast_kind
+ */
+ case ZEND_AST_ZNODE:
+ case ZEND_AST_ZVAL:
+ // ZEND_AST_CONSTANT was added in PHP 7.3
+ #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */
+ case ZEND_AST_CONSTANT:
+ #endif
+ return ELASTIC_APM_MAKE_EMPTY_ARRAY_VIEW( ZendAstPtrArrayView );
+
+ default:
+ return ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, (size_t)zend_ast_get_num_children( ast ), &( ast->child[ 0 ] ) );
+ }
+}
diff --git a/src/ext/ConfigManager.c b/src/ext/ConfigManager.c
index 7fce5c9d2..ff8a148e0 100644
--- a/src/ext/ConfigManager.c
+++ b/src/ext/ConfigManager.c
@@ -764,6 +764,10 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, apiKey )
# if ( ELASTIC_APM_ASSERT_ENABLED_01 != 0 )
ELASTIC_APM_DEFINE_ENUM_FIELD_ACCESS_FUNCS( AssertLevel, assertLevel )
# endif
+ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, astProcessEnabled )
+ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, astProcessDebugDumpConvertedBackToSource )
+ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, astProcessDebugDumpForPathPrefix )
+ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, astProcessDebugDumpOutDir )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( optionalBoolValue, asyncBackendComm )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, bootstrapPhpPartFile )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, breakdownMetrics )
@@ -802,6 +806,7 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceVersion )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, spanCompressionEnabled )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, spanCompressionExactMatchMaxDuration )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, spanCompressionSameKindMaxDuration )
+ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, stackTraceLimit )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionIgnoreUrls )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionMaxSpans )
ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionSampleRate )
@@ -921,6 +926,30 @@ static void initOptionsMetadata( OptionMetadata* optsMeta )
/* isUniquePrefixEnough: */ true );
#endif
+ ELASTIC_APM_INIT_METADATA(
+ buildBoolOptionMetadata,
+ astProcessEnabled,
+ ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED,
+ /* defaultValue: */ true );
+
+ ELASTIC_APM_INIT_METADATA(
+ buildBoolOptionMetadata,
+ astProcessDebugDumpConvertedBackToSource,
+ ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE,
+ /* defaultValue: */ true );
+
+ ELASTIC_APM_INIT_METADATA(
+ buildStringOptionMetadata,
+ astProcessDebugDumpForPathPrefix,
+ ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX,
+ /* defaultValue: */ NULL );
+
+ ELASTIC_APM_INIT_METADATA(
+ buildStringOptionMetadata,
+ astProcessDebugDumpOutDir,
+ ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_OUT_DIR,
+ /* defaultValue: */ NULL );
+
ELASTIC_APM_INIT_METADATA(
buildOptionalBoolOptionMetadata,
asyncBackendComm,
@@ -1110,6 +1139,12 @@ static void initOptionsMetadata( OptionMetadata* optsMeta )
ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION,
/* defaultValue: */ NULL );
+ ELASTIC_APM_INIT_METADATA(
+ buildStringOptionMetadata,
+ stackTraceLimit,
+ ELASTIC_APM_CFG_OPT_NAME_STACK_TRACE_LIMIT,
+ /* defaultValue: */ NULL );
+
ELASTIC_APM_INIT_METADATA(
buildStringOptionMetadata,
transactionIgnoreUrls,
diff --git a/src/ext/ConfigManager.h b/src/ext/ConfigManager.h
index 686067057..d1cf321ea 100644
--- a/src/ext/ConfigManager.h
+++ b/src/ext/ConfigManager.h
@@ -66,6 +66,10 @@ enum OptionId
#if ( ELASTIC_APM_ASSERT_ENABLED_01 != 0 )
optionId_assertLevel,
#endif
+ optionId_astProcessEnabled,
+ optionId_astProcessDebugDumpConvertedBackToSource,
+ optionId_astProcessDebugDumpForPathPrefix,
+ optionId_astProcessDebugDumpOutDir,
optionId_asyncBackendComm,
optionId_bootstrapPhpPartFile,
optionId_breakdownMetrics,
@@ -104,6 +108,7 @@ enum OptionId
optionId_spanCompressionEnabled,
optionId_spanCompressionExactMatchMaxDuration,
optionId_spanCompressionSameKindMaxDuration,
+ optionId_stackTraceLimit,
optionId_transactionIgnoreUrls,
optionId_transactionMaxSpans,
optionId_transactionSampleRate,
@@ -218,6 +223,20 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot();
#define ELASTIC_APM_CFG_OPT_NAME_ASSERT_LEVEL "assert_level"
# endif
+/**
+ * Internal configuration option (not included in public documentation)
+ */
+#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED "ast_process_enabled"
+
+/**
+ * Internal configuration options (not included in public documentation)
+ * In addition to supportability this option is used by component tests as well.
+ * @see tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php
+ */
+#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE "ast_process_debug_dump_converted_back_to_source"
+#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX "ast_process_debug_dump_for_path_prefix"
+#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_OUT_DIR "ast_process_debug_dump_out_dir"
+
/**
* Internal configuration option (not included in public documentation)
*/
@@ -296,6 +315,7 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot();
#define ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_ENABLED "span_compression_enabled"
#define ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION "span_compression_exact_match_max_duration"
#define ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION "span_compression_same_kind_max_duration"
+#define ELASTIC_APM_CFG_OPT_NAME_STACK_TRACE_LIMIT "stack_trace_limit"
#define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_IGNORE_URLS "transaction_ignore_urls"
#define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_MAX_SPANS "transaction_max_spans"
#define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_SAMPLE_RATE "transaction_sample_rate"
diff --git a/src/ext/ConfigSnapshot.h b/src/ext/ConfigSnapshot.h
index 1628d3462..0cf9740b5 100644
--- a/src/ext/ConfigSnapshot.h
+++ b/src/ext/ConfigSnapshot.h
@@ -79,6 +79,7 @@ struct ConfigSnapshot
bool spanCompressionEnabled;
String spanCompressionExactMatchMaxDuration;
String spanCompressionSameKindMaxDuration;
+ String stackTraceLimit;
String transactionIgnoreUrls;
String transactionMaxSpans;
String transactionSampleRate;
diff --git a/src/ext/WordPress_instrumentation.c b/src/ext/WordPress_instrumentation.c
new file mode 100644
index 000000000..7f64baacf
--- /dev/null
+++ b/src/ext/WordPress_instrumentation.c
@@ -0,0 +1,251 @@
+/*
+ * 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.
+ */
+
+#include "WordPress_instrumentation.h"
+#include "log.h"
+#include "AST_instrumentation.h"
+#include "util.h"
+#include "TextOutputStream.h"
+
+#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT
+
+enum WordPressInstrumentationFileToTransformAstIndex
+{
+ wordPress_instrumentation_file_to_transform_AST_plugin_php,
+ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php,
+ wordPress_instrumentation_file_to_transform_AST_theme_php,
+
+ number_of_WordPress_instrumentation_files_to_transform_AST
+};
+
+#define ELASTIC_APM_WP_INCLUDES_PREFIX "wp-includes/"
+
+static StringView g_filesToTransformAstPathSuffix[ number_of_WordPress_instrumentation_files_to_transform_AST ] =
+{
+ [ wordPress_instrumentation_file_to_transform_AST_plugin_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "plugin.php" ),
+ [ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "class-wp-hook.php" ),
+ [ wordPress_instrumentation_file_to_transform_AST_theme_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "theme.php" )
+};
+
+#undef ELASTIC_APM_WP_INCLUDES_PREFIX
+
+struct WordPressInstrumentationRequestScopedState
+{
+ bool isInFailedMode;
+ bool seenFile[ number_of_WordPress_instrumentation_files_to_transform_AST ];
+};
+typedef struct WordPressInstrumentationRequestScopedState WordPressInstrumentationRequestScopedState;
+
+static WordPressInstrumentationRequestScopedState g_wordPressInstrumentationRequestScopedState;
+
+void wordPressInstrumentationSwitchToFailedMode( String dbgCalledFromFunc )
+{
+ if ( g_wordPressInstrumentationRequestScopedState.isInFailedMode )
+ {
+ return;
+ }
+
+ ELASTIC_APM_LOG_ERROR( "Switched to failed mode; dbgCalledFromFunc: %s", dbgCalledFromFunc );
+
+ g_wordPressInstrumentationRequestScopedState.isInFailedMode = true;
+}
+
+void wordPressInstrumentationOnRequestInit()
+{
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
+
+ g_wordPressInstrumentationRequestScopedState.isInFailedMode = false;
+
+ ELASTIC_APM_FOR_EACH_INDEX( i, number_of_WordPress_instrumentation_files_to_transform_AST )
+ {
+ g_wordPressInstrumentationRequestScopedState.seenFile[ i ] = false;
+ }
+
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
+}
+
+void wordPressInstrumentationOnRequestShutdown()
+{
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT();
+}
+
+static
+ResultCode insertPreHookForFunctionWithHookNameCallbackParams( zend_ast_decl* astDecl )
+{
+ // standalone function:
+ // function _wp_filter_build_unique_id( $hook_name, $callback, $priority )
+ // class WP_Hook method
+ // public function add_filter( $hook_name, $callback, $priority, $accepted_args )
+ ArgCaptureSpec argCaptureSpecArr[] = { /* capture $hook_name by value */ captureArgByValue, /* capture $callback by reference */ captureArgByRef };
+ return insertAstForFunctionPreHook( astDecl, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ArgCaptureSpecArrayView, argCaptureSpecArr ) );
+}
+
+static StringView g_globalNamespace = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "" );
+
+static StringView g_wp_filter_build_unique_id_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "_wp_filter_build_unique_id" );
+
+ResultCode wordPressInstrumentationTransformFile_plugin_php( zend_ast* ast )
+{
+ ResultCode resultCode;
+ zend_ast_decl** p_wp_filter_build_unique_id_astFuncDecl = NULL;
+
+ // function _wp_filter_build_unique_id( $hook_name, $callback, $priority )
+ p_wp_filter_build_unique_id_astFuncDecl = findChildSlotForStandaloneFunctionAst( ast, g_globalNamespace, g_wp_filter_build_unique_id_funcName, /* minParamsCount */ 3 );
+
+ if ( p_wp_filter_build_unique_id_astFuncDecl == NULL )
+ {
+ ELASTIC_APM_LOG_ERROR( "Function %s are not found", g_wp_filter_build_unique_id_funcName.begin );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( insertPreHookForFunctionWithHookNameCallbackParams( *p_wp_filter_build_unique_id_astFuncDecl ) );
+ // It's important to record if we instrumented _wp_filter_build_unique_id successfully.
+ // _wp_filter_build_unique_id instrumentation alone cannot make application work incorrectly
+ // because it checks if $callback is an instance our WordPressFilterCallbackWrapper class before unwrapping it.
+ // On the other hand add_filter instrumentation alone CAN make application work incorrectly
+ // if _wp_filter_build_unique_id was not instrumented as well.
+ // So we record if we instrumented _wp_filter_build_unique_id successfully
+ // and PHP part wraps callbacks only if it sees the record that _wp_filter_build_unique_id was instrumented successfully.
+ StringView setReadyToWrapFilterCallbacksConstName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME );
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( appendDirectCallToInstrumentation( p_wp_filter_build_unique_id_astFuncDecl, setReadyToWrapFilterCallbacksConstName ) );
+
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+static StringView g_WP_Hook_className = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "WP_Hook" );
+static StringView g_add_filter_methodName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "add_filter" );
+
+ResultCode wordPressInstrumentationTransformFile_class_wp_hook_php( zend_ast* ast )
+{
+ ResultCode resultCode;
+ zend_ast_decl* WP_Hook_astClassDecl = NULL;
+ zend_ast_decl** p_add_filter_astMethod = NULL;
+
+ WP_Hook_astClassDecl = findClassAst( ast, g_globalNamespace, g_WP_Hook_className );
+ if ( WP_Hook_astClassDecl == NULL )
+ {
+ ELASTIC_APM_LOG_TRACE( "Class %s not found", g_WP_Hook_className.begin );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ // public function add_filter( $hook_name, $callback, $priority, $accepted_args )
+ p_add_filter_astMethod = findChildSlotForMethodAst( WP_Hook_astClassDecl, g_add_filter_methodName, /* minParamsCount */ 4 );
+ if ( p_add_filter_astMethod == NULL )
+ {
+ ELASTIC_APM_LOG_ERROR( "Method %s (in class %s) not found", g_add_filter_methodName.begin, g_WP_Hook_className.begin );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( insertPreHookForFunctionWithHookNameCallbackParams( *p_add_filter_astMethod ) );
+
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+static StringView g_get_template_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "get_template" );
+
+ResultCode wordPressInstrumentationTransformFile_theme_php( zend_ast* ast )
+{
+ ResultCode resultCode;
+ zend_ast_decl** p_get_template_astFuncDecl = NULL;
+
+ // function get_template()
+ p_get_template_astFuncDecl = findChildSlotForStandaloneFunctionAst( ast, g_globalNamespace, g_get_template_funcName, /* minParamsCount */ 0 );
+
+ if ( p_get_template_astFuncDecl == NULL )
+ {
+ ELASTIC_APM_LOG_ERROR( "Function %s was not found", g_get_template_funcName.begin );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( wrapStandaloneFunctionAstWithPrePostHooks( p_get_template_astFuncDecl ) );
+
+ resultCode = resultSuccess;
+ finally:
+ return resultCode;
+
+ failure:
+ goto finally;
+}
+
+bool wordPressInstrumentationShouldTransformAstInFile( StringView compiledFileFullPath, size_t* pFileIndex )
+{
+ if ( g_wordPressInstrumentationRequestScopedState.isInFailedMode )
+ {
+ return false;
+ }
+
+ ELASTIC_APM_FOR_EACH_INDEX( i, number_of_WordPress_instrumentation_files_to_transform_AST )
+ {
+ if ( g_wordPressInstrumentationRequestScopedState.seenFile[ i ] )
+ {
+ continue;
+ }
+
+ if ( isStringViewSuffix( compiledFileFullPath, g_filesToTransformAstPathSuffix[ i ] ) )
+ {
+ *pFileIndex = i;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+typedef ResultCode (* WordPressTransformAstForFileFunc )( zend_ast* ast );
+
+static WordPressTransformAstForFileFunc g_transformAstForFileFuncs[ number_of_WordPress_instrumentation_files_to_transform_AST ] =
+{
+ [ wordPress_instrumentation_file_to_transform_AST_plugin_php ] = wordPressInstrumentationTransformFile_plugin_php,
+ [ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php ] = wordPressInstrumentationTransformFile_class_wp_hook_php,
+ [ wordPress_instrumentation_file_to_transform_AST_theme_php ] = wordPressInstrumentationTransformFile_theme_php
+};
+
+void wordPressInstrumentationTransformAst( size_t fileIndex, StringView compiledFileFullPath, zend_ast* ast )
+{
+ ELASTIC_APM_ASSERT_LT_UINT64( fileIndex, number_of_WordPress_instrumentation_files_to_transform_AST );
+ ELASTIC_APM_ASSERT( ! g_wordPressInstrumentationRequestScopedState.seenFile[ fileIndex ], "fileIndex: %u, file: %s", (UInt)fileIndex, g_filesToTransformAstPathSuffix[ fileIndex ].begin );
+ g_wordPressInstrumentationRequestScopedState.seenFile[ fileIndex ] = true;
+
+ ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin );
+
+ ResultCode resultCode;
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( g_transformAstForFileFuncs[ fileIndex ]( ast ) );
+
+ resultCode = resultSuccess;
+ finally:
+ ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG();
+ ELASTIC_APM_UNUSED( resultCode );
+ return;
+
+ failure:
+ wordPressInstrumentationSwitchToFailedMode( __FUNCTION__ );
+ goto finally;
+}
diff --git a/src/ext/WordPress_instrumentation.h b/src/ext/WordPress_instrumentation.h
new file mode 100644
index 000000000..8656ade0b
--- /dev/null
+++ b/src/ext/WordPress_instrumentation.h
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include
+#include "StringView.h"
+
+void wordPressInstrumentationOnRequestInit();
+void wordPressInstrumentationOnRequestShutdown();
+
+bool wordPressInstrumentationShouldTransformAstInFile( StringView compiledFileFullPath, /* out */ size_t* pFileIndex );
+void wordPressInstrumentationTransformAst( size_t fileIndex, StringView compiledFileFullPath, zend_ast* ast );
+
diff --git a/src/ext/config.m4 b/src/ext/config.m4
index 0eecd664f..ec76c6b7a 100644
--- a/src/ext/config.m4
+++ b/src/ext/config.m4
@@ -79,6 +79,7 @@ if test "$PHP_ELASTIC_APM" != "no"; then
AC_DEFINE(HAVE_ELASTIC_APM, 1, [ Have elastic_apm support ])
ELASTIC_APM_PHP_EXT_SOURCES="\
+ AST_debug.c \
AST_instrumentation.c \
backend_comm.c \
ConfigManager.c \
@@ -101,6 +102,7 @@ if test "$PHP_ELASTIC_APM" != "no"; then
tracer_PHP_part.c \
util.c \
util_for_PHP.c \
+ WordPress_instrumentation.c \
"
PHP_NEW_EXTENSION(elastic_apm, $ELASTIC_APM_PHP_EXT_SOURCES, $ext_shared)
diff --git a/src/ext/config.w32 b/src/ext/config.w32
index 5321f310c..dc72f4f11 100644
--- a/src/ext/config.w32
+++ b/src/ext/config.w32
@@ -13,6 +13,7 @@ if (PHP_ELASTIC_APM != 'no') {
&& CHECK_LIB("normaliz.lib", "elastic_apm", PHP_ELASTIC_APM)
) {
ELASTIC_APM_PHP_EXT_SOURCES =
+ "AST_debug.c" + " " +
"AST_instrumentation.c" + " " +
"backend_comm.c" + " " +
"ConfigManager.c" + " " +
@@ -34,6 +35,7 @@ if (PHP_ELASTIC_APM != 'no') {
"tracer_PHP_part.c" + " " +
"util.c" + " " +
"util_for_PHP.c" + " " +
+ "WordPress_instrumentation.c" + " " +
"";
EXTENSION('elastic_apm', ELASTIC_APM_PHP_EXT_SOURCES, null, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');
diff --git a/src/ext/constants.h b/src/ext/constants.h
index 524fb56c8..48067fd69 100644
--- a/src/ext/constants.h
+++ b/src/ext/constants.h
@@ -53,3 +53,6 @@ ELASTIC_APM_STATIC_ASSERT( errorIdSizeInBytes <= idMaxSizeInBytes );
#define ELASTIC_APM_NUMBER_OF_MICROSECONDS_IN_MILLISECOND (1000) // 10^3
#define ELASTIC_APM_NUMBER_OF_NANOSECONDS_IN_SECOND (1000000000L) // 10^9
#define ELASTIC_APM_NUMBER_OF_NANOSECONDS_IN_MILLISECOND (1000000L) // 10^6
+
+#define ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS "setReadyToWrapFilterCallbacks"
+#define ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME "ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS"
diff --git a/src/ext/elastic_apm.c b/src/ext/elastic_apm.c
index cfdd4dbc4..e42ea2238 100644
--- a/src/ext/elastic_apm.c
+++ b/src/ext/elastic_apm.c
@@ -25,12 +25,14 @@
#include
#include
#include
+#include "constants.h"
#include "lifecycle.h"
#include "supportability_zend.h"
#include "elastic_apm_API.h"
#include "ConfigManager.h"
#include "elastic_apm_assert.h"
#include "elastic_apm_alloc.h"
+#include "tracer_PHP_part.h"
#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_EXT_INFRA
@@ -139,6 +141,10 @@ PHP_INI_BEGIN()
#if ( ELASTIC_APM_ASSERT_ENABLED_01 != 0 )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_ASSERT_LEVEL )
#endif
+ ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED )
+ ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE )
+ ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX )
+ ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_OUT_DIR )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_ASYNC_BACKEND_COMM )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_BOOTSTRAP_PHP_PART_FILE )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_BREAKDOWN_METRICS )
@@ -177,6 +183,7 @@ PHP_INI_BEGIN()
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_ENABLED )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION )
+ ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_STACK_TRACE_LIMIT )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_IGNORE_URLS )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_MAX_SPANS )
ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_SAMPLE_RATE )
@@ -282,6 +289,10 @@ PHP_MINIT_FUNCTION(elastic_apm)
REGISTER_LONG_CONSTANT( "ELASTIC_APM_MEMORY_TRACKING_LEVEL_EACH_ALLOCATION_WITH_STACK_TRACE", memoryTrackingLevel_eachAllocationWithStackTrace, CONST_CS|CONST_PERSISTENT );
REGISTER_LONG_CONSTANT( "ELASTIC_APM_MEMORY_TRACKING_LEVEL_ALL", memoryTrackingLevel_all, CONST_CS|CONST_PERSISTENT );
+ REGISTER_STRING_CONSTANT( ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME
+ , ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS
+ , CONST_CS | CONST_PERSISTENT );
+
elasticApmModuleInit( type, module_number );
// We ignore errors because we want the monitored application to continue working
@@ -615,6 +626,46 @@ PHP_FUNCTION( elastic_apm_get_last_php_error )
}
/* }}} */
+ZEND_BEGIN_ARG_INFO_EX( elastic_apm_before_loading_agent_php_code_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 )
+ZEND_END_ARG_INFO()
+/* {{{ elastic_apm_before_loading_agent_php_code(): void
+ */
+PHP_FUNCTION( elastic_apm_before_loading_agent_php_code )
+{
+ elasticApmBeforeLoadingAgentPhpCode();
+}
+/* }}} */
+
+ZEND_BEGIN_ARG_INFO_EX( elastic_apm_after_loading_agent_php_code_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 )
+ZEND_END_ARG_INFO()
+/* {{{ elastic_apm_after_loading_agent_php_code(): void
+ */
+PHP_FUNCTION( elastic_apm_after_loading_agent_php_code )
+{
+ elasticApmAfterLoadingAgentPhpCode();
+}
+/* }}} */
+
+ZEND_BEGIN_ARG_INFO_EX( elastic_apm_ast_instrumentation_pre_hook_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 )
+ZEND_END_ARG_INFO()
+/* {{{ elastic_apm_after_loading_agent_php_code(): void
+ */
+PHP_FUNCTION( elastic_apm_ast_instrumentation_pre_hook )
+{
+ tracerPhpPartAstInstrumentationCallPreHook( execute_data, return_value );
+}
+/* }}} */
+
+ZEND_BEGIN_ARG_INFO_EX( elastic_apm_ast_instrumentation_direct_call_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 )
+ZEND_END_ARG_INFO()
+/* {{{ elastic_apm_after_loading_agent_php_code(): void
+ */
+PHP_FUNCTION( elastic_apm_ast_instrumentation_direct_call )
+{
+ tracerPhpPartAstInstrumentationDirectCall( execute_data );
+}
+/* }}} */
+
/* {{{ arginfo
*/
ZEND_BEGIN_ARG_INFO(elastic_apm_no_paramters_arginfo, 0)
@@ -634,6 +685,10 @@ static const zend_function_entry elastic_apm_functions[] =
PHP_FE( elastic_apm_log, elastic_apm_log_arginfo )
PHP_FE( elastic_apm_get_last_thrown, elastic_apm_get_last_thrown_arginfo )
PHP_FE( elastic_apm_get_last_php_error, elastic_apm_get_last_php_error_arginfo )
+ PHP_FE( elastic_apm_before_loading_agent_php_code, elastic_apm_before_loading_agent_php_code_arginfo )
+ PHP_FE( elastic_apm_after_loading_agent_php_code, elastic_apm_after_loading_agent_php_code_arginfo )
+ PHP_FE( elastic_apm_ast_instrumentation_pre_hook, elastic_apm_ast_instrumentation_pre_hook_arginfo )
+ PHP_FE( elastic_apm_ast_instrumentation_direct_call, elastic_apm_ast_instrumentation_direct_call_arginfo )
PHP_FE_END
};
/* }}} */
diff --git a/src/ext/elastic_apm_API.h b/src/ext/elastic_apm_API.h
index 221a9d4c2..b8c6b9241 100644
--- a/src/ext/elastic_apm_API.h
+++ b/src/ext/elastic_apm_API.h
@@ -42,3 +42,6 @@ void resetCallInterceptionOnRequestShutdown();
ResultCode elasticApmSendToServer( StringView userAgentHttpHeader, StringView serializedEvents );
ResultCode replaceSleepWithResumingAfterSignalImpl();
+
+void elasticApmBeforeLoadingAgentPhpCode();
+void elasticApmAfterLoadingAgentPhpCode();
diff --git a/src/ext/lifecycle.c b/src/ext/lifecycle.c
index 10869e8d8..6b3b49477 100644
--- a/src/ext/lifecycle.c
+++ b/src/ext/lifecycle.c
@@ -41,6 +41,7 @@
#include "elastic_apm_API.h"
#include "tracer_PHP_part.h"
#include "backend_comm.h"
+#include "AST_instrumentation.h"
#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_LIFECYCLE
@@ -166,6 +167,8 @@ void elasticApmModuleInit( int moduleType, int moduleNumber )
}
tracer->curlInited = true;
+ astInstrumentationOnModuleInit( config );
+
resultCode = resultSuccess;
finally:
@@ -195,6 +198,8 @@ void elasticApmModuleShutdown( int moduleType, int moduleNumber )
goto finally;
}
+ astInstrumentationOnModuleShutdown();
+
backgroundBackendCommOnModuleShutdown( config );
if ( tracer->curlInited )
@@ -595,9 +600,6 @@ void elasticApmRequestInit()
ELASTIC_APM_CALL_IF_FAILED_GOTO( replaceSleepWithResumingAfterSignalImpl() );
}
- ELASTIC_APM_CALL_IF_FAILED_GOTO( bootstrapTracerPhpPart( config, &requestInitStartTime ) );
-
-// readSystemMetrics( &tracer->startSystemMetricsReading );
if ( config->captureErrors )
{
@@ -620,6 +622,13 @@ void elasticApmRequestInit()
ELASTIC_APM_LOG_DEBUG( "capture_errors (captureErrors) configuration option is set to false which means errors will NOT be captured" );
}
+ if ( config->astProcessEnabled )
+ {
+ astInstrumentationOnRequestInit( config );
+ }
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( tracerPhpPartOnRequestInit( config, &requestInitStartTime ) );
+
resultCode = resultSuccess;
finally:
@@ -685,7 +694,13 @@ void elasticApmRequestShutdown()
resultCode = resultSuccess;
goto finally;
}
+
+ tracerPhpPartOnRequestShutdown();
+ if ( config->astProcessEnabled )
+ {
+ astInstrumentationOnRequestShutdown();
+ }
if ( isOriginalZendThrowExceptionHookSet )
{
@@ -709,11 +724,6 @@ void elasticApmRequestShutdown()
isOriginalZendErrorCallbackSet = false;
}
- // We should shutdown PHP part first because sendMetrics() uses metadata sent by PHP part on shutdown
- shutdownTracerPhpPart( config );
-
- // sendMetrics( tracer, config );
-
resetCallInterceptionOnRequestShutdown();
resetLastPhpErrorData();
diff --git a/src/ext/tracer_PHP_part.c b/src/ext/tracer_PHP_part.c
index db133ea26..64466472f 100644
--- a/src/ext/tracer_PHP_part.c
+++ b/src/ext/tracer_PHP_part.c
@@ -22,6 +22,7 @@
#include "Tracer.h"
#include "util_for_PHP.h"
#include "basic_macros.h"
+#include "elastic_apm_API.h"
#include "ConfigSnapshot.h"
#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_C_TO_PHP
@@ -32,31 +33,94 @@
#define ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_PRE_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "internalFuncCallPreHook"
#define ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_POST_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "internalFuncCallPostHook"
#define ELASTIC_APM_PHP_PART_EMPTY_METHOD_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "emptyMethod"
+#define ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_PRE_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "astInstrumentationPreHook"
+#define ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_DIRECT_CALL_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "astInstrumentationDirectCall"
-ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime )
+enum TracerPhpPartState
{
- char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
- TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
- ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "config->bootstrapPhpPartFile: %s"
- , streamUserString( config->bootstrapPhpPartFile, &txtOutStream ) );
+ tracerPhpPartState_before_bootstrap,
+ tracerPhpPartState_after_bootstrap,
+ tracerPhpPartState_after_shutdown,
+ tracerPhpPartState_failed,
+
+ numberOfTracerPhpPartState
+};
+typedef enum TracerPhpPartState TracerPhpPartState;
+
+StringView tracerPhpPartStateNames[ numberOfTracerPhpPartState ] =
+{
+ ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_before_bootstrap ),
+ ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_after_bootstrap ),
+ ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_after_shutdown ),
+ ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_failed ),
+};
+
+#define ELASTIC_APM_UNKNOWN_TRACER_PHP_PART_STATE_AS_STRING ""
+
+static inline
+bool isValidTracerPhpPartState( TracerPhpPartState value )
+{
+ return ( tracerPhpPartState_before_bootstrap <= value ) && ( value < numberOfTracerPhpPartState );
+}
+
+static inline
+String tracerPhpPartStateToString( TracerPhpPartState value )
+{
+ if ( isValidTracerPhpPartState( value ) )
+ {
+ return tracerPhpPartStateNames[ value ].begin;
+ }
+ return ELASTIC_APM_UNKNOWN_TRACER_PHP_PART_STATE_AS_STRING;
+}
+
+static TracerPhpPartState g_tracerPhpPartState = numberOfTracerPhpPartState;
+
+void switchTracerPhpPartStateToFailed( String reason, String dbgCalledFromFunc )
+{
+ if ( g_tracerPhpPartState == tracerPhpPartState_failed )
+ {
+ return;
+ }
+
+ ELASTIC_APM_LOG_ERROR( "Switching tracer PHP part state to failed; reason: %s, current state: %s, called from %s"
+ , reason, tracerPhpPartStateToString( g_tracerPhpPartState ), dbgCalledFromFunc );
+ g_tracerPhpPartState = tracerPhpPartState_failed;
+}
+
+ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime )
+{
ResultCode resultCode;
+ bool shouldRevertLoadingAgentPhpCode = false;
bool bootstrapTracerPhpPartRetVal;
zval maxEnabledLevel;
ZVAL_UNDEF( &maxEnabledLevel );
zval requestInitStartTimeZval;
ZVAL_UNDEF( &requestInitStartTimeZval );
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+ ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "config->bootstrapPhpPartFile: %s, g_tracerPhpPartState: %s"
+ , streamUserString( config->bootstrapPhpPartFile, &txtOutStream ), tracerPhpPartStateToString( g_tracerPhpPartState ) );
+ textOutputStreamRewind( &txtOutStream );
+
+ if ( g_tracerPhpPartState != tracerPhpPartState_before_bootstrap )
+ {
+ switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
if ( config->bootstrapPhpPartFile == NULL )
{
- // For now, we don't consider `bootstrap_php_part_file' option not being set as a failure
GetConfigManagerOptionMetadataResult getMetaRes;
getConfigManagerOptionMetadata( getGlobalTracer()->configManager, optionId_bootstrapPhpPartFile, &getMetaRes );
- ELASTIC_APM_LOG_ERROR( "Configuration option `%s' is not set", getMetaRes.optName );
- resultCode = resultSuccess;
- goto finally;
+ switchTracerPhpPartStateToFailed( /* reason */ streamPrintf( &txtOutStream, "Configuration option `%s' is not set", getMetaRes.optName ), __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
+ elasticApmBeforeLoadingAgentPhpCode();
+ shouldRevertLoadingAgentPhpCode = true;
+
ELASTIC_APM_CALL_IF_FAILED_GOTO( loadPhpFile( config->bootstrapPhpPartFile ) );
ZVAL_LONG( &maxEnabledLevel, getGlobalTracer()->logger.maxEnabledLevel );
@@ -73,32 +137,34 @@ ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint
ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
+ g_tracerPhpPartState = tracerPhpPartState_after_bootstrap;
resultCode = resultSuccess;
finally:
zval_dtor( &requestInitStartTimeZval );
zval_dtor( &maxEnabledLevel );
+ if ( shouldRevertLoadingAgentPhpCode )
+ {
+ elasticApmAfterLoadingAgentPhpCode();
+ }
ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT();
return resultCode;
failure:
+ switchTracerPhpPartStateToFailed( /* reason */ "Failed to bootstrap tracer PHP part", __FUNCTION__ );
goto finally;
}
-void shutdownTracerPhpPart( const ConfigSnapshot* config )
+void shutdownTracerPhpPart()
{
ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY();
ResultCode resultCode;
- if ( config->bootstrapPhpPartFile == NULL )
+ if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap )
{
- // For now, we don't consider `bootstrap_php_part_file' option not being set as a failure
- GetConfigManagerOptionMetadataResult getMetaRes;
- getConfigManagerOptionMetadata( getGlobalTracer()->configManager, optionId_bootstrapPhpPartFile, &getMetaRes );
- ELASTIC_APM_LOG_ERROR( "Configuration option `%s' is not set", getMetaRes.optName );
- resultCode = resultSuccess;
- goto finally;
+ switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
}
ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetVoid(
@@ -106,6 +172,7 @@ void shutdownTracerPhpPart( const ConfigSnapshot* config )
, /* argsCount */ 0
, /* args */ NULL ) );
+ g_tracerPhpPartState = tracerPhpPartState_after_shutdown;
resultCode = resultSuccess;
finally:
@@ -116,25 +183,29 @@ void shutdownTracerPhpPart( const ConfigSnapshot* config )
return;
failure:
+ switchTracerPhpPartStateToFailed( /* reason */ "Failed to shut down tracer PHP part", __FUNCTION__ );
goto finally;
}
+static uint32_t g_maxInterceptedCallArgsCount = 100;
+
bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zend_execute_data* execute_data )
{
ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "interceptRegistrationId: %u", interceptRegistrationId );
ResultCode resultCode;
zval preHookRetVal;
- bool shouldCallPostHook;
-
+ ZVAL_UNDEF( &preHookRetVal );
+ bool shouldCallPostHook = false;
zval interceptRegistrationIdAsZval;
ZVAL_UNDEF( &interceptRegistrationIdAsZval );
+ zval phpPartArgs[ g_maxInterceptedCallArgsCount + 2 ];
- enum
+ if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap )
{
- maxInterceptedCallArgsCount = 100
- };
- zval phpPartArgs[maxInterceptedCallArgsCount + 2];
+ switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
// The first argument to PHP part's interceptedCallPreHook() is $interceptRegistrationId
ZVAL_LONG( &interceptRegistrationIdAsZval, interceptRegistrationId );
@@ -151,7 +222,7 @@ bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zen
}
uint32_t interceptedCallArgsCount;
- getArgsFromZendExecuteData( execute_data, maxInterceptedCallArgsCount, &( phpPartArgs[ 2 ] ), &interceptedCallArgsCount );
+ getArgsFromZendExecuteData( execute_data, g_maxInterceptedCallArgsCount, &( phpPartArgs[ 2 ] ), &interceptedCallArgsCount );
ELASTIC_APM_CALL_IF_FAILED_GOTO(
callPhpFunctionRetZval(
ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_PRE_HOOK_FUNC )
@@ -177,6 +248,7 @@ bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zen
return shouldCallPostHook;
failure:
+ switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ );
goto finally;
}
@@ -188,6 +260,12 @@ void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId,
ResultCode resultCode;
zval phpPartArgs[ 2 ];
+ if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap )
+ {
+ switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
// The first argument to PHP part's interceptedCallPostHook() is $hasExitedByException (bool)
ZVAL_FALSE( &( phpPartArgs[ 0 ] ) );
@@ -211,6 +289,7 @@ void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId,
return;
failure:
+ switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ );
goto finally;
}
@@ -222,6 +301,12 @@ void tracerPhpPartInterceptedCallEmptyMethod()
zval phpPartDummyArgs[ 1 ];
ZVAL_UNDEF( &( phpPartDummyArgs[ 0 ] ) );
+ if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap )
+ {
+ switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
ELASTIC_APM_CALL_IF_FAILED_GOTO(
callPhpFunctionRetVoid(
ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_EMPTY_METHOD_FUNC )
@@ -230,7 +315,6 @@ void tracerPhpPartInterceptedCallEmptyMethod()
ELASTIC_APM_LOG_TRACE( "Successfully finished call to PHP part" );
resultCode = resultSuccess;
-
finally:
ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT();
@@ -238,5 +322,76 @@ void tracerPhpPartInterceptedCallEmptyMethod()
return;
failure:
+ switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ );
+ goto finally;
+}
+
+void tracerPhpPartLogArguments( LogLevel logLevel, uint32_t argsCount, zval args[] )
+{
+ if ( maxEnabledLogLevel() < logLevel )
+ {
+ return;
+ }
+
+ char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE];
+ TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
+
+ ELASTIC_APM_FOR_EACH_INDEX( i, argsCount )
+ {
+ ELASTIC_APM_LOG_WITH_LEVEL( logLevel, "Argument #%u: %s", (unsigned)i, streamZVal( &( args[ i ] ), &txtOutStream ) );
+ }
+}
+
+void tracerPhpPartForwardCall( StringView phpFuncName, zend_execute_data* execute_data, /* out */ zval* retVal, String dbgCalledFrom )
+{
+ ResultCode resultCode;
+ uint32_t callArgsCount;
+ zval callArgs[ g_maxInterceptedCallArgsCount ];
+
+ ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "phpFuncName: %s, dbgCalledFrom: %s", phpFuncName.begin, dbgCalledFrom );
+
+ if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap )
+ {
+ switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ );
+ ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE();
+ }
+
+ getArgsFromZendExecuteData( execute_data, g_maxInterceptedCallArgsCount, &( callArgs[ 0 ] ), &callArgsCount );
+ tracerPhpPartLogArguments( logLevel_trace, callArgsCount, callArgs );
+
+ ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetZval( phpFuncName, callArgsCount, callArgs, /* out */ retVal ) );
+
+ resultCode = resultSuccess;
+ finally:
+
+ ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG( "retVal type: %s (type ID as int: %d)", zend_get_type_by_const( (int)Z_TYPE_P( retVal ) ), (int)Z_TYPE_P( retVal ) );
+ ELASTIC_APM_UNUSED( resultCode );
+ return;
+
+ failure:
+ switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ );
+ ZVAL_NULL( retVal );
goto finally;
}
+
+void tracerPhpPartAstInstrumentationCallPreHook( zend_execute_data* execute_data, zval* return_value )
+{
+ tracerPhpPartForwardCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_PRE_HOOK_FUNC ), execute_data, /* out */ return_value, __FUNCTION__ );
+}
+
+void tracerPhpPartAstInstrumentationDirectCall( zend_execute_data* execute_data )
+{
+ zval unusedRetVal;
+ tracerPhpPartForwardCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_DIRECT_CALL_FUNC ), execute_data, /* out */ &unusedRetVal, __FUNCTION__ );
+}
+
+ResultCode tracerPhpPartOnRequestInit( const ConfigSnapshot* config, const TimePoint* requestInitStartTime )
+{
+ g_tracerPhpPartState = tracerPhpPartState_before_bootstrap;
+ return bootstrapTracerPhpPart( config, requestInitStartTime );
+}
+
+void tracerPhpPartOnRequestShutdown()
+{
+ shutdownTracerPhpPart();
+}
diff --git a/src/ext/tracer_PHP_part.h b/src/ext/tracer_PHP_part.h
index 0569d3471..9380c85eb 100644
--- a/src/ext/tracer_PHP_part.h
+++ b/src/ext/tracer_PHP_part.h
@@ -24,12 +24,13 @@
#include "ConfigSnapshot_forward_decl.h"
#include "time_util.h"
-ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime );
-
-void shutdownTracerPhpPart( const ConfigSnapshot* config );
+ResultCode tracerPhpPartOnRequestInit( const ConfigSnapshot* config, const TimePoint* requestInitStartTime );
+void tracerPhpPartOnRequestShutdown();
bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zend_execute_data* execute_data );
-
void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId, zval* interceptedCallRetValOrThrown );
void tracerPhpPartInterceptedCallEmptyMethod();
+
+void tracerPhpPartAstInstrumentationCallPreHook( zend_execute_data* execute_data, zval* return_value );
+void tracerPhpPartAstInstrumentationDirectCall( zend_execute_data* execute_data );
diff --git a/src/ext/unit_tests/util_tests.c b/src/ext/unit_tests/util_tests.c
index bf7f09c75..fb54c8c9a 100644
--- a/src/ext/unit_tests/util_tests.c
+++ b/src/ext/unit_tests/util_tests.c
@@ -226,12 +226,41 @@ void test_sizeToBytes( void** testFixtureState )
}
}
+static
+bool isStringViewSuffixWrapperForTest( String str, String suffix )
+{
+ return isStringViewSuffix( stringToView(str), stringToView(suffix) );
+}
+
+static
+void isStringViewSuffix_test( void** testFixtureState )
+{
+ ELASTIC_APM_UNUSED( testFixtureState );
+
+ ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "", "" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "a", "a" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "ab", "b" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "ab", "ab" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "abc", "bc" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "abc", "abc" ) );
+
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "", "a" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "a", "A" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "A", "a" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "a", "ab" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "ab", "aB" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "ab", "abc" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "abc", "abC" ) );
+ ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "abc", "abcd" ) );
+}
+
int run_util_tests()
{
const struct CMUnitTest tests [] =
{
ELASTIC_APM_CMOCKA_UNIT_TEST( areStringViewsEqual_test ),
ELASTIC_APM_CMOCKA_UNIT_TEST( areStringsEqualIgnoringCase_test ),
+ ELASTIC_APM_CMOCKA_UNIT_TEST( isStringViewSuffix_test ),
ELASTIC_APM_CMOCKA_UNIT_TEST( calcAlignedSize_test ),
ELASTIC_APM_CMOCKA_UNIT_TEST( trim_StringView_test ),
ELASTIC_APM_CMOCKA_UNIT_TEST( test_parseDecimalInteger ),
diff --git a/src/ext/util_for_PHP.c b/src/ext/util_for_PHP.c
index c7368f4fd..8b1388f1f 100644
--- a/src/ext/util_for_PHP.c
+++ b/src/ext/util_for_PHP.c
@@ -309,4 +309,39 @@ bool detectOpcachePreload() {
void enableAccessToServerGlobal() {
zend_is_auto_global_str(ZEND_STRL("_SERVER"));
-}
\ No newline at end of file
+}
+
+String streamZVal( const zval* zVal, TextOutputStream* txtOutStream )
+{
+ if ( zVal == NULL )
+ {
+ return "NULL";
+ }
+
+ int zValType = (int)Z_TYPE_P( zVal );
+ switch ( zValType )
+ {
+ case IS_STRING:
+ {
+ StringView strVw = zStringToStringView( Z_STR_P( zVal ) );
+ return streamPrintf( txtOutStream, "type: string, value [length: %"PRIu64"]: %.*s", (UInt64)(strVw.length), (int)(strVw.length), strVw.begin );
+ }
+
+ case IS_LONG:
+ return streamPrintf( txtOutStream, "type: long, value: %"PRId64, (Int64)(Z_LVAL_P( zVal )) );
+
+ case IS_DOUBLE:
+ return streamPrintf( txtOutStream, "type: double, value: %f", (double)(Z_DVAL_P( zVal )) );
+
+ case IS_NULL:
+ return streamPrintf( txtOutStream, "type: null" );
+
+ case IS_FALSE:
+ return streamPrintf( txtOutStream, "type: false" );
+ case IS_TRUE:
+ return streamPrintf( txtOutStream, "type: true " );
+
+ default:
+ return streamPrintf( txtOutStream, "type: %s (type ID as int: %d)", zend_get_type_by_const( zValType ), (int)zValType );
+ }
+}
diff --git a/src/ext/util_for_PHP.h b/src/ext/util_for_PHP.h
index 8a6f69e7e..ad0f852bc 100644
--- a/src/ext/util_for_PHP.h
+++ b/src/ext/util_for_PHP.h
@@ -105,3 +105,6 @@ void enableAccessToServerGlobal();
} \
} while( 0 ) \
/**/
+
+
+String streamZVal( const zval* zVal, TextOutputStream* txtOutStream );
diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php
index c6b059e61..a0213b54f 100644
--- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php
+++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php
@@ -122,6 +122,12 @@ private static function buildOptionNameToRawToValue(): array
return [
OptionNames::API_KEY => $stringRawToParsedValues(['1my_api_key3', "my api \t key"]),
+ OptionNames::AST_PROCESS_ENABLED => $boolRawToParsedValues(),
+ OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE
+ => $boolRawToParsedValues(),
+ OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX
+ => $stringRawToParsedValues(['/', '/myDir']),
+ OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR => $stringRawToParsedValues(['/', '/myDir']),
OptionNames::ASYNC_BACKEND_COMM => $asyncBackendCommValues,
OptionNames::BREAKDOWN_METRICS => $boolRawToParsedValues(),
OptionNames::CAPTURE_ERRORS => $boolRawToParsedValues(),
@@ -154,6 +160,7 @@ private static function buildOptionNameToRawToValue(): array
=> $durationRawToParsedValues,
OptionNames::SPAN_COMPRESSION_SAME_KIND_MAX_DURATION
=> $durationRawToParsedValues,
+ OptionNames::STACK_TRACE_LIMIT => $intRawToParsedValues,
OptionNames::TRANSACTION_IGNORE_URLS => $wildcardListRawToParsedValues,
OptionNames::TRANSACTION_MAX_SPANS => $intRawToParsedValues,
OptionNames::TRANSACTION_SAMPLE_RATE => $doubleRawToParsedValues,
diff --git a/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php b/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php
index d40a1bfd8..ce2ddd47f 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php
@@ -66,6 +66,7 @@ public static function get(): array
self::APP_CODE_HOST_KIND_OPTION_NAME => new NullableAppCodeHostKindOptionMetadata(),
'app_code_php_exe' => new NullableStringOptionMetadata(),
self::APP_CODE_PHP_INI_OPTION_NAME => new NullableStringOptionMetadata(),
+ 'compare_ast_converted_back_to_source' => new BoolOptionMetadata(true),
self::DATA_PER_PROCESS_OPTION_NAME => new NullableCustomOptionMetadata(
function (string $rawValue): TestInfraDataPerProcess {
$deserializedObj = new TestInfraDataPerProcess();
diff --git a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php
index 5787e865c..6b8bcd0ed 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php
@@ -100,6 +100,18 @@ public function setAgentOption(string $optName, $optVal, ?AgentConfigSourceKind
$this->agentOptions[$sourceKindAsString][$optName] = $optVal;
}
+ /**
+ * @param string $optName
+ * @param string|int|float|bool $optVal
+ * @param ?AgentConfigSourceKind $sourceKind
+ */
+ public function setAgentOptionIfNotDefaultValue(string $optName, $optVal, ?AgentConfigSourceKind $sourceKind = null): void
+ {
+ if (!ConfigUtilForTests::isAgentOptionDefaultValue($optName, $optVal)) {
+ $this->setAgentOption($optName, $optVal, $sourceKind);
+ }
+ }
+
/**
* @param array $optsMap
* @param ?AgentConfigSourceKind $sourceKind
diff --git a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php
index 12c1cf771..94bc89068 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php
@@ -29,8 +29,10 @@
use Elastic\Apm\Impl\Log\Level as LogLevel;
use Elastic\Apm\Impl\Log\LoggableToString;
use Elastic\Apm\Impl\Tracer;
+use Elastic\Apm\Impl\Util\BoolUtil;
use Elastic\Apm\Impl\Util\ClassNameUtil;
use Elastic\Apm\Impl\Util\RangeUtil;
+use ElasticApmTests\Util\AssertMessageStack;
use ElasticApmTests\Util\DataFromAgent;
use ElasticApmTests\Util\IterableUtilForTests;
use ElasticApmTests\Util\MixedMap;
@@ -185,6 +187,9 @@ protected function verifyOneTransactionNoSpans(DataFromAgent $dataFromAgent): Tr
*/
protected static function implTestIsAutoInstrumentationEnabled(string $instrClassName, array $expectedNames): void
{
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['instrClassName' => $instrClassName, 'expectedNames' => $expectedNames]);
+
/** @var AutoInstrumentationBase $instr */
$instr = new $instrClassName(self::buildTracerForTests()->build());
$actualNames = $instr->keywords();
@@ -208,13 +213,14 @@ protected static function implTestIsAutoInstrumentationEnabled(string $instrClas
};
foreach ($expectedNames as $name) {
+ $dbgCtx->pushSubScope();
foreach ($genDisabledVariants($name) as $disableInstrumentationsOptVal) {
- $tracer = self::buildTracerForTests()
- ->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal)
- ->build();
+ $dbgCtx->clearCurrentSubScope(['name' => $name, 'disableInstrumentationsOptVal' => $disableInstrumentationsOptVal]);
+ $tracer = self::buildTracerForTests()->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal)->build();
$instr = new $instrClassName($tracer);
- self::assertFalse($instr->isEnabled(), $disableInstrumentationsOptVal);
+ self::assertFalse($instr->isEnabled());
}
+ $dbgCtx->popSubScope();
}
/**
@@ -225,13 +231,24 @@ protected static function implTestIsAutoInstrumentationEnabled(string $instrClas
yield '*someOtherDummyInstrumentationA*, *someOtherDummyInstrumentationB*';
};
+ $dbgCtx->pushSubScope();
foreach ($genEnabledVariants() as $disableInstrumentationsOptVal) {
- $tracer = self::buildTracerForTests()
- ->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal)
- ->build();
+ $dbgCtx->clearCurrentSubScope(['disableInstrumentationsOptVal' => $disableInstrumentationsOptVal]);
+ $tracer = self::buildTracerForTests()->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal)->build();
$instr = new $instrClassName($tracer);
- self::assertTrue($instr->isEnabled(), $disableInstrumentationsOptVal);
+ self::assertTrue($instr->isEnabled());
}
+ $dbgCtx->popSubScope();
+
+ $dbgCtx->pushSubScope();
+ foreach ([true, false] as $astProcessEnabled) {
+ $dbgCtx->clearCurrentSubScope(['astProcessEnabled' => $astProcessEnabled]);
+ $expectedIsEnabled = $astProcessEnabled || (!$instr->requiresUserlandCodeInstrumentation());
+ $tracer = self::buildTracerForTests()->withConfig(OptionNames::AST_PROCESS_ENABLED, BoolUtil::toString($astProcessEnabled))->build();
+ $instr = new $instrClassName($tracer);
+ self::assertSame($expectedIsEnabled, $instr->isEnabled());
+ }
+ $dbgCtx->popSubScope();
}
/**
@@ -252,6 +269,25 @@ public function adaptToSmoke(iterable $variants): iterable
return [];
}
+ /**
+ * @template TKey of array-key
+ * @template TValue
+ *
+ * @param iterable $variants
+ *
+ * @return iterable
+ */
+ public function adaptKeyValueToSmoke(iterable $variants): iterable
+ {
+ if (!self::isSmoke()) {
+ return $variants;
+ }
+ foreach ($variants as $key => $value) {
+ return [$key => $value];
+ }
+ return [];
+ }
+
/**
* @return iterable
*/
diff --git a/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php b/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php
index faa9f74de..49ae08e05 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php
@@ -48,6 +48,9 @@ final class ConfigSnapshotForTests implements LoggableInterface
/** @var ?string */
public $appCodePhpIni;
+ /** @var bool */
+ public $compareAstConvertedBackToSource;
+
/** @var TestInfraDataPerProcess */
public $dataPerProcess;
diff --git a/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php b/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php
index 25e129dc3..8bbd004f1 100644
--- a/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php
+++ b/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php
@@ -110,4 +110,16 @@ public static function allAgentLogLevelRelatedOptionNames(): iterable
}
}
}
+
+ /**
+ * @param string $optName
+ * @param string|int|float|bool $optVal
+ *
+ * @return bool
+ */
+ public static function isAgentOptionDefaultValue(string $optName, $optVal): bool
+ {
+ $optMeta = AllOptionsMetadata::get()[$optName];
+ return $optVal === $optMeta->defaultValue();
+ }
}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockBridge.php b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockBridge.php
new file mode 100644
index 000000000..664074bc9
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockBridge.php
@@ -0,0 +1,323 @@
+ */
+ public static $expectedCallbackArgs = null;
+
+ /** @var mixed */
+ public static $expectedCallbackReturnValue = null;
+
+ /** @var int */
+ public static $mockMuPluginCallbackCallsCount = 0;
+
+ /** @var int */
+ public static $mockPluginCallbackCallsCount = 0;
+
+ /** @var int */
+ public static $mockThemeCallbackCallsCount = 0;
+
+ /** @var int */
+ public static $mockPartOfCoreCallbackCallsCount = 0;
+
+ public const MOCK_MU_PLUGIN_HOOK_NAME = 'save_post';
+ public const MOCK_PLUGIN_HOOK_NAME = 'media_upload_newtab';
+ public const MOCK_THEME_HOOK_NAME = 'set_header_font';
+ public const MOCK_PART_OF_CORE_HOOK_NAME = 'update_footer';
+
+ public const MOCK_MU_PLUGIN_NAME = 'my_mock_mu_plugin';
+ public const MOCK_PLUGIN_NAME = 'my_mock_plugin';
+ public const MOCK_THEME_NAME = 'my_mock_theme';
+
+ public static function loadMockSource(string $srcVariantBaseDir, bool $isExpectedVariant): void
+ {
+ $wpIncludesDir = $srcVariantBaseDir . DIRECTORY_SEPARATOR . WordPressAutoInstrumentationTest::WP_INCLUDES_DIR_NAME;
+ require $wpIncludesDir . DIRECTORY_SEPARATOR . 'plugin.php';
+ require $wpIncludesDir . DIRECTORY_SEPARATOR . 'theme.php';
+
+ if ($isExpectedVariant) {
+ return;
+ }
+
+ $wpContentDir = $srcVariantBaseDir . DIRECTORY_SEPARATOR . 'wp-content';
+ require FileUtilForTests::listToPath([$wpContentDir, 'mu-plugins', self::MOCK_MU_PLUGIN_NAME . '.php']);
+ require FileUtilForTests::listToPath([$wpContentDir, 'plugins', self::MOCK_PLUGIN_NAME, 'main.php']);
+ require FileUtilForTests::listToPath([$wpContentDir, 'themes', self::MOCK_THEME_NAME, 'index.php']);
+ require FileUtilForTests::listToPath([$srcVariantBaseDir, 'wp-admin', 'part_of_core.php']);
+ }
+
+ /**
+ * @return mixed
+ *
+ * @noinspection PhpReturnDocTypeMismatchInspection
+ */
+ private static function callWpGetTemplate()
+ {
+ /**
+ * @noinspection PhpFullyQualifiedNameUsageInspection
+ * @phpstan-ignore-next-line
+ */
+ return \get_template();
+ }
+
+ /**
+ * @param string $hookName
+ * @param mixed $firstArg
+ * @param mixed ...$args
+ *
+ * @return mixed
+ */
+ private static function callWpApplyFilters(string $hookName, $firstArg, ...$args)
+ {
+ /**
+ * @noinspection PhpFullyQualifiedNameUsageInspection
+ * @phpstan-ignore-next-line
+ */
+ return \apply_filters($hookName, $firstArg, ...$args);
+ }
+
+ /**
+ * @param string $hookName
+ * @param mixed ...$args
+ */
+ private static function callWpDoAction(string $hookName, ...$args): void
+ {
+ /**
+ * @noinspection PhpFullyQualifiedNameUsageInspection
+ * @phpstan-ignore-next-line
+ */
+ \do_action($hookName, ...$args);
+ }
+
+ public static function runMockSource(MixedMap $appCodeArgs): void
+ {
+ $activeTheme = $appCodeArgs->getNullableString(WordPressAutoInstrumentationTest::EXPECTED_THEME_KEY);
+ $muPluginCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::MU_PLUGIN_CALLS_COUNT_KEY);
+ $pluginCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::PLUGIN_CALLS_COUNT_KEY);
+ $themeCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::THEME_CALLS_COUNT_KEY);
+ $partOfCoreCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::PART_OF_CORE_CALLS_COUNT_KEY);
+
+ // Have instrumented get_template() return non-string value first to test that instrumentation handles it correctly
+ WordPressMockBridge::$activeTheme = false;
+ TestCaseBase::assertSame(self::$activeTheme, self::callWpGetTemplate());
+ WordPressMockBridge::$activeTheme = $activeTheme;
+ TestCaseBase::assertSame(self::$activeTheme, self::callWpGetTemplate());
+
+ $applyFiltersCallsCount = 0;
+ $doActionCallsCount = 0;
+ $callIndex = 0;
+
+ $invokeCallbacks = function (string $hookName) use (&$callIndex, &$applyFiltersCallsCount, &$doActionCallsCount): void {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['callIndex' => $callIndex, 'applyFiltersCallsCount' => $applyFiltersCallsCount, 'doActionCallsCount' => $doActionCallsCount]);
+ $shouldUseApplyFilters = ($callIndex % 2) === 0;
+ $dbgCtx->add(['shouldUseApplyFilters' => $shouldUseApplyFilters]);
+ // do_action allows no arguments but apply_filters requires at least one argument
+ $argsCount = ($callIndex % 3) + ($shouldUseApplyFilters ? 1 : 0);
+ $dbgCtx->add(['argsCount' => $argsCount]);
+ $args = [];
+ foreach (RangeUtil::generateUpTo($argsCount) as $i) {
+ switch ($i % 4) {
+ case 0:
+ $args[] = 'dummy string arg';
+ break;
+ case 1:
+ $args[] = 98761234;
+ break;
+ case 2:
+ $args[] = 3.1416;
+ break;
+ case 3:
+ $args[] = new stdClass();
+ break;
+ }
+ }
+ $dbgCtx->add(['args' => $args]);
+ TestCaseBase::assertCount($argsCount, $args);
+
+ self::$expectedCallbackArgs = $args;
+ self::$expectedCallbackReturnValue = ($callIndex % 5) === 0 ? new stdClass() : 'dummy string return value';
+ if ($shouldUseApplyFilters) {
+ $actualRetVal = self::callWpApplyFilters($hookName, ...$args);
+ ++$applyFiltersCallsCount;
+ TestCaseBase::assertSame(self::$expectedCallbackReturnValue, $actualRetVal);
+ } else {
+ self::callWpDoAction($hookName, ...$args);
+ ++$doActionCallsCount;
+ }
+ self::$expectedCallbackArgs = null;
+ self::$expectedCallbackReturnValue = null;
+ ++$callIndex;
+ };
+
+ foreach (RangeUtil::generateUpTo($muPluginCallsCount) as $ignored) {
+ $invokeCallbacks(self::MOCK_MU_PLUGIN_HOOK_NAME);
+ }
+
+ foreach (RangeUtil::generateUpTo($pluginCallsCount) as $ignored) {
+ $invokeCallbacks(self::MOCK_PLUGIN_HOOK_NAME);
+ }
+
+ foreach (RangeUtil::generateUpTo($themeCallsCount) as $ignored) {
+ $invokeCallbacks(self::MOCK_THEME_HOOK_NAME);
+ }
+
+ foreach (RangeUtil::generateUpTo($partOfCoreCallsCount) as $ignored) {
+ $invokeCallbacks(self::MOCK_PART_OF_CORE_HOOK_NAME);
+ }
+
+ $txCtx = ElasticApm::getCurrentTransaction()->context();
+ $txCtx->setLabel(WordPressAutoInstrumentationTest::APPLY_FILTERS_CALLS_COUNT_KEY, $applyFiltersCallsCount);
+ $txCtx->setLabel(WordPressAutoInstrumentationTest::DO_ACTION_CALLS_COUNT_KEY, $doActionCallsCount);
+ $txCtx->setLabel(WordPressAutoInstrumentationTest::MU_PLUGIN_CALLS_COUNT_KEY, self::$mockMuPluginCallbackCallsCount);
+ $txCtx->setLabel(WordPressAutoInstrumentationTest::PLUGIN_CALLS_COUNT_KEY, self::$mockPluginCallbackCallsCount);
+ $txCtx->setLabel(WordPressAutoInstrumentationTest::THEME_CALLS_COUNT_KEY, self::$mockThemeCallbackCallsCount);
+ $txCtx->setLabel(WordPressAutoInstrumentationTest::PART_OF_CORE_CALLS_COUNT_KEY, self::$mockPartOfCoreCallbackCallsCount);
+ }
+
+ /**
+ * @param array $actualArgs
+ */
+ public static function assertCallbackArgsAsExpected(array $actualArgs): void
+ {
+ TestCaseBase::assertNotNull(self::$expectedCallbackArgs);
+ TestCaseBase::assertEqualLists(self::$expectedCallbackArgs, $actualArgs);
+ }
+
+ public static function callWpFilterBuildUniqueId(string $hookName, callable $callback, int $priority): ?string
+ {
+ /**
+ * @noinspection PhpFullyQualifiedNameUsageInspection
+ * @phpstan-ignore-next-line
+ */
+ return \_wp_filter_build_unique_id($hookName, $callback, $priority);
+ }
+
+ private static function newWpHook(): WordPressMockWpHook
+ {
+ /**
+ * @noinspection PhpFullyQualifiedNameUsageInspection
+ * @phpstan-ignore-next-line
+ */
+ return new \WP_Hook();
+ }
+
+ /**
+ * @param array &$wpFilterGlobal
+ * @param string $hookName
+ * @param callable $callback
+ * @param int $priority
+ * @param int $acceptedArgsCount
+ */
+ public static function mockImplAddFiler(array &$wpFilterGlobal, string $hookName, callable $callback, int $priority, int $acceptedArgsCount): void
+ {
+ if (!array_key_exists($hookName, $wpFilterGlobal)) {
+ $wpFilterGlobal[$hookName] = self::newWpHook();
+ }
+
+ /**
+ * @phpstan-ignore-next-line
+ */
+ $wpFilterGlobal[$hookName]->add_filter($hookName, $callback, $priority, $acceptedArgsCount);
+ }
+
+ /**
+ * @param array &$wpFilterGlobal
+ * @param string $hookName
+ * @param callable $callback
+ * @param int $priority
+ *
+ * @return bool
+ */
+ public static function mockImplRemoveFilter(array &$wpFilterGlobal, string $hookName, callable $callback, int $priority): bool
+ {
+ if (!array_key_exists($hookName, $wpFilterGlobal)) {
+ return false;
+ }
+
+ $wpHook = $wpFilterGlobal[$hookName];
+ $wpHook->mockImplRemoveFilter($hookName, $callback, $priority);
+ if ($wpHook->mockImplIsEmpty()) {
+ unset($wpFilterGlobal[$hookName]);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $wpFilterGlobal
+ * @param string $hookName
+ * @param mixed $firstArg
+ * @param mixed ...$restOfArgs
+ *
+ * @return mixed
+ */
+ public static function mockImplApplyFilters(array $wpFilterGlobal, string $hookName, $firstArg, ...$restOfArgs)
+ {
+ if (!array_key_exists($hookName, $wpFilterGlobal)) {
+ return $firstArg;
+ }
+
+ $allArgs = $restOfArgs;
+ array_unshift(/* ref */ $allArgs, $firstArg);
+
+ return $wpFilterGlobal[$hookName]->mockApplyFilters($allArgs);
+ }
+
+ /**
+ * @param array $wpFilterGlobal
+ * @param string $hookName
+ * @param mixed ...$args
+ */
+ public static function mockImplDoAction(array $wpFilterGlobal, string $hookName, ...$args): void
+ {
+ if (!array_key_exists($hookName, $wpFilterGlobal)) {
+ return;
+ }
+
+ $wpFilterGlobal[$hookName]->mockApplyFilters($args);
+ }
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockWpHook.php b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockWpHook.php
new file mode 100644
index 000000000..19a126cb0
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockWpHook.php
@@ -0,0 +1,75 @@
+ */
+ private $idToCallback = [];
+
+ public function __construct()
+ {
+ }
+
+ public function mockImplAddFilter(string $hookName, callable $callback, int $priority): void
+ {
+ $id = WordPressMockBridge::callWpFilterBuildUniqueId($hookName, $callback, $priority);
+ Assert::assertIsString($id);
+ $this->idToCallback[$id] = $callback;
+ }
+
+ public function mockImplRemoveFilter(string $hookName, callable $callback, int $priority): void
+ {
+ $id = WordPressMockBridge::callWpFilterBuildUniqueId($hookName, $callback, $priority);
+ Assert::assertIsString($id);
+ unset($this->idToCallback[$id]);
+ }
+
+ public function mockImplIsEmpty(): bool
+ {
+ return ArrayUtil::isEmpty($this->idToCallback);
+ }
+
+ /**
+ * @param array $args
+ *
+ * @return mixed
+ */
+ public function mockApplyFilters(array $args)
+ {
+ $retVal = null;
+ foreach ($this->idToCallback as $callback) {
+ $retVal = call_user_func_array($callback, $args);
+ }
+ return $retVal;
+ }
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/WordPressSpanExpectationsBuilder.php b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressSpanExpectationsBuilder.php
new file mode 100644
index 000000000..fae111745
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressSpanExpectationsBuilder.php
@@ -0,0 +1,75 @@
+startNew();
+ $result->name->setValue($hookName . ' - ' . $addonName);
+ $result->type->setValue($addonGroup);
+ $result->subtype->setValue($addonName);
+ $result->action->setValue($hookName);
+ return $result;
+ }
+
+ public function forPluginFilterCallback(string $hookName, string $pluginName): SpanExpectations
+ {
+ return $this->forAddonFilterCallback($hookName, /* addonGroup */ self::EXPECTED_SPAN_TYPE_FOR_PLUGIN, $pluginName);
+ }
+
+ public function forThemeFilterCallback(string $hookName, string $themeName): SpanExpectations
+ {
+ return $this->forAddonFilterCallback($hookName, /* addonGroup */ self::EXPECTED_SPAN_TYPE_FOR_THEME, $themeName);
+ }
+
+ public function forCoreFilterCallback(string $hookName): SpanExpectations
+ {
+ /**
+ * @see WordPressFilterCallbackWrapper::__invoke
+ */
+ $result = $this->startNew();
+ $result->name->setValue($hookName . ' - WordPress core');
+ $result->type->setValue(self::EXPECTED_SPAN_TYPE_FOR_CORE);
+ $result->subtype->setValue(null);
+ $result->action->setValue($hookName);
+ return $result;
+ }
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/README.md b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/README.md
new file mode 100644
index 000000000..d8e1ba168
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/README.md
@@ -0,0 +1 @@
+# Source code equivalent to WordPress mock source code after AST auto instrumentation
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/class-wp-hook.php b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/class-wp-hook.php
new file mode 100644
index 000000000..8c9439d66
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/class-wp-hook.php
@@ -0,0 +1,28 @@
+>> END Elasitc APM tests marker to fold into one line */
+ $this->mockImplAddFilter($hook_name, $callback, $priority);
+ } }
+
+ private function dummy_last_method(int $x): int
+ {
+ return $x * 2;
+ }
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/plugin.php b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/plugin.php
new file mode 100644
index 000000000..a0c1742e5
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/plugin.php
@@ -0,0 +1,122 @@
+>> END Elasitc APM tests marker to fold into one line */
+ if ( is_string( $callback ) ) {
+ return $callback;
+ }
+
+ if ( is_object( $callback ) ) {
+ // Closures are currently implemented as objects.
+ $callback = array( $callback, '' );
+ } else {
+ $callback = (array) $callback;
+ }
+
+ if ( is_object( $callback[0] ) ) {
+ // Object class calling.
+ return spl_object_hash( $callback[0] ) . $callback[1];
+ } elseif ( is_string( $callback[0] ) ) {
+ // Static calling.
+ return $callback[0] . '::' . $callback[1];
+ }
+} }/* <<< BEGIN Elasitc APM tests marker to fold into one line */
+
+ \elastic_apm_ast_instrumentation_direct_call(\ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS);
+}
+/* >>> END Elasitc APM tests marker to fold into one line */
+
+function dummy_last_function_in_plugin_php(int $x): int
+{
+ return $x * 2;
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/theme.php b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/theme.php
new file mode 100644
index 000000000..ed57f913c
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/theme.php
@@ -0,0 +1,79 @@
+value = $value;
+ }
+}
+
+define('MY_DUMMY_GLOBAL_FLOAT_CONST', 3.1416);
+
+/**
+ * Retrieves name of the active theme.
+ *
+ * @since 1.5.0
+ *
+ * @return string Template name.
+ */
+{ #[MyDummyAttribute('get_template attribute arg')]
+function get_templateElasticApmWrapped_suffixToBeRemovedByElasticApmTests(
+ /** doc comment for param1 */
+ #[MyDummyAttribute('param1 attribute arg')]
+ int $param1 = __LINE__,
+ ?string $param2 = MyDummyAttribute::DUMMY_CONST,
+ /** doc comment for param3 */
+ #[MyDummyAttribute()]
+ float &$param3 = MY_DUMMY_GLOBAL_FLOAT_CONST
+) {
+ /**
+ * get_template() body
+ */
+ return WordPressMockBridge::$activeTheme;
+}/* <<< BEGIN Elasitc APM tests marker to fold into one line */
+
+/**
+ * Retrieves name of the active theme.
+ *
+ * @since 1.5.0
+ *
+ * @return string Template name.
+ */
+#[MyDummyAttribute('get_template attribute arg')]
+function get_template(
+ /** doc comment for param1 */
+ #[MyDummyAttribute('param1 attribute arg')]
+ int $param1 = __LINE__,
+ ?string $param2 = MyDummyAttribute::DUMMY_CONST,
+ /** doc comment for param3 */
+ #[MyDummyAttribute()]
+ float &$param3 = MY_DUMMY_GLOBAL_FLOAT_CONST
+) {
+ $args = \func_get_args();
+ $postHook = \elastic_apm_ast_instrumentation_pre_hook(/* instrumentedClassFullName */ null, __FUNCTION__, $args);
+ try {
+ $retVal = get_templateElasticApmWrapped_suffixToBeRemovedByElasticApmTests(...$args);
+ if ($postHook !== null) $postHook(/* thrown */ null, $retVal);
+ return $retVal;
+ } catch (\Throwable $thrown) {
+ if ($postHook !== null) $postHook($thrown, /* retVal */ null);
+ throw $thrown;
+ }
+}
+}
+/* >>> END Elasitc APM tests marker to fold into one line */
+
+function &dummy_last_function_in_theme_php(int &$x): int {
+ return $x;
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/README.md b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/README.md
new file mode 100644
index 000000000..86513e060
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/README.md
@@ -0,0 +1 @@
+# WordPress mock source code
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-admin/part_of_core.php b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-admin/part_of_core.php
new file mode 100644
index 000000000..8c4fe7913
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-admin/part_of_core.php
@@ -0,0 +1,30 @@
+mockImplAddFilter($hook_name, $callback, $priority);
+ }
+
+ private function dummy_last_method(int $x): int
+ {
+ return $x * 2;
+ }
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-includes/plugin.php b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-includes/plugin.php
new file mode 100644
index 000000000..e007ed02d
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-includes/plugin.php
@@ -0,0 +1,113 @@
+value = $value;
+ }
+}
+
+define('MY_DUMMY_GLOBAL_FLOAT_CONST', 3.1416);
+
+/**
+ * Retrieves name of the active theme.
+ *
+ * @since 1.5.0
+ *
+ * @return string Template name.
+ */
+#[MyDummyAttribute('get_template attribute arg')]
+function get_template(
+ /** doc comment for param1 */
+ #[MyDummyAttribute('param1 attribute arg')]
+ int $param1 = __LINE__,
+ ?string $param2 = MyDummyAttribute::DUMMY_CONST,
+ /** doc comment for param3 */
+ #[MyDummyAttribute()]
+ float &$param3 = MY_DUMMY_GLOBAL_FLOAT_CONST
+) {
+ /**
+ * get_template() body
+ */
+ return WordPressMockBridge::$activeTheme;
+}
+
+function &dummy_last_function_in_theme_php(int &$x): int {
+ return $x;
+}
diff --git a/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php b/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php
new file mode 100644
index 000000000..2f26b5071
--- /dev/null
+++ b/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php
@@ -0,0 +1,603 @@
+>> END Elasitc APM tests marker to fold into one line */';
+
+ private const DEBUG_DUMP_DIR_NAME = 'debug_dump';
+
+ private const DEBUG_DUMP_FILE_EXTENSION = 'txt';
+ private const BEFORE_AST_PROCESS_FILE_NAME_SUFFIX = 'before_AST_process';
+ private const AFTER_AST_PROCESS_FILE_NAME_SUFFIX = 'after_AST_process';
+
+ private const CONVERTED_BACK_TO_SOURCE_FILE_EXTENSION = 'php';
+ private const AST_CONVERTED_BACK_TO_SOURCE_FILE_NAME_SUFFIX = 'converted_back_to_source';
+
+ private const ADAPTED_FILE_NAME_SUFFIX = 'adapted';
+ private const SUFFIX_TO_BE_REMOVED_BY_ELASTIC_APM_TESTS = '_suffixToBeRemovedByElasticApmTests';
+
+ /**
+ * @return string[]
+ */
+ private static function expectedFilesToBeAstTransformed(): array
+ {
+ /**
+ * @see src/ext/WordPress_instrumentation.c
+ */
+
+ $result = [];
+ $result[] = self::WP_INCLUDES_DIR_NAME . DIRECTORY_SEPARATOR . 'plugin.php';
+ $result[] = self::WP_INCLUDES_DIR_NAME . DIRECTORY_SEPARATOR . 'class-wp-hook.php';
+ $result[] = self::WP_INCLUDES_DIR_NAME . DIRECTORY_SEPARATOR . 'theme.php';
+ return $result;
+ }
+
+ private static function getLoggerForThisClass(): Logger
+ {
+ static $logger = null;
+ if ($logger === null) {
+ $logger = AmbientContextForTests::loggerFactory()->loggerForClass(LogCategoryForTests::TEST, __NAMESPACE__, __CLASS__, __FILE__);
+ }
+ return $logger;
+ }
+
+ public function testIsAutoInstrumentationEnabled(): void
+ {
+ // In production code ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS is defined by the native part of the agent
+ // but if we don't load elastic_apm extension in the component tests so we need to define a dummy
+ $constantName = 'ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS';
+ if (!defined($constantName)) {
+ define($constantName, 'dummy unused value');
+ }
+
+ self::implTestIsAutoInstrumentationEnabled(WordPressAutoInstrumentation::class, /* expectedNames */ ['wordpress']);
+ }
+
+ private static function buildInputOrExpectedOutputVariantSubDir(string $baseDir, bool $isExpectedVariant): string
+ {
+ return $baseDir . DIRECTORY_SEPARATOR . ($isExpectedVariant ? 'expected_process_AST_output' : 'mock_src');
+ }
+
+ public static function removeAttributes(string $fileContents): string
+ {
+ $adaptedLines = [];
+ foreach (TextUtilForTests::iterateLinesEx($fileContents) as [$line, $endOfLine]) {
+ $line = trim($line);
+
+ if (TextUtil::isSuffixOf(']', $line)) {
+ $attrStartPos = strpos($line, '#[');
+ if ($attrStartPos !== false) {
+ $line = substr($line, /* offset */ 0, $attrStartPos);
+ $endOfLine = '';
+ }
+ }
+
+ $adaptedLines[] = $line . $endOfLine;
+ }
+
+ return implode(/* separator */ '', $adaptedLines);
+ }
+
+ public static function foldTextWithMarkersIntoOneLine(string $fileContents): string
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['fileContents' => $fileContents]);
+
+ $adaptedLines = [];
+ $isBetweenMarkers = false;
+ foreach (TextUtilForTests::iterateLinesEx($fileContents) as [$line, $endOfLine]) {
+ if ($isBetweenMarkers) {
+ if (($endMarkerStartPos = strpos($line, WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_END_MARKER)) !== false) {
+ // Verify that line with end marker does not have any other non-whitespace text
+ self::assertTrue(TextUtil::isEmptyString(trim(substr($line, /* offset */ 0, $endMarkerStartPos))));
+ self::assertTrue(TextUtil::isEmptyString(trim(substr($line, /* offset */ $endMarkerStartPos + strlen(WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_END_MARKER)))));
+ $adaptedLines[] = $endOfLine;
+ $isBetweenMarkers = false;
+ continue;
+ }
+
+ $line = trim($line);
+
+ // Attributes were introduced in PHP 8 and earlier PHP versions interpret the rest of the line after # as a comment
+ if (PHP_MAJOR_VERSION < 8 && TextUtil::isPrefixOf('#', $line)) {
+ continue;
+ }
+
+ if (!TextUtil::isEmptyString($line)) {
+ $adaptedLines[] = ' ' . $line;
+ }
+ continue;
+ }
+
+ if (($beginMarkerStartPos = strpos($line, WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_BEGIN_MARKER)) !== false) {
+ $isBetweenMarkers = true;
+ // Add part before begin marker start position
+ $adaptedLines[] = substr($line, /* offset */ 0, $beginMarkerStartPos);
+ // Verify that there is no non-whitespace text after marker end
+ $partAfterMarkerEnd = substr($line, /* offset */ $beginMarkerStartPos + strlen(WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_BEGIN_MARKER));
+ $dbgCtx->add(['partAfterMarkerEnd' => $partAfterMarkerEnd]);
+ self::assertTrue(TextUtil::isEmptyString(trim($partAfterMarkerEnd)));
+ continue;
+ }
+
+ $adaptedLines[] = $line . $endOfLine;
+ }
+
+ return implode(/* separator */ '', $adaptedLines);
+ }
+
+ private static function adaptSourceFileContent(bool $isExpectedVariant, string $fileContents): string
+ {
+ $adaptedFileContents = $fileContents;
+
+ // Attributes were introduced in PHP 8 and earlier PHP versions interpret the rest of the line after # as a comment
+ if (PHP_MAJOR_VERSION < 8) {
+ $adaptedFileContents = self::removeAttributes($adaptedFileContents);
+ }
+
+ if ($isExpectedVariant) {
+ $adaptedFileContents = self::foldTextWithMarkersIntoOneLine($adaptedFileContents);
+ }
+
+ return $adaptedFileContents;
+ }
+
+ private static function adaptSourceTree(bool $isExpectedVariant, string $fromDir, string $toDir): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['isExpectedVariant' => $isExpectedVariant, 'fromDir' => $fromDir, 'toDir' => $toDir]);
+
+ $logger = self::getLoggerForThisClass();
+ $loggerProxyDebug = $logger->ifDebugLevelEnabledNoLine(__FUNCTION__);
+
+ self::assertNotFalse($fromDirEntries = scandir($fromDir));
+ $dbgCtx->pushSubScope();
+ foreach ($fromDirEntries as $entryName) {
+ if ($entryName == '.' || $entryName == '..') {
+ continue;
+ }
+ $dbgCtx->clearCurrentSubScope(['entryName' => $entryName]);
+ $fromDirEntryFullPath = $fromDir . DIRECTORY_SEPARATOR . $entryName;
+ if (is_dir($fromDirEntryFullPath)) {
+ $toSubDirFullPath = $toDir . DIRECTORY_SEPARATOR . $entryName;
+ $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Creating directory...', ['toSubDirFullPath' => $toSubDirFullPath]);
+ self::assertTrue(mkdir($toSubDirFullPath));
+ self::adaptSourceTree($isExpectedVariant, $fromDirEntryFullPath, $toSubDirFullPath);
+ continue;
+ }
+
+ $srcFileInfo = new SplFileInfo($fromDirEntryFullPath);
+ if (!($srcFileInfo->isFile() && ($srcFileInfo->getExtension() === 'php'))) {
+ continue;
+ }
+ $srcFileRelPath = FileUtilForTests::convertPathRelativeTo($fromDirEntryFullPath, $fromDir);
+ $adaptedSrcFileFullPath = FileUtilForTests::listToPath([$toDir, $srcFileRelPath]);
+ $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Creating file...', ['adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]);
+ $dbgCtx->add(['fromDirEntryFullPath' => $fromDirEntryFullPath, 'adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]);
+ self::assertFileExists($fromDirEntryFullPath);
+ self::assertNotFalse($srcFileContents = file_get_contents($fromDirEntryFullPath));
+ $adaptedSrcFileContents = self::adaptSourceFileContent($isExpectedVariant, $srcFileContents);
+ if ($adaptedSrcFileContents !== $srcFileContents) {
+ ($loggerProxy = $logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('Contents of ' . $adaptedSrcFileFullPath . ':' . "\n" . $adaptedSrcFileContents);
+ }
+ self::assertNotFalse(file_put_contents($adaptedSrcFileFullPath, $adaptedSrcFileContents));
+ self::assertFileExists($adaptedSrcFileFullPath);
+ $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Created file', ['adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]);
+ }
+ $dbgCtx->popSubScope();
+ }
+
+ /**
+ * @return iterable
+ */
+ public function dataProviderForTestAstProcessOnMockSource(): iterable
+ {
+ $result = (new DataProviderForTestBuilder())
+ ->addBoolKeyedDimensionAllValuesCombinable(self::IS_AST_PROCESS_ENABLED_KEY)
+ ->addBoolKeyedDimensionAllValuesCombinable(self::IS_AST_PROCESS_DEBUG_DUMP_ENABLED_KEY)
+ ->build();
+
+ return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result));
+ }
+
+ /**
+ * @dataProvider dataProviderForTestAstProcessOnMockSource
+ */
+ public function testAstProcessOnMockSource(MixedMap $testArgs): void
+ {
+ $subDirName = FileUtilForTests::buildTempSubDirName(__CLASS__, __FUNCTION__);
+ self::runAndEscalateLogLevelOnFailure(
+ self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs),
+ function () use ($subDirName, $testArgs): void {
+ $tempOutDir = FileUtilForTests::createTempSubDir($subDirName);
+ try {
+ $this->implTestAstProcessOnMockSource($tempOutDir, $testArgs);
+ } finally {
+ FileUtilForTests::deleteTempSubDir($subDirName);
+ }
+ }
+ );
+ }
+
+ private static function removeSuffixToBeRemovedByElasticApmTests(string $fileContents): string
+ {
+ $adaptedLines = [];
+ foreach (TextUtilForTests::iterateLines($fileContents, /* keepEndOfLine */ true) as $line) {
+ $adaptedLines[] = str_replace(self::SUFFIX_TO_BE_REMOVED_BY_ELASTIC_APM_TESTS, '', $line);
+ }
+
+ return implode(/* separator */ '', $adaptedLines);
+ }
+
+ private static function adaptManuallyInstrumentedGeneratedFile(/* in,out */ string &$filePath, /* in,out */ string &$fileContents): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['filePath' => $filePath, 'fileContents' => $fileContents]);
+
+ $fileInfo = new SplFileInfo($filePath);
+ $adaptedFileName = $fileInfo->getBasename($fileInfo->getExtension()) . self::ADAPTED_FILE_NAME_SUFFIX . '.' . $fileInfo->getExtension();
+ $adaptedFilePath = $fileInfo->getPath() . DIRECTORY_SEPARATOR . $adaptedFileName;
+ $adaptedFileContents = $fileContents;
+ $adaptedFileContents = self::removeSuffixToBeRemovedByElasticApmTests($adaptedFileContents);
+ $dbgCtx->add(['adaptedFilePath' => $adaptedFilePath, 'adaptedFileContents' => $adaptedFileContents]);
+ self::assertNotFalse(file_put_contents($adaptedFilePath, $adaptedFileContents));
+ $filePath = $adaptedFilePath;
+ $fileContents = $adaptedFileContents;
+ }
+
+ private static function logFileContentOnMismatch(string $filePath, string $fileContents): void
+ {
+ $logger = self::getLoggerForThisClass();
+ $loggerProxy = $logger->ifCriticalLevelEnabledNoLine(__FUNCTION__);
+ if ($loggerProxy === null) {
+ return;
+ }
+
+ $loggerProxy->log(__LINE__, 'Content of ' . $filePath . ' begin [length: ' . strlen($fileContents) . ']:' . PHP_EOL . $fileContents);
+ $loggerProxy->log(__LINE__, 'Content of ' . $filePath . ' end');
+ }
+
+ private static function verifyAstProcessGeneratedFiles(string $astProcessDebugDumpOutDir, string $phpFileRelativePath): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['astProcessDebugDumpOutDir' => $astProcessDebugDumpOutDir, 'phpFileRelativePath' => $phpFileRelativePath]);
+
+ $logger = self::getLoggerForThisClass()->addAllContext(['astProcessDebugDumpOutDir' => $astProcessDebugDumpOutDir, 'phpFileRelativePath' => $phpFileRelativePath]);
+
+ /**
+ * @param-out string $fileFullPath
+ * @param-out string $fileContents
+ */
+ $getGeneratedFileContents = function (
+ bool $isExpectedVariant,
+ bool $isAstDebugDump,
+ ?string &$fileFullPath,
+ ?string &$fileContents
+ ) use (
+ $astProcessDebugDumpOutDir,
+ $phpFileRelativePath
+ ): void {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['isExpectedVariant' => $isExpectedVariant, 'isAstDebugDump' => $isAstDebugDump, 'fileFullPath' => $fileFullPath]);
+
+ $outSubDir = self::buildInputOrExpectedOutputVariantSubDir($astProcessDebugDumpOutDir, $isExpectedVariant);
+ $fileName = $phpFileRelativePath . '.' . ($isExpectedVariant ? self::BEFORE_AST_PROCESS_FILE_NAME_SUFFIX : self::AFTER_AST_PROCESS_FILE_NAME_SUFFIX);
+ $fileName .= '.' . ($isAstDebugDump ? self::DEBUG_DUMP_FILE_EXTENSION : (self::AST_CONVERTED_BACK_TO_SOURCE_FILE_NAME_SUFFIX . '.' . self::CONVERTED_BACK_TO_SOURCE_FILE_EXTENSION));
+
+ $fileFullPath = FileUtilForTests::listToPath([$outSubDir, $fileName]);
+ self::assertFileExists($fileFullPath);
+ $fileContents = file_get_contents($fileFullPath);
+ self::assertNotFalse($fileContents);
+ };
+
+ ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Starting...');
+
+ $getGeneratedFileContents(/* isExpectedVariant */ true, /* isAstDebugDump */ true, /* out */ $expectedAstFilePath, /* out */ $expectedAstFileContents);
+ self::adaptManuallyInstrumentedGeneratedFile(/* in,out */ $expectedAstFilePath, /* out */ $expectedAstFileContents);
+ $getGeneratedFileContents(/* isExpectedVariant */ false, /* isAstDebugDump */ true, /* out */ $actualAstFilePath, /* out */ $actualAstFileContents);
+ $astMatches = ($actualAstFileContents === $expectedAstFileContents);
+
+ if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) {
+ $getGeneratedFileContents(/* isExpectedVariant */ true, /* isAstDebugDump */ false, /* out */ $expectedPhpFilePath, /* out */ $expectedPhpFileContents);
+ self::adaptManuallyInstrumentedGeneratedFile(/* in,out */ $expectedPhpFilePath, /* out */ $expectedPhpFileContents);
+ $getGeneratedFileContents(/* isExpectedVariant */ false, /* isAstDebugDump */ false, /* out */ $actualPhpFilePath, /* out */ $actualPhpFileContents);
+ $phpMatches = ($actualPhpFileContents === $expectedPhpFileContents);
+ } else {
+ $phpMatches = true;
+ $expectedPhpFilePath = '';
+ $expectedPhpFileContents = '';
+ $actualPhpFilePath = '';
+ $actualPhpFileContents = '';
+ }
+
+ if ($astMatches && $phpMatches) {
+ return;
+ }
+
+ $logCtx = ['astMatches' => $astMatches];
+ if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) {
+ $logCtx['phpMatches'] = $phpMatches;
+ }
+
+ ($loggerProxy = $logger->ifCriticalLevelEnabled(__LINE__, __FUNCTION__))
+ && $loggerProxy->log('Actual generated files do not match the expected', $logCtx);
+
+ self::logFileContentOnMismatch($expectedAstFilePath, $expectedAstFileContents);
+ if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) {
+ self::logFileContentOnMismatch($expectedPhpFilePath, $expectedPhpFileContents);
+ }
+ self::logFileContentOnMismatch($actualAstFilePath, $actualAstFileContents);
+ if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) {
+ self::logFileContentOnMismatch($actualPhpFilePath, $actualPhpFileContents);
+ }
+
+ if (!$astMatches) {
+ self::assertSame($expectedAstFilePath, $actualAstFilePath);
+ } elseif (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) {
+ self::assertSame($expectedPhpFilePath, $actualPhpFileContents);
+ }
+ }
+
+ public static function appCodeForTestAstProcessOnMockSource(MixedMap $appCodeArgs): void
+ {
+ $srcVariantBaseDir = $appCodeArgs->getString(self::SRC_VARIANT_DIR_KEY);
+ $isExpectedVariant = $appCodeArgs->getBool(self::IS_EXPECTED_VARIANT_KEY);
+ WordPressMockBridge::loadMockSource($srcVariantBaseDir, $isExpectedVariant);
+ }
+
+ private function implTestAstProcessOnMockSource(string $tempOutDir, MixedMap $testArgs): void
+ {
+ $adaptedSrcBaseDir = FileUtilForTests::listToPath([$tempOutDir, self::ADAPTED_SOURCE_DIR_NAME]);
+ $astProcessDebugDumpOutDir = FileUtilForTests::listToPath([$tempOutDir, self::DEBUG_DUMP_DIR_NAME]);
+ self::assertTrue(mkdir($adaptedSrcBaseDir));
+
+ $isAstProcessEnabled = $testArgs->getBool(self::IS_AST_PROCESS_ENABLED_KEY);
+ $isAstProcessDebugDumpEnabled = $testArgs->getBool(self::IS_AST_PROCESS_DEBUG_DUMP_ENABLED_KEY);
+
+ $testCaseHandle = $this->getTestCaseHandle();
+ $appCodeHost = $testCaseHandle->ensureMainAppCodeHost(
+ function (AppCodeHostParams $appCodeParams) use ($adaptedSrcBaseDir, $astProcessDebugDumpOutDir, $isAstProcessEnabled, $isAstProcessDebugDumpEnabled): void {
+ $appCodeParams->setAgentOptionIfNotDefaultValue(OptionNames::AST_PROCESS_ENABLED, $isAstProcessEnabled);
+ $appCodeParams->setAgentOption(OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX, $adaptedSrcBaseDir);
+ if ($isAstProcessDebugDumpEnabled) {
+ $appCodeParams->setAgentOption(OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR, $astProcessDebugDumpOutDir);
+ }
+ $appCodeParams->setAgentOptionIfNotDefaultValue(OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE, AmbientContextForTests::testConfig()->compareAstConvertedBackToSource);
+ }
+ );
+
+ foreach ([true, false] as $isExpectedVariant) {
+ $srcVariantBaseDir = self::buildInputOrExpectedOutputVariantSubDir(self::SRC_VARIANTS_DIR, $isExpectedVariant);
+ $adaptedSrcVariantBaseDir = self::buildInputOrExpectedOutputVariantSubDir($adaptedSrcBaseDir, $isExpectedVariant);
+ self::assertTrue(mkdir($adaptedSrcVariantBaseDir));
+ self::adaptSourceTree($isExpectedVariant, $srcVariantBaseDir, $adaptedSrcVariantBaseDir);
+ $appCodeHost->sendRequest(
+ AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestAstProcessOnMockSource']),
+ function (AppCodeRequestParams $appCodeRequestParams) use ($adaptedSrcVariantBaseDir, $isExpectedVariant): void {
+ $appCodeRequestParams->setAppCodeArgs([self::SRC_VARIANT_DIR_KEY => $adaptedSrcVariantBaseDir, self::IS_EXPECTED_VARIANT_KEY => $isExpectedVariant]);
+ }
+ );
+ }
+
+ if ($isAstProcessEnabled && $isAstProcessDebugDumpEnabled) {
+ foreach (self::expectedFilesToBeAstTransformed() as $phpFileRelativePath) {
+ self::verifyAstProcessGeneratedFiles($astProcessDebugDumpOutDir, $phpFileRelativePath);
+ }
+ } else {
+ self::assertDirectoryDoesNotExist($astProcessDebugDumpOutDir);
+ }
+ }
+
+ /**
+ * @return iterable
+ */
+ public function dataProviderForTestOnMockSource(): iterable
+ {
+ $disableInstrumentationsVariants = [
+ '' => true,
+ 'WordPress' => false,
+ ];
+
+ $result = (new DataProviderForTestBuilder())
+ ->addBoolKeyedDimensionAllValuesCombinable(self::IS_AST_PROCESS_ENABLED_KEY)
+ ->addGeneratorOnlyFirstValueCombinable(
+ AutoInstrumentationUtilForTests::disableInstrumentationsDataProviderGenerator(
+ $disableInstrumentationsVariants
+ )
+ )
+ ->addKeyedDimensionOnlyFirstValueCombinable(self::EXPECTED_THEME_KEY, ['my_favorite_theme', null, 'some_other_theme'])
+ ->addKeyedDimensionOnlyFirstValueCombinable(self::PLUGIN_CALLS_COUNT_KEY, [10, 1, 2, 0])
+ ->addKeyedDimensionOnlyFirstValueCombinable(self::MU_PLUGIN_CALLS_COUNT_KEY, [7, 1, 2, 0])
+ ->addKeyedDimensionOnlyFirstValueCombinable(self::THEME_CALLS_COUNT_KEY, [13, 1, 2, 0])
+ ->addKeyedDimensionOnlyFirstValueCombinable(self::PART_OF_CORE_CALLS_COUNT_KEY, [11, 1, 2, 0])
+ ->build();
+
+ return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result));
+ }
+
+ /**
+ * @dataProvider dataProviderForTestOnMockSource
+ */
+ public function testOnMockSource(MixedMap $testArgs): void
+ {
+ self::runAndEscalateLogLevelOnFailure(
+ self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs),
+ function () use ($testArgs): void {
+ $this->implTestOnMockSource($testArgs);
+ }
+ );
+ }
+
+ public static function appCodeForTestOnMockSource(MixedMap $appCodeArgs): void
+ {
+ $srcVariantBaseDir = self::buildInputOrExpectedOutputVariantSubDir(self::SRC_VARIANTS_DIR, /* isExpectedVariant */ false);
+ WordPressMockBridge::loadMockSource($srcVariantBaseDir, /* isExpectedVariant */ false);
+
+ WordPressMockBridge::runMockSource($appCodeArgs);
+ }
+
+ private function implTestOnMockSource(MixedMap $testArgs): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['testArgs' => $testArgs]);
+
+ $isAstProcessEnabled = $testArgs->getBool(self::IS_AST_PROCESS_ENABLED_KEY);
+ $disableInstrumentationsOptVal = $testArgs->getString(AutoInstrumentationUtilForTests::DISABLE_INSTRUMENTATIONS_KEY);
+ $isInstrumentationEnabled = $testArgs->getBool(AutoInstrumentationUtilForTests::IS_INSTRUMENTATION_ENABLED_KEY);
+ $isWordPressDataToBeExpected = $isAstProcessEnabled && $isInstrumentationEnabled;
+ $expectedTheme = $testArgs->getNullableString(self::EXPECTED_THEME_KEY);
+ $expectedMuPluginCallsCount = $testArgs->getInt(self::MU_PLUGIN_CALLS_COUNT_KEY);
+ $expectedPluginCallsCount = $testArgs->getInt(self::PLUGIN_CALLS_COUNT_KEY);
+ $expectedThemeCallsCount = $testArgs->getInt(self::THEME_CALLS_COUNT_KEY);
+ $expectedPartOfCoreCallsCount = $testArgs->getInt(self::PART_OF_CORE_CALLS_COUNT_KEY);
+
+ $testCaseHandle = $this->getTestCaseHandle();
+ $appCodeHost = $testCaseHandle->ensureMainAppCodeHost(
+ function (AppCodeHostParams $appCodeParams) use ($isAstProcessEnabled, $disableInstrumentationsOptVal): void {
+ $appCodeParams->setAgentOptionIfNotDefaultValue(OptionNames::AST_PROCESS_ENABLED, $isAstProcessEnabled);
+ $appCodeParams->setAgentOption(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal);
+ // Disable span compression to have all the expected spans individually
+ $appCodeParams->setAgentOption(OptionNames::SPAN_COMPRESSION_ENABLED, false);
+ }
+ );
+
+ $expectationsBuilder = new WordPressSpanExpectationsBuilder();
+ /** @var SpanExpectations[] $expectedSpans */
+ $expectedSpans = [];
+ if ($isWordPressDataToBeExpected) {
+ /**
+ * @see WordPressMockBridge::runMockSource
+ */
+ foreach (RangeUtil::generateUpTo($expectedMuPluginCallsCount) as $ignored) {
+ $expectedSpans[] = $expectationsBuilder->forPluginFilterCallback(WordPressMockBridge::MOCK_MU_PLUGIN_HOOK_NAME, WordPressMockBridge::MOCK_MU_PLUGIN_NAME);
+ }
+ foreach (RangeUtil::generateUpTo($expectedPluginCallsCount) as $ignored) {
+ $expectedSpans[] = $expectationsBuilder->forPluginFilterCallback(WordPressMockBridge::MOCK_PLUGIN_HOOK_NAME, WordPressMockBridge::MOCK_PLUGIN_NAME);
+ }
+ foreach (RangeUtil::generateUpTo($expectedThemeCallsCount) as $ignored) {
+ $expectedSpans[] = $expectationsBuilder->forThemeFilterCallback(WordPressMockBridge::MOCK_THEME_HOOK_NAME, WordPressMockBridge::MOCK_THEME_NAME);
+ }
+ foreach (RangeUtil::generateUpTo($expectedPartOfCoreCallsCount) as $ignored) {
+ $expectedSpans[] = $expectationsBuilder->forCoreFilterCallback(WordPressMockBridge::MOCK_PART_OF_CORE_HOOK_NAME);
+ }
+ }
+
+ $appCodeHost->sendRequest(
+ AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestOnMockSource']),
+ function (AppCodeRequestParams $appCodeRequestParams) use ($testArgs): void {
+ $appCodeRequestParams->setAppCodeArgs($testArgs);
+ }
+ );
+
+ $dataFromAgent = $testCaseHandle->waitForDataFromAgent(
+ (new ExpectedEventCounts())->transactions(1)->spans(count($expectedSpans))
+ );
+
+ $tx = $dataFromAgent->singleTransaction();
+ $dbgCtx->add(['tx' => $tx]);
+
+ if ((!$isWordPressDataToBeExpected) || ($expectedTheme === null)) {
+ self::assertTrue($tx->context === null || $tx->context->labels === null || !array_key_exists(self::EXPECTED_LABEL_KEY_FOR_WORDPRESS_THEME, $tx->context->labels));
+ } else {
+ self::assertSame($expectedTheme, self::getLabel($tx, self::EXPECTED_LABEL_KEY_FOR_WORDPRESS_THEME));
+ }
+
+ $actualMuPluginCallsCount = self::getLabel($tx, self::MU_PLUGIN_CALLS_COUNT_KEY);
+ self::assertSame($expectedMuPluginCallsCount, $actualMuPluginCallsCount);
+ $actualPluginCallsCount = self::getLabel($tx, self::PLUGIN_CALLS_COUNT_KEY);
+ self::assertSame($expectedPluginCallsCount, $actualPluginCallsCount);
+ $actualThemeCallsCount = self::getLabel($tx, self::THEME_CALLS_COUNT_KEY);
+ self::assertSame($expectedThemeCallsCount, $actualThemeCallsCount);
+ $actualPartOfCoreCallsCount = self::getLabel($tx, self::PART_OF_CORE_CALLS_COUNT_KEY);
+ self::assertSame($expectedPartOfCoreCallsCount, $actualPartOfCoreCallsCount);
+
+ $applyFiltersCallsCount = self::getLabel($tx, self::APPLY_FILTERS_CALLS_COUNT_KEY);
+ self::assertIsInt($applyFiltersCallsCount);
+ $doActionCallsCount = self::getLabel($tx, self::DO_ACTION_CALLS_COUNT_KEY);
+ self::assertIsInt($doActionCallsCount);
+ self::assertSame($applyFiltersCallsCount + $doActionCallsCount, $expectedMuPluginCallsCount + $expectedPluginCallsCount + $actualThemeCallsCount + $expectedPartOfCoreCallsCount);
+
+ if (!$isWordPressDataToBeExpected) {
+ return;
+ }
+
+ SpanSequenceValidator::updateExpectationsEndTime($expectedSpans);
+ SpanSequenceValidator::assertSequenceAsExpected($expectedSpans, array_values($dataFromAgent->idToSpan));
+ }
+}
diff --git a/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php b/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php
new file mode 100644
index 000000000..72e8d466a
--- /dev/null
+++ b/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php
@@ -0,0 +1,258 @@
+add(
+ [
+ 'filePath' => $filePath,
+ 'adaptedFilePath' => $adaptedFilePath,
+ 'expectedGroupKind' => $expectedGroupKind,
+ 'actualGroupKind' => $actualGroupKind,
+ 'expectedGroupName' => $expectedGroupName,
+ 'actualGroupName' => $actualGroupName,
+ ]
+ );
+ self::assertSame($expectedGroupKind, $actualGroupKind);
+ self::assertSame($expectedGroupName, $actualGroupName);
+ };
+
+ $pluginFilePathToExpectedName = [
+ '/var/www/html/wp-content/plugins/hello-dolly/hello.php' => 'hello-dolly',
+ '/var/www/html/wp-content/plugins/hello-dolly/' => 'hello-dolly',
+ '/wp-content/plugins/hello-dolly/hello.php' => 'hello-dolly',
+ '/wp-content/plugins/hello.php' => 'hello',
+
+ '/var/www/html/wp-content/mu-plugins/hello-dolly/hello.php' => 'hello-dolly',
+ '/var/www/html/wp-content/mu-plugins/hello-dolly/' => 'hello-dolly',
+ '/wp-content/mu-plugins/hello-dolly/hello.php' => 'hello-dolly',
+ '/wp-content/mu-plugins/hello.php' => 'hello',
+ 'mock_src/wp-content/mu-plugins/my_mock_mu_plugin.php' => 'my_mock_mu_plugin',
+ ];
+ foreach ($pluginFilePathToExpectedName as $filePath => $expectedGroupName) {
+ $testImpl($filePath, /* $expectedGroupKind */ WordPressAutoInstrumentation::CALLBACK_GROUP_KIND_PLUGIN, $expectedGroupName);
+ }
+
+ $themeFilePathToExpectedName = [
+ '/var/www/html/wp-content/themes/hello-dolly/hello.php' => 'hello-dolly',
+ '/var/www/html/wp-content/themes/hello-dolly/' => 'hello-dolly',
+ '/wp-content/themes/hello-dolly/hello.php' => 'hello-dolly',
+ '/wp-content/themes/hello.php' => 'hello',
+ ];
+ foreach ($themeFilePathToExpectedName as $filePath => $expectedGroupName) {
+ $testImpl($filePath, /* $expectedGroupKind */ WordPressAutoInstrumentation::CALLBACK_GROUP_KIND_THEME, $expectedGroupName);
+ }
+
+ $coreFilePaths = [
+ 'wp-content/plugins/hello-dolly/hello.php',
+ '/var/www/html/wp-content/plugins/hello-dolly',
+ '/var/www/html/wp-content/mu-plugins/hello-dolly',
+ 'wp-content/mu-plugins/hello-dolly/hello.php',
+ '/var/www/html/wp-content/themes/hello-dolly',
+ 'wp-content/themes/hello-dolly/hello.php',
+ '',
+ '/',
+ '//',
+ '/abc',
+ ];
+ foreach ($coreFilePaths as $filePath) {
+ $testImpl($filePath, /* $expectedGroupKind */ WordPressAutoInstrumentation::CALLBACK_GROUP_KIND_CORE, /* expectedGroupName */ null);
+ }
+ }
+
+ /**
+ * @return iterable
+ */
+ public static function dataProviderForTestFoldTextWithMarkersIntoOneLine(): iterable
+ {
+ yield ['', ''];
+ yield ['some text', 'some text'];
+
+ $indent = "\t \t";
+ $whiteSpace = "\t\t \t\t";
+ $whiteSpaceBeforeBeginMarker = "\t\t\t \t\t\t \t\t\t";
+ $input
+ = $indent . 'Original line 1' . PHP_EOL .
+ $indent . 'Original line 2' . $whiteSpaceBeforeBeginMarker . WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_BEGIN_MARKER . $whiteSpace . PHP_EOL .
+ $indent . 'Injected into line 2 - part A' . $whiteSpace . PHP_EOL .
+ $indent . 'Injected into line 2 - part B' . $whiteSpace . PHP_EOL .
+ $indent . WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_END_MARKER . $whiteSpace . PHP_EOL .
+ $indent . 'Original line 3' . PHP_EOL;
+ $expectedOutput
+ = $indent . 'Original line 1' . PHP_EOL .
+ $indent . 'Original line 2' . $whiteSpaceBeforeBeginMarker . ' ' . 'Injected into line 2 - part A' . ' ' . 'Injected into line 2 - part B' . PHP_EOL .
+ $indent . 'Original line 3' . PHP_EOL;
+ yield [$input, $expectedOutput];
+ yield [$expectedOutput, $expectedOutput];
+ }
+
+ /**
+ * @dataProvider dataProviderForTestFoldTextWithMarkersIntoOneLine
+ *
+ * @param string $input
+ * @param string $expectedOutput
+ */
+ public static function testFoldTextWithMarkersIntoOneLine(string $input, string $expectedOutput): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['input' => $input, 'expectedOutput' => $expectedOutput]);
+
+ $actualOutput = WordPressAutoInstrumentationTest::foldTextWithMarkersIntoOneLine($input);
+ self::assertSame($expectedOutput, $actualOutput);
+ }
+
+ public static function dummyStaticMethodForTestGetCallbackSourceFilePath(): string
+ {
+ return __FUNCTION__;
+ }
+
+ public function dummyMethodForTestGetCallbackSourceFilePath(): string
+ {
+ return __FUNCTION__;
+ }
+
+ /**
+ * @param string $methodName
+ * @param mixed[] $args
+ */
+ public function __call(string $methodName, array $args): void
+ {
+ }
+
+ /**
+ * @param string $methodName
+ * @param mixed[] $args
+ */
+ public static function __callStatic(string $methodName, array $args): void
+ {
+ }
+
+ public function testGetCallbackSourceFilePath(): void
+ {
+ $testImpl =
+ /**
+ * @param mixed $callback
+ * @param ?string $expectedResult
+ */
+ function ($callback, ?string $expectedResult): void {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['callback' => $callback, 'expectedResult' => $expectedResult]);
+ $actualResult = WordPressAutoInstrumentation::getCallbackSourceFilePath($callback, AmbientContextForTests::loggerFactory());
+ $dbgCtx->add(['actualResult' => $actualResult]);
+ self::assertSame($expectedResult, $actualResult);
+ };
+
+ $testImpl('\dummyFuncForTestsWithoutNamespace', DUMMY_FUNC_FOR_TESTS_WITHOUT_NAMESPACE_CALLABLE_FILE_NAME);
+ $testImpl('dummyFuncForTestsWithoutNamespace', DUMMY_FUNC_FOR_TESTS_WITHOUT_NAMESPACE_CALLABLE_FILE_NAME);
+ $testImpl('\ElasticApmTests\dummyFuncForTestsWithNamespace', DUMMY_FUNC_FOR_TESTS_WITH_NAMESPACE_CALLABLE_FILE_NAME);
+ $testImpl('ElasticApmTests\dummyFuncForTestsWithNamespace', DUMMY_FUNC_FOR_TESTS_WITH_NAMESPACE_CALLABLE_FILE_NAME);
+
+ $testImpl(__CLASS__ . '::' . self::dummyStaticMethodForTestGetCallbackSourceFilePath(), __FILE__);
+ $testImpl([__CLASS__, self::dummyStaticMethodForTestGetCallbackSourceFilePath()], __FILE__);
+
+ $testImpl(__CLASS__ . '::' . self::dummyMethodForTestGetCallbackSourceFilePath(), __FILE__);
+ $testImpl([__CLASS__, self::dummyMethodForTestGetCallbackSourceFilePath()], __FILE__);
+ $testImpl([$this, self::dummyMethodForTestGetCallbackSourceFilePath()], __FILE__);
+
+ $testImpl(__CLASS__ . '::' . 'implictNonExistentMethod', __FILE__);
+ $testImpl([__CLASS__, 'implictNonExistentMethod'], __FILE__);
+ $testImpl([$this, 'implictNonExistentMethod'], __FILE__);
+
+ $dummyClosure = function () {
+ };
+ $testImpl($dummyClosure, __FILE__);
+
+ $testImpl('', null);
+ $testImpl(null, null);
+ $testImpl(new stdClass(), null);
+ $testImpl(1, null);
+ $testImpl([stdClass::class, 'implictNonExistentMethod'], null);
+ $testImpl('invalid name', null);
+ $testImpl([], null);
+ $testImpl([1], null);
+ $testImpl([1, 2], null);
+ $testImpl(['a', 'b'], null);
+ $testImpl(['a'], null);
+ $testImpl([stdClass::class], null);
+ }
+
+ /**
+ * @return iterable
+ */
+ public static function dataProviderForTestRemoveAttributes(): iterable
+ {
+ yield ['', ''];
+ yield ['some text', 'some text'];
+
+ $input
+ = '#[MyDummyAttribute]' . PHP_EOL .
+ 'function get_template()' . PHP_EOL;
+ $expectedOutput
+ = 'function get_template()' . PHP_EOL;
+ yield [$input, $expectedOutput];
+ yield [$expectedOutput, $expectedOutput];
+
+ $input
+ = '{ #[MyDummyAttribute]' . PHP_EOL .
+ 'function get_template()' . PHP_EOL;
+ $expectedOutput
+ = '{ function get_template()' . PHP_EOL;
+ yield [$input, $expectedOutput];
+ yield [$expectedOutput, $expectedOutput];
+ }
+
+ /**
+ * @dataProvider dataProviderForTestRemoveAttributes
+ *
+ * @param string $input
+ * @param string $expectedOutput
+ */
+ public static function testRemoveAttributes(string $input, string $expectedOutput): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['input' => $input, 'expectedOutput' => $expectedOutput]);
+
+ $actualOutput = WordPressAutoInstrumentationTest::removeAttributes($input);
+ self::assertSame($expectedOutput, $actualOutput);
+ }
+}
diff --git a/tests/ElasticApmTests/Util/ArrayUtilTest.php b/tests/ElasticApmTests/Util/ArrayUtilTest.php
index 1d8f92026..3b0a7a98d 100644
--- a/tests/ElasticApmTests/Util/ArrayUtilTest.php
+++ b/tests/ElasticApmTests/Util/ArrayUtilTest.php
@@ -74,4 +74,60 @@ public static function testIterateListInReverse(array $inputArray, array $expect
self::assertSame($expectedVal, $actualVal);
}
}
+
+ /**
+ * @param mixed[] $args
+ *
+ * @return void
+ */
+ private static function verifyArgs(array $args): void
+ {
+ self::assertCount(1, $args);
+ $arg0 = $args[0];
+ self::assertIsString($arg0);
+ }
+
+ /**
+ * @param mixed[] $args
+ *
+ * @return void
+ */
+ private static function instrumentationFunc(array $args): void
+ {
+ self::assertCount(1, $args);
+ self::verifyArgs($args);
+ $someParam =& $args[0];
+ self::assertSame('value set by instrumentedFunc caller', $someParam);
+ self::assertIsString($someParam);
+ $someParam = 'value set by instrumentationFunc';
+ }
+
+ /** @noinspection PhpSameParameterValueInspection */
+ private static function instrumentedFunc(string $someParam): string
+ {
+ self::instrumentationFunc([&$someParam]);
+ return $someParam;
+ }
+
+ public static function testReferencesInArray(): void
+ {
+ $instrumentedFuncRetVal = self::instrumentedFunc('value set by instrumentedFunc caller');
+ self::assertSame('value set by instrumentationFunc', $instrumentedFuncRetVal);
+ }
+
+ public static function testRemoveElementFromTwoLevelArrayViaReferenceToFirstLevel(): void
+ {
+ $myArr = [
+ 'level 1 - a' => [
+ 'level 2 - a' => 'value for level 2 - a',
+ 'level 2 - b' => 'value for level 2 - b',
+ ]
+ ];
+ $level1ValRef =& $myArr['level 1 - a'];
+ self::assertArrayHasKey('level 2 - a', $level1ValRef);
+ self::assertSame('value for level 2 - a', $level1ValRef['level 2 - a']);
+ unset($level1ValRef['level 2 - a']);
+ self::assertArrayNotHasKey('level 2 - a', $myArr['level 1 - a']);
+ self::assertArrayHasKey('level 2 - b', $myArr['level 1 - a']);
+ }
}
diff --git a/tests/ElasticApmTests/Util/FileUtilForTests.php b/tests/ElasticApmTests/Util/FileUtilForTests.php
index 34be22ced..1a5eb6ac3 100644
--- a/tests/ElasticApmTests/Util/FileUtilForTests.php
+++ b/tests/ElasticApmTests/Util/FileUtilForTests.php
@@ -25,11 +25,14 @@
use Closure;
use Elastic\Apm\Impl\Log\LoggableToString;
+use Elastic\Apm\Impl\Util\ClassNameUtil;
use Elastic\Apm\Impl\Util\ExceptionUtil;
use Elastic\Apm\Impl\Util\StaticClassTrait;
use Elastic\Apm\Impl\Util\TextUtil;
use ElasticApmTests\ComponentTests\Util\AmbientContextForTests;
-use PHPUnit\Framework\Assert;
+use ElasticApmTests\ComponentTests\Util\EnvVarUtilForTests;
+use ElasticApmTests\ComponentTests\Util\OsUtilForTests;
+use ElasticApmTests\ComponentTests\Util\ProcessUtilForTests;
use RuntimeException;
/**
@@ -101,8 +104,7 @@ public static function createTempFile(?string $dbgTempFilePurpose = null): strin
{
$tempFileFullPath = tempnam(sys_get_temp_dir(), /* prefix */ 'ElasticApmTests_');
$logCategory = LogCategoryForTests::TEST;
- $logger
- = AmbientContextForTests::loggerFactory()->loggerForClass($logCategory, __NAMESPACE__, __CLASS__, __FILE__);
+ $logger = AmbientContextForTests::loggerFactory()->loggerForClass($logCategory, __NAMESPACE__, __CLASS__, __FILE__);
if ($tempFileFullPath === false) {
($loggerProxy = $logger->ifCriticalLevelEnabled(__LINE__, __FUNCTION__))
@@ -110,7 +112,7 @@ public static function createTempFile(?string $dbgTempFilePurpose = null): strin
'Failed to create a temporary file',
['$dbgTempFilePurpose' => $dbgTempFilePurpose]
);
- Assert::fail(LoggableToString::convert(['$dbgTempFilePurpose' => $dbgTempFilePurpose]));
+ TestCaseBase::fail(LoggableToString::convert(['$dbgTempFilePurpose' => $dbgTempFilePurpose]));
}
($loggerProxy = $logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__))
@@ -121,4 +123,63 @@ public static function createTempFile(?string $dbgTempFilePurpose = null): strin
return $tempFileFullPath;
}
+
+ private static function buildTempSubDirFullPath(string $subDirName): string
+ {
+ return sys_get_temp_dir() . DIRECTORY_SEPARATOR . $subDirName;
+ }
+
+ public static function deleteTempSubDir(string $subDirName): void
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['subDirName' => $subDirName]);
+
+ $tempSubDirFullPath = self::buildTempSubDirFullPath($subDirName);
+ $dbgCtx->add(['tempSubDirFullPath' => $tempSubDirFullPath]);
+ if (!file_exists($tempSubDirFullPath)) {
+ return;
+ }
+ TestCaseBase::assertTrue(is_dir($tempSubDirFullPath));
+
+ $deleteDirShellCmd = OsUtilForTests::isWindows()
+ ? sprintf('rd /s /q "%s"', $tempSubDirFullPath)
+ : sprintf('rm -rf "%s"', $tempSubDirFullPath);
+
+ ProcessUtilForTests::startProcessAndWaitUntilExit($deleteDirShellCmd, EnvVarUtilForTests::getAll(), /* shouldCaptureStdOutErr */ true, /* $expectedExitCode */ 0);
+ }
+
+ /**
+ * @param class-string $testClassName
+ * @param string $testMethodName
+ *
+ * @return string
+ */
+ public static function buildTempSubDirName(string $testClassName, string $testMethodName): string
+ {
+ return 'ElasticApmTests_' . ClassNameUtil::fqToShort($testClassName) . '_' . $testMethodName . '_PID=' . getmypid();
+ }
+
+ public static function createTempSubDir(string $subDirName): string
+ {
+ AssertMessageStack::newScope(/* out */ $dbgCtx);
+ $dbgCtx->add(['subDirName' => $subDirName]);
+
+ $tempSubDirFullPath = self::buildTempSubDirFullPath($subDirName);
+ $dbgCtx->add(['tempSubDirFullPath' => $tempSubDirFullPath]);
+ self::deleteTempSubDir($tempSubDirFullPath);
+ TestCaseBase::assertTrue(mkdir($tempSubDirFullPath));
+ return $tempSubDirFullPath;
+ }
+
+ public static function convertPathRelativeTo(string $absPath, string $relativeToAbsPath): string
+ {
+ TestCaseBase::assertTrue(TextUtil::isPrefixOf($relativeToAbsPath, $absPath));
+ $relPath = substr($absPath, /* offset */ strlen($relativeToAbsPath));
+ foreach (['/', DIRECTORY_SEPARATOR] as $dirSeparator) {
+ while (TextUtil::isPrefixOf($dirSeparator, $relPath)) {
+ $relPath = substr($relPath, /* offset */ strlen($dirSeparator));
+ }
+ }
+ return $relPath;
+ }
}
diff --git a/tests/ElasticApmTests/Util/TestCaseBase.php b/tests/ElasticApmTests/Util/TestCaseBase.php
index 274fbfc0c..26a8f15ed 100644
--- a/tests/ElasticApmTests/Util/TestCaseBase.php
+++ b/tests/ElasticApmTests/Util/TestCaseBase.php
@@ -1050,4 +1050,22 @@ public static function assertContainsEx($needle, iterable $haystack, string $mes
throw $ex;
}
}
+
+ public static function assertDirectoryDoesNotExist(string $directory, string $message = ''): void
+ {
+ /**
+ * Method assertDirectoryDoesNotExist was added in PHPUnit 9 as an alias for already existing assertDirectoryNotExists
+ * and assertDirectoryNotExists was deprecated.
+ * We still use PHPUnit 8.5 when testing under older PHP versions so we need a facade to work on both
+ * - PHPUnit 8.5, where assertDirectoryDoesNotExist does not exist
+ * and
+ * - PHPUnit 9, where assertDirectoryNotExists is deprecated
+ */
+ if (method_exists(Assert::class, __FUNCTION__)) {
+ Assert::assertDirectoryDoesNotExist($directory, $message);
+ } else {
+ /** @noinspection PhpDeprecationInspection, PhpUnitDeprecatedCallsIn10VersionInspection */
+ Assert::assertDirectoryNotExists($directory, $message);
+ }
+ }
}