From 0746f5443d81461467acd55b208fc568bf46974a Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Mon, 29 Dec 2025 14:23:40 +0200 Subject: [PATCH 1/3] Moved error capturing from native to PHP --- agent/native/ext/ConfigManager.cpp | 7 + agent/native/ext/ConfigManager.h | 5 +- agent/native/ext/ConfigSnapshot.h | 1 + agent/native/ext/Hooking.cpp | 54 +---- agent/native/ext/Hooking.h | 24 +- agent/native/ext/elastic_apm.cpp | 59 +---- agent/native/ext/lifecycle.cpp | 131 +---------- agent/native/ext/lifecycle.h | 1 - agent/native/ext/php_elastic_apm.h | 3 - .../Impl/AutoInstrument/PhpErrorData.php | 27 ++- .../Impl/AutoInstrument/PhpPartFacade.php | 209 ------------------ .../TransactionForExtensionRequest.php | 125 +++++------ .../Impl/Config/AllOptionsMetadata.php | 1 + .../ElasticApm/Impl/Config/OptionNames.php | 2 + agent/php/ElasticApm/Impl/Config/Snapshot.php | 8 + .../ElasticApm/Impl/ErrorExceptionData.php | 1 + agent/php/ElasticApm/Impl/Log/Backend.php | 5 + agent/php/ElasticApm/Impl/Log/Logger.php | 5 + agent/php/ElasticApm/Impl/Tracer.php | 4 +- .../ComponentTests/ConfigSettingTest.php | 1 + .../ComponentTests/ErrorComponentTest.php | 106 ++++----- tests/ElasticApmTests/Util/MixedMap.php | 17 ++ 22 files changed, 185 insertions(+), 611 deletions(-) diff --git a/agent/native/ext/ConfigManager.cpp b/agent/native/ext/ConfigManager.cpp index 5fb2f0dc3..84f82c3e8 100644 --- a/agent/native/ext/ConfigManager.cpp +++ b/agent/native/ext/ConfigManager.cpp @@ -801,6 +801,7 @@ 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, captureExceptions ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, devInternal ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, devInternalBackendCommLogVerbose ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, disableInstrumentations ) @@ -1008,6 +1009,12 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) ELASTIC_APM_CFG_OPT_NAME_CAPTURE_ERRORS, /* defaultValue: */ true ); + ELASTIC_APM_INIT_METADATA( + buildBoolOptionMetadata, + captureExceptions, + ELASTIC_APM_CFG_OPT_NAME_CAPTURE_EXCEPTIONS, + /* defaultValue: */ true ); + ELASTIC_APM_INIT_METADATA( buildStringOptionMetadata, devInternal, diff --git a/agent/native/ext/ConfigManager.h b/agent/native/ext/ConfigManager.h index fc27fde09..fadc4a602 100644 --- a/agent/native/ext/ConfigManager.h +++ b/agent/native/ext/ConfigManager.h @@ -74,6 +74,7 @@ enum OptionId optionId_bootstrapPhpPartFile, optionId_breakdownMetrics, optionId_captureErrors, + optionId_captureExceptions, optionId_devInternal, optionId_devInternalBackendCommLogVerbose, optionId_disableInstrumentations, @@ -257,10 +258,8 @@ 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_EXCEPTIONS "capture_exceptions" /** * Internal configuration option (not included in public documentation) diff --git a/agent/native/ext/ConfigSnapshot.h b/agent/native/ext/ConfigSnapshot.h index 9c96a58fd..8832c752f 100644 --- a/agent/native/ext/ConfigSnapshot.h +++ b/agent/native/ext/ConfigSnapshot.h @@ -45,6 +45,7 @@ struct ConfigSnapshot String bootstrapPhpPartFile = nullptr; bool breakdownMetrics = false; bool captureErrors = false; + bool captureExceptions = false; String devInternal = nullptr; bool devInternalBackendCommLogVerbose = false; String disableInstrumentations = nullptr; diff --git a/agent/native/ext/Hooking.cpp b/agent/native/ext/Hooking.cpp index 5b5f8208a..ccbd097cf 100644 --- a/agent/native/ext/Hooking.cpp +++ b/agent/native/ext/Hooking.cpp @@ -17,51 +17,6 @@ namespace elasticapm::php { -#if PHP_VERSION_ID < 80000 -void elastic_apm_error_cb(int type, const char *error_filename, const Hooking::zend_error_cb_lineno_t error_lineno, const char *format, va_list args) { //<8.0 -#elif PHP_VERSION_ID < 80100 -void elastic_apm_error_cb(int type, const char *error_filename, const uint32_t error_lineno, zend_string *message) { // 8.0 -#else -void elastic_apm_error_cb(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message) { // 8.1+ -#endif - using namespace std::string_view_literals; - - if (ELASTICAPM_G(captureErrors)) { -#if PHP_VERSION_ID < 80000 - 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); - - ELASTICAPM_G(lastErrorData) = std::make_unique(type, error_filename ? error_filename : ""sv, error_lineno, message ? message : ""sv); - - 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); -#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); -#endif - } - - auto original = Hooking::getInstance().getOriginalZendErrorCb(); - if (original == elastic_apm_error_cb) { - ELASTIC_APM_LOG_DIRECT_CRITICAL("originalZendErrorCallback == elasticApmZendErrorCallback dead loop detected"); - return; - } - - if (original) { -#if PHP_VERSION_ID < 80000 - original(type, error_filename, error_lineno, format, args); -#else - original(type, error_filename, error_lineno, message); -#endif - } -} - static void elastic_execute_internal(INTERNAL_FUNCTION_PARAMETERS) { zend_try { @@ -92,7 +47,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 cfgInferredSpansEnabled) { if (cfgInferredSpansEnabled) { zend_execute_internal = elastic_execute_internal; zend_interrupt_function = elastic_interrupt_function; @@ -100,13 +55,6 @@ void Hooking::replaceHooks(bool cfgCaptureErrors, bool cfgInferredSpansEnabled) } else { 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) { - 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" ); - } } } diff --git a/agent/native/ext/Hooking.h b/agent/native/ext/Hooking.h index b25faa2e0..dc6951347 100644 --- a/agent/native/ext/Hooking.h +++ b/agent/native/ext/Hooking.h @@ -11,20 +11,6 @@ namespace elasticapm::php { class Hooking { public: -#if PHP_VERSION_ID < 70400 - using zend_error_cb_lineno_t = uint; -#else - using zend_error_cb_lineno_t = uint32_t; -#endif - -#if PHP_VERSION_ID < 80000 - using zend_error_cb_t = void (*)(int type, const char *error_filename, const zend_error_cb_lineno_t error_lineno, const char *format, va_list args); //<8.0 -#elif PHP_VERSION_ID < 80100 - using zend_error_cb_t = void (*)(int type, const char *error_filename, const uint32_t error_lineno, zend_string *message); // 8.0 -#else - using zend_error_cb_t = void (*)(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message); // 8.1+ -#endif - using zend_execute_internal_t = void (*)(zend_execute_data *execute_data, zval *return_value); using zend_interrupt_function_t = void (*)(zend_execute_data *execute_data); @@ -37,16 +23,14 @@ class Hooking { void fetchOriginalHooks() { original_execute_internal_ = zend_execute_internal; original_zend_interrupt_function_ = zend_interrupt_function; - original_zend_error_cb_ = zend_error_cb; } void restoreOriginalHooks() { zend_execute_internal = original_execute_internal_; zend_interrupt_function = original_zend_interrupt_function_; - zend_error_cb = original_zend_error_cb_; } - void replaceHooks(bool cfgCaptureErrors, bool cfgInferredSpansEnabled); + void replaceHooks(bool cfgInferredSpansEnabled); zend_execute_internal_t getOriginalExecuteInternal() { return original_execute_internal_; @@ -56,11 +40,6 @@ class Hooking { return original_zend_interrupt_function_; } - zend_error_cb_t getOriginalZendErrorCb() { - return original_zend_error_cb_; - } - - private: Hooking(Hooking const &) = delete; void operator=(Hooking const &) = delete; @@ -68,7 +47,6 @@ class Hooking { zend_execute_internal_t original_execute_internal_ = nullptr; zend_interrupt_function_t original_zend_interrupt_function_ = nullptr; - zend_error_cb_t original_zend_error_cb_ = nullptr; }; diff --git a/agent/native/ext/elastic_apm.cpp b/agent/native/ext/elastic_apm.cpp index 66c03feaa..8cbbf670f 100644 --- a/agent/native/ext/elastic_apm.cpp +++ b/agent/native/ext/elastic_apm.cpp @@ -151,6 +151,7 @@ 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_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_DISABLE_INSTRUMENTATIONS ) @@ -287,10 +288,6 @@ static PHP_GINIT_FUNCTION(elastic_apm) } catch (std::exception const &e) { ELASTIC_APM_LOG_DIRECT_CRITICAL( "Unable to allocate AgentGlobals. '%s'", e.what()); } - - ZVAL_UNDEF(&elastic_apm_globals->lastException); - new (&elastic_apm_globals->lastErrorData) std::unique_ptr; - elastic_apm_globals->captureErrors = false; } static PHP_GSHUTDOWN_FUNCTION(elastic_apm) { @@ -298,12 +295,6 @@ static PHP_GSHUTDOWN_FUNCTION(elastic_apm) { if (elastic_apm_globals->globals) { delete elastic_apm_globals->globals; } - - if (elastic_apm_globals->lastErrorData) { - ELASTIC_APM_LOG_DIRECT_WARNING( "%s: still holding error", __FUNCTION__); - // we need to relese any dangling php error data beacause it is already freed (it was allocated in request pool) - elastic_apm_globals->lastErrorData.release(); - } } PHP_MINIT_FUNCTION(elastic_apm) @@ -603,52 +594,6 @@ PHP_FUNCTION( elastic_apm_log ) } /* }}} */ -ZEND_BEGIN_ARG_INFO_EX( elastic_apm_get_last_thrown_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) -ZEND_END_ARG_INFO() -/* {{{ elastic_apm_get_last_thrown(): mixed - */ -PHP_FUNCTION( elastic_apm_get_last_thrown ) -{ - ResultCode resultCode; - ZVAL_NULL( /* out */ return_value ); - - // We SHOULD NOT log before resetting state if forked because logging might be using thread synchronization - // which might deadlock in forked child - ELASTIC_APM_CALL_IF_FAILED_GOTO( elasticApmApiEntered( __FILE__, __LINE__, __FUNCTION__ ) ); - - elasticApmGetLastThrown( /* out */ return_value ); - - finally: - return; - - failure: - goto finally; -} -/* }}} */ - -ZEND_BEGIN_ARG_INFO_EX( elastic_apm_get_last_php_error_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) -ZEND_END_ARG_INFO() -/* {{{ elastic_apm_get_last_error(): array - */ -PHP_FUNCTION( elastic_apm_get_last_php_error ) -{ - ResultCode resultCode; - ZVAL_NULL( /* out */ return_value ); - - // We SHOULD NOT log before resetting state if forked because logging might be using thread synchronization - // which might deadlock in forked child - ELASTIC_APM_CALL_IF_FAILED_GOTO( elasticApmApiEntered( __FILE__, __LINE__, __FUNCTION__ ) ); - - elasticApmGetLastPhpError( /* out */ return_value ); - - finally: - return; - - failure: - goto finally; -} -/* }}} */ - ZEND_BEGIN_ARG_INFO_EX( elastic_apm_before_loading_agent_php_code_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) ZEND_END_ARG_INFO() /* {{{ elastic_apm_before_loading_agent_php_code(): void @@ -717,8 +662,6 @@ static const zend_function_entry elastic_apm_functions[] = PHP_FE( elastic_apm_intercept_calls_to_internal_function, elastic_apm_intercept_calls_to_internal_function_arginfo ) PHP_FE( elastic_apm_send_to_server, elastic_apm_send_to_server_arginfo ) PHP_FE( elastic_apm_log, elastic_apm_log_arginfo ) - PHP_FE( elastic_apm_get_last_thrown, elastic_apm_get_last_thrown_arginfo ) - PHP_FE( elastic_apm_get_last_php_error, elastic_apm_get_last_php_error_arginfo ) PHP_FE( elastic_apm_before_loading_agent_php_code, elastic_apm_before_loading_agent_php_code_arginfo ) PHP_FE( elastic_apm_after_loading_agent_php_code, elastic_apm_after_loading_agent_php_code_arginfo ) PHP_FE( elastic_apm_ast_instrumentation_pre_hook, elastic_apm_ast_instrumentation_pre_hook_arginfo ) diff --git a/agent/native/ext/lifecycle.cpp b/agent/native/ext/lifecycle.cpp index e4fb860b8..086789d3d 100644 --- a/agent/native/ext/lifecycle.cpp +++ b/agent/native/ext/lifecycle.cpp @@ -131,108 +131,6 @@ bool doesCurrentPidMatchPidOnInit( pid_t pidOnInit, String dbgDesc ) return true; } -typedef void (* ZendThrowExceptionHook )( -#if PHP_MAJOR_VERSION >= 8 /* if PHP version is 8.* and later */ - zend_object* exception -#else - zval* exception -#endif -); - -// static bool elasticApmZendErrorCallbackSet = false; -static bool elasticApmZendThrowExceptionHookReplaced = false; -static ZendThrowExceptionHook originalZendThrowExceptionHook = NULL; - -void resetLastThrown() { - zval_dtor(&ELASTICAPM_G(lastException)); - ZVAL_UNDEF(&ELASTICAPM_G(lastException)); -} - -void elasticApmZendThrowExceptionHookImpl( -#if PHP_MAJOR_VERSION >= 8 /* if PHP version is 8.* and later */ - zend_object* thrownAsPzobj -#else - zval* thrownAsPzval -#endif -) -{ - - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "lastException set: %s", boolToString( Z_TYPE(ELASTICAPM_G(lastException)) != IS_UNDEF ) ); - - resetLastThrown(); - -#if PHP_MAJOR_VERSION >= 8 /* if PHP version is 8.* and later */ - ZVAL_OBJ_COPY(&ELASTICAPM_G( lastException ), thrownAsPzobj ); -#else - ZVAL_COPY(&ELASTICAPM_G(lastException), thrownAsPzval ); -#endif - - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); -} - -void elasticApmGetLastThrown(zval *return_value) { - if (Z_TYPE(ELASTICAPM_G(lastException)) == IS_UNDEF) { - RETURN_NULL(); - } - - RETURN_ZVAL(&ELASTICAPM_G(lastException), /* copy */ true, /* dtor */ false ); -} - -void elasticApmZendThrowExceptionHook( -#if PHP_MAJOR_VERSION >= 8 /* if PHP version is 8.* and later */ - zend_object* thrownObj -#else - zval* thrownObj -#endif -) -{ - elasticApmZendThrowExceptionHookImpl( thrownObj ); - - if (originalZendThrowExceptionHook == elasticApmZendThrowExceptionHook) { - ELASTIC_APM_LOG_CRITICAL( "originalZendThrowExceptionHook == elasticApmZendThrowExceptionHook" ); - return; - } - - if ( originalZendThrowExceptionHook != NULL ) - { - originalZendThrowExceptionHook( thrownObj ); - } -} - - -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" ); - return; - } - - if (elasticApmZendThrowExceptionHookReplaced) { - ELASTIC_APM_LOG_WARNING( "zend_throw_exception_hook already replaced: %p. Original: %p, Elastic: %p", zend_throw_exception_hook, originalZendThrowExceptionHook, elasticApmZendThrowExceptionHook ); - return; - } - - originalZendThrowExceptionHook = zend_throw_exception_hook; - zend_throw_exception_hook = elasticApmZendThrowExceptionHook; - elasticApmZendThrowExceptionHookReplaced = true; - ELASTIC_APM_LOG_DEBUG( "Replaced zend_throw_exception_hook: %p (%s elasticApmZendThrowExceptionHook) -> %p" - , originalZendThrowExceptionHook, originalZendThrowExceptionHook == elasticApmZendThrowExceptionHook ? "==" : "!=" - , elasticApmZendThrowExceptionHook ); -} - - -static void unregisterExceptionHooks() { - if (elasticApmZendThrowExceptionHookReplaced) { - ZendThrowExceptionHook zendThrowExceptionHookBeforeRestore = zend_throw_exception_hook; - zend_throw_exception_hook = originalZendThrowExceptionHook; - ELASTIC_APM_LOG_DEBUG( "Restored zend_throw_exception_hook: %p (%s elasticApmZendThrowExceptionHook: %p) -> %p" - , zendThrowExceptionHookBeforeRestore, zendThrowExceptionHookBeforeRestore == elasticApmZendThrowExceptionHook ? "==" : "!=" - , elasticApmZendThrowExceptionHook, originalZendThrowExceptionHook ); - originalZendThrowExceptionHook = NULL; - } else { - ELASTIC_APM_LOG_DEBUG("zend_throw_exception_hook not restored: %p, elastic: %p", zend_throw_exception_hook, elasticApmZendThrowExceptionHook); - } -} - void elasticApmModuleInit( int moduleType, int moduleNumber ) { auto const &sapi = ELASTICAPM_G(globals)->sapi_; @@ -278,7 +176,6 @@ void elasticApmModuleInit( int moduleType, int moduleNumber ) registerCallbacksToLogFork(); registerAtExitLogging(); - registerExceptionHooks(*config); curlCode = curl_global_init( CURL_GLOBAL_ALL ); if ( curlCode != CURLE_OK ) @@ -291,7 +188,7 @@ void elasticApmModuleInit( int moduleType, int moduleNumber ) astInstrumentationOnModuleInit( config ); - elasticapm::php::Hooking::getInstance().replaceHooks(config->captureErrors, config->profilingInferredSpansEnabled); + elasticapm::php::Hooking::getInstance().replaceHooks(config->profilingInferredSpansEnabled); if (php_check_open_basedir_ex(config->bootstrapPhpPartFile, false) != 0) { ELASTIC_APM_LOG_WARNING( @@ -337,8 +234,6 @@ void elasticApmModuleShutdown( int moduleType, int moduleNumber ) elasticapm::php::Hooking::getInstance().restoreOriginalHooks(); astInstrumentationOnModuleShutdown(); - unregisterExceptionHooks(); - backgroundBackendCommOnModuleShutdown( config ); if ( tracer->curlInited ) @@ -365,20 +260,6 @@ void elasticApmModuleShutdown( int moduleType, int moduleNumber ) ELASTIC_APM_LOG_DIRECT_DEBUG( "%s exiting...", __FUNCTION__ ); } -void elasticApmGetLastPhpError(zval* return_value) { - if (!ELASTICAPM_G(lastErrorData)) { - RETURN_NULL(); - } - - array_init( return_value ); - ELASTIC_APM_ZEND_ADD_ASSOC(return_value, "type", long, static_cast(ELASTICAPM_G(lastErrorData)->getType())); - ELASTIC_APM_ZEND_ADD_ASSOC_NULLABLE_STRING( return_value, "fileName", ELASTICAPM_G(lastErrorData)->getFileName().data() ); - ELASTIC_APM_ZEND_ADD_ASSOC(return_value, "lineNumber", long, static_cast(ELASTICAPM_G(lastErrorData)->getLineNumber())); - ELASTIC_APM_ZEND_ADD_ASSOC_NULLABLE_STRING( return_value, "message", ELASTICAPM_G(lastErrorData)->getMessage().data()); - Z_TRY_ADDREF_P((ELASTICAPM_G(lastErrorData)->getStackTrace())); - ELASTIC_APM_ZEND_ADD_ASSOC(return_value, "stackTrace", zval, (ELASTICAPM_G(lastErrorData)->getStackTrace())); -} - auto buildPeriodicTaskExecutor() { auto periodicTaskExecutor = std::make_unique( std::vector{ @@ -484,11 +365,6 @@ void elasticApmRequestInit() goto finally; } - if (!config->captureErrors) { - 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; - if ( config->astProcessEnabled ) { astInstrumentationOnRequestInit( config ); @@ -590,8 +466,6 @@ void elasticApmRequestShutdown() ELASTICAPM_G(globals)->periodicTaskExecutor_->suspendPeriodicTasks(); } - ELASTICAPM_G(captureErrors) = false; // disabling error capturing on shutdown - tracerPhpPartOnRequestShutdown(); // there is no guarantee that following code will be executed - in case of error on php side @@ -627,9 +501,6 @@ int elasticApmRequestPostDeactivate(void) { resetCallInterceptionOnRequestShutdown(); - ELASTICAPM_G(lastErrorData).reset(nullptr); - resetLastThrown(); - if ( tracer->isInited && isMemoryTrackingEnabled( &tracer->memTracker ) ) { memoryTrackerRequestShutdown( &tracer->memTracker ); diff --git a/agent/native/ext/lifecycle.h b/agent/native/ext/lifecycle.h index 548586eff..8dfd90183 100644 --- a/agent/native/ext/lifecycle.h +++ b/agent/native/ext/lifecycle.h @@ -36,7 +36,6 @@ int elasticApmRequestPostDeactivate(void); struct _zval_struct; typedef struct _zval_struct zval; -void elasticApmGetLastThrown( zval* return_value ); void elasticApmGetLastPhpError( zval* return_value ); diff --git a/agent/native/ext/php_elastic_apm.h b/agent/native/ext/php_elastic_apm.h index ada2a9a00..8a3ca7d98 100644 --- a/agent/native/ext/php_elastic_apm.h +++ b/agent/native/ext/php_elastic_apm.h @@ -45,9 +45,6 @@ ZEND_TSRMLS_CACHE_EXTERN() ZEND_BEGIN_MODULE_GLOBALS(elastic_apm) Tracer globalTracer; elasticapm::php::AgentGlobals *globals; - zval lastException; - std::unique_ptr lastErrorData; - bool captureErrors; ZEND_END_MODULE_GLOBALS(elastic_apm) ZEND_EXTERN_MODULE_GLOBALS(elastic_apm) diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/PhpErrorData.php b/agent/php/ElasticApm/Impl/AutoInstrument/PhpErrorData.php index e8cc9133d..eade6c502 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/PhpErrorData.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/PhpErrorData.php @@ -30,8 +30,8 @@ */ final class PhpErrorData { - /** @var ?int */ - public $type = null; + /** @var int */ + public $type; /** @var ?string */ public $fileName = null; @@ -39,9 +39,26 @@ final class PhpErrorData /** @var ?int */ public $lineNumber = null; - /** @var ?string */ - public $message = null; + /** @var string */ + public $message; - /** @var null|array[] */ + /** @var array[] */ public $stackTrace = null; + + /** + * @param array[] $stackTrace + */ + public function __construct( + int $type, + ?string $fileName, + ?int $lineNumber, + string $message, + array $stackTrace + ) { + $this->type = $type; + $this->fileName = $fileName; + $this->lineNumber = $lineNumber; + $this->message = $message; + $this->stackTrace = $stackTrace; + } } diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php b/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php index d5043bf3f..4af8ea8ae 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php @@ -25,11 +25,8 @@ use Closure; use Elastic\Apm\Impl\GlobalTracerHolder; -use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Tracer; -use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\Assert; -use Elastic\Apm\Impl\Util\DbgUtil; use Elastic\Apm\Impl\Util\ElasticApmExtensionUtil; use Elastic\Apm\Impl\Util\HiddenConstructorTrait; use RuntimeException; @@ -158,8 +155,6 @@ public static function internalFuncCallPreHook( return false; } - self::ensureHaveLatestDataDeferredByExtension(); - return $interceptionManager->internalFuncCallPreHook( $interceptRegistrationId, $thisObj, @@ -180,8 +175,6 @@ public static function internalFuncCallPostHook(bool $hasExitedByException, $ret $interceptionManager = self::singletonInstance()->interceptionManager; assert($interceptionManager !== null); - self::ensureHaveLatestDataDeferredByExtension(); - $interceptionManager->internalFuncCallPostHook( 1 /* <- $numberOfStackFramesToSkip */, $hasExitedByException, @@ -263,208 +256,6 @@ function (PhpPartFacade $singletonInstance) use ($implFunc): void { ); } - public static function ensureHaveLatestDataDeferredByExtension(): void - { - self::callWithTransactionForExtensionRequest( - __FUNCTION__, - function (TransactionForExtensionRequest $transactionForExtensionRequest): void { - self::ensureHaveLastErrorData($transactionForExtensionRequest); - } - ); - } - - private static function ensureHaveLastErrorData( - TransactionForExtensionRequest $transactionForExtensionRequest - ): void { - if (!$transactionForExtensionRequest->getConfig()->captureErrors()) { - return; - } - - /** - * The last thrown should be fetched before last PHP error because if the error is for "Uncaught Exception" - * agent will use the last thrown exception - */ - self::ensureHaveLastThrown($transactionForExtensionRequest); - self::ensureHaveLastPhpError($transactionForExtensionRequest); - } - - private static function ensureHaveLastThrown(TransactionForExtensionRequest $transactionForExtensionRequest): void - { - /** - * elastic_apm_* functions are provided by the elastic_apm extension - * - * @var mixed $lastThrown - * - * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection - * @phpstan-ignore-next-line - */ - $lastThrown = \elastic_apm_get_last_thrown(); - if ($lastThrown === null) { - return; - } - - $transactionForExtensionRequest->setLastThrown($lastThrown); - } - - /** - * @param string $expectedType - * @param mixed $actualValue - * - * @return void - */ - private static function logUnexpectedType(string $expectedType, $actualValue): void - { - BootstrapStageLogger::logCritical( - 'Actual type does not match the expected type' - . '; ' . 'expected type: ' . $expectedType - . ', ' . 'actual type: ' . DbgUtil::getType($actualValue) - . ', ' . 'actual value: ' . LoggableToString::convert($actualValue), - __LINE__, - __FUNCTION__ - ); - } - - /** - * @param string $expectedKey - * @param array $actualArray - * - * @return bool - */ - private static function verifyKeyExists(string $expectedKey, array $actualArray): bool - { - if (array_key_exists($expectedKey, $actualArray)) { - return true; - } - - BootstrapStageLogger::logCritical( - 'Expected key does not exist' - . '; ' . 'expected key: ' . $expectedKey - . ', ' . 'actual array keys: ' . json_encode(array_keys($actualArray)), - __LINE__, - __FUNCTION__ - ); - return false; - } - - /** - * @param array $dataFromExt - * @param string $key - * - * @return ?int - */ - private static function getIntFromPhpErrorData(array $dataFromExt, string $key): ?int - { - if (!self::verifyKeyExists($key, $dataFromExt)) { - return null; - } - $value = $dataFromExt[$key]; - if (!is_int($value)) { - self::logUnexpectedType('int', $value); - return null; - } - return $value; - } - - /** - * @param array $dataFromExt - * @param string $key - * - * @return ?string - */ - private static function getNullableStringFromPhpErrorData(array $dataFromExt, string $key): ?string - { - if (!self::verifyKeyExists($key, $dataFromExt)) { - return null; - } - $value = $dataFromExt[$key]; - if (!($value === null || is_string($value))) { - self::logUnexpectedType('string|null', $value); - return null; - } - return $value; - } - - /** - * @param array $dataFromExt - * @param string $key - * - * @return null|array[] - */ - private static function getStackTraceFromPhpErrorData(array $dataFromExt, string $key): ?array - { - if (!self::verifyKeyExists($key, $dataFromExt)) { - return null; - } - $stackTrace = $dataFromExt[$key]; - if (!is_array($stackTrace)) { - self::logUnexpectedType('array', $stackTrace); - return null; - } - if (!ArrayUtil::isList($stackTrace)) { - BootstrapStageLogger::logCritical( - 'Stack trace array should be a list but it is not' - . '; ' . 'stackTrace keys: ' . json_encode(array_keys($stackTrace)) - . ', ' . 'stackTrace: ' . LoggableToString::convert($stackTrace), - __LINE__, - __FUNCTION__ - ); - return null; - } - - /** @var array[] $stackTrace */ - return $stackTrace; - } - - /** - * @param array $dataFromExt - * - * @return PhpErrorData - */ - private static function buildPhpErrorData(array $dataFromExt): PhpErrorData - { - $result = new PhpErrorData(); - $result->type = self::getIntFromPhpErrorData($dataFromExt, 'type'); - $result->fileName = self::getNullableStringFromPhpErrorData($dataFromExt, 'fileName'); - $result->lineNumber = self::getIntFromPhpErrorData($dataFromExt, 'lineNumber'); - $result->message = self::getNullableStringFromPhpErrorData($dataFromExt, 'message'); - $result->stackTrace = self::getStackTraceFromPhpErrorData($dataFromExt, 'stackTrace'); - return $result; - } - - private static function ensureHaveLastPhpError(TransactionForExtensionRequest $transactionForExtensionRequest): void - { - /** - * elastic_apm_* functions are provided by the elastic_apm extension - * - * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection - * @phpstan-ignore-next-line - */ - $lastPhpErrorData = \elastic_apm_get_last_php_error(); - if ($lastPhpErrorData === null) { - return; - } - - if (is_array($lastPhpErrorData)) { - BootstrapStageLogger::logDebug( - 'Type of value returned by elastic_apm_get_last_php_error(): ' . DbgUtil::getType($lastPhpErrorData), - __LINE__, - __FUNCTION__ - ); - } else { - BootstrapStageLogger::logCritical( - 'Value returned by elastic_apm_get_last_php_error() is not an array' - . ', ' . 'returned value type: ' . DbgUtil::getType($lastPhpErrorData) - . ', ' . 'returned value: ' . $lastPhpErrorData, - __LINE__, - __FUNCTION__ - ); - return; - } - /** @var array $lastPhpErrorData */ - - $transactionForExtensionRequest->onPhpError(self::buildPhpErrorData($lastPhpErrorData)); - } - /** * Called by elastic_apm extension * diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php index c3ab63af7..de7c0d142 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php @@ -31,7 +31,6 @@ use Elastic\Apm\Impl\InferredSpansManager; use Elastic\Apm\Impl\Log\LogCategory; use Elastic\Apm\Impl\Log\Logger; -use Elastic\Apm\Impl\Span; use Elastic\Apm\Impl\Tracer; use Elastic\Apm\Impl\Transaction; use Elastic\Apm\Impl\Util\ArrayUtil; @@ -44,6 +43,8 @@ use Elastic\Apm\TransactionInterface; use Throwable; +use function set_exception_handler; + /** * Code in this file is part of implementation internals and thus it is not covered by the backward compatibility. * @@ -73,8 +74,11 @@ final class TransactionForExtensionRequest /** @var ?TransactionInterface */ private $transactionForRequest; - /** @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; @@ -82,37 +86,43 @@ final class TransactionForExtensionRequest 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__)->addContext('this', $this); $this->transactionForRequest = $this->beginTransaction($requestInitStartTime); if ($this->transactionForRequest instanceof Transaction && $this->transactionForRequest->isSampled()) { $this->inferredSpansManager = new InferredSpansManager($tracer); } - $this->tracer->onNewCurrentTransactionHasBegun->add( - function (Transaction $transaction): void { - PhpPartFacade::ensureHaveLatestDataDeferredByExtension(); - $transaction->onAboutToEnd->add( - function (/** @noinspection PhpUnusedParameterInspection */ Transaction $ignored): void { - PhpPartFacade::ensureHaveLatestDataDeferredByExtension(); - } - ); - $transaction->onCurrentSpanChanged->add( - function (?Span $span): void { - PhpPartFacade::ensureHaveLatestDataDeferredByExtension(); - if ($span !== null) { - $span->onAboutToEnd->add( - function (/** @noinspection PhpUnusedParameterInspection */ Span $ignored): void { - PhpPartFacade::ensureHaveLatestDataDeferredByExtension(); - } - ); - } - } - ); - } - ); + $logDebug = $this->logger->ifDebugLevelEnabledNoLine(__FUNCTION__); + $logDebug && $logDebug->log(__LINE__, '$this->logger->maxEnabledLevel(): ' . $this->logger->maxEnabledLevel()); + 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->onPhpError(/* 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'); + } + + // captureExceptions can be null, true or false and only false disables exception capturing even if captureErrors is true + if ($this->tracer->getConfig()->captureErrors() && ($this->tracer->getConfig()->captureExceptions() !== false)) { + $this->prevExceptionHandler = set_exception_handler( + function (Throwable $thrown): void { + $this->onNotCaughtThrowable($thrown); + } + ); + $logDebug && $logDebug->log(__LINE__, 'Registered exception handler'); + } else { + $optName = ($this->tracer->getConfig()->captureErrors() ? 'capture_exceptions' : 'capture_errors'); + $logDebug && $logDebug->log(__LINE__, $optName . ' configuration option is set to false - not registering exception handler'); + } + + $this->logger->addContext('this', $this); } public function getConfig(): ConfigSnapshot @@ -313,10 +323,6 @@ private function beforeHttpEnd(TransactionInterface $tx): void if ($tx->getOutcome() === null) { $this->discoverHttpOutcome($tx); } - - if ($tx->getOutcome() === Constants::OUTCOME_FAILURE && $this->lastThrown !== null) { - $this->tracer->createErrorFromThrowable($this->lastThrown); - } } private function logGcStatus(): void @@ -332,40 +338,37 @@ private function logGcStatus(): void && $loggerProxy->log('Called gc_status()', ['gc_status() return value' => $gcStatusRetVal]); } - public function onPhpError(PhpErrorData $phpErrorData): void - { - $relatedThrowable = null; - if ( - $this->lastThrown !== null - && $phpErrorData->message !== null - && TextUtil::isPrefixOf('Uncaught Exception: ', $phpErrorData->message, /* isCaseSensitive: */ false) - ) { - $relatedThrowable = $this->lastThrown; - $this->lastThrown = null; - } - $this->tracer->onPhpError($phpErrorData, $relatedThrowable, /* numberOfStackFramesToSkip */ 1); - } - /** - * @param mixed $lastThrown + * @param mixed ...$otherArgs + * + * @phpstan-param 0|positive-int $numberOfStackFramesToSkip * - * @return void + * @noinspection PhpSameParameterValueInspection */ - public function setLastThrown($lastThrown): void + private function onPhpError(int $numberOfStackFramesToSkip, int $errno, string $errstr, ?string $errfile, ?int $errline, ...$otherArgs): bool { - ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log('Entered', ['lastThrown' => $lastThrown]); + ($logDebug = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $logDebug->log('Entered', compact('errno', 'errstr', 'errfile', 'errline')); - if (!($lastThrown instanceof Throwable)) { - ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log( - 'lastThrown is not an instance of Throwable - ignoring it...', - ['lastThrown' => $lastThrown] - ); - return; - } + $phpErrorData = new PhpErrorData($errno, $errfile, $errline, $errstr, array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), $numberOfStackFramesToSkip + 1)); + $this->tracer->onPhpError($phpErrorData, /* relatedThrowable */ null, $numberOfStackFramesToSkip + 1); - $this->lastThrown = $lastThrown; + /** + * 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); + } + + public function onNotCaughtThrowable(Throwable $thrown): void + { + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Entered', compact('thrown')); + + $this->tracer->createErrorFromThrowable($thrown); + + if ($this->prevExceptionHandler !== null) { + ($this->prevExceptionHandler)($thrown); + } } public function onShutdown(): void @@ -373,8 +376,6 @@ public function onShutdown(): void ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Entered'); - PhpPartFacade::ensureHaveLatestDataDeferredByExtension(); - if ($this->inferredSpansManager !== null) { $this->inferredSpansManager->shutdown(); } diff --git a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php index cf9dbbe01..3e1220af5 100644 --- a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php +++ b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php @@ -90,6 +90,7 @@ 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_EXCEPTIONS => new NullableBoolOptionMetadata(), OptionNames::DEV_INTERNAL => new NullableWildcardListOptionMetadata(), OptionNames::DISABLE_INSTRUMENTATIONS => new NullableWildcardListOptionMetadata(), OptionNames::DISABLE_SEND => new BoolOptionMetadata(/* default */ false), diff --git a/agent/php/ElasticApm/Impl/Config/OptionNames.php b/agent/php/ElasticApm/Impl/Config/OptionNames.php index 7c9ca7be6..b6e31c952 100644 --- a/agent/php/ElasticApm/Impl/Config/OptionNames.php +++ b/agent/php/ElasticApm/Impl/Config/OptionNames.php @@ -42,6 +42,7 @@ 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_EXCEPTIONS = 'capture_exceptions'; public const DEV_INTERNAL = 'dev_internal'; public const DISABLE_INSTRUMENTATIONS = 'disable_instrumentations'; public const DISABLE_SEND = 'disable_send'; @@ -49,6 +50,7 @@ final class OptionNames 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..648e3f6f3 100644 --- a/agent/php/ElasticApm/Impl/Config/Snapshot.php +++ b/agent/php/ElasticApm/Impl/Config/Snapshot.php @@ -119,6 +119,9 @@ final class Snapshot implements LoggableInterface /** @var bool */ private $captureErrors; + /** @var ?bool */ + private $captureExceptions; + /** @var ?WildcardListMatcher */ private $devInternal; @@ -282,6 +285,11 @@ public function captureErrors(): bool return $this->captureErrors; } + public function captureExceptions(): ?bool + { + return $this->captureExceptions; + } + public function devInternal(): SnapshotDevInternal { return $this->devInternalParsed; 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/agent/php/ElasticApm/Impl/Tracer.php b/agent/php/ElasticApm/Impl/Tracer.php index edd9513b0..db7dbd517 100644 --- a/agent/php/ElasticApm/Impl/Tracer.php +++ b/agent/php/ElasticApm/Impl/Tracer.php @@ -328,9 +328,7 @@ public function onPhpError(PhpErrorData $phpErrorData, ?Throwable $relatedThrowa $customErrorData->message = $phpErrorData->message . $messageSuffix; } - if ($phpErrorData->type !== null) { - $customErrorData->type = PhpErrorUtil::getTypeName($phpErrorData->type); - } + $customErrorData->type = PhpErrorUtil::getTypeName($phpErrorData->type); $this->createError($customErrorData, $phpErrorData, $relatedThrowable, $numberOfStackFramesToSkip + 1); } diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index a7ff0240e..cac514cf9 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -138,6 +138,7 @@ private static function buildOptionNameToRawToValue(): array OptionNames::ASYNC_BACKEND_COMM => $asyncBackendCommValues, OptionNames::BREAKDOWN_METRICS => $boolRawToParsedValues(), OptionNames::CAPTURE_ERRORS => $boolRawToParsedValues(), + OptionNames::CAPTURE_EXCEPTIONS => $boolRawToParsedValues(), OptionNames::ENABLED => $boolRawToParsedValues(/* valueToExclude: */ false), OptionNames::DEV_INTERNAL => $wildcardListRawToParsedValues, OptionNames::DISABLE_INSTRUMENTATIONS => $wildcardListRawToParsedValues, diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index 5db9fd9e0..3562f75c2 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -27,7 +27,6 @@ use Elastic\Apm\Impl\Config\OptionNames; use Elastic\Apm\Impl\StackTraceFrame; use Elastic\Apm\Impl\Util\ArrayUtil; -use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\PhpErrorUtil; use Elastic\Apm\Impl\Util\RangeUtil; use ElasticApmTests\ComponentTests\Util\AppCodeHostHandle; @@ -61,6 +60,7 @@ final class ErrorComponentTest extends ComponentTestCaseBase private const INCLUDE_IN_ERROR_REPORTING_KEY = 'include_in_error_reporting'; private const CAPTURE_ERRORS_KEY = 'capture_errors'; + private const CAPTURE_EXCEPTIONS_KEY = 'capture_exceptions'; private function verifyError(DataFromAgent $dataFromAgent): ErrorDto { @@ -207,7 +207,7 @@ private function implTestPhpErrorUndefinedVariable(MixedMap $testArgs): void $captureErrorsConfigOptVal = $testArgs->getBool(self::CAPTURE_ERRORS_KEY); $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 { @@ -299,17 +299,30 @@ public static function appCodeForTestPhpErrorUncaughtExceptionWrapper(bool $just return $justReturnLineNumber ? $callLineNumber : appCodeForTestPhpErrorUncaughtException(); } + /** - * @dataProvider boolDataProviderAdaptedToSmoke - * - * @param bool $captureErrorsConfigOptVal + * @return iterable */ - public function testPhpErrorUncaughtException(bool $captureErrorsConfigOptVal): void + public function dataProviderCaptureErrorsExceptions(): iterable + { + $result = (new DataProviderForTestBuilder()) + ->addBoolKeyedDimensionAllValuesCombinable(self::CAPTURE_ERRORS_KEY) + ->addKeyedDimensionAllValuesCombinable(self::CAPTURE_EXCEPTIONS_KEY, [null, false, true]) + ->build(); + + return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); + } + + private function implTestPhpErrorUncaughtException(MixedMap $testArgs): void { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + $captureErrorsOptVal = $testArgs->getBool(self::CAPTURE_ERRORS_KEY); + $captureExceptionsOptVal = $testArgs->getNullableBool(self::CAPTURE_EXCEPTIONS_KEY); + $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 +337,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } ); - $isErrorExpected = $captureErrorsConfigOptVal; + $isErrorExpected = $captureErrorsOptVal && ($captureExceptionsOptVal !== false); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); $dbgCtx->add(['dataFromAgent' => $dataFromAgent]); @@ -369,32 +382,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 ([OptionNames::CAPTURE_ERRORS, OptionNames::CAPTURE_EXCEPTIONS] as $optName) { + if ($testArgs->hasKey($optName)) { + $appCodeParams->setAgentOptionIfNotDefaultValue($optName, $testArgs->get($optName)); + } } } ); } /** - * @dataProvider boolDataProviderAdaptedToSmoke - * - * @param bool $captureErrorsConfigOptVal + * @dataProvider dataProviderCaptureErrorsExceptions */ - public function testCaughtExceptionResponded500(bool $captureErrorsConfigOptVal): void + public function testCaughtExceptionResponded500(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,50 +429,8 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } } ); - $isErrorExpected = self::isMainAppCodeHostHttp() && $captureErrorsConfigOptVal; - $expectedErrorCount = $isErrorExpected ? 1 : 0; - $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); - self::assertCount($expectedErrorCount, $dataFromAgent->idToError); - if (!$isErrorExpected) { - return; - } - $err = $this->verifyError($dataFromAgent); - - $appCodeFile = FileUtilForTests::listToPath([dirname(__FILE__), 'appCodeForTestCaughtExceptionResponded500.php']); - self::assertNotNull($err->exception); - self::assertSame(APP_CODE_FOR_TEST_CAUGHT_EXCEPTION_RESPONDED_500_CODE, $err->exception->code); - - $exceptionNamespace = ''; - $exceptionClassName = ''; - ClassNameUtil::splitFqClassName(DummyExceptionForTests::class, /* out */ $exceptionNamespace, /* out */ $exceptionClassName); - self::assertSame('ElasticApmTests\\Util', $exceptionNamespace); - self::assertSame('DummyExceptionForTests', $exceptionClassName); - - self::assertSame($exceptionNamespace, $err->exception->module); - self::assertSame($exceptionClassName, $err->exception->type); - self::assertSame(APP_CODE_FOR_TEST_CAUGHT_EXCEPTION_RESPONDED_500_MESSAGE, $err->exception->message); - - $expectedStackTraceTop = [ - [ - self::STACK_TRACE_FILE_NAME => $appCodeFile, - self::STACK_TRACE_FUNCTION => null, - self::STACK_TRACE_LINE_NUMBER => APP_CODE_FOR_TEST_CAUGHT_EXCEPTION_RESPONDED_500_THROW_LINE_NUMBER, - ], - [ - self::STACK_TRACE_FILE_NAME => $appCodeFile, - self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestCaughtExceptionResponded500Impl', - self::STACK_TRACE_LINE_NUMBER => APP_CODE_FOR_TEST_CAUGHT_EXCEPTION_RESPONDED_500_CALL_TO_IMPL_LINE_NUMBER, - ], - [ - self::STACK_TRACE_FILE_NAME => __FILE__, - self::STACK_TRACE_FUNCTION => __NAMESPACE__ . '\\appCodeForTestCaughtExceptionResponded500', - self::STACK_TRACE_LINE_NUMBER => self::appCodeForTestCaughtExceptionResponded500Wrapper(/* justReturnLineNumber */ true), - ], - [ - self::STACK_TRACE_FUNCTION => __CLASS__ . '::appCodeForTestCaughtExceptionResponded500Wrapper', - ], - ]; - self::verifyAppCodeStackTraceTop($expectedStackTraceTop, $err); + $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)); + self::assertEmpty($dataFromAgent->idToError); } } 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 3af821cee71e7899e480211e4ad87d91d0faf6f3 Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Mon, 29 Dec 2025 14:30:44 +0200 Subject: [PATCH 2/3] Fixed issue found static analysis --- tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index 3562f75c2..ebbaea7aa 100644 --- a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php +++ b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php @@ -407,7 +407,7 @@ private static function ensureMainAppCodeHost(TestCaseHandle $testCaseHandle, Mi function (AppCodeHostParams $appCodeParams) use ($testArgs): void { foreach ([OptionNames::CAPTURE_ERRORS, OptionNames::CAPTURE_EXCEPTIONS] as $optName) { if ($testArgs->hasKey($optName)) { - $appCodeParams->setAgentOptionIfNotDefaultValue($optName, $testArgs->get($optName)); + $appCodeParams->setAgentOptionIfNotDefaultValue($optName, $testArgs->get($optName)); // @phpstan-ignore argument.type } } } From 660e0b0358e75fdde7da0554fc0a6a6adb0be2cc Mon Sep 17 00:00:00 2001 From: Sergey Kleyman Date: Fri, 2 Jan 2026 14:15:08 +0200 Subject: [PATCH 3/3] Added dev_internal_capture_errors_only_to_log --- agent/native/ext/ConfigManager.cpp | 7 +++++ agent/native/ext/ConfigManager.h | 2 ++ agent/native/ext/ConfigSnapshot.h | 1 + agent/native/ext/elastic_apm.cpp | 1 + .../TransactionForExtensionRequest.php | 19 +++++++++---- .../Impl/Config/AllOptionsMetadata.php | 1 + .../ElasticApm/Impl/Config/OptionNames.php | 1 + agent/php/ElasticApm/Impl/Config/Snapshot.php | 12 ++++++++ .../ComponentTests/ConfigSettingTest.php | 2 ++ .../ComponentTests/ErrorComponentTest.php | 28 +++++++++++-------- 10 files changed, 57 insertions(+), 17 deletions(-) diff --git a/agent/native/ext/ConfigManager.cpp b/agent/native/ext/ConfigManager.cpp index 84f82c3e8..61eddf2eb 100644 --- a/agent/native/ext/ConfigManager.cpp +++ b/agent/native/ext/ConfigManager.cpp @@ -804,6 +804,7 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, captureErrors ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, 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 ) @@ -1027,6 +1028,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 fadc4a602..add8f9650 100644 --- a/agent/native/ext/ConfigManager.h +++ b/agent/native/ext/ConfigManager.h @@ -77,6 +77,7 @@ enum OptionId optionId_captureExceptions, optionId_devInternal, optionId_devInternalBackendCommLogVerbose, + optionId_devInternalCaptureErrorsOnlyToLog, optionId_disableInstrumentations, optionId_disableSend, optionId_enabled, @@ -266,6 +267,7 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot(); */ #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 8832c752f..d31b202d8 100644 --- a/agent/native/ext/ConfigSnapshot.h +++ b/agent/native/ext/ConfigSnapshot.h @@ -48,6 +48,7 @@ struct ConfigSnapshot bool captureExceptions = 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/elastic_apm.cpp b/agent/native/ext/elastic_apm.cpp index 8cbbf670f..a8b1fd228 100644 --- a/agent/native/ext/elastic_apm.cpp +++ b/agent/native/ext/elastic_apm.cpp @@ -154,6 +154,7 @@ PHP_INI_BEGIN() 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 ) diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php index de7c0d142..352c043c7 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php @@ -109,8 +109,9 @@ function (int $errno, string $errstr, ?string $errfile, ?int $errline, ...$other $logDebug && $logDebug->log(__LINE__, 'capture_errors configuration option is set to false - not registering PHP error handler'); } - // captureExceptions can be null, true or false and only false disables exception capturing even if captureErrors is true - if ($this->tracer->getConfig()->captureErrors() && ($this->tracer->getConfig()->captureExceptions() !== false)) { + // 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 + if ($this->tracer->getConfig()->captureExceptions() === null ? $this->tracer->getConfig()->captureErrors() : $this->tracer->getConfig()->captureExceptions()) { $this->prevExceptionHandler = set_exception_handler( function (Throwable $thrown): void { $this->onNotCaughtThrowable($thrown); @@ -118,7 +119,7 @@ function (Throwable $thrown): void { ); $logDebug && $logDebug->log(__LINE__, 'Registered exception handler'); } else { - $optName = ($this->tracer->getConfig()->captureErrors() ? 'capture_exceptions' : 'capture_errors'); + $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'); } @@ -339,6 +340,8 @@ private function logGcStatus(): void } /** + * 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 @@ -349,8 +352,10 @@ private function onPhpError(int $numberOfStackFramesToSkip, int $errno, string $ { ($logDebug = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $logDebug->log('Entered', compact('errno', 'errstr', 'errfile', 'errline')); - $phpErrorData = new PhpErrorData($errno, $errfile, $errline, $errstr, array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), $numberOfStackFramesToSkip + 1)); - $this->tracer->onPhpError($phpErrorData, /* relatedThrowable */ null, $numberOfStackFramesToSkip + 1); + if (!$this->tracer->getConfig()->devInternalCaptureErrorsOnlyToLog()) { + $phpErrorData = new PhpErrorData($errno, $errfile, $errline, $errstr, 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. @@ -364,7 +369,9 @@ public function onNotCaughtThrowable(Throwable $thrown): void { ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Entered', compact('thrown')); - $this->tracer->createErrorFromThrowable($thrown); + if (!$this->tracer->getConfig()->devInternalCaptureErrorsOnlyToLog()) { + $this->tracer->createErrorFromThrowable($thrown); + } if ($this->prevExceptionHandler !== null) { ($this->prevExceptionHandler)($thrown); diff --git a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php index 3e1220af5..b10b4e664 100644 --- a/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php +++ b/agent/php/ElasticApm/Impl/Config/AllOptionsMetadata.php @@ -92,6 +92,7 @@ public static function get(): array OptionNames::CAPTURE_ERRORS => new BoolOptionMetadata(/* default */ true), 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 b6e31c952..f1a65b902 100644 --- a/agent/php/ElasticApm/Impl/Config/OptionNames.php +++ b/agent/php/ElasticApm/Impl/Config/OptionNames.php @@ -44,6 +44,7 @@ final class OptionNames public const CAPTURE_ERRORS = 'capture_errors'; 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'; diff --git a/agent/php/ElasticApm/Impl/Config/Snapshot.php b/agent/php/ElasticApm/Impl/Config/Snapshot.php index 648e3f6f3..3f2a9e936 100644 --- a/agent/php/ElasticApm/Impl/Config/Snapshot.php +++ b/agent/php/ElasticApm/Impl/Config/Snapshot.php @@ -125,6 +125,9 @@ final class Snapshot implements LoggableInterface /** @var ?WildcardListMatcher */ private $devInternal; + /** @var bool */ + private $devInternalCaptureErrorsOnlyToLog; + /** @var SnapshotDevInternal */ private $devInternalParsed; @@ -295,11 +298,17 @@ 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; @@ -348,16 +357,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/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index cac514cf9..f16dc10e7 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -141,6 +141,8 @@ private static function buildOptionNameToRawToValue(): array 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 "]), diff --git a/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php b/tests/ElasticApmTests/ComponentTests/ErrorComponentTest.php index ebbaea7aa..fe86053fa 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; @@ -59,8 +60,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 const CAPTURE_EXCEPTIONS_KEY = 'capture_exceptions'; private function verifyError(DataFromAgent $dataFromAgent): ErrorDto { @@ -193,7 +192,8 @@ public function dataProviderForTestPhpErrorUndefinedVariable(): iterable { $result = (new DataProviderForTestBuilder()) ->addBoolKeyedDimensionAllValuesCombinable(self::INCLUDE_IN_ERROR_REPORTING_KEY) - ->addBoolKeyedDimensionAllValuesCombinable(self::CAPTURE_ERRORS_KEY) + ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS) + ->addKeyedDimensionAllValuesCombinable(OptionNames::DEV_INTERNAL_CAPTURE_ERRORS_ONLY_TO_LOG, [false, true]) ->build(); return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); @@ -204,7 +204,8 @@ 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, $testArgs); @@ -215,7 +216,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]); @@ -306,8 +310,9 @@ public static function appCodeForTestPhpErrorUncaughtExceptionWrapper(bool $just public function dataProviderCaptureErrorsExceptions(): iterable { $result = (new DataProviderForTestBuilder()) - ->addBoolKeyedDimensionAllValuesCombinable(self::CAPTURE_ERRORS_KEY) - ->addKeyedDimensionAllValuesCombinable(self::CAPTURE_EXCEPTIONS_KEY, [null, false, true]) + ->addBoolKeyedDimensionAllValuesCombinable(OptionNames::CAPTURE_ERRORS) + ->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)); @@ -317,8 +322,9 @@ private function implTestPhpErrorUncaughtException(MixedMap $testArgs): void { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); - $captureErrorsOptVal = $testArgs->getBool(self::CAPTURE_ERRORS_KEY); - $captureExceptionsOptVal = $testArgs->getNullableBool(self::CAPTURE_EXCEPTIONS_KEY); + $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, $testArgs); @@ -337,7 +343,7 @@ function (AppCodeRequestParams $appCodeRequestParams): void { } ); - $isErrorExpected = $captureErrorsOptVal && ($captureExceptionsOptVal !== false); + $isErrorExpected = ($captureExceptionsOptVal ?? $captureErrorsOptVal) && (!$devInternalCaptureErrorsOnlyToLogOptVal); $expectedErrorCount = $isErrorExpected ? 1 : 0; $dataFromAgent = $testCaseHandle->waitForDataFromAgent((new ExpectedEventCounts())->transactions(1)->errors($expectedErrorCount)); $dbgCtx->add(['dataFromAgent' => $dataFromAgent]); @@ -405,7 +411,7 @@ private static function ensureMainAppCodeHost(TestCaseHandle $testCaseHandle, Mi { return $testCaseHandle->ensureMainAppCodeHost( function (AppCodeHostParams $appCodeParams) use ($testArgs): void { - foreach ([OptionNames::CAPTURE_ERRORS, OptionNames::CAPTURE_EXCEPTIONS] as $optName) { + foreach (AllOptionsMetadata::get() as $optName => $_) { if ($testArgs->hasKey($optName)) { $appCodeParams->setAgentOptionIfNotDefaultValue($optName, $testArgs->get($optName)); // @phpstan-ignore argument.type }