agent/native/ext/tracer_PHP_part.cpp (313 lines of code) (raw):

/* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #include "ConfigSnapshot.h" #include "Exceptions.h" #include "Tracer.h" #include "basic_macros.h" #include "elastic_apm_API.h" #include "log.h" #include "util_for_PHP.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_C_TO_PHP #define ELASTIC_APM_PHP_PART_FUNC_PREFIX "\\Elastic\\Apm\\Impl\\AutoInstrument\\PhpPartFacade::" #define ELASTIC_APM_PHP_PART_BOOTSTRAP_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "bootstrap" #define ELASTIC_APM_PHP_PART_SHUTDOWN_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "shutdown" #define ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_PRE_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "internalFuncCallPreHook" #define ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_POST_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "internalFuncCallPostHook" #define ELASTIC_APM_PHP_PART_EMPTY_METHOD_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "emptyMethod" #define ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_PRE_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "astInstrumentationPreHook" #define ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_DIRECT_CALL_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "astInstrumentationDirectCall" enum TracerPhpPartState { tracerPhpPartState_before_bootstrap, tracerPhpPartState_after_bootstrap, tracerPhpPartState_after_shutdown, tracerPhpPartState_failed, numberOfTracerPhpPartState }; typedef enum TracerPhpPartState TracerPhpPartState; StringView tracerPhpPartStateNames[ numberOfTracerPhpPartState ] = { ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_before_bootstrap ), ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_after_bootstrap ), ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_after_shutdown ), ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_failed ), }; #define ELASTIC_APM_UNKNOWN_TRACER_PHP_PART_STATE_AS_STRING "<UNKNOWN TracerPhpPartState>" static inline bool isValidTracerPhpPartState( TracerPhpPartState value ) { return ( tracerPhpPartState_before_bootstrap <= value ) && ( value < numberOfTracerPhpPartState ); } static inline String tracerPhpPartStateToString( TracerPhpPartState value ) { if ( isValidTracerPhpPartState( value ) ) { return tracerPhpPartStateNames[ value ].begin; } return ELASTIC_APM_UNKNOWN_TRACER_PHP_PART_STATE_AS_STRING; } static TracerPhpPartState g_tracerPhpPartState = numberOfTracerPhpPartState; bool canInvokeTracerPhpPart() { return g_tracerPhpPartState == tracerPhpPartState_after_bootstrap; } // returns true if state was changed or false it was same before bool switchTracerPhpPartStateToFailed( String reason, String dbgCalledFromFunc ) { if ( g_tracerPhpPartState == tracerPhpPartState_failed ) { return false; } ELASTIC_APM_LOG_ERROR( "Switching tracer PHP part state to failed; reason: %s, current state: %s, called from %s" , reason, tracerPhpPartStateToString( g_tracerPhpPartState ), dbgCalledFromFunc ); g_tracerPhpPartState = tracerPhpPartState_failed; return true; } ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ) { ResultCode resultCode; bool shouldRevertLoadingAgentPhpCode = false; bool bootstrapTracerPhpPartRetVal; zval bootstrapTracerPhpPartArgs[2]; ZVAL_UNDEF( &bootstrapTracerPhpPartArgs[0] ); ZVAL_UNDEF( &bootstrapTracerPhpPartArgs[1] ); char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "config->bootstrapPhpPartFile: %s, g_tracerPhpPartState: %s" , streamUserString( config->bootstrapPhpPartFile, &txtOutStream ), tracerPhpPartStateToString( g_tracerPhpPartState ) ); textOutputStreamRewind( &txtOutStream ); if ( g_tracerPhpPartState != tracerPhpPartState_before_bootstrap ) { switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } if ( config->bootstrapPhpPartFile == NULL ) { GetConfigManagerOptionMetadataResult getMetaRes; getConfigManagerOptionMetadata( getGlobalTracer()->configManager, optionId_bootstrapPhpPartFile, &getMetaRes ); switchTracerPhpPartStateToFailed( /* reason */ streamPrintf( &txtOutStream, "Configuration option `%s' is not set", getMetaRes.optName ), __FUNCTION__ ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } elasticApmBeforeLoadingAgentPhpCode(); shouldRevertLoadingAgentPhpCode = true; ELASTIC_APM_CALL_IF_FAILED_GOTO( loadPhpFile( config->bootstrapPhpPartFile ) ); ZVAL_LONG(&bootstrapTracerPhpPartArgs[0], getGlobalTracer()->logger.maxEnabledLevel); ZVAL_DOUBLE(&bootstrapTracerPhpPartArgs[1], (double)timePointToEpochMicroseconds(requestInitStartTime)); ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetBool( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_BOOTSTRAP_FUNC ) , /* argsCount */ ELASTIC_APM_STATIC_ARRAY_SIZE( bootstrapTracerPhpPartArgs ) , /* args */ bootstrapTracerPhpPartArgs , &bootstrapTracerPhpPartRetVal ) ); if ( ! bootstrapTracerPhpPartRetVal ) { ELASTIC_APM_LOG_CRITICAL( "%s failed (returned false). See log for more details.", ELASTIC_APM_PHP_PART_BOOTSTRAP_FUNC ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } g_tracerPhpPartState = tracerPhpPartState_after_bootstrap; resultCode = resultSuccess; finally: zval_dtor( &bootstrapTracerPhpPartArgs[0] ); // long is not refcounted - would not do anything zval_dtor( &bootstrapTracerPhpPartArgs[0] ); // double is not refcounted - would not do anything if ( shouldRevertLoadingAgentPhpCode ) { elasticApmAfterLoadingAgentPhpCode(); } ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); return resultCode; failure: switchTracerPhpPartStateToFailed( /* reason */ "Failed to bootstrap tracer PHP part", __FUNCTION__ ); goto finally; } void shutdownTracerPhpPart() { ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); ResultCode resultCode; if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap ) { switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetVoid( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_SHUTDOWN_FUNC ) , /* argsCount */ 0 , /* args */ NULL ) ); g_tracerPhpPartState = tracerPhpPartState_after_shutdown; resultCode = resultSuccess; finally: ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); // We ignore errors because we want the monitored application to continue working // even if APM encountered an issue that prevent it from working ELASTIC_APM_UNUSED( resultCode ); return; failure: switchTracerPhpPartStateToFailed( /* reason */ "Failed to shut down tracer PHP part", __FUNCTION__ ); goto finally; } static uint32_t g_maxInterceptedCallArgsCount = 100; bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zend_execute_data* execute_data ) { ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "interceptRegistrationId: %u", interceptRegistrationId ); ResultCode resultCode; zval preHookRetVal; ZVAL_UNDEF( &preHookRetVal ); bool shouldCallPostHook = false; zval interceptRegistrationIdAsZval; ZVAL_UNDEF( &interceptRegistrationIdAsZval ); zval phpPartArgs[ g_maxInterceptedCallArgsCount + 2 ]; if (!canInvokeTracerPhpPart()) { if (switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ )) { ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } else { ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); } } // The first argument to PHP part's interceptedCallPreHook() is $interceptRegistrationId ZVAL_LONG( &interceptRegistrationIdAsZval, interceptRegistrationId ); phpPartArgs[ 0 ] = interceptRegistrationIdAsZval; // The second argument to PHP part's interceptedCallPreHook() is $thisObj if ( Z_TYPE( execute_data->This ) == IS_UNDEF ) { ZVAL_NULL( &phpPartArgs[ 1 ] ); } else { phpPartArgs[ 1 ] = execute_data->This; } uint32_t interceptedCallArgsCount; getArgsFromZendExecuteData( execute_data, g_maxInterceptedCallArgsCount, &( phpPartArgs[ 2 ] ), &interceptedCallArgsCount ); ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetZval( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_PRE_HOOK_FUNC ) , interceptedCallArgsCount + 2 , phpPartArgs , /* out */ &preHookRetVal ) ); ELASTIC_APM_LOG_TRACE( "Successfully finished call to PHP part. Return value type: %u", Z_TYPE_P( &preHookRetVal ) ); if ( Z_TYPE( preHookRetVal ) != IS_FALSE && Z_TYPE( preHookRetVal ) != IS_TRUE ) { ELASTIC_APM_LOG_ERROR( "Call to PHP part returned value that is not bool. Return value type: %u", Z_TYPE_P( &preHookRetVal ) ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } shouldCallPostHook = ( Z_TYPE( preHookRetVal ) == IS_TRUE ); resultCode = resultSuccess; finally: zval_dtor( &interceptRegistrationIdAsZval ); ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT(); ELASTIC_APM_UNUSED( resultCode ); return shouldCallPostHook; failure: switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); goto finally; } void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId, zval* interceptedCallRetValOrThrown ) { ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "dbgInterceptRegistrationId: %u; interceptedCallRetValOrThrown type: %u" , dbgInterceptRegistrationId, Z_TYPE_P( interceptedCallRetValOrThrown ) ); ResultCode resultCode; zval phpPartArgs[ 2 ]; elasticapm::php::AutomaticExceptionStateRestorer restorer; if (!canInvokeTracerPhpPart()) { if (switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ )) { ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } else { ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); } } // The first argument to PHP part's interceptedCallPostHook() is $hasExitedByException (bool) ZVAL_FALSE( &( phpPartArgs[ 0 ] ) ); // The second argument to PHP part's interceptedCallPreHook() is $returnValueOrThrown (mixed|Throwable) phpPartArgs[ 1 ] = *interceptedCallRetValOrThrown; ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetVoid( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_POST_HOOK_FUNC ) , ELASTIC_APM_STATIC_ARRAY_SIZE( phpPartArgs ) , phpPartArgs ) ); ELASTIC_APM_LOG_TRACE( "Successfully finished call to PHP part" ); resultCode = resultSuccess; finally: ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG( "dbgInterceptRegistrationId: %u; interceptedCallRetValOrThrown type: %u." , dbgInterceptRegistrationId, Z_TYPE_P( interceptedCallRetValOrThrown ) ); ELASTIC_APM_UNUSED( resultCode ); return; failure: switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); goto finally; } void tracerPhpPartInterceptedCallEmptyMethod() { ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY(); ResultCode resultCode; zval phpPartDummyArgs[ 1 ]; ZVAL_UNDEF( &( phpPartDummyArgs[ 0 ] ) ); if (!canInvokeTracerPhpPart()) { if (switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ )) { ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } else { ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); } } ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetVoid( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_EMPTY_METHOD_FUNC ) , 0 /* <- argsCount */ , phpPartDummyArgs ) ); ELASTIC_APM_LOG_TRACE( "Successfully finished call to PHP part" ); resultCode = resultSuccess; finally: ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT(); ELASTIC_APM_UNUSED( resultCode ); return; failure: switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); goto finally; } void tracerPhpPartLogArguments( LogLevel logLevel, uint32_t argsCount, zval args[] ) { if ( maxEnabledLogLevel() < logLevel ) { return; } char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); ELASTIC_APM_FOR_EACH_INDEX( i, argsCount ) { ELASTIC_APM_LOG_WITH_LEVEL( logLevel, "Argument #%u: %s", (unsigned)i, streamZVal( &( args[ i ] ), &txtOutStream ) ); } } void tracerPhpPartForwardCall( StringView phpFuncName, zend_execute_data* execute_data, /* out */ zval* retVal, String dbgCalledFrom ) { ResultCode resultCode = resultFailure; ZVAL_NULL(retVal); uint32_t callArgsCount; zval callArgs[ g_maxInterceptedCallArgsCount ]; ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "phpFuncName: %s, dbgCalledFrom: %s", phpFuncName.begin, dbgCalledFrom ); if (!canInvokeTracerPhpPart()) { if (switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ )) { ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } else { ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); } } getArgsFromZendExecuteData( execute_data, g_maxInterceptedCallArgsCount, &( callArgs[ 0 ] ), &callArgsCount ); tracerPhpPartLogArguments( logLevel_trace, callArgsCount, callArgs ); ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetZval( phpFuncName, callArgsCount, callArgs, /* out */ retVal ) ); resultCode = resultSuccess; finally: ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG( "retVal type: %s (type ID as int: %d)", zend_get_type_by_const( (int)Z_TYPE_P( retVal ) ), (int)Z_TYPE_P( retVal ) ); ELASTIC_APM_UNUSED( resultCode ); return; failure: switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); ZVAL_NULL( retVal ); goto finally; } void tracerPhpPartAstInstrumentationCallPreHook( zend_execute_data* execute_data, zval* return_value ) { tracerPhpPartForwardCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_PRE_HOOK_FUNC ), execute_data, /* out */ return_value, __FUNCTION__ ); } void tracerPhpPartAstInstrumentationDirectCall( zend_execute_data* execute_data ) { zval unusedRetVal; tracerPhpPartForwardCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_DIRECT_CALL_FUNC ), execute_data, /* out */ &unusedRetVal, __FUNCTION__ ); } void tracerPhpPartOnRequestInitSetInitialTracerState() { g_tracerPhpPartState = tracerPhpPartState_before_bootstrap; } ResultCode tracerPhpPartOnRequestInit( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ) { return bootstrapTracerPhpPart( config, requestInitStartTime ); } void tracerPhpPartOnRequestShutdown() { shutdownTracerPhpPart(); }