From 037639ae87bbce18f773d15297d300a23085a5bc Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Mon, 16 Feb 2026 09:18:10 +0200 Subject: [PATCH 1/9] Added capture_errors_with_php_part, capture_exceptions and dev_internal_capture_errors_only_to_log options --- agent/native/ext/ConfigManager.cpp | 21 +++++ agent/native/ext/ConfigManager.h | 9 +- agent/native/ext/ConfigSnapshot.h | 5 +- agent/native/ext/Hooking.cpp | 45 ++++++--- agent/native/ext/Hooking.h | 2 +- agent/native/ext/elastic_apm.cpp | 6 +- agent/native/ext/lifecycle.cpp | 42 ++++++--- agent/native/ext/php_elastic_apm.h | 3 +- .../Impl/AutoInstrument/PhpPartFacade.php | 26 +++--- .../TransactionForExtensionRequest.php | 90 +++++++++++++++++- .../Impl/Config/AllOptionsMetadata.php | 3 + .../ElasticApm/Impl/Config/OptionNames.php | 4 + agent/php/ElasticApm/Impl/Config/Snapshot.php | 35 +++++++ .../ElasticApm/Impl/ErrorExceptionData.php | 1 + agent/php/ElasticApm/Impl/Log/Backend.php | 5 + agent/php/ElasticApm/Impl/Log/Logger.php | 5 + .../ComponentTests/ConfigSettingTest.php | 5 +- .../ComponentTests/ErrorComponentTest.php | 92 ++++++++++++++----- tests/ElasticApmTests/Util/MixedMap.php | 17 ++++ 19 files changed, 341 insertions(+), 75 deletions(-) diff --git a/agent/native/ext/ConfigManager.cpp b/agent/native/ext/ConfigManager.cpp index 5fb2f0dc3..5d75977d2 100644 --- a/agent/native/ext/ConfigManager.cpp +++ b/agent/native/ext/ConfigManager.cpp @@ -801,8 +801,11 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( optionalBoolValue, asyncBackendComm ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, bootstrapPhpPartFile ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, breakdownMetrics ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, captureErrors ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, captureErrorsWithPhpPart ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( optionalBoolValue, captureExceptions ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, devInternal ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, devInternalBackendCommLogVerbose ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, devInternalCaptureErrorsOnlyToLog ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, disableInstrumentations ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, disableSend ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, enabled ) @@ -1008,6 +1011,18 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS, /* defaultValue: */ true ); + ELASTIC_APM_INIT_METADATA( + buildBoolOptionMetadata, + captureErrorsWithPhpPart, + ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS_WITH_PHP_PART, + /* defaultValue: */ false ); + + ELASTIC_APM_INIT_METADATA( + buildOptionalBoolOptionMetadata, + captureExceptions, + ELASTIC_APM_CFG_OPT_NAME_CAPTURE_EXCEPTIONS, + /* defaultValue: */ makeNotSetOptionalBool() ); + ELASTIC_APM_INIT_METADATA( buildStringOptionMetadata, devInternal, @@ -1020,6 +1035,12 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL_BACKEND_COMM_LOG_VERBOSE, /* defaultValue: */ false ); + ELASTIC_APM_INIT_METADATA( + buildBoolOptionMetadata, + devInternalCaptureErrorsOnlyToLog, + ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG, + /* defaultValue: */ false ); + ELASTIC_APM_INIT_METADATA( buildStringOptionMetadata, disableInstrumentations, diff --git a/agent/native/ext/ConfigManager.h b/agent/native/ext/ConfigManager.h index fc27fde09..f47f867f1 100644 --- a/agent/native/ext/ConfigManager.h +++ b/agent/native/ext/ConfigManager.h @@ -74,8 +74,11 @@ enum OptionId optionId_bootstrapPhpPartFile, optionId_breakdownMetrics, optionId_captureErrors, + optionId_captureErrorsWithPhpPart, + optionId_captureExceptions, optionId_devInternal, optionId_devInternalBackendCommLogVerbose, + optionId_devInternalCaptureErrorsOnlyToLog, optionId_disableInstrumentations, optionId_disableSend, optionId_enabled, @@ -257,16 +260,16 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot(); #define ELASTIC_APM_CFG_OPT_NAME_BOOTSTRAP_PHP_PART_FILE "bootstrap_php_part_file" #define ELASTIC_APM_CFG_OPT_NAME_BREAKDOWN_METRICS "breakdown_metrics" -/** - * Internal configuration option (not included in public documentation) - */ #define ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS "capture_errors" +#define ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS_WITH_PHP_PART "capture_errors_with_php_part" +#define ELASTIC_APM_CFG_OPT_NAME_CAPTURE_EXCEPTIONS "capture_exceptions" /** * Internal configuration option (not included in public documentation) */ #define ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL "dev_internal" #define ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL_BACKEND_COMM_LOG_VERBOSE "dev_internal_backend_comm_log_verbose" +#define ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG "dev_internal_capture_errors_only_to_log" #define ELASTIC_APM_CFG_OPT_NAME_DISABLE_INSTRUMENTATIONS "disable_instrumentations" #define ELASTIC_APM_CFG_OPT_NAME_DISABLE_SEND "disable_send" diff --git a/agent/native/ext/ConfigSnapshot.h b/agent/native/ext/ConfigSnapshot.h index 9c96a58fd..1e3227c50 100644 --- a/agent/native/ext/ConfigSnapshot.h +++ b/agent/native/ext/ConfigSnapshot.h @@ -41,12 +41,15 @@ struct ConfigSnapshot bool astProcessDebugDumpConvertedBackToSource = false; String astProcessDebugDumpForPathPrefix = nullptr; String astProcessDebugDumpOutDir = nullptr; - OptionalBool asyncBackendComm = {false, false}; + OptionalBool asyncBackendComm = (OptionalBool){ .isSet = false, .value = false }; String bootstrapPhpPartFile = nullptr; bool breakdownMetrics = false; bool captureErrors = false; + bool captureErrorsWithPhpPart = false; + OptionalBool captureExceptions = (OptionalBool){ .isSet = false, .value = false }; String devInternal = nullptr; bool devInternalBackendCommLogVerbose = false; + bool devInternalCaptureErrorsOnlyToLog = false; String disableInstrumentations = nullptr; bool disableSend = false; bool enabled = false; diff --git a/agent/native/ext/Hooking.cpp b/agent/native/ext/Hooking.cpp index 5b5f8208a..682ab2754 100644 --- a/agent/native/ext/Hooking.cpp +++ b/agent/native/ext/Hooking.cpp @@ -26,25 +26,37 @@ void elastic_apm_error_cb(int type, zend_string *error_filename, const uint32_t #endif using namespace std::string_view_literals; - if (ELASTICAPM_G(captureErrors)) { + if (ELASTICAPM_G(captureErrorsUsingNative)) { + ELASTICAPM_G(lastErrorData) = nullptr; + std::unique_ptr errorData; #if PHP_VERSION_ID < 80000 - char * message = nullptr; - va_list messageArgsCopy; + char * message = nullptr; + va_list messageArgsCopy; va_copy(messageArgsCopy, args); - vspprintf(/* out */ &message, 0, format, messageArgsCopy); // vspprintf allocates memory for the resulted string buffer and it needs to be freed with efree() - va_end(messageArgsCopy); + vspprintf(/* out */ &message, 0, format, messageArgsCopy); // vspprintf allocates memory for the resulted string buffer and it needs to be freed with efree() + va_end(messageArgsCopy); - ELASTICAPM_G(lastErrorData) = std::make_unique(type, error_filename ? error_filename : ""sv, error_lineno, message ? message : ""sv); + errorData = std::make_unique(type, error_filename ? error_filename : ""sv, error_lineno, message ? message : ""sv); - if (message) { - efree(message); - } + if (message) { + efree(message); + } #elif PHP_VERSION_ID < 80100 - ELASTICAPM_G(lastErrorData) = std::make_unique(type, error_filename ? error_filename : ""sv, error_lineno, message ? std::string_view{ZSTR_VAL(message), ZSTR_LEN(message)} : ""sv); + errorData = std::make_unique(type, error_filename ? error_filename : ""sv, error_lineno, message ? std::string_view{ZSTR_VAL(message), ZSTR_LEN(message)} : ""sv); #else - ELASTICAPM_G(lastErrorData) = nullptr; - ELASTICAPM_G(lastErrorData) = std::make_unique(type, error_filename ? std::string_view{ZSTR_VAL(error_filename), ZSTR_LEN(error_filename)} : ""sv, error_lineno, message ? std::string_view{ZSTR_VAL(message), ZSTR_LEN(message)} : ""sv); + errorData = std::make_unique(type, error_filename ? std::string_view{ZSTR_VAL(error_filename), ZSTR_LEN(error_filename)} : ""sv, error_lineno, message ? std::string_view{ZSTR_VAL(message), ZSTR_LEN(message)} : ""sv); #endif + + if (ELASTICAPM_G(captureErrorsToLogOnly)) { + ELASTIC_APM_LOG_DEBUG( + "Captured error but only to log it; error_filename: %.*s; error_lineno: %d; message: %.*s", + static_cast(errorData->getFileName().length()), errorData->getFileName().data(), + errorData->getLineNumber(), + static_cast(errorData->getMessage().length()), errorData->getMessage().data() + ); + } else { + ELASTICAPM_G(lastErrorData) = std::move(errorData); + } } auto original = Hooking::getInstance().getOriginalZendErrorCb(); @@ -92,7 +104,7 @@ static void elastic_interrupt_function(zend_execute_data *execute_data) { } zend_end_try(); } -void Hooking::replaceHooks(bool cfgCaptureErrors, bool cfgInferredSpansEnabled) { +void Hooking::replaceHooks(bool cfgCaptureErrors, bool cfgCaptureErrorsWithPhpPart, bool cfgInferredSpansEnabled) { if (cfgInferredSpansEnabled) { zend_execute_internal = elastic_execute_internal; zend_interrupt_function = elastic_interrupt_function; @@ -101,11 +113,14 @@ void Hooking::replaceHooks(bool cfgCaptureErrors, bool cfgInferredSpansEnabled) ELASTIC_APM_LOG_DEBUG( "NOT replacing zend_execute_internal and zend_interrupt_function hooks because profiling_inferred_spans_enabled configuration option is set to false" ); } - if (cfgCaptureErrors) { + if (cfgCaptureErrors && (!cfgCaptureErrorsWithPhpPart)) { zend_error_cb = elastic_apm_error_cb; ELASTIC_APM_LOG_DEBUG( "Replaced zend_error_cb hook" ); } else { - ELASTIC_APM_LOG_DEBUG( "NOT replacing zend_error_cb hook because capture_errors configuration option is set to false" ); + ELASTIC_APM_LOG_DEBUG( + "NOT replacing zend_error_cb hook because configuration options capture_errors is %s and capture_errors_with_php_part is %s", + boolToString(cfgCaptureErrors), boolToString(cfgCaptureErrorsWithPhpPart) + ); } } diff --git a/agent/native/ext/Hooking.h b/agent/native/ext/Hooking.h index b25faa2e0..6e0f6a4fd 100644 --- a/agent/native/ext/Hooking.h +++ b/agent/native/ext/Hooking.h @@ -46,7 +46,7 @@ class Hooking { zend_error_cb = original_zend_error_cb_; } - void replaceHooks(bool cfgCaptureErrors, bool cfgInferredSpansEnabled); + void replaceHooks(bool cfgCaptureErrors, bool cfgCaptureErrorsWithPhpPart, bool cfgInferredSpansEnabled); zend_execute_internal_t getOriginalExecuteInternal() { return original_execute_internal_; diff --git a/agent/native/ext/elastic_apm.cpp b/agent/native/ext/elastic_apm.cpp index 66c03feaa..0416de4d7 100644 --- a/agent/native/ext/elastic_apm.cpp +++ b/agent/native/ext/elastic_apm.cpp @@ -151,8 +151,11 @@ PHP_INI_BEGIN() ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_BOOTSTRAP_PHP_PART_FILE ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_BREAKDOWN_METRICS ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS_WITH_PHP_PART ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_CAPTURE_EXCEPTIONS ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL_BACKEND_COMM_LOG_VERBOSE ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_DISABLE_INSTRUMENTATIONS ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_DISABLE_SEND ) ELASTIC_APM_NOT_RELOADABLE_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_ENABLED ) @@ -290,7 +293,8 @@ static PHP_GINIT_FUNCTION(elastic_apm) ZVAL_UNDEF(&elastic_apm_globals->lastException); new (&elastic_apm_globals->lastErrorData) std::unique_ptr; - elastic_apm_globals->captureErrors = false; + elastic_apm_globals->captureErrorsUsingNative = false; + elastic_apm_globals->captureErrorsToLogOnly = false; } static PHP_GSHUTDOWN_FUNCTION(elastic_apm) { diff --git a/agent/native/ext/lifecycle.cpp b/agent/native/ext/lifecycle.cpp index e4fb860b8..26675d419 100644 --- a/agent/native/ext/lifecycle.cpp +++ b/agent/native/ext/lifecycle.cpp @@ -156,16 +156,18 @@ void elasticApmZendThrowExceptionHookImpl( #endif ) { + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "lastException set: %s", boolToString( Z_TYPE(ELASTICAPM_G(lastException)) != IS_UNDEF ) ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "lastException set: %s", boolToString( Z_TYPE(ELASTICAPM_G(lastException)) != IS_UNDEF ) ); - - resetLastThrown(); - + if (ELASTICAPM_G(captureErrorsToLogOnly)) { + ELASTIC_APM_LOG_DEBUG("Captured exception but only to log it"); + } else { + resetLastThrown(); #if PHP_MAJOR_VERSION >= 8 /* if PHP version is 8.* and later */ - ZVAL_OBJ_COPY(&ELASTICAPM_G( lastException ), thrownAsPzobj ); + ZVAL_OBJ_COPY(&ELASTICAPM_G( lastException ), thrownAsPzobj ); #else - ZVAL_COPY(&ELASTICAPM_G(lastException), thrownAsPzval ); + ZVAL_COPY(&ELASTICAPM_G(lastException), thrownAsPzval ); #endif + } ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); } @@ -201,8 +203,12 @@ void elasticApmZendThrowExceptionHook( static void registerExceptionHooks(const ConfigSnapshot& config) { - if (!config.captureErrors) { - ELASTIC_APM_LOG_DEBUG( "NOT replacing zend_throw_exception_hook hook because capture_errors configuration option is set to false" ); + bool shouldCaptureExceptions = config.captureExceptions.isSet ? config.captureExceptions.value : config.captureErrors; + if ((!shouldCaptureExceptions) || config.captureErrorsWithPhpPart) { + ELASTIC_APM_LOG_DEBUG( + "NOT replacing zend_throw_exception_hook hook because configuration options capture_exceptions is %s, capture_errors is %s and capture_errors_with_php_part is %s", + optionalBoolToString(config.captureExceptions), boolToString(config.captureErrors), boolToString(config.captureErrorsWithPhpPart) + ); return; } @@ -291,7 +297,7 @@ void elasticApmModuleInit( int moduleType, int moduleNumber ) astInstrumentationOnModuleInit( config ); - elasticapm::php::Hooking::getInstance().replaceHooks(config->captureErrors, config->profilingInferredSpansEnabled); + elasticapm::php::Hooking::getInstance().replaceHooks(config->captureErrors, config->captureErrorsWithPhpPart, config->profilingInferredSpansEnabled); if (php_check_open_basedir_ex(config->bootstrapPhpPartFile, false) != 0) { ELASTIC_APM_LOG_WARNING( @@ -484,10 +490,20 @@ void elasticApmRequestInit() goto finally; } - if (!config->captureErrors) { + ELASTICAPM_G(captureErrorsUsingNative) = false; + if (config->captureErrors) { + if (config->captureErrorsWithPhpPart) { + ELASTIC_APM_LOG_DEBUG( "capture_errors_with_php_part (captureErrorsWithPhpPart) configuration option is set to true which means errors will be captured by PHP part of the agent" ); + } else { + ELASTICAPM_G(captureErrorsUsingNative) = true; + } + if (config->devInternalCaptureErrorsOnlyToLog) { + ELASTIC_APM_LOG_DEBUG( "dev_internal_capture_errors_only_to_log (devInternalCaptureErrorsOnlyToLog) configuration option is set to true which means errors will be logged only" ); + } + } else { ELASTIC_APM_LOG_DEBUG( "capture_errors (captureErrors) configuration option is set to false which means errors will NOT be captured" ); - } - ELASTICAPM_G(captureErrors) = config->captureErrors; + } + ELASTICAPM_G(captureErrorsToLogOnly) = config->devInternalCaptureErrorsOnlyToLog; if ( config->astProcessEnabled ) { @@ -590,7 +606,7 @@ void elasticApmRequestShutdown() ELASTICAPM_G(globals)->periodicTaskExecutor_->suspendPeriodicTasks(); } - ELASTICAPM_G(captureErrors) = false; // disabling error capturing on shutdown + ELASTICAPM_G(captureErrorsUsingNative) = false; // disabling error capturing on shutdown tracerPhpPartOnRequestShutdown(); diff --git a/agent/native/ext/php_elastic_apm.h b/agent/native/ext/php_elastic_apm.h index ada2a9a00..079d3738b 100644 --- a/agent/native/ext/php_elastic_apm.h +++ b/agent/native/ext/php_elastic_apm.h @@ -47,7 +47,8 @@ ZEND_BEGIN_MODULE_GLOBALS(elastic_apm) elasticapm::php::AgentGlobals *globals; zval lastException; std::unique_ptr lastErrorData; - bool captureErrors; + bool captureErrorsUsingNative; + bool captureErrorsToLogOnly; ZEND_END_MODULE_GLOBALS(elastic_apm) ZEND_EXTERN_MODULE_GLOBALS(elastic_apm) diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php b/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php index d5043bf3f..1882d9824 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php @@ -276,19 +276,19 @@ function (TransactionForExtensionRequest $transactionForExtensionRequest): void private static function ensureHaveLastErrorData( TransactionForExtensionRequest $transactionForExtensionRequest ): void { - if (!$transactionForExtensionRequest->getConfig()->captureErrors()) { - return; + if ($transactionForExtensionRequest->getConfig()->captureErrors() && (!$transactionForExtensionRequest->getConfig()->captureErrorsWithPhpPart())) { + /** + * The last thrown should be fetched before last PHP error because if the error is for "Uncaught Exception" + * agent will use the last thrown exception + */ + if ($transactionForExtensionRequest->getConfig()->shouldCaptureExceptions()) { + self::ensureHaveLastThrownCapturedByNativePart($transactionForExtensionRequest); + } + self::ensureHaveLastPhpErrorCapturedByNativePart($transactionForExtensionRequest); } - - /** - * The last thrown should be fetched before last PHP error because if the error is for "Uncaught Exception" - * agent will use the last thrown exception - */ - self::ensureHaveLastThrown($transactionForExtensionRequest); - self::ensureHaveLastPhpError($transactionForExtensionRequest); } - private static function ensureHaveLastThrown(TransactionForExtensionRequest $transactionForExtensionRequest): void + private static function ensureHaveLastThrownCapturedByNativePart(TransactionForExtensionRequest $transactionForExtensionRequest): void { /** * elastic_apm_* functions are provided by the elastic_apm extension @@ -303,7 +303,7 @@ private static function ensureHaveLastThrown(TransactionForExtensionRequest $tra return; } - $transactionForExtensionRequest->setLastThrown($lastThrown); + $transactionForExtensionRequest->setLastThrownCapturedByNativePart($lastThrown); } /** @@ -431,7 +431,7 @@ private static function buildPhpErrorData(array $dataFromExt): PhpErrorData return $result; } - private static function ensureHaveLastPhpError(TransactionForExtensionRequest $transactionForExtensionRequest): void + private static function ensureHaveLastPhpErrorCapturedByNativePart(TransactionForExtensionRequest $transactionForExtensionRequest): void { /** * elastic_apm_* functions are provided by the elastic_apm extension @@ -462,7 +462,7 @@ private static function ensureHaveLastPhpError(TransactionForExtensionRequest $t } /** @var array $lastPhpErrorData */ - $transactionForExtensionRequest->onPhpError(self::buildPhpErrorData($lastPhpErrorData)); + $transactionForExtensionRequest->onPhpErrorCapturedByNativePart(self::buildPhpErrorData($lastPhpErrorData)); } /** diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php index c3ab63af7..3ac5358b4 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php @@ -76,15 +76,19 @@ final class TransactionForExtensionRequest /** @var ?Throwable */ private $lastThrown = null; + /** @var null|callable(mixed ...$args): bool */ + private $prevErrorHandler; + + /** @var null|callable(Throwable): void */ + private $prevExceptionHandler; + /** @var ?InferredSpansManager */ private $inferredSpansManager = null; public function __construct(Tracer $tracer, float $requestInitStartTime) { $this->tracer = $tracer; - $this->logger = $tracer->loggerFactory() - ->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__) - ->addContext('this', $this); + $this->logger = $tracer->loggerFactory()->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__); $this->transactionForRequest = $this->beginTransaction($requestInitStartTime); if ($this->transactionForRequest instanceof Transaction && $this->transactionForRequest->isSampled()) { @@ -113,6 +117,38 @@ function (/** @noinspection PhpUnusedParameterInspection */ Span $ignored): void ); } ); + + $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); + $logDebug && $logDebug->log(__LINE__, '$this->logger->maxEnabledLevel(): ' . $this->logger->maxEnabledLevel()); + if ($this->tracer->getConfig()->captureErrorsWithPhpPart()) { + if ($this->tracer->getConfig()->captureErrors()) { + $this->prevErrorHandler = set_error_handler( + /** + * @param mixed ...$otherArgs + */ + function (int $errno, string $errstr, ?string $errfile, ?int $errline, ...$otherArgs): bool { + return $this->onPhpErrorCapturedByPhpPart(/* numberOfStackFramesToSkip */ 1, $errno, $errstr, $errfile, $errline, ...$otherArgs); + } + ); + $logDebug && $logDebug->log(__LINE__, 'Registered PHP error handler'); + } else { + $logDebug && $logDebug->log(__LINE__, 'capture_errors configuration option is set to false - not registering PHP error handler'); + } + + if ($this->tracer->getConfig()->shouldCaptureExceptions()) { + $this->prevExceptionHandler = set_exception_handler( + function (Throwable $thrown): void { + $this->onNotCaughtThrowableCapturedByPhpPart($thrown); + } + ); + $logDebug && $logDebug->log(__LINE__, 'Registered exception handler'); + } else { + $optName = ($this->tracer->getConfig()->captureExceptions() === null ? 'capture_errors' : 'capture_exceptions'); + $logDebug && $logDebug->log(__LINE__, $optName . ' configuration option is set to false - not registering exception handler'); + } + } + + $this->logger->addContext('this', $this); } public function getConfig(): ConfigSnapshot @@ -332,7 +368,7 @@ private function logGcStatus(): void && $loggerProxy->log('Called gc_status()', ['gc_status() return value' => $gcStatusRetVal]); } - public function onPhpError(PhpErrorData $phpErrorData): void + public function onPhpErrorCapturedByNativePart(PhpErrorData $phpErrorData): void { $relatedThrowable = null; if ( @@ -346,12 +382,43 @@ public function onPhpError(PhpErrorData $phpErrorData): void $this->tracer->onPhpError($phpErrorData, $relatedThrowable, /* numberOfStackFramesToSkip */ 1); } + /** + * Callable passed to set_error_handler had 5th parameter - $errcontext (array). This parameter was deprecated in PHP 7.2.0 and removed in PHP 8.0.0 + * + * @param mixed ...$otherArgs + * + * @phpstan-param 0|positive-int $numberOfStackFramesToSkip + * + * @noinspection PhpSameParameterValueInspection + */ + private function onPhpErrorCapturedByPhpPart(int $numberOfStackFramesToSkip, int $errno, string $errstr, ?string $errfile, ?int $errline, ...$otherArgs): bool + { + ($logDebug = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $logDebug->log('Entered', compact('errno', 'errstr', 'errfile', 'errline')); + + if (!$this->tracer->getConfig()->devInternalCaptureErrorsOnlyToLog()) { + $phpErrorData = new PhpErrorData(); + $phpErrorData->type = $errno; + $phpErrorData->fileName = $errfile; + $phpErrorData->lineNumber = $errline; + $phpErrorData->message = $errstr; + $phpErrorData->stackTrace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), $numberOfStackFramesToSkip + 1); + $this->tracer->onPhpError($phpErrorData, /* relatedThrowable */ null, $numberOfStackFramesToSkip + 1); + } + + /** + * If the function returns false then the normal error handler continues. + * + * @link https://www.php.net/manual/en/function.set-error-handler.php + */ + return $this->prevErrorHandler === null ? false : ($this->prevErrorHandler)($errno, $errstr, $errfile, $errline, ...$otherArgs); + } + /** * @param mixed $lastThrown * * @return void */ - public function setLastThrown($lastThrown): void + public function setLastThrownCapturedByNativePart($lastThrown): void { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Entered', ['lastThrown' => $lastThrown]); @@ -368,6 +435,19 @@ public function setLastThrown($lastThrown): void $this->lastThrown = $lastThrown; } + public function onNotCaughtThrowableCapturedByPhpPart(Throwable $thrown): void + { + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Entered', compact('thrown')); + + if (!$this->tracer->getConfig()->devInternalCaptureErrorsOnlyToLog()) { + $this->tracer->createErrorFromThrowable($thrown); + } + + if ($this->prevExceptionHandler !== null) { + ($this->prevExceptionHandler)($thrown); + } + } + public function onShutdown(): void { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) diff --git a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php index cf9dbbe01..7725b9809 100644 --- a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php +++ b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php @@ -90,7 +90,10 @@ public static function get(): array OptionNames::ASYNC_BACKEND_COMM => new BoolOptionMetadata(/* default */ true), OptionNames::BREAKDOWN_METRICS => new BoolOptionMetadata(/* default */ true), OptionNames::CAPTURE_ERRORS => new BoolOptionMetadata(/* default */ true), + OptionNames::CAPTURE_ERRORS_WITH_PHP_PART => new BoolOptionMetadata(/* default */ false), + OptionNames::CAPTURE_EXCEPTIONS => new NullableBoolOptionMetadata(), OptionNames::DEV_INTERNAL => new NullableWildcardListOptionMetadata(), + OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG => new BoolOptionMetadata(/* default */ false), OptionNames::DISABLE_INSTRUMENTATIONS => new NullableWildcardListOptionMetadata(), OptionNames::DISABLE_SEND => new BoolOptionMetadata(/* default */ false), OptionNames::ENABLED => new BoolOptionMetadata(/* default */ true), diff --git a/agent/php/ElasticApm/Impl/Config/OptionNames.php b/agent/php/ElasticApm/Impl/Config/OptionNames.php index 7c9ca7be6..addd6c38c 100644 --- a/agent/php/ElasticApm/Impl/Config/OptionNames.php +++ b/agent/php/ElasticApm/Impl/Config/OptionNames.php @@ -42,13 +42,17 @@ final class OptionNames public const ASYNC_BACKEND_COMM = 'async_backend_comm'; public const BREAKDOWN_METRICS = 'breakdown_metrics'; public const CAPTURE_ERRORS = 'capture_errors'; + public const CAPTURE_ERRORS_WITH_PHP_PART = 'capture_errors_with_php_part'; + public const CAPTURE_EXCEPTIONS = 'capture_exceptions'; public const DEV_INTERNAL = 'dev_internal'; + public const DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG = 'dev_internal_capture_errors_only_to_log'; public const DISABLE_INSTRUMENTATIONS = 'disable_instrumentations'; public const DISABLE_SEND = 'disable_send'; public const ENABLED = 'enabled'; public const ENVIRONMENT = 'environment'; public const GLOBAL_LABELS = 'global_labels'; public const HOSTNAME = 'hostname'; + /** @noinspection PhpUnused */ public const INTERNAL_CHECKS_LEVEL = 'internal_checks_level'; public const LOG_LEVEL = 'log_level'; public const LOG_LEVEL_SYSLOG = 'log_level_syslog'; diff --git a/agent/php/ElasticApm/Impl/Config/Snapshot.php b/agent/php/ElasticApm/Impl/Config/Snapshot.php index a6aab762b..47f42e685 100644 --- a/agent/php/ElasticApm/Impl/Config/Snapshot.php +++ b/agent/php/ElasticApm/Impl/Config/Snapshot.php @@ -119,9 +119,18 @@ final class Snapshot implements LoggableInterface /** @var bool */ private $captureErrors; + /** @var bool */ + private $captureErrorsWithPhpPart; + + /** @var ?bool */ + private $captureExceptions; + /** @var ?WildcardListMatcher */ private $devInternal; + /** @var bool */ + private $devInternalCaptureErrorsOnlyToLog; + /** @var SnapshotDevInternal */ private $devInternalParsed; @@ -282,16 +291,39 @@ public function captureErrors(): bool return $this->captureErrors; } + public function captureErrorsWithPhpPart(): bool + { + return $this->captureErrorsWithPhpPart; + } + + public function captureExceptions(): ?bool + { + return $this->captureExceptions; + } + + public function shouldCaptureExceptions(): bool + { + // If captureExceptions is set explcitly (i.e., it is not null) can be null then it overrides captureErrors + // If captureExceptions is NOT set explcitly (i.e., it is null) then captureErrors is used to decide whether to capture exceptions + return $this->captureExceptions === null ? $this->captureErrors : $this->captureExceptions; + } + public function devInternal(): SnapshotDevInternal { return $this->devInternalParsed; } + public function devInternalCaptureErrorsOnlyToLog(): bool + { + return $this->devInternalCaptureErrorsOnlyToLog; + } + public function disableInstrumentations(): ?WildcardListMatcher { return $this->disableInstrumentations; } + /** @noinspection PhpUnused */ public function disableSend(): bool { return $this->disableSend; @@ -340,16 +372,19 @@ public function profilingInferredSpansMinDurationInMilliseconds(): float return $this->profilingInferredSpansMinDuration; } + /** @noinspection PhpUnused */ public function profilingInferredSpansSamplingInterval(): float { return $this->profilingInferredSpansSamplingInterval; } + /** @noinspection PhpUnused */ public function sanitizeFieldNames(): WildcardListMatcher { return $this->sanitizeFieldNames; } + /** @noinspection PhpUnused */ public function serverTimeout(): float { return $this->serverTimeout; diff --git a/agent/php/ElasticApm/Impl/ErrorExceptionData.php b/agent/php/ElasticApm/Impl/ErrorExceptionData.php index 9e9fcc939..67013f429 100644 --- a/agent/php/ElasticApm/Impl/ErrorExceptionData.php +++ b/agent/php/ElasticApm/Impl/ErrorExceptionData.php @@ -117,6 +117,7 @@ public static function build(Tracer $tracer, ?CustomErrorData $customErrorData, } $message = $throwable->getMessage(); + /** @noinspection PhpConditionAlreadyCheckedInspection */ if (is_string($message)) { $result->message = $tracer->limitNullableNonKeywordString($message); } diff --git a/agent/php/ElasticApm/Impl/Log/Backend.php b/agent/php/ElasticApm/Impl/Log/Backend.php index 8c2908ae0..610548f36 100644 --- a/agent/php/ElasticApm/Impl/Log/Backend.php +++ b/agent/php/ElasticApm/Impl/Log/Backend.php @@ -57,6 +57,11 @@ public function isEnabledForLevel(int $level): bool return $this->maxEnabledLevel >= $level; } + public function maxEnabledLevel(): int + { + return $this->maxEnabledLevel; + } + public function clone(): self { return new self($this->maxEnabledLevel, $this->logSink); diff --git a/agent/php/ElasticApm/Impl/Log/Logger.php b/agent/php/ElasticApm/Impl/Log/Logger.php index 5f888f20e..12429e0af 100644 --- a/agent/php/ElasticApm/Impl/Log/Logger.php +++ b/agent/php/ElasticApm/Impl/Log/Logger.php @@ -197,6 +197,11 @@ public function isTraceLevelEnabled(): bool return $this->isEnabledForLevel(Level::TRACE); } + public function maxEnabledLevel(): int + { + return $this->data->backend->maxEnabledLevel(); + } + /** * @param mixed $value * diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index a7ff0240e..472ba4afe 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -138,8 +138,12 @@ private static function buildOptionNameToRawToValue(): array OptionNames::ASYNC_BACKEND_COMM => $asyncBackendCommValues, OptionNames::BREAKDOWN_METRICS => $boolRawToParsedValues(), OptionNames::CAPTURE_ERRORS => $boolRawToParsedValues(), + OptionNames::CAPTURE_ERRORS_WITH_PHP_PART => $boolRawToParsedValues(), + OptionNames::CAPTURE_EXCEPTIONS => $boolRawToParsedValues(), OptionNames::ENABLED => $boolRawToParsedValues(/* valueToExclude: */ false), OptionNames::DEV_INTERNAL => $wildcardListRawToParsedValues, + OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG + => $boolRawToParsedValues(), OptionNames::DISABLE_INSTRUMENTATIONS => $wildcardListRawToParsedValues, OptionNames::DISABLE_SEND => $boolRawToParsedValues(/* valueToExclude: */ true), OptionNames::ENVIRONMENT => $stringRawToParsedValues([" my_environment \t "]), @@ -149,7 +153,6 @@ private static function buildOptionNameToRawToValue(): array OptionNames::LOG_LEVEL_STDERR => $logLevelRawToParsedValues, OptionNames::LOG_LEVEL_SYSLOG => $logLevelRawToParsedValues, OptionNames::NON_KEYWORD_STRING_MAX_LENGTH => $intRawToParsedValues, - // OLD TODO: Sergey Kleyman: Implement: test with PROFILING_INFERRED_SPANS_ENABLED set to true OptionNames::PROFILING_INFERRED_SPANS_ENABLED => $boolRawToParsedValues(/* valueToExclude: */ true), OptionNames::PROFILING_INFERRED_SPANS_MIN_DURATION diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index 5db9fd9e0..c1fe5a72f 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -24,6 +24,7 @@ namespace ElasticApmTests\ComponentTests; use Elastic\Apm\ElasticApm; +use Elastic\Apm\Impl\Config\AllOptionsMetadata; use Elastic\Apm\Impl\Config\OptionNames; use Elastic\Apm\Impl\StackTraceFrame; use Elastic\Apm\Impl\Util\ArrayUtil; @@ -60,7 +61,6 @@ final class ErrorComponentTest extends ComponentTestCaseBase private const STACK_TRACE_LINE_NUMBER = 'STACK_TRACE_LINE_NUMBER'; private const INCLUDE_IN_ERROR_REPORTING_KEY = 'include_in_error_reporting'; - private const CAPTURE_ERRORS_KEY = 'capture_errors'; private function verifyError(DataFromAgent $dataFromAgent): ErrorDto { @@ -193,7 +193,10 @@ public function dataProviderForTestPhpErrorUndefinedVariable(): iterable { $result = (new DataProviderForTestBuilder()) ->addBoolKeyedDimensionAllValuesCombinable(self::INCLUDE_IN_ERROR_REPORTING_KEY) - ->addBoolKeyedDimensionAllValuesCombinable(self::CAPTURE_ERRORS_KEY) + ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS) + ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART) + ->addKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_EXCEPTIONS, [null, false, true]) + ->addKeyedDimensionAllValuesCombinable(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG, [false, true]) ->build(); return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); @@ -204,10 +207,11 @@ private function implTestPhpErrorUndefinedVariable(MixedMap $testArgs): void AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); $includeInErrorReporting = $testArgs->getBool(self::INCLUDE_IN_ERROR_REPORTING_KEY); - $captureErrorsConfigOptVal = $testArgs->getBool(self::CAPTURE_ERRORS_KEY); + $captureErrorsConfigOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS); + $devInternalCaptureErrorsOnlyToLogOptVal = $testArgs->getBool(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG); $testCaseHandle = $this->getTestCaseHandle(); - $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $captureErrorsConfigOptVal); + $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $testArgs); $appCodeHost->sendRequest( AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestPhpErrorUndefinedVariableWrapper']), function (AppCodeRequestParams $appCodeRequestParams) use ($includeInErrorReporting): void { @@ -215,7 +219,10 @@ function (AppCodeRequestParams $appCodeRequestParams) use ($includeInErrorReport } ); - $isErrorExpected = $captureErrorsConfigOptVal || (!$includeInErrorReporting); + // If $includeInErrorReporting is false then substitute error is created by the app code calling ElasticApm::createErrorFromThrowable + // If $includeInErrorReporting is true then error is created only when captureErrors is true and devInternalCaptureErrorsOnlyToLog is false + $isErrorExpected = (!$includeInErrorReporting) || ($captureErrorsConfigOptVal && !$devInternalCaptureErrorsOnlyToLogOptVal); + $dbgCtx->add(compact('isErrorExpected')); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); $dbgCtx->add(['dataFromAgent' => $dataFromAgent]); @@ -300,16 +307,31 @@ public static function appCodeForTestPhpErrorUncaughtExceptionWrapper(bool $just } /** - * @dataProvider boolDataProviderAdaptedToSmoke - * - * @param bool $captureErrorsConfigOptVal + * @return iterable */ - public function testPhpErrorUncaughtException(bool $captureErrorsConfigOptVal): void + public function dataProviderCaptureErrorsExceptions(): iterable + { + $result = (new DataProviderForTestBuilder()) + ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS) + ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART) + ->addKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_EXCEPTIONS, [null, false, true]) + ->addKeyedDimensionAllValuesCombinable(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG, [false, true]) + ->build(); + + return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); + } + + private function implTestPhpErrorUncaughtException(MixedMap $testArgs): void { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + $captureErrorsOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS); + $captureExceptionsOptVal = $testArgs->getNullableBool(OptionNames::CAPTURE_EXCEPTIONS); + $devInternalCaptureErrorsOnlyToLogOptVal = $testArgs->getBool(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG); + $testCaseHandle = $this->getTestCaseHandle(); - $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $captureErrorsConfigOptVal); + $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $testArgs); + $appCodeHost->sendRequest( AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestPhpErrorUncaughtExceptionWrapper']), function (AppCodeRequestParams $appCodeRequestParams): void { @@ -324,7 +346,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } ); - $isErrorExpected = $captureErrorsConfigOptVal; + $isErrorExpected = ($captureExceptionsOptVal ?? $captureErrorsOptVal) && (!$devInternalCaptureErrorsOnlyToLogOptVal); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); $dbgCtx->add(['dataFromAgent' => $dataFromAgent]); @@ -369,32 +391,45 @@ function (AppCodeRequestParams $appCodeRequestParams): void { self::verifyAppCodeStackTraceTop($expectedStackTraceTop, $err); } + /** + * @dataProvider dataProviderCaptureErrorsExceptions + */ + public function testPhpErrorUncaughtException(MixedMap $testArgs): void + { + self::runAndEscalateLogLevelOnFailure( + self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), + function () use ($testArgs): void { + $this->implTestPhpErrorUncaughtException($testArgs); + } + ); + } + public static function appCodeForTestCaughtExceptionResponded500Wrapper(bool $justReturnLineNumber = false): int { $callLineNumber = __LINE__ + 1; return $justReturnLineNumber ? $callLineNumber : appCodeForTestCaughtExceptionResponded500(); } - private static function ensureMainAppCodeHost(TestCaseHandle $testCaseHandle, bool $captureErrorsConfigOptVal): AppCodeHostHandle + private static function ensureMainAppCodeHost(TestCaseHandle $testCaseHandle, MixedMap $testArgs): AppCodeHostHandle { return $testCaseHandle->ensureMainAppCodeHost( - function (AppCodeHostParams $appCodeParams) use ($captureErrorsConfigOptVal): void { - if (!self::equalsConfigDefaultValue(OptionNames::CAPTURE_ERRORS, $captureErrorsConfigOptVal)) { - $appCodeParams->setAgentOption(OptionNames::CAPTURE_ERRORS, $captureErrorsConfigOptVal); + function (AppCodeHostParams $appCodeParams) use ($testArgs): void { + foreach (AllOptionsMetadata::get() as $optName => $_) { + if ($testArgs->hasKey($optName)) { + $appCodeParams->setAgentOptionIfNotDefaultValue($optName, $testArgs->get($optName)); // @phpstan-ignore argument.type + } } } ); } /** - * @dataProvider boolDataProviderAdaptedToSmoke - * - * @param bool $captureErrorsConfigOptVal + * @dataProvider dataProviderCaptureErrorsExceptions */ - public function testCaughtExceptionResponded500(bool $captureErrorsConfigOptVal): void + public function implTestCaughtExceptionResponded500(MixedMap $testArgs): void { $testCaseHandle = $this->getTestCaseHandle(); - $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $captureErrorsConfigOptVal); + $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $testArgs); $appCodeHost->sendRequest( AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestCaughtExceptionResponded500Wrapper']), function (AppCodeRequestParams $appCodeRequestParams): void { @@ -403,7 +438,9 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } } ); - $isErrorExpected = self::isMainAppCodeHostHttp() && $captureErrorsConfigOptVal; + $captureErrorsOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS); + $captureErrorsWithPhpPartOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART); + $isErrorExpected = self::isMainAppCodeHostHttp() && $captureErrorsOptVal && (!$captureErrorsWithPhpPartOptVal); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); self::assertCount($expectedErrorCount, $dataFromAgent->idToError); @@ -449,4 +486,17 @@ function (AppCodeRequestParams $appCodeRequestParams): void { ]; self::verifyAppCodeStackTraceTop($expectedStackTraceTop, $err); } + + /** + * @dataProvider dataProviderCaptureErrorsExceptions + */ + public function testCaughtExceptionResponded500(MixedMap $testArgs): void + { + self::runAndEscalateLogLevelOnFailure( + self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), + function () use ($testArgs): void { + $this->implTestCaughtExceptionResponded500($testArgs); + } + ); + } } diff --git a/tests/ElasticApmTests/Util/MixedMap.php b/tests/ElasticApmTests/Util/MixedMap.php index 26836503c..e6690c4b9 100644 --- a/tests/ElasticApmTests/Util/MixedMap.php +++ b/tests/ElasticApmTests/Util/MixedMap.php @@ -111,11 +111,28 @@ public static function getBoolFrom(string $key, array $from): bool return $value; } + public function hasKey(string $key): bool + { + return array_key_exists($key, $this->map); + } + public function getBool(string $key): bool { return self::getBoolFrom($key, $this->map); } + public function getNullableBool(string $key): ?bool + { + AssertMessageStack::newScope(/* out */ $dbgCtx, array_merge(['this' => $this], AssertMessageStack::funcArgs())); + $value = $this->get($key); + if ($value === null || is_bool($value)) { + return $value; + } + + $dbgCtx->add(['value type' => DbgUtil::getType($value), 'value' => $value]); + TestCaseBase::fail('Value is not a bool'); + } + /** * @param string $key * @param array $from From 7ea5c0c3156a1b837b7eddd0ccf4e1f16426a845 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 17 Feb 2026 12:33:47 +0200 Subject: [PATCH 2/9] Added check for parsed by native part to ConfigSettingTest --- agent/native/ext/ConfigManager.cpp | 6 +- agent/native/ext/ConfigSnapshot.h | 6 +- agent/native/ext/Hooking.cpp | 6 +- agent/native/ext/OptionalBool.h | 4 +- agent/native/ext/log.h | 3 + .../Impl/Config/AllOptionsMetadata.php | 2 +- .../ComponentTests/ConfigSettingTest.php | 293 ++++++++++++------ .../ComponentTests/ErrorComponentTest.php | 44 +-- .../Util/AgentConfigSourceKind.php | 9 +- .../ComponentTests/Util/AppCodeHostParams.php | 32 +- .../Util/EnvVarUtilForTests.php | 3 +- .../Util/ArrayUtilForTests.php | 31 ++ tests/ElasticApmTests/Util/MixedMap.php | 28 +- .../ElasticApmTests/Util/TextUtilForTests.php | 10 + 14 files changed, 328 insertions(+), 149 deletions(-) diff --git a/agent/native/ext/ConfigManager.cpp b/agent/native/ext/ConfigManager.cpp index 5d75977d2..e1129d51d 100644 --- a/agent/native/ext/ConfigManager.cpp +++ b/agent/native/ext/ConfigManager.cpp @@ -365,7 +365,11 @@ static void parsedOptionalBoolValueToZval( const OptionMetadata* optMeta, Parsed ELASTIC_APM_ASSERT_EQ_UINT64( parsedValue.type, optMeta->defaultValue.type ); ELASTIC_APM_ASSERT_VALID_PTR( return_value ); - RETURN_STRING( optionalBoolToString( parsedValue.u.optionalBoolValue ) ); + if (parsedValue.u.optionalBoolValue.isSet) { + RETURN_BOOL(parsedValue.u.optionalBoolValue.value); + } else { + RETURN_NULL(); + } } static ResultCode parseDurationValue( const OptionMetadata* optMeta, String rawValue, /* out */ ParsedOptionValue* parsedValue ) diff --git a/agent/native/ext/ConfigSnapshot.h b/agent/native/ext/ConfigSnapshot.h index 1e3227c50..8c3397222 100644 --- a/agent/native/ext/ConfigSnapshot.h +++ b/agent/native/ext/ConfigSnapshot.h @@ -41,12 +41,13 @@ struct ConfigSnapshot bool astProcessDebugDumpConvertedBackToSource = false; String astProcessDebugDumpForPathPrefix = nullptr; String astProcessDebugDumpOutDir = nullptr; - OptionalBool asyncBackendComm = (OptionalBool){ .isSet = false, .value = false }; + OptionalBool asyncBackendComm = ELASTIC_APM_MAKE_NOT_SET_OPTIONAL_BOOL(); String bootstrapPhpPartFile = nullptr; bool breakdownMetrics = false; bool captureErrors = false; bool captureErrorsWithPhpPart = false; - OptionalBool captureExceptions = (OptionalBool){ .isSet = false, .value = false }; + OptionalBool captureExceptions = ELASTIC_APM_MAKE_NOT_SET_OPTIONAL_BOOL(); + String debugDiagnosticsFile = nullptr; String devInternal = nullptr; bool devInternalBackendCommLogVerbose = false; bool devInternalCaptureErrorsOnlyToLog = false; @@ -91,5 +92,4 @@ struct ConfigSnapshot String transactionSampleRate = nullptr; String urlGroups = nullptr; bool verifyServerCert = false; - String debugDiagnosticsFile = nullptr; }; diff --git a/agent/native/ext/Hooking.cpp b/agent/native/ext/Hooking.cpp index 682ab2754..a6664d13d 100644 --- a/agent/native/ext/Hooking.cpp +++ b/agent/native/ext/Hooking.cpp @@ -49,10 +49,8 @@ void elastic_apm_error_cb(int type, zend_string *error_filename, const uint32_t if (ELASTICAPM_G(captureErrorsToLogOnly)) { ELASTIC_APM_LOG_DEBUG( - "Captured error but only to log it; error_filename: %.*s; error_lineno: %d; message: %.*s", - static_cast(errorData->getFileName().length()), errorData->getFileName().data(), - errorData->getLineNumber(), - static_cast(errorData->getMessage().length()), errorData->getMessage().data() + "Captured error but only to log it; error_filename: " ELASTIC_APM_PRINTF_STRING_VIEW_FMT_SPEC() "; error_lineno: %d; message: " ELASTIC_APM_PRINTF_STRING_VIEW_FMT_SPEC(), + ELASTIC_APM_PRINTF_STD_STRING_VIEW_ARG(errorData->getFileName()), errorData->getLineNumber(), ELASTIC_APM_PRINTF_STD_STRING_VIEW_ARG(errorData->getMessage()) ); } else { ELASTICAPM_G(lastErrorData) = std::move(errorData); diff --git a/agent/native/ext/OptionalBool.h b/agent/native/ext/OptionalBool.h index fe715ef92..a83b52ce8 100644 --- a/agent/native/ext/OptionalBool.h +++ b/agent/native/ext/OptionalBool.h @@ -35,9 +35,11 @@ static inline String optionalBoolToString( OptionalBool optionalBoolValue ) return optionalBoolValue.isSet ? "not set" : boolToString( optionalBoolValue.value ); } +#define ELASTIC_APM_MAKE_NOT_SET_OPTIONAL_BOOL() ((OptionalBool){ .isSet = false, .value = false }) + static inline OptionalBool makeNotSetOptionalBool() { - return (OptionalBool){ .isSet = false, .value = false }; + return ELASTIC_APM_MAKE_NOT_SET_OPTIONAL_BOOL(); } static inline OptionalBool makeSetOptionalBool( bool value ) diff --git a/agent/native/ext/log.h b/agent/native/ext/log.h index 05072d5d0..972b1ca5a 100644 --- a/agent/native/ext/log.h +++ b/agent/native/ext/log.h @@ -31,6 +31,9 @@ #include "TextOutputStream.h" #include "platform.h" +#define ELASTIC_APM_PRINTF_STRING_VIEW_FMT_SPEC() "%.*s" +#define ELASTIC_APM_PRINTF_STD_STRING_VIEW_ARG(stdStrVw) (static_cast((stdStrVw).length())), ((stdStrVw).data()) + extern String logLevelNames[ numberOfLogLevels ]; enum LogSinkType diff --git a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php index 7725b9809..50217cc15 100644 --- a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php +++ b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php @@ -87,7 +87,7 @@ public static function get(): array => 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::ASYNC_BACKEND_COMM => new NullableBoolOptionMetadata(), OptionNames::BREAKDOWN_METRICS => new BoolOptionMetadata(/* default */ true), OptionNames::CAPTURE_ERRORS => new BoolOptionMetadata(/* default */ true), OptionNames::CAPTURE_ERRORS_WITH_PHP_PART => new BoolOptionMetadata(/* default */ false), diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index 472ba4afe..17b5953bf 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -24,21 +24,25 @@ namespace ElasticApmTests\ComponentTests; use Elastic\Apm\Impl\Config\AllOptionsMetadata; +use Elastic\Apm\Impl\Config\NullableOptionMetadata; use Elastic\Apm\Impl\Config\OptionNames; use Elastic\Apm\Impl\Log\Level as LogLevel; use Elastic\Apm\Impl\Util\DbgUtil; -use Elastic\Apm\Impl\Util\ExceptionUtil; -use Elastic\Apm\Impl\Util\WildcardListMatcher; +use Elastic\Apm\Impl\Util\TextUtil; use ElasticApmTests\ComponentTests\Util\AgentConfigSourceKind; use ElasticApmTests\ComponentTests\Util\AppCodeHostParams; use ElasticApmTests\ComponentTests\Util\AppCodeRequestParams; use ElasticApmTests\ComponentTests\Util\AppCodeTarget; use ElasticApmTests\ComponentTests\Util\ComponentTestCaseBase; use ElasticApmTests\ComponentTests\Util\HttpAppCodeRequestParams; +use ElasticApmTests\Util\ArrayUtilForTests; +use ElasticApmTests\Util\AssertMessageStack; +use ElasticApmTests\Util\DataProviderForTestBuilder; use ElasticApmTests\Util\MetadataExpectations; use ElasticApmTests\Util\MixedMap; +use ElasticApmTests\Util\TextUtilForTests; use ElasticApmTests\Util\TransactionExpectations; -use RuntimeException; +use Stringable; /** * @group smoke @@ -46,11 +50,38 @@ */ final class ConfigSettingTest extends ComponentTestCaseBase { - private const APP_CODE_ARGS_KEY_OPTION_NAME = 'APP_CODE_ARGS_KEY_OPTION_NAME'; - private const APP_CODE_ARGS_KEY_OPTION_EXPECTED_VALUE = 'APP_CODE_ARGS_KEY_OPTION_EXPECTED_VALUE'; - private const APP_CODE_RESPONSE_HTTP_STATUS_CODE = 234; + private const AGENT_CONFIG_SOURCE_KIND_KEY = 'agent_config_source_kind'; + private const OPTION_NAME_KEY = 'option_name'; + private const OPTION_RAW_VALUE_KEY = 'option_raw_value'; + private const OPTION_EXPECTED_PARSED_VALUE_KEY = 'option_expected_parsed_value'; + + private const OPTIONS_PARSED_BY_NATIVE = [ + OptionNames::AST_PROCESS_ENABLED, + OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE, + OptionNames::ASYNC_BACKEND_COMM, + OptionNames::BREAKDOWN_METRICS, + OptionNames::CAPTURE_ERRORS, + OptionNames::CAPTURE_ERRORS_WITH_PHP_PART, + OptionNames::CAPTURE_EXCEPTIONS, + OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG, + OptionNames::DISABLE_SEND, + OptionNames::ENABLED, + OptionNames::LOG_LEVEL, + OptionNames::LOG_LEVEL_STDERR, + OptionNames::LOG_LEVEL_SYSLOG, + OptionNames::PROFILING_INFERRED_SPANS_ENABLED, + OptionNames::SERVER_TIMEOUT, + OptionNames::SPAN_COMPRESSION_ENABLED, + OptionNames::VERIFY_SERVER_CERT, + ]; + + private static function isOptionLogLevelRelated(string $optName): bool + { + return TextUtil::contains($optName, 'log_level'); + } + /** * @return array> */ @@ -117,10 +148,6 @@ private static function buildOptionNameToRawToValue(): array " /a/*, \t(?-i)/b1/ /b2 \t \n, (?-i) **c*\t * \t " => "/a/*, (?-i)/b1/ /b2, (?-i) *c*\t *", ]; - $asyncBackendCommValues = $boolRawToParsedValues( - self::isMainAppCodeHostHttp() ? null : true /* <- valueToExclude */ - ); - $keyValuePairsRawToParsedValues = [ 'dept=engineering,rack=number8' => ['dept' => 'engineering', 'rack' => 'number8'], " \t key1 = \t value1 \t, \t key2 \n= value2 \t" => ['key1' => 'value1', 'key2' => 'value2'], @@ -135,17 +162,17 @@ private static function buildOptionNameToRawToValue(): array 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::ASYNC_BACKEND_COMM => $boolRawToParsedValues(), OptionNames::BREAKDOWN_METRICS => $boolRawToParsedValues(), OptionNames::CAPTURE_ERRORS => $boolRawToParsedValues(), OptionNames::CAPTURE_ERRORS_WITH_PHP_PART => $boolRawToParsedValues(), OptionNames::CAPTURE_EXCEPTIONS => $boolRawToParsedValues(), - OptionNames::ENABLED => $boolRawToParsedValues(/* valueToExclude: */ false), OptionNames::DEV_INTERNAL => $wildcardListRawToParsedValues, OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG => $boolRawToParsedValues(), OptionNames::DISABLE_INSTRUMENTATIONS => $wildcardListRawToParsedValues, OptionNames::DISABLE_SEND => $boolRawToParsedValues(/* valueToExclude: */ true), + OptionNames::ENABLED => $boolRawToParsedValues(/* valueToExclude: */ false), OptionNames::ENVIRONMENT => $stringRawToParsedValues([" my_environment \t "]), OptionNames::GLOBAL_LABELS => $keyValuePairsRawToParsedValues, OptionNames::HOSTNAME => $stringRawToParsedValues([" \t my_hostname"]), @@ -186,133 +213,188 @@ public function testBuildOptionNameToRawToValueIncludesAllOptions(): void self::assertTrue(sort(/* ref */ $optNamesFromAllOptionsMetadata)); $optNamesFromBuildOptionNameToRawToParsedValue = array_keys(self::buildOptionNameToRawToValue()); self::assertTrue(sort(/* ref */ $optNamesFromAllOptionsMetadata)); - self::assertEqualsCanonicalizing( - $optNamesFromAllOptionsMetadata, - $optNamesFromBuildOptionNameToRawToParsedValue - ); + self::assertEqualsCanonicalizing($optNamesFromAllOptionsMetadata, $optNamesFromBuildOptionNameToRawToParsedValue); } /** - * @return iterable> + * @return iterable */ public function dataProviderForTestAllWaysToSetConfig(): iterable { - $optNameToRawToParsedValue = self::buildOptionNameToRawToValue(); - - if (self::isSmoke()) { - $optNameToRawToParsedValueForSmoke = []; - foreach ($optNameToRawToParsedValue as $optName => $optRawToExpectedParsedValues) { - $optNameToRawToParsedValueForSmoke[$optName] = self::adaptToSmoke($optRawToExpectedParsedValues); - } - $optNameToRawToParsedValue = $optNameToRawToParsedValueForSmoke; - } - - $agentConfigSourceKindIndex = 0; /** - * @return AgentConfigSourceKind[] + * @return iterable> */ - $agentConfigSourceKindVariants = function () use (&$agentConfigSourceKindIndex): array { - if (!self::isSmoke()) { - return AgentConfigSourceKind::all(); + $genDataSets = function (): iterable { + $optNameToRawToParsedValue = self::buildOptionNameToRawToValue(); + + if (self::isSmoke()) { + $optNameToRawToParsedValue = array_map( + function ($optRawToExpectedParsedValues) { + return self::adaptToSmoke($optRawToExpectedParsedValues); + }, + $optNameToRawToParsedValue + ); } - $result = [AgentConfigSourceKind::all()[$agentConfigSourceKindIndex]]; - ++$agentConfigSourceKindIndex; - if ($agentConfigSourceKindIndex === count(AgentConfigSourceKind::all())) { - $agentConfigSourceKindIndex = 0; - } - return $result; - }; + $agentConfigSourceKindIndex = 0; + /** + * @return AgentConfigSourceKind[] + */ + $agentConfigSourceKindVariants = function () use (&$agentConfigSourceKindIndex): array { + if (!self::isSmoke()) { + return AgentConfigSourceKind::all(); + } - foreach ($optNameToRawToParsedValue as $optName => $optRawToExpectedParsedValues) { - foreach ($optRawToExpectedParsedValues as $optRawVal => $optExpectedVal) { - foreach ($agentConfigSourceKindVariants() as $agentConfigSourceKind) { - if ($agentConfigSourceKind === AgentConfigSourceKind::iniFile() && is_string($optRawVal)) { - $optRawVal = str_replace("\n", "\t", $optRawVal); + $result = [AgentConfigSourceKind::all()[$agentConfigSourceKindIndex]]; + ++$agentConfigSourceKindIndex; + if ($agentConfigSourceKindIndex === count(AgentConfigSourceKind::all())) { + $agentConfigSourceKindIndex = 0; + } + return $result; + }; + + foreach ($optNameToRawToParsedValue as $optName => $optRawToExpectedParsedValues) { + if (AllOptionsMetadata::get()[$optName] instanceof NullableOptionMetadata) { + yield [ + self::AGENT_CONFIG_SOURCE_KIND_KEY => null, + self::OPTION_NAME_KEY => $optName, + self::OPTION_RAW_VALUE_KEY => null, + self::OPTION_EXPECTED_PARSED_VALUE_KEY => AllOptionsMetadata::get()[$optName]->defaultValue() + ]; + } + foreach ($optRawToExpectedParsedValues as $optRawVal => $optExpectedVal) { + foreach ($agentConfigSourceKindVariants() as $agentConfigSourceKind) { + if ($agentConfigSourceKind === AgentConfigSourceKind::iniFile() && is_string($optRawVal)) { + $optRawVal = trim(str_replace("\n", "\t", $optRawVal)); + } + yield [ + self::AGENT_CONFIG_SOURCE_KIND_KEY => $agentConfigSourceKind, + self::OPTION_NAME_KEY => $optName, + self::OPTION_RAW_VALUE_KEY => TextUtilForTests::valuetoString($optRawVal), + self::OPTION_EXPECTED_PARSED_VALUE_KEY => $optExpectedVal ?? AllOptionsMetadata::get()[$optName]->defaultValue() + ]; } - $optExpectedVal = $optExpectedVal ?? AllOptionsMetadata::get()[$optName]->defaultValue(); - /** @phpstan-ignore-next-line */ - yield [$agentConfigSourceKind, $optName, strval($optRawVal), $optExpectedVal]; } } + }; + + return DataProviderForTestBuilder::convertEachDataSetToMixedMapAndAddDesc($genDataSets); + } + + /** + * @param mixed $optParsedValue + * + * @return mixed + */ + private static function adaptParsedValueToCompare($optParsedValue) + { + if ($optParsedValue === null || is_scalar($optParsedValue) || is_array($optParsedValue)) { + return $optParsedValue; + } + + if (is_object($optParsedValue)) { + self::assertInstanceOf(Stringable::class, $optParsedValue); + return strval($optParsedValue); } + + self::fail('Unexpected $optParsedValue type: ' . DbgUtil::getType($optParsedValue)); } public static function appCodeForTestAllWaysToSetConfig(MixedMap $appCodeArgs): void { - $optName = $appCodeArgs->getString(self::APP_CODE_ARGS_KEY_OPTION_NAME); - $optExpectedVal = $appCodeArgs->get(self::APP_CODE_ARGS_KEY_OPTION_EXPECTED_VALUE); + AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); - $tracer = self::getTracerFromAppCode(); + $optName = $appCodeArgs->getString(self::OPTION_NAME_KEY); + $dbgCtx->add(compact('optName')); - $optActualVal = $tracer->getConfig()->parsedValueFor($optName); + /** + * @param array $varNameToValue + */ + $addValueAndTypeToDbgCtx = function (array $varNameToValue) use ($dbgCtx): void { + $dbgCtx->add($varNameToValue); + $varName = ArrayUtilForTests::getSingleValue(array_keys($varNameToValue)); + $dbgCtx->add([$varName . ' type' => DbgUtil::getType($varNameToValue[$varName])]); + }; - if ($optActualVal instanceof WildcardListMatcher) { - $areValuesEqual = (strval($optActualVal) === $optExpectedVal); + $optExpectedParsedValue = $appCodeArgs->get(self::OPTION_EXPECTED_PARSED_VALUE_KEY); + // When passed from test to app code the expected parsed value might be converted from float to int if it does not have fractional part. + // We need to convert it back to float for expected and actual value types to match. + if (is_int($optExpectedParsedValue) && is_float(AllOptionsMetadata::get()[$optName]->defaultValue())) { + $optExpectedParsedValue = floatval($optExpectedParsedValue); + } + $addValueAndTypeToDbgCtx(compact('optExpectedParsedValue')); + + $tracer = self::getTracerFromAppCode(); + $optActualValueParsedByPhpPart = $tracer->getConfig()->parsedValueFor($optName); + $addValueAndTypeToDbgCtx(compact('optActualValueParsedByPhpPart')); + $optActualValuesToVerify = compact('optActualValueParsedByPhpPart'); + + /** + * elastic_apm_* functions are provided by the elastic_apm extension + * + * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection + * @phpstan-ignore-next-line + */ + $optActualValueParsedByNativePart = \elastic_apm_get_config_option_by_name($optName); + // Native part uses -1 for not set log level options. + // See logLevel_not_set in LogLevel.h + if (self::isOptionLogLevelRelated($optName) && $optActualValueParsedByNativePart === -1) { + $optActualValueParsedByNativePart = null; + } + $addValueAndTypeToDbgCtx(compact('optActualValueParsedByNativePart')); + if (in_array($optName, self::OPTIONS_PARSED_BY_NATIVE)) { + $optActualValuesToVerify += compact('optActualValueParsedByNativePart'); } else { - $areValuesEqual = ($optActualVal == $optExpectedVal); + $optRawValue = $appCodeArgs->get(self::OPTION_RAW_VALUE_KEY); + self::assertSame($optRawValue === null ? $optRawValue : TextUtilForTests::valuetoString($optRawValue), $optActualValueParsedByNativePart); } - if (!$areValuesEqual) { - throw new RuntimeException( - ExceptionUtil::buildMessage( - 'Expected option parsed value is not equal to the actual parsed value', - [ - 'optName' => $optName, - 'optExpectedVal' => $optExpectedVal, - 'optExpectedVal type' => DbgUtil::getType($optExpectedVal), - 'optActualVal' => $optActualVal, - 'optActualVal type' => DbgUtil::getType($optActualVal), - ] - ) - ); + $dbgCtx->pushSubScope(); + foreach ($optActualValuesToVerify as $optActualValueParsedBy => $optActualParsedValue) { + $dbgCtx->clearCurrentSubScope(compact('optActualValueParsedBy')); + $adaptedExpectedValue = self::adaptParsedValueToCompare($optExpectedParsedValue); + $addValueAndTypeToDbgCtx(compact('adaptedExpectedValue')); + $adaptedActualValue = self::adaptParsedValueToCompare($optActualParsedValue); + $addValueAndTypeToDbgCtx(compact('adaptedActualValue')); + if (is_scalar($adaptedExpectedValue) || $adaptedExpectedValue === null) { + self::assertSame($adaptedExpectedValue, $adaptedActualValue); + } else { + self::assertEquals($adaptedExpectedValue, $adaptedActualValue); + } } + $dbgCtx->popSubScope(); http_response_code(self::APP_CODE_RESPONSE_HTTP_STATUS_CODE); } - /** - * @dataProvider dataProviderForTestAllWaysToSetConfig - * - * @param AgentConfigSourceKind $agentConfigSourceKind - * @param string $optName - * @param string $optRawVal - * @param mixed $optExpectedVal - */ - public function testAllWaysToSetConfig(AgentConfigSourceKind $agentConfigSourceKind, string $optName, string $optRawVal, $optExpectedVal): void + private function implTestAllWaysToSetConfig(MixedMap $testArgs): void { - $dbgTestArgs = ['agentConfigSourceKind' => $agentConfigSourceKind, 'optName' => $optName, 'optRawVal' => $optRawVal, 'optExpectedVal' => $optExpectedVal]; - self::runAndEscalateLogLevelOnFailure( - self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, new MixedMap($dbgTestArgs)), - function () use ($agentConfigSourceKind, $optName, $optRawVal, $optExpectedVal): void { - $this->implTestAllWaysToSetConfig($agentConfigSourceKind, $optName, $optRawVal, $optExpectedVal); - } - ); - } + $agentConfigSourceKind = $testArgs->get(self::AGENT_CONFIG_SOURCE_KIND_KEY); + if ($agentConfigSourceKind !== null) { + self::assertInstanceOf(AgentConfigSourceKind::class, $agentConfigSourceKind); + } + $optName = $testArgs->getString(self::OPTION_NAME_KEY); - /** - * @param AgentConfigSourceKind $agentConfigSourceKind - * @param string $optName - * @param string $optRawVal - * @param mixed $optExpectedVal - */ - private function implTestAllWaysToSetConfig(AgentConfigSourceKind $agentConfigSourceKind, string $optName, string $optRawVal, $optExpectedVal): void - { TransactionExpectations::$defaultIsSampled = null; TransactionExpectations::$defaultDroppedSpansCount = null; MetadataExpectations::$labelsDefault->reset(); $testCaseHandle = $this->getTestCaseHandle(); $appCodeHost = $testCaseHandle->ensureMainAppCodeHost( - function (AppCodeHostParams $appCodeParams) use ($agentConfigSourceKind, $optName, $optRawVal): void { - $appCodeParams->setDefaultAgentConfigSource($agentConfigSourceKind); - $appCodeParams->setAgentOption($optName, $optRawVal); + function (AppCodeHostParams $appCodeParams) use ($agentConfigSourceKind, $optName, $testArgs): void { + if ($agentConfigSourceKind !== null) { + $appCodeParams->setDefaultAgentConfigSource($agentConfigSourceKind); + $appCodeParams->setAgentOption($optName, $testArgs->getString(self::OPTION_RAW_VALUE_KEY)); + } } ); $appCodeHost->sendRequest( AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestAllWaysToSetConfig']), - function (AppCodeRequestParams $appCodeRequestParams) use ($optName, $optExpectedVal): void { - $appCodeRequestParams->setAppCodeArgs([self::APP_CODE_ARGS_KEY_OPTION_NAME => $optName, self::APP_CODE_ARGS_KEY_OPTION_EXPECTED_VALUE => $optExpectedVal]); + function (AppCodeRequestParams $appCodeRequestParams) use ($testArgs): void { + // Remove agentConfigSourceKind because it's an object and thus cannot be sent to app code + $appCodeArgs = $testArgs->cloneAsArray(); + unset($appCodeArgs[self::AGENT_CONFIG_SOURCE_KIND_KEY]); + $appCodeRequestParams->setAppCodeArgs($appCodeArgs); if ($appCodeRequestParams instanceof HttpAppCodeRequestParams) { $appCodeRequestParams->expectedHttpResponseStatusCode = self::APP_CODE_RESPONSE_HTTP_STATUS_CODE; } @@ -320,4 +402,21 @@ function (AppCodeRequestParams $appCodeRequestParams) use ($optName, $optExpecte ); $this->waitForOneEmptyTransaction($testCaseHandle); } + + /** + * @dataProvider dataProviderForTestAllWaysToSetConfig + */ + public function testAllWaysToSetConfig(MixedMap $testArgs): void + { + if (self::isOptionLogLevelRelated($testArgs->getString(self::OPTION_NAME_KEY))) { + $this->implTestAllWaysToSetConfig($testArgs); + } else { + self::runAndEscalateLogLevelOnFailure( + self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), + function () use ($testArgs): void { + $this->implTestAllWaysToSetConfig($testArgs); + } + ); + } + } } diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index c1fe5a72f..813871b59 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -146,9 +146,9 @@ private static function verifySubstituteError(ErrorDto $err): void self::assertSame(123, $err->exception->code); } - public static function appCodeForTestPhpErrorUndefinedVariableWrapper(MixedMap $appCodeArgs): void + public static function appCodeForTestPhpErrorUndefinedVariableWrapper(MixedMap $testArgs): void { - $includeInErrorReporting = $appCodeArgs->getBool(self::INCLUDE_IN_ERROR_REPORTING_KEY); + $includeInErrorReporting = $testArgs->getBool(self::INCLUDE_IN_ERROR_REPORTING_KEY); $logger = self::getLoggerStatic(__NAMESPACE__, __CLASS__, __FILE__); ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) @@ -214,8 +214,8 @@ private function implTestPhpErrorUndefinedVariable(MixedMap $testArgs): void $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $testArgs); $appCodeHost->sendRequest( AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestPhpErrorUndefinedVariableWrapper']), - function (AppCodeRequestParams $appCodeRequestParams) use ($includeInErrorReporting): void { - $appCodeRequestParams->setAppCodeArgs([self::INCLUDE_IN_ERROR_REPORTING_KEY => $includeInErrorReporting]); + function (AppCodeRequestParams $appCodeRequestParams) use ($testArgs): void { + $appCodeRequestParams->setAppCodeArgs($testArgs); } ); @@ -225,14 +225,14 @@ function (AppCodeRequestParams $appCodeRequestParams) use ($includeInErrorReport $dbgCtx->add(compact('isErrorExpected')); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); - $dbgCtx->add(['dataFromAgent' => $dataFromAgent]); + $dbgCtx->add(compact('dataFromAgent')); self::assertCount($expectedErrorCount, $dataFromAgent->idToError); if (!$isErrorExpected) { return; } $actualError = $this->verifyError($dataFromAgent); - $dbgCtx->add(['actualError' => $actualError]); + $dbgCtx->add(compact('actualError')); if (!$includeInErrorReporting) { self::verifySubstituteError($actualError); return; @@ -302,8 +302,7 @@ function () use ($testArgs): void { public static function appCodeForTestPhpErrorUncaughtExceptionWrapper(bool $justReturnLineNumber = false): int { - $callLineNumber = __LINE__ + 1; - return $justReturnLineNumber ? $callLineNumber : appCodeForTestPhpErrorUncaughtException(); + return $justReturnLineNumber ? __LINE__ : appCodeForTestPhpErrorUncaughtException(); } /** @@ -346,10 +345,11 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } ); - $isErrorExpected = ($captureExceptionsOptVal ?? $captureErrorsOptVal) && (!$devInternalCaptureErrorsOnlyToLogOptVal); + // + $isErrorExpected = $captureErrorsOptVal && (!$devInternalCaptureErrorsOnlyToLogOptVal); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); - $dbgCtx->add(['dataFromAgent' => $dataFromAgent]); + $dbgCtx->add(compact('dataFromAgent')); self::assertCount($expectedErrorCount, $dataFromAgent->idToError); if (!$isErrorExpected) { return; @@ -358,13 +358,18 @@ function (AppCodeRequestParams $appCodeRequestParams): void { $err = $this->verifyError($dataFromAgent); $appCodeFile = FileUtilForTests::listToPath([dirname(__FILE__), 'appCodeForTestPhpErrorUncaughtException.php']); - self::assertNotNull($err->exception); - $defaultCode = (new Exception(""))->getCode(); - self::assertSame($defaultCode, $err->exception->code); - self::assertSame(Exception::class, $err->exception->type); - self::assertNotNull($err->exception->message); - self::assertSame(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE, $err->exception->message); - self::assertNull($err->exception->module); + if ($captureExceptionsOptVal !== null && !$captureExceptionsOptVal) { + self::assertNull($err->exception); + } else { + self::assertNotNull($err->exception); + $defaultCode = (new Exception(""))->getCode(); + self::assertSame($defaultCode, $err->exception->code); + self::assertSame(Exception::class, $err->exception->type); + self::assertNotNull($err->exception->message); + self::assertSame(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE, $err->exception->message); + self::assertNull($err->exception->module); + } + $culpritFunction = __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtExceptionImpl'; self::assertSame($culpritFunction, $err->culprit); @@ -382,7 +387,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { [ self::STACK_TRACE_FILE_NAME => __FILE__, self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtException', - self::STACK_TRACE_LINE_NUMBER => self::appCodeForTestPhpErrorUncaughtExceptionWrapper(/* justReturnLineNumber */ true), + self::STACK_TRACE_LINE_NUMBER => self::appCodeForTestPhpErrorUncaughtExceptionWrapper(), ], [ self::STACK_TRACE_FUNCTION => __CLASS__ . '::appCodeForTestPhpErrorUncaughtExceptionWrapper', @@ -428,6 +433,8 @@ function (AppCodeHostParams $appCodeParams) use ($testArgs): void { */ public function implTestCaughtExceptionResponded500(MixedMap $testArgs): void { + AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + $testCaseHandle = $this->getTestCaseHandle(); $appCodeHost = self::ensureMainAppCodeHost($testCaseHandle, $testArgs); $appCodeHost->sendRequest( @@ -443,6 +450,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { $isErrorExpected = self::isMainAppCodeHostHttp() && $captureErrorsOptVal && (!$captureErrorsWithPhpPartOptVal); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); + $dbgCtx->add(compact('dataFromAgent')); self::assertCount($expectedErrorCount, $dataFromAgent->idToError); if (!$isErrorExpected) { return; diff --git a/tests/ElasticApmTests/ComponentTests/Util/AgentConfigSourceKind.php b/tests/ElasticApmTests/ComponentTests/Util/AgentConfigSourceKind.php index 42b0be126..aae720c59 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AgentConfigSourceKind.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AgentConfigSourceKind.php @@ -24,7 +24,7 @@ namespace ElasticApmTests\ComponentTests\Util; use Elastic\Apm\Impl\Log\LoggableInterface; -use Elastic\Apm\Impl\Log\LoggableTrait; +use Elastic\Apm\Impl\Log\LogStreamInterface; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. @@ -33,8 +33,6 @@ */ final class AgentConfigSourceKind implements LoggableInterface { - use LoggableTrait; - /** @var ?self */ private static $iniFile = null; @@ -90,4 +88,9 @@ public static function all(): array self::ensureInited(); return self::$all; // @phpstan-ignore-line } + + public function toLog(LogStreamInterface $stream): void + { + $stream->toLogAs($this->asString); + } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php index 6b8bcd0ed..39844c5e3 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php @@ -130,16 +130,8 @@ public function setAgentOptions(array $optsMap, ?AgentConfigSourceKind $sourceKi */ private function removeLogLevelEnvVarsIfSetByOptions(array $input): array { - $isAnyLogLevelOptionsSet = false; - foreach ($this->getExplicitlySetAgentOptionsNames() as $optName) { - if (ConfigUtilForTests::isOptionLogLevelRelated($optName)) { - $isAnyLogLevelOptionsSet = true; - break; - } - } - if (!$isAnyLogLevelOptionsSet) { - return $input; - } + $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); + $logDebug && $logDebug->log(__LINE__, 'Entered', compact('input')); $output = $input; foreach (ConfigUtilForTests::allAgentLogLevelRelatedOptionNames() as $optName) { @@ -149,6 +141,7 @@ private function removeLogLevelEnvVarsIfSetByOptions(array $input): array } } + $logDebug && $logDebug->log(__LINE__, 'Exiting', compact('output')); return $output; } @@ -159,19 +152,21 @@ private function removeLogLevelEnvVarsIfSetByOptions(array $input): array */ public function selectEnvVarsToInherit(array $baseEnvVars): array { - $envVars = $baseEnvVars; + $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); + $logDebug && $logDebug->log(__LINE__, 'Entered', compact('baseEnvVars')); - $envVars = $this->removeLogLevelEnvVarsIfSetByOptions($envVars); + $result = $baseEnvVars; + $result = $this->removeLogLevelEnvVarsIfSetByOptions($result); foreach ($this->getExplicitlySetAgentOptionsNames() as $optName) { $envVarName = ConfigUtilForTests::agentOptionNameToEnvVarName($optName); - if (array_key_exists($envVarName, $envVars)) { - unset($envVars[$envVarName]); + if (array_key_exists($envVarName, $result)) { + unset($result[$envVarName]); } } - return array_filter( - $envVars, + $result = array_filter( + $result, function (string $envVarName): bool { // Return false for entries to be removed @@ -204,6 +199,9 @@ function (string $envVarName): bool { }, ARRAY_FILTER_USE_KEY ); + + $logDebug && $logDebug->log(__LINE__, 'Exiting', compact('result')); + return $result; } /** @@ -247,6 +245,8 @@ private function getExplicitlySetAgentOptions(): array * @param string $optName * * @return mixed + * + * @noinspection PhpReturnDocTypeMismatchInspection */ private function getExplicitlySetAgentOptionValue(string $optName) { diff --git a/tests/ElasticApmTests/ComponentTests/Util/EnvVarUtilForTests.php b/tests/ElasticApmTests/ComponentTests/Util/EnvVarUtilForTests.php index 2b5acdc0d..774c069ee 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/EnvVarUtilForTests.php +++ b/tests/ElasticApmTests/ComponentTests/Util/EnvVarUtilForTests.php @@ -24,6 +24,7 @@ namespace ElasticApmTests\ComponentTests\Util; use Elastic\Apm\Impl\Util\StaticClassTrait; +use ElasticApmTests\Util\ArrayUtilForTests; use PHPUnit\Framework\Assert; final class EnvVarUtilForTests @@ -62,6 +63,6 @@ public static function setOrUnset(string $envVarName, ?string $envVarValue): voi */ public static function getAll(): array { - return getenv(); + return ArrayUtilForTests::sortCloneByKey(getenv()); } } diff --git a/tests/ElasticApmTests/Util/ArrayUtilForTests.php b/tests/ElasticApmTests/Util/ArrayUtilForTests.php index 8045f1049..47df97ab9 100644 --- a/tests/ElasticApmTests/Util/ArrayUtilForTests.php +++ b/tests/ElasticApmTests/Util/ArrayUtilForTests.php @@ -187,4 +187,35 @@ public static function addToListIfNotAlreadyPresent($value, array &$list, bool $ $list[] = $value; } } + + /** + * @template TKey of array-key + * @template TValue + * + * @param array $arr + * + * @return array + */ + public static function sortByKey(array &$arr, ?int $flags = null): array + { + if ($flags === null) { + ksort($arr); + } else { + ksort($arr, $flags); + } + return $arr; + } + + /** + * @template TKey of array-key + * @template TValue + * + * @param array $arr + * + * @return array + */ + public static function sortCloneByKey(array $arr, ?int $flags = null): array + { + return self::sortByKey($arr, $flags); + } } diff --git a/tests/ElasticApmTests/Util/MixedMap.php b/tests/ElasticApmTests/Util/MixedMap.php index e6690c4b9..729962e72 100644 --- a/tests/ElasticApmTests/Util/MixedMap.php +++ b/tests/ElasticApmTests/Util/MixedMap.php @@ -28,11 +28,14 @@ use Elastic\Apm\Impl\Log\LogStreamInterface; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\DbgUtil; +use IteratorAggregate; +use Traversable; /** * @implements ArrayAccess + * @implements IteratorAggregate */ -class MixedMap implements LoggableInterface, ArrayAccess +class MixedMap implements LoggableInterface, ArrayAccess, IteratorAggregate { /** @var array */ private $map; @@ -59,7 +62,6 @@ public static function assertValidMixedMapArray(array $array): array } /** * @var array $array - * @noinspection PhpRedundantVariableDocTypeInspection */ return $array; } @@ -210,7 +212,10 @@ public function getNullablePositiveOrZeroInt(string $key): ?int if ($value !== null) { TestCaseBase::assertGreaterThanOrEqual(0, $value); } - /** @var null|positive-int|0 $value */ + /** + * @var null|positive-int|0 $value + * @noinspection PhpVarTagWithoutVariableNameInspection + */ $dbgCtx->pop(); return $value; } @@ -233,7 +238,10 @@ public function getPositiveOrZeroInt(string $key): int AssertMessageStack::newScope(/* out */ $dbgCtx, array_merge(['this' => $this], AssertMessageStack::funcArgs())); $value = $this->getInt($key); TestCaseBase::assertGreaterThanOrEqual(0, $value); - /** @var positive-int|0 $value */ + /** + * @var positive-int|0 $value + * @noinspection PhpVarTagWithoutVariableNameInspection + */ $dbgCtx->pop(); return $value; } @@ -332,6 +340,18 @@ public function offsetUnset($offset): void unset($this->map[$offset]); } + /** + * @inheritDoc + * + * @return Traversable + */ + public function getIterator(): Traversable + { + foreach ($this->map as $key => $value) { + yield $key => $value; + } + } + public function toLog(LogStreamInterface $stream): void { $stream->toLogAs($this->map); diff --git a/tests/ElasticApmTests/Util/TextUtilForTests.php b/tests/ElasticApmTests/Util/TextUtilForTests.php index c9ffd7ca4..73caca60f 100644 --- a/tests/ElasticApmTests/Util/TextUtilForTests.php +++ b/tests/ElasticApmTests/Util/TextUtilForTests.php @@ -126,4 +126,14 @@ public static function emptyIfNull($input): string /** @phpstan-ignore-next-line */ return $input === null ? '' : strval($input); } + + /** + * @param mixed $value + * + * @return string + */ + public static function valuetoString($value): string + { + return strval($value); // @phpstan-ignore-line + } } From 1b92789a387858894772994bcfc21ac610ac0b23 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 17 Feb 2026 13:53:47 +0200 Subject: [PATCH 3/9] Fixed ConfigSettingTest to work even if env_vars_to_pass_through is set --- .../ComponentTests/ConfigSettingTest.php | 39 +++++++++++++++---- .../ComponentTests/Util/AppCodeHostParams.php | 33 ---------------- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index 17b5953bf..f2cd1623a 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -27,13 +27,17 @@ use Elastic\Apm\Impl\Config\NullableOptionMetadata; use Elastic\Apm\Impl\Config\OptionNames; use Elastic\Apm\Impl\Log\Level as LogLevel; +use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\DbgUtil; use Elastic\Apm\Impl\Util\TextUtil; use ElasticApmTests\ComponentTests\Util\AgentConfigSourceKind; +use ElasticApmTests\ComponentTests\Util\AmbientContextForTests; use ElasticApmTests\ComponentTests\Util\AppCodeHostParams; use ElasticApmTests\ComponentTests\Util\AppCodeRequestParams; use ElasticApmTests\ComponentTests\Util\AppCodeTarget; use ElasticApmTests\ComponentTests\Util\ComponentTestCaseBase; +use ElasticApmTests\ComponentTests\Util\ConfigUtilForTests; +use ElasticApmTests\ComponentTests\Util\EnvVarUtilForTests; use ElasticApmTests\ComponentTests\Util\HttpAppCodeRequestParams; use ElasticApmTests\Util\ArrayUtilForTests; use ElasticApmTests\Util\AssertMessageStack; @@ -408,15 +412,34 @@ function (AppCodeRequestParams $appCodeRequestParams) use ($testArgs): void { */ public function testAllWaysToSetConfig(MixedMap $testArgs): void { - if (self::isOptionLogLevelRelated($testArgs->getString(self::OPTION_NAME_KEY))) { - $this->implTestAllWaysToSetConfig($testArgs); + $optName = $testArgs->getString(self::OPTION_NAME_KEY); + // The environment variable for the option should NOT be inherited from the parent process even if tests configuration passes it through to app code + $envVarName = ConfigUtilForTests::agentOptionNameToEnvVarName($optName); + $envVarValue = ArrayUtil::getValueIfKeyExistsElse($envVarName, EnvVarUtilForTests::getAll(), null); + if ($envVarValue !== null && AmbientContextForTests::testConfig()->isEnvVarToPassThrough($envVarName)) { + EnvVarUtilForTests::unset($envVarName); + $envVarValueWasUnset = true; } else { - self::runAndEscalateLogLevelOnFailure( - self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), - function () use ($testArgs): void { - $this->implTestAllWaysToSetConfig($testArgs); - } - ); + $envVarValueWasUnset = false; + } + + try { + // If the option is log level related we cannot use "escalate log level on failure" feature of the component tests infrastructure + if (self::isOptionLogLevelRelated($optName)) { + $this->implTestAllWaysToSetConfig($testArgs); + } else { + self::runAndEscalateLogLevelOnFailure( + self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), + function () use ($testArgs): void { + $this->implTestAllWaysToSetConfig($testArgs); + } + ); + } + } finally { + if ($envVarValueWasUnset) { + self::assertNotNull($envVarValue); + EnvVarUtilForTests::set($envVarName, $envVarValue); + } } } } diff --git a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php index 39844c5e3..829b194f5 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php @@ -123,28 +123,6 @@ public function setAgentOptions(array $optsMap, ?AgentConfigSourceKind $sourceKi } } - /** - * @param array $input - * - * @return array - */ - private function removeLogLevelEnvVarsIfSetByOptions(array $input): array - { - $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); - $logDebug && $logDebug->log(__LINE__, 'Entered', compact('input')); - - $output = $input; - foreach (ConfigUtilForTests::allAgentLogLevelRelatedOptionNames() as $optName) { - $envVarName = ConfigUtilForTests::agentOptionNameToEnvVarName($optName); - if (array_key_exists($envVarName, $output)) { - unset($output[$envVarName]); - } - } - - $logDebug && $logDebug->log(__LINE__, 'Exiting', compact('output')); - return $output; - } - /** * @param array $baseEnvVars * @@ -156,7 +134,6 @@ public function selectEnvVarsToInherit(array $baseEnvVars): array $logDebug && $logDebug->log(__LINE__, 'Entered', compact('baseEnvVars')); $result = $baseEnvVars; - $result = $this->removeLogLevelEnvVarsIfSetByOptions($result); foreach ($this->getExplicitlySetAgentOptionsNames() as $optName) { $envVarName = ConfigUtilForTests::agentOptionNameToEnvVarName($optName); @@ -175,16 +152,6 @@ function (string $envVarName): bool { return true; } - // Keep environment variables related to agent's logging - if ( - TextUtil::isPrefixOfIgnoreCase( - EnvVarsRawSnapshotSource::DEFAULT_NAME_PREFIX . 'LOG_', - $envVarName - ) - ) { - return true; - } - // Keep environment variables explicitly configured to be passed through if (AmbientContextForTests::testConfig()->isEnvVarToPassThrough($envVarName)) { return true; From 84967e5dbeb23cb1ca98223e6c3bac8dfd2606ad Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 17 Feb 2026 14:28:57 +0200 Subject: [PATCH 4/9] Fixed testCaughtExceptionResponded500 --- agent/native/ext/lifecycle.cpp | 4 ++-- tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/agent/native/ext/lifecycle.cpp b/agent/native/ext/lifecycle.cpp index 26675d419..d174deeb7 100644 --- a/agent/native/ext/lifecycle.cpp +++ b/agent/native/ext/lifecycle.cpp @@ -156,7 +156,7 @@ void elasticApmZendThrowExceptionHookImpl( #endif ) { - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "lastException set: %s", boolToString( Z_TYPE(ELASTICAPM_G(lastException)) != IS_UNDEF ) ); + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG("lastException set: %s", boolToString(Z_TYPE(ELASTICAPM_G(lastException)) != IS_UNDEF)); if (ELASTICAPM_G(captureErrorsToLogOnly)) { ELASTIC_APM_LOG_DEBUG("Captured exception but only to log it"); @@ -169,7 +169,7 @@ void elasticApmZendThrowExceptionHookImpl( #endif } - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); + ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG("lastException set: %s", boolToString(Z_TYPE(ELASTICAPM_G(lastException)) != IS_UNDEF)); } void elasticApmGetLastThrown(zval *return_value) { diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index 813871b59..5666ec0b5 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -447,7 +447,10 @@ function (AppCodeRequestParams $appCodeRequestParams): void { ); $captureErrorsOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS); $captureErrorsWithPhpPartOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART); - $isErrorExpected = self::isMainAppCodeHostHttp() && $captureErrorsOptVal && (!$captureErrorsWithPhpPartOptVal); + $captureExceptionsOptVal = $testArgs->getNullableBool(OptionNames::CAPTURE_EXCEPTIONS); + $devInternalCaptureErrorsOnlyToLogOptVal = $testArgs->getBool(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG); + // If exception handler is implemented by PHP it can only capture unhandled exception + $isErrorExpected = self::isMainAppCodeHostHttp() && ($captureExceptionsOptVal ?? $captureErrorsOptVal) && (!$captureErrorsWithPhpPartOptVal) && (!$devInternalCaptureErrorsOnlyToLogOptVal); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); $dbgCtx->add(compact('dataFromAgent')); From 9bc0903e4353336b73758cf7475669c0bea2598d Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 17 Feb 2026 17:21:07 +0200 Subject: [PATCH 5/9] Added docs --- .../Impl/AutoInstrument/PhpPartFacade.php | 28 ++++++++++----- .../TransactionForExtensionRequest.php | 5 ++- agent/php/ElasticApm/Impl/Util/BoolUtil.php | 5 +++ docs/reference/configuration-reference.md | 35 +++++++++++++++++++ 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php b/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php index 1882d9824..0ec5da2e5 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php @@ -29,6 +29,7 @@ use Elastic\Apm\Impl\Tracer; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\Assert; +use Elastic\Apm\Impl\Util\BoolUtil; use Elastic\Apm\Impl\Util\DbgUtil; use Elastic\Apm\Impl\Util\ElasticApmExtensionUtil; use Elastic\Apm\Impl\Util\HiddenConstructorTrait; @@ -273,18 +274,27 @@ function (TransactionForExtensionRequest $transactionForExtensionRequest): void ); } - private static function ensureHaveLastErrorData( - TransactionForExtensionRequest $transactionForExtensionRequest - ): void { - if ($transactionForExtensionRequest->getConfig()->captureErrors() && (!$transactionForExtensionRequest->getConfig()->captureErrorsWithPhpPart())) { - /** - * The last thrown should be fetched before last PHP error because if the error is for "Uncaught Exception" - * agent will use the last thrown exception - */ + private static function ensureHaveLastErrorData(TransactionForExtensionRequest $transactionForExtensionRequest): void + { + BootstrapStageLogger::logDebug( + 'Entered' + . '; captureErrors: ' . BoolUtil::toString($transactionForExtensionRequest->getConfig()->captureErrors()) + . '; captureExceptions: ' . BoolUtil::nullableToString($transactionForExtensionRequest->getConfig()->captureExceptions()) + . '; captureErrorsWithPhpPart: ' . BoolUtil::toString($transactionForExtensionRequest->getConfig()->captureErrorsWithPhpPart()) + . '; shouldCaptureExceptions: ' . BoolUtil::toString($transactionForExtensionRequest->getConfig()->shouldCaptureExceptions()), + __LINE__, + __FUNCTION__ + ); + + if (!$transactionForExtensionRequest->getConfig()->captureErrorsWithPhpPart()) { + // The last thrown should be fetched before last PHP error because if the error is for "Uncaught Exception" + // agent will use the last thrown exception if ($transactionForExtensionRequest->getConfig()->shouldCaptureExceptions()) { self::ensureHaveLastThrownCapturedByNativePart($transactionForExtensionRequest); } - self::ensureHaveLastPhpErrorCapturedByNativePart($transactionForExtensionRequest); + if ($transactionForExtensionRequest->getConfig()->captureErrors()) { + self::ensureHaveLastPhpErrorCapturedByNativePart($transactionForExtensionRequest); + } } } diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php index 3ac5358b4..40884dbc0 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php @@ -29,6 +29,7 @@ use Elastic\Apm\Impl\Constants; use Elastic\Apm\Impl\HttpDistributedTracing; use Elastic\Apm\Impl\InferredSpansManager; +use Elastic\Apm\Impl\Log\Level as LogLevel; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Span; @@ -119,7 +120,7 @@ function (/** @noinspection PhpUnusedParameterInspection */ Span $ignored): void ); $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); - $logDebug && $logDebug->log(__LINE__, '$this->logger->maxEnabledLevel(): ' . $this->logger->maxEnabledLevel()); + $logDebug && $logDebug->log(__LINE__, '$this->logger->maxEnabledLevel(): ' . LogLevel::intToName($this->logger->maxEnabledLevel()) . ' (as int: ' . $this->logger->maxEnabledLevel() . ')'); if ($this->tracer->getConfig()->captureErrorsWithPhpPart()) { if ($this->tracer->getConfig()->captureErrors()) { $this->prevErrorHandler = set_error_handler( @@ -146,6 +147,8 @@ function (Throwable $thrown): void { $optName = ($this->tracer->getConfig()->captureExceptions() === null ? 'capture_errors' : 'capture_exceptions'); $logDebug && $logDebug->log(__LINE__, $optName . ' configuration option is set to false - not registering exception handler'); } + } else { + $logDebug && $logDebug->log(__LINE__, 'Error capturing is implemented by native part'); } $this->logger->addContext('this', $this); diff --git a/agent/php/ElasticApm/Impl/Util/BoolUtil.php b/agent/php/ElasticApm/Impl/Util/BoolUtil.php index 6844f72c8..5ef9d9cf1 100644 --- a/agent/php/ElasticApm/Impl/Util/BoolUtil.php +++ b/agent/php/ElasticApm/Impl/Util/BoolUtil.php @@ -45,6 +45,11 @@ public static function toString(bool $val): string return $val ? 'true' : 'false'; } + public static function nullableToString(?bool $val): string + { + return $val === null ? 'null' : self::toString($val); + } + public static function toInt(bool $val): int { return $val ? self::INT_FOR_TRUE : self::INT_FOR_FALSE; diff --git a/docs/reference/configuration-reference.md b/docs/reference/configuration-reference.md index 0c52e3ac0..408cdd821 100644 --- a/docs/reference/configuration-reference.md +++ b/docs/reference/configuration-reference.md @@ -70,6 +70,41 @@ If this configuration option is set to `true` the agent will collect and report Also see [PHP errors as APM error events](/reference/configuration.md#configure-php-error-reporting). + +## `capture_errors_with_php_part` [config-capture-errors-with-php-part] + +| Environment variable name | Option name in `php.ini` | +| --- | --- | +| `ELASTIC_APM_CAPTURE_ERRORS_WITH_PHP_PART` | `elastic_apm.capture_errors_with_php_part` | + +| Default | Type | +|---------| --- | +| false | Boolean | + +If this configuration option is set to `false` (the default) the agent will capture errors and exceptions using native API. +If this configuration option is set to `true` the agent will capture errors and exceptions using PHP user-land API. + +Also see [PHP errors as APM error events](/reference/configuration.md#configure-php-error-reporting). + + + +## `capture_exceptions` [config-capture-exceptions] + +| Environment variable name | Option name in `php.ini` | +| --- | --- | +| `ELASTIC_APM_CAPTURE_EXCEPTIONS` | `elastic_apm.capture_exceptions` | + +| Default | Type | +| --- | --- | +| true | Boolean | + +If this configuration option is set to `true` the agent will capture exceptions and report error events for transaction with failure outcome. +If this configuration option is not set then [`capture_errors`](#config-capture-errors) takes effect. +Set it to `true`/`false` to enable/disable the collection of exceptions and reporting them as APM error events regardless of [`capture_errors`](#config-capture-errors). + +Also see [PHP errors as APM error events](/reference/configuration.md#configure-php-error-reporting). + + ## `disable_instrumentations` [config-disable-instrumentations] | Environment variable name | Option name in `php.ini` | From b7d46ce00ff293615c1f3a6f65cbf9b3c3910229 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Tue, 17 Feb 2026 22:28:17 +0200 Subject: [PATCH 6/9] Fixed ErrorComponentTest --- agent/php/ElasticApm/Impl/Error.php | 4 + .../ComponentTests/ErrorComponentTest.php | 85 ++++++++++++------- ...ppCodeForTestPhpErrorUncaughtException.php | 10 ++- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/agent/php/ElasticApm/Impl/Error.php b/agent/php/ElasticApm/Impl/Error.php index 5addbe0dc..d94abd91e 100644 --- a/agent/php/ElasticApm/Impl/Error.php +++ b/agent/php/ElasticApm/Impl/Error.php @@ -24,6 +24,7 @@ namespace Elastic\Apm\Impl; use Elastic\Apm\Impl\BackendComm\SerializationUtil; +use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\LoggableInterface; use Elastic\Apm\Impl\Log\LoggableTrait; use Elastic\Apm\Impl\Util\IdGenerator; @@ -135,6 +136,9 @@ class Error implements SerializableDataInterface, LoggableInterface public static function build(Tracer $tracer, ErrorExceptionData $errorExceptionData, ?Transaction $transaction, ?Span $span): Error { + $logger = $tracer->loggerFactory()->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__); + ($logDebug = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $logDebug->includeStackTrace()->log('Entered', compact('errorExceptionData', 'transaction', 'span')); + $result = new Error(); $result->timestamp = $tracer->getClock()->getSystemClockCurrentTime(); diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index 5666ec0b5..c675cf1db 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -325,6 +325,7 @@ private function implTestPhpErrorUncaughtException(MixedMap $testArgs): void AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); $captureErrorsOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS); + $captureErrorsWithPhpPartOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART); $captureExceptionsOptVal = $testArgs->getNullableBool(OptionNames::CAPTURE_EXCEPTIONS); $devInternalCaptureErrorsOnlyToLogOptVal = $testArgs->getBool(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG); @@ -345,8 +346,20 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } ); - // - $isErrorExpected = $captureErrorsOptVal && (!$devInternalCaptureErrorsOnlyToLogOptVal); + if ($captureErrorsWithPhpPartOptVal) { + // If error capturing is implemented by PHP part then on uncaught exception only exception handler is called + $isErrorExpected = ($captureExceptionsOptVal ?? $captureErrorsOptVal); + } else { + // If error capturing is implemented by native part then on uncaught exception both error and exception handler are called + if (self::isMainAppCodeHostHttp()) { + // If it's HTTP app then outcome will be failure and error will be created even if capture_error config is false + $isErrorExpected = $captureErrorsOptVal || $captureExceptionsOptVal; + } else { + $isErrorExpected = $captureErrorsOptVal; + } + } + $isErrorExpected = $isErrorExpected && !$devInternalCaptureErrorsOnlyToLogOptVal; + $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); $dbgCtx->add(compact('dataFromAgent')); @@ -358,42 +371,48 @@ function (AppCodeRequestParams $appCodeRequestParams): void { $err = $this->verifyError($dataFromAgent); $appCodeFile = FileUtilForTests::listToPath([dirname(__FILE__), 'appCodeForTestPhpErrorUncaughtException.php']); - if ($captureExceptionsOptVal !== null && !$captureExceptionsOptVal) { - self::assertNull($err->exception); - } else { - self::assertNotNull($err->exception); + self::assertNotNull($err->exception); + self::assertNull($err->exception->module); + if ($captureExceptionsOptVal ?? $captureErrorsOptVal) { + $culpritFunction = __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtExceptionImpl'; + self::assertSame($culpritFunction, $err->culprit); + $defaultCode = (new Exception(""))->getCode(); self::assertSame($defaultCode, $err->exception->code); + self::assertSame(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE, $err->exception->message); self::assertSame(Exception::class, $err->exception->type); + $expectedStackTraceTop = [ + [ + self::STACK_TRACE_FILE_NAME => $appCodeFile, + self::STACK_TRACE_FUNCTION => null, + self::STACK_TRACE_LINE_NUMBER => APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_ERROR_LINE_NUMBER, + ], + [ + self::STACK_TRACE_FILE_NAME => $appCodeFile, + self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtExceptionImpl', + self::STACK_TRACE_LINE_NUMBER => APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_CALL_TO_IMPL_LINE_NUMBER, + ], + [ + self::STACK_TRACE_FILE_NAME => __FILE__, + self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtException', + self::STACK_TRACE_LINE_NUMBER => self::appCodeForTestPhpErrorUncaughtExceptionWrapper(/* justReturnLineNumber */ true), + ], + [ + self::STACK_TRACE_FUNCTION => __CLASS__ . '::appCodeForTestPhpErrorUncaughtExceptionWrapper', + ], + ]; + self::verifyAppCodeStackTraceTop($expectedStackTraceTop, $err); + } else { + self::assertNull($err->culprit); + + // TODO: Sergey Kleyman: Implement: ErrorComponentTest:: + self::assertSame(32769, $err->exception->code); self::assertNotNull($err->exception->message); - self::assertSame(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE, $err->exception->message); - self::assertNull($err->exception->module); + self::assertStringContainsString(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE, $err->exception->message); + self::assertNull($err->exception->type); + self::assertNotNull($err->exception->stacktrace); + self::assertCount(0, $err->exception->stacktrace); } - - $culpritFunction = __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtExceptionImpl'; - self::assertSame($culpritFunction, $err->culprit); - - $expectedStackTraceTop = [ - [ - self::STACK_TRACE_FILE_NAME => $appCodeFile, - self::STACK_TRACE_FUNCTION => null, - self::STACK_TRACE_LINE_NUMBER => APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_ERROR_LINE_NUMBER, - ], - [ - self::STACK_TRACE_FILE_NAME => $appCodeFile, - self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtExceptionImpl', - self::STACK_TRACE_LINE_NUMBER => APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_CALL_TO_IMPL_LINE_NUMBER, - ], - [ - self::STACK_TRACE_FILE_NAME => __FILE__, - self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtException', - self::STACK_TRACE_LINE_NUMBER => self::appCodeForTestPhpErrorUncaughtExceptionWrapper(), - ], - [ - self::STACK_TRACE_FUNCTION => __CLASS__ . '::appCodeForTestPhpErrorUncaughtExceptionWrapper', - ], - ]; - self::verifyAppCodeStackTraceTop($expectedStackTraceTop, $err); } /** diff --git a/tests/ElasticApmTests/ComponentTests/appCodeForTestPhpErrorUncaughtException.php b/tests/ElasticApmTests/ComponentTests/appCodeForTestPhpErrorUncaughtException.php index f9d1654c2..07993a4fe 100644 --- a/tests/ElasticApmTests/ComponentTests/appCodeForTestPhpErrorUncaughtException.php +++ b/tests/ElasticApmTests/ComponentTests/appCodeForTestPhpErrorUncaughtException.php @@ -39,12 +39,14 @@ function appCodeForTestPhpErrorUncaughtExceptionImpl2() throw new Exception('Message for caught exception'); } -const APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_ERROR_LINE_NUMBER = 57; +const APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_ERROR_LINE_NUMBER = 59; /** * @return never * * @throws Exception + * + * @noinspection PhpReturnDocTypeMismatchInspection */ function appCodeForTestPhpErrorUncaughtExceptionImpl() { @@ -54,10 +56,10 @@ function appCodeForTestPhpErrorUncaughtExceptionImpl() } TestCase::assertSame(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_ERROR_LINE_NUMBER, __LINE__ + 1); - throw new Exception(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE); + throw new Exception(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE); // <- APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_ERROR_LINE_NUMBER } -const APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_CALL_TO_IMPL_LINE_NUMBER = 70; +const APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_CALL_TO_IMPL_LINE_NUMBER = 72; /** * @return never @@ -67,5 +69,5 @@ function appCodeForTestPhpErrorUncaughtExceptionImpl() function appCodeForTestPhpErrorUncaughtException(): int { TestCase::assertSame(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_CALL_TO_IMPL_LINE_NUMBER, __LINE__ + 1); - appCodeForTestPhpErrorUncaughtExceptionImpl(); + appCodeForTestPhpErrorUncaughtExceptionImpl(); // <- APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_CALL_TO_IMPL_LINE_NUMBER } From 3a89085d282437bca77c9094352e417436f6d99f Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Wed, 18 Feb 2026 12:35:21 +0200 Subject: [PATCH 7/9] Refactored ErrorComponentTest --- agent/php/ElasticApm/Impl/Tracer.php | 23 ++------ .../ElasticApm/Impl/Util/StackTraceUtil.php | 36 +------------ .../ComponentTests/ErrorComponentTest.php | 30 +++++------ .../Util/DataProviderForTestBuilder.php | 54 +++++++++++++++++++ 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/agent/php/ElasticApm/Impl/Tracer.php b/agent/php/ElasticApm/Impl/Tracer.php index edd9513b0..6ff4265e8 100644 --- a/agent/php/ElasticApm/Impl/Tracer.php +++ b/agent/php/ElasticApm/Impl/Tracer.php @@ -289,28 +289,15 @@ public function captureTransactionWithBuilder(TransactionBuilder $builder, Closu */ public function onPhpError(PhpErrorData $phpErrorData, ?Throwable $relatedThrowable, int $numberOfStackFramesToSkip): void { - ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log( - 'Entered', - [ - 'phpErrorData' => $phpErrorData, - 'relatedThrowable' => $relatedThrowable, - ] - ); + $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); + $logDebug && $logDebug->log(__LINE__, 'Entered', compact('phpErrorData', 'relatedThrowable')); + $errorReporting = error_reporting(); if ((error_reporting() & $phpErrorData->type) === 0) { - ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log( - 'Not creating error event because error_reporting() does not include its type', - ['type' => $phpErrorData->type, 'error_reporting()' => error_reporting()] - ); + $logDebug && $logDebug->log(__LINE__, 'Not creating error event because error_reporting() does not include its type', compact('phpErrorData', 'errorReporting')); return; } - ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log( - 'Creating error event because error_reporting() includes its type...', - ['type' => $phpErrorData->type, 'error_reporting()' => error_reporting()] - ); + $logDebug && $logDebug->log(__LINE__, 'Creating error event because error_reporting() includes its type...', compact('phpErrorData', 'errorReporting')); $customErrorData = new CustomErrorData(); $customErrorData->code = $phpErrorData->type; diff --git a/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php b/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php index 4e7f7b3f6..9407d8dd6 100644 --- a/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php +++ b/agent/php/ElasticApm/Impl/Util/StackTraceUtil.php @@ -261,6 +261,7 @@ private function excludeCodeToHide(array $inFrames, ?int $maxNumberOfFrames): ar return $outFrames; } + /** @noinspection PhpUnusedParameterInspection */ public static function buildApmFormatFunctionForClassMethod(?string $classicName, ?bool $isStaticMethod, ?string $methodName): ?string { if ($methodName === null) { @@ -477,41 +478,6 @@ public function convertThrowableTraceToApmFormat(Throwable $throwable, ?int $max return $this->convertPhpToApmFormat(IterableUtil::prepend($frameForThrowLocation, $throwable->getTrace()), $maxNumberOfFrames); } - // TODO: Sergey Kleyman: REMOVE: - // /** - // * @param iterable $inFrames - // * @param ?positive-int $maxNumberOfFrames - // * - // * @return StackTraceFrame[] - // */ - // private static function convertClassicToApmFormat(iterable $inFrames, ?int $maxNumberOfFrames): array - // { - // /** @var StackTraceFrame[] $outFrames */ - // $outFrames = []; - // - // /** @var ?ClassicFormatStackTraceFrame $prevInFrame */ - // $prevInFrame = null; - // foreach ($inFrames as $currentInFrame) { - // if ($currentInFrame->file === null) { - // $isOutFrameEmpty = true; - // $outFrame = new StackTraceFrame(self::FILE_NAME_NOT_AVAILABLE_SUBSTITUTE, self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); - // } else { - // $isOutFrameEmpty = false; - // $outFrame = new StackTraceFrame($currentInFrame->file, $currentInFrame->line ?? self::LINE_NUMBER_NOT_AVAILABLE_SUBSTITUTE); - // } - // if ($prevInFrame !== null && $prevInFrame->function !== null) { - // $isOutFrameEmpty = false; - // $outFrame->function = self::buildApmFormatFunctionForClassMethod($prevInFrame->class, $prevInFrame->isStaticMethod, $prevInFrame->function); - // } - // if (!$isOutFrameEmpty && !self::addToOutputFrames($outFrame, $maxNumberOfFrames, /* ref */ $outFrames)) { - // break; - // } - // $prevInFrame = $currentInFrame; - // } - // - // return $outFrames; - // } - /** * @param iterable $inputFrames * @param ?positive-int $maxNumberOfFrames diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index c675cf1db..1a329c6ea 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -308,13 +308,13 @@ public static function appCodeForTestPhpErrorUncaughtExceptionWrapper(bool $just /** * @return iterable */ - public function dataProviderCaptureErrorsExceptions(): iterable + public function dataProviderForTestsForErrorCausedByException(): iterable { $result = (new DataProviderForTestBuilder()) - ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS) - ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART) - ->addKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_EXCEPTIONS, [null, false, true]) - ->addKeyedDimensionAllValuesCombinable(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG, [false, true]) + ->addAgentBoolConfigOptionKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS) + ->addAgentBoolConfigOptionKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART) + ->addAgentNullableBoolConfigOptionKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_EXCEPTIONS) + ->addAgentBoolConfigOptionKeyedDimensionAllValuesCombinable(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG) ->build(); return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); @@ -327,6 +327,7 @@ private function implTestPhpErrorUncaughtException(MixedMap $testArgs): void $captureErrorsOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS); $captureErrorsWithPhpPartOptVal = $testArgs->getBool(OptionNames::CAPTURE_ERRORS_WITH_PHP_PART); $captureExceptionsOptVal = $testArgs->getNullableBool(OptionNames::CAPTURE_EXCEPTIONS); + $shouldCaptureExceptionsDerivedCfg = $captureExceptionsOptVal ?? $captureErrorsOptVal; $devInternalCaptureErrorsOnlyToLogOptVal = $testArgs->getBool(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG); $testCaseHandle = $this->getTestCaseHandle(); @@ -348,12 +349,12 @@ function (AppCodeRequestParams $appCodeRequestParams): void { if ($captureErrorsWithPhpPartOptVal) { // If error capturing is implemented by PHP part then on uncaught exception only exception handler is called - $isErrorExpected = ($captureExceptionsOptVal ?? $captureErrorsOptVal); + $isErrorExpected = $shouldCaptureExceptionsDerivedCfg; } else { // If error capturing is implemented by native part then on uncaught exception both error and exception handler are called if (self::isMainAppCodeHostHttp()) { // If it's HTTP app then outcome will be failure and error will be created even if capture_error config is false - $isErrorExpected = $captureErrorsOptVal || $captureExceptionsOptVal; + $isErrorExpected = $captureErrorsOptVal || $shouldCaptureExceptionsDerivedCfg; } else { $isErrorExpected = $captureErrorsOptVal; } @@ -373,7 +374,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { $appCodeFile = FileUtilForTests::listToPath([dirname(__FILE__), 'appCodeForTestPhpErrorUncaughtException.php']); self::assertNotNull($err->exception); self::assertNull($err->exception->module); - if ($captureExceptionsOptVal ?? $captureErrorsOptVal) { + if ($shouldCaptureExceptionsDerivedCfg) { $culpritFunction = __NAMESPACE__ . '\\appCodeForTestPhpErrorUncaughtExceptionImpl'; self::assertSame($culpritFunction, $err->culprit); @@ -402,11 +403,9 @@ function (AppCodeRequestParams $appCodeRequestParams): void { ], ]; self::verifyAppCodeStackTraceTop($expectedStackTraceTop, $err); - } else { + } else { // if ($shouldCaptureExceptionsDerivedCfg) self::assertNull($err->culprit); - // TODO: Sergey Kleyman: Implement: ErrorComponentTest:: - self::assertSame(32769, $err->exception->code); self::assertNotNull($err->exception->message); self::assertStringContainsString(APP_CODE_FOR_TEST_PHP_ERROR_UNCAUGHT_EXCEPTION_MESSAGE, $err->exception->message); self::assertNull($err->exception->type); @@ -416,7 +415,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } /** - * @dataProvider dataProviderCaptureErrorsExceptions + * @dataProvider dataProviderForTestsForErrorCausedByException */ public function testPhpErrorUncaughtException(MixedMap $testArgs): void { @@ -447,10 +446,7 @@ function (AppCodeHostParams $appCodeParams) use ($testArgs): void { ); } - /** - * @dataProvider dataProviderCaptureErrorsExceptions - */ - public function implTestCaughtExceptionResponded500(MixedMap $testArgs): void + private function implTestCaughtExceptionResponded500(MixedMap $testArgs): void { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); @@ -518,7 +514,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } /** - * @dataProvider dataProviderCaptureErrorsExceptions + * @dataProvider dataProviderForTestsForErrorCausedByException */ public function testCaughtExceptionResponded500(MixedMap $testArgs): void { diff --git a/tests/ElasticApmTests/Util/DataProviderForTestBuilder.php b/tests/ElasticApmTests/Util/DataProviderForTestBuilder.php index 77bfb5802..c4833e386 100644 --- a/tests/ElasticApmTests/Util/DataProviderForTestBuilder.php +++ b/tests/ElasticApmTests/Util/DataProviderForTestBuilder.php @@ -23,6 +23,7 @@ namespace ElasticApmTests\Util; +use Elastic\Apm\Impl\Config\AllOptionsMetadata; use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Log\NoopLoggerFactory; use Elastic\Apm\Impl\Util\RangeUtil; @@ -460,6 +461,59 @@ public function addConditionalKeyedDimensionAllValueCombinable( return $this->addConditionalKeyedDimension($dimensionKey, /* onlyFirstValueCombinable: */ false, $prevDimensionKey, $prevDimensionTrueValue, $iterableForTrue, $iterableForFalse); } + /** + * @return $this + */ + public function addAgentBoolConfigOptionKeyedDimension(string $optionName, bool $onlyFirstValueCombinable): self + { + $defaultValue = AllOptionsMetadata::get()[$optionName]->defaultValue(); + return $this->addKeyedDimension($optionName, $onlyFirstValueCombinable, [$defaultValue, !$defaultValue]); + } + + /** + * @return $this + * + * @noinspection PhpUnused + */ + public function addAgentBoolConfigOptionKeyedDimensionOnlyFirstValueCombinable(string $optionName): self + { + return $this->addAgentBoolConfigOptionKeyedDimension($optionName, /* onlyFirstValueCombinable: */ true); + } + + /** + * @return $this + */ + public function addAgentBoolConfigOptionKeyedDimensionAllValuesCombinable(string $optionName): self + { + return $this->addAgentBoolConfigOptionKeyedDimension($optionName, /* onlyFirstValueCombinable: */ false); + } + + /** + * @return $this + */ + public function addAgentNullableBoolConfigOptionKeyedDimension(string $optionName, bool $onlyFirstValueCombinable): self + { + return $this->addKeyedDimension($optionName, $onlyFirstValueCombinable, [null, false, true]); + } + + /** + * @return $this + * + * @noinspection PhpUnused + */ + public function addAgentNullableBoolConfigOptionKeyedDimensionOnlyFirstValueCombinable(string $optionName): self + { + return $this->addAgentNullableBoolConfigOptionKeyedDimension($optionName, /* onlyFirstValueCombinable: */ true); + } + + /** + * @return $this + */ + public function addAgentNullableBoolConfigOptionKeyedDimensionAllValuesCombinable(string $optionName): self + { + return $this->addAgentNullableBoolConfigOptionKeyedDimension($optionName, /* onlyFirstValueCombinable: */ false); + } + /** * @return iterable> */ From 50a2a60d6ea6f3e7e47acbe8c9b6815d1ec7e5b1 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Wed, 18 Feb 2026 13:21:05 +0200 Subject: [PATCH 8/9] Added implements Stringable for classes with function __toString() --- agent/php/ElasticApm/Impl/Util/UrlParts.php | 3 ++- agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php | 4 +++- agent/php/ElasticApm/Impl/Util/WildcardMatcher.php | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/agent/php/ElasticApm/Impl/Util/UrlParts.php b/agent/php/ElasticApm/Impl/Util/UrlParts.php index 6bfa01fab..0a48a29e9 100644 --- a/agent/php/ElasticApm/Impl/Util/UrlParts.php +++ b/agent/php/ElasticApm/Impl/Util/UrlParts.php @@ -25,13 +25,14 @@ use Elastic\Apm\Impl\Log\LoggableInterface; use Elastic\Apm\Impl\Log\LoggableTrait; +use Stringable; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ -final class UrlParts implements LoggableInterface +final class UrlParts implements LoggableInterface, Stringable { use LoggableTrait; diff --git a/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php b/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php index 7bc5d7a3f..3a26deb2a 100644 --- a/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php +++ b/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php @@ -23,12 +23,14 @@ namespace Elastic\Apm\Impl\Util; +use Stringable; + /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ -final class WildcardListMatcher +final class WildcardListMatcher implements Stringable { /** @var WildcardMatcher[] */ private $matchers; diff --git a/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php b/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php index a20334385..799015be0 100644 --- a/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php +++ b/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php @@ -23,12 +23,14 @@ namespace Elastic\Apm\Impl\Util; +use Stringable; + /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ -final class WildcardMatcher +final class WildcardMatcher implements Stringable { private const CASE_SENSITIVE_PREFIX = '(?-i)'; private const WILDCARD = '*'; From 2af4ddb49bd3651d0afb1fb5367a580b6850dce9 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Wed, 18 Feb 2026 14:27:49 +0200 Subject: [PATCH 9/9] Removed use of Stringable in production code --- agent/php/ElasticApm/Impl/Util/UrlParts.php | 3 +-- agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php | 4 +--- agent/php/ElasticApm/Impl/Util/WildcardMatcher.php | 4 +--- tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php | 4 +--- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/agent/php/ElasticApm/Impl/Util/UrlParts.php b/agent/php/ElasticApm/Impl/Util/UrlParts.php index 0a48a29e9..6bfa01fab 100644 --- a/agent/php/ElasticApm/Impl/Util/UrlParts.php +++ b/agent/php/ElasticApm/Impl/Util/UrlParts.php @@ -25,14 +25,13 @@ use Elastic\Apm\Impl\Log\LoggableInterface; use Elastic\Apm\Impl\Log\LoggableTrait; -use Stringable; /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ -final class UrlParts implements LoggableInterface, Stringable +final class UrlParts implements LoggableInterface { use LoggableTrait; diff --git a/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php b/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php index 3a26deb2a..7bc5d7a3f 100644 --- a/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php +++ b/agent/php/ElasticApm/Impl/Util/WildcardListMatcher.php @@ -23,14 +23,12 @@ namespace Elastic\Apm\Impl\Util; -use Stringable; - /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ -final class WildcardListMatcher implements Stringable +final class WildcardListMatcher { /** @var WildcardMatcher[] */ private $matchers; diff --git a/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php b/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php index 799015be0..a20334385 100644 --- a/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php +++ b/agent/php/ElasticApm/Impl/Util/WildcardMatcher.php @@ -23,14 +23,12 @@ namespace Elastic\Apm\Impl\Util; -use Stringable; - /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * * @internal */ -final class WildcardMatcher implements Stringable +final class WildcardMatcher { private const CASE_SENSITIVE_PREFIX = '(?-i)'; private const WILDCARD = '*'; diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index f2cd1623a..fa24a696b 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -46,7 +46,6 @@ use ElasticApmTests\Util\MixedMap; use ElasticApmTests\Util\TextUtilForTests; use ElasticApmTests\Util\TransactionExpectations; -use Stringable; /** * @group smoke @@ -297,8 +296,7 @@ private static function adaptParsedValueToCompare($optParsedValue) } if (is_object($optParsedValue)) { - self::assertInstanceOf(Stringable::class, $optParsedValue); - return strval($optParsedValue); + return TextUtilForTests::valuetoString($optParsedValue); } self::fail('Unexpected $optParsedValue type: ' . DbgUtil::getType($optParsedValue));