diff --git a/Makefile b/Makefile index 8200e25d9..9ab2394a9 100644 --- a/Makefile +++ b/Makefile @@ -315,13 +315,74 @@ src/newrelic/infinite_tracing/com_newrelic_trace_v1/v1.pb.go: protocol/infinite_ # .PHONY: integration -integration: Makefile daemon lasp-test-all +integration: Makefile daemon lasp-test-all integration-events-limits for PHP in $${PHPS:-8.1 8.0 7.4 7.3 7.2 7.1 7.0 5.6 5.5}; do \ echo; echo "# PHP=$${PHP}"; \ env NRLAMP_PHP=$${PHP} bin/integration_runner $(INTEGRATION_ARGS) || exit 1; \ echo "# PHP=$${PHP}"; \ done +# +# Agent event limits integration testing +# +# Because of how the integration_runner connects to the collector (once before +# running any tests) we have to tell the runner the value the agent would +# have passed to it for each test. This means these tests are not testing +# the agent <-> daemon communcations of the agent's requested custom event limit +# via the daemon to the collector and daemon sending back the collectors harvest +# limit value. +# + +.PHONY: integration-events-limits +integration-events-limits: daemon + # create array with the collector response for each agent requested custom events max samples + # currently based on fast harvest cycle being 5 seconds so ratio is 12:1 + declare -A custom_limits_tests; \ + custom_limits_tests[240]=20; \ + custom_limits_tests[7000]=583; \ + custom_limits_tests[30000]=2500; \ + custom_limits_tests[100000]=8333; \ + for PHP in $${PHPS:-8.1 8.0 7.4 7.3 7.2 7.1 7.0 5.6 5.5}; do \ + echo; echo "# PHP=$${PHP}"; \ + for custom_max in "$${!custom_limits_tests[@]}"; do \ + collector_limit=$${custom_limits_tests[$$custom_max]}; \ + php_test_file="tests/event_limits/custom/test_custom_events_max_samples_stored_$${custom_max}_limit.php"; \ + env NRLAMP_PHP=$${PHP} bin/integration_runner $(INTEGRATION_ARGS) \ + -max_custom_events $${custom_max} $${php_test_file} || exit 1; \ + done; \ + echo "# PHP=$${PHP}"; \ + done; + + # test for invalid value (-1) and (1000000) + # Should use default (30000) for -1 and max (100000) for 1000000 + for PHP in $${PHPS:-8.1 8.0 7.4 7.3 7.2 7.1 7.0 5.6 5.5}; do \ + env NRLAMP_PHP=$${PHP} bin/integration_runner $(INTEGRATION_ARGS) \ + -max_custom_events 100000 \ + tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toolarge_limit.php || exit 1; \ + env NRLAMP_PHP=$${PHP} bin/integration_runner $(INTEGRATION_ARGS) \ + -max_custom_events 30000 \ + tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toosmall_limit.php || exit 1; \ + echo "# PHP=$${PHP}"; \ + done; + + # also run a test where limit is set to 0 + # default value is used + for PHP in $${PHPS:-8.1 8.0 7.4 7.3 7.2 7.1 7.0 5.6 5.5}; do \ + env NRLAMP_PHP=$${PHP} bin/integration_runner $(INTEGRATION_ARGS) \ + -max_custom_events 0 \ + tests/event_limits/custom/test_custom_events_max_samples_stored_0_limit.php || exit 1; \ + echo "# PHP=$${PHP}"; \ + done; + + # also run a test where no agent custom event limit is specified and verify + # default value is used + for PHP in $${PHPS:-8.1 8.0 7.4 7.3 7.2 7.1 7.0 5.6 5.5}; do \ + env NRLAMP_PHP=$${PHP} bin/integration_runner $(INTEGRATION_ARGS) \ + -max_custom_events 30000 \ + tests/event_limits/custom/test_custom_events_max_samples_stored_not_specified.php || exit 1; \ + echo "# PHP=$${PHP}"; \ + done; + # # Code profiling # diff --git a/VERSION b/VERSION index 0719d8102..816c0711a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.3.0 +10.4.0 diff --git a/agent/php_agent.h b/agent/php_agent.h index 0d5415a99..005daebbc 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -79,6 +79,7 @@ * Returns : A newly allocated JSON stack trace string or NULL on error. */ #define NR_PHP_STACKTRACE_LIMIT 300 + extern char* nr_php_backtrace_to_json(zval* itrace TSRMLS_DC); /* @@ -802,7 +803,7 @@ extern zend_execute_data* nr_get_zend_execute_data(NR_EXECUTE_PROTO TSRMLS_DC); * */ static inline uint32_t nr_php_zend_function_lineno(const zend_function* func) { - if (NULL != func) { + if (NULL != func && ZEND_USER_FUNCTION == func->op_array.type) { return func->op_array.line_start; } return 0; diff --git a/agent/php_execute.c b/agent/php_execute.c index 5703b2f8d..a238e2d1e 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -72,8 +72,8 @@ * This too is erroneous. The cost of calling a function is about 4 assembler * instructions. This is negligible. Therefore, as a means of reducing stack * usage, if you need stack space it is better to put that usage into a static - * function and call it from the main function, because then that stack space - * in genuinely only allocated when needed. + * function and call it from the main function, because then that stack space is + * genuinely only allocated when needed. * * A not-insignificant performance boost comes from accurate branch hinting * using the nrlikely() and nrunlikely() macros. This prevents pipeline stalls @@ -150,7 +150,7 @@ static int nr_format_zval_for_debug(zval* arg, break; } -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ if (NULL == Z_STR_P(arg)) { safe_append("invalid string", 14); break; @@ -194,7 +194,7 @@ static int nr_format_zval_for_debug(zval* arg, safe_append(tmp, len); break; -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ case IS_TRUE: safe_append("true", 4); break; @@ -222,7 +222,7 @@ static int nr_format_zval_for_debug(zval* arg, break; case IS_OBJECT: -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ if (NULL == Z_OBJ_P(arg)) { safe_append("invalid object", 14); break; @@ -993,9 +993,11 @@ static void nr_php_execute_file(const zend_op_array* op_array, * Therefore we have to be more selective in our approach. */ typedef struct { -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ zend_string* scope; zend_string* function; + zend_string* filepath; + uint32_t function_lineno; #else zend_op_array* op_array; #endif /* PHP7 */ @@ -1013,7 +1015,7 @@ typedef struct { */ static void nr_php_execute_metadata_init(nr_php_execute_metadata_t* metadata, zend_op_array* op_array) { -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ if (op_array->scope && op_array->scope->name && op_array->scope->name->len) { metadata->scope = op_array->scope->name; zend_string_addref(metadata->scope); @@ -1027,11 +1029,165 @@ static void nr_php_execute_metadata_init(nr_php_execute_metadata_t* metadata, } else { metadata->function = NULL; } + if (!NRINI(code_level_metrics_enabled) + || ZEND_USER_FUNCTION != op_array->type) { + metadata->filepath = NULL; + return; + } + if (op_array->filename && op_array->filename->len) { + metadata->filepath = op_array->filename; + zend_string_addref(metadata->filepath); + } else { + metadata->filepath = NULL; + } + + metadata->function_lineno = op_array->line_start; + #else metadata->op_array = op_array; #endif /* PHP7 */ } +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ +/* + * Purpose : If code level metrics are enabled, use the metadata to create agent + * attributes in the segment with code level metrics. + * + * Params : 1. segment to create and add agent attributes to + * 2. metadata that will populate the CLM attributes + * + * Returns : void + * + * Note: PHP has a concept of calling files with no function names. In the + * case of a file being called when there is no function name, the agent + * instruments the file. In this case, we provide the filename to CLM + * as the "function" name. + * Current CLM functionality only works with PHP 7+ + */ +static inline void nr_php_execute_segment_add_code_level_metrics( + nr_segment_t* segment, + const nr_php_execute_metadata_t* metadata) { + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, exit and don't add any attributes. + */ + if (!NRINI(code_level_metrics_enabled)) { + return; + } + + if (NULL == metadata) { + return; + } + + if (NULL == segment) { + return; + } + + /* + * At a minimum, at least one of the following attribute combinations MUST be + * implemented in order for customers to be able to accurately identify their + * instrumented functions: + * - code.filepath AND code.function + * - code.namespace AND code.function + * + * If we don't have the minimum requirements, exit and don't add any + * attributes. + * + * Additionally, none of the needed attributes can exceed 255 characters. + */ + +#define CLM_STRLEN_MAX (255) + +#define CHK_CLM_STRLEN(s, zstr_len) \ + if (CLM_STRLEN_MAX < zstr_len) { \ + s = NULL; \ + } + + const char* namespace = NULL; + const char* function = NULL; + const char* filepath = NULL; + + if (NULL != metadata->scope) { + namespace = ZSTR_VAL(metadata->scope); + CHK_CLM_STRLEN(namespace, ZSTR_LEN(metadata->scope)); + } + + if (NULL != metadata->function) { + function = ZSTR_VAL(metadata->function); + CHK_CLM_STRLEN(function, ZSTR_LEN(metadata->function)); + } + + if (NULL != metadata->filepath) { + filepath = ZSTR_VAL(metadata->filepath); + CHK_CLM_STRLEN(filepath, ZSTR_LEN(metadata->filepath)); + } + +#undef CHK_CLM_STRLEN + + if (1 == metadata->function_lineno) { + /* + * It's a file. For CLM purposes, the "function" name is the filepath. + */ + function = filepath; + } + + if (nr_strempty(function)) { + /* + * Name isn't set so don't do anything + */ + return; + } + if (nr_strempty(namespace) && nr_strempty(filepath)) { + /* + * CLM MUST have either function+namespace or function+filepath. + */ + return; + } + + /* + * Only go through the trouble of actually allocating agent attributes if we + * know we have valid values to turn into attributes. + */ + + if (NULL == segment->attributes) { + segment->attributes = nr_attributes_create(segment->txn->attribute_config); + } + + if (nrunlikely(NULL == segment->attributes)) { + return; + } + +#define CLM_ATTRIBUTE_DESTINATION \ + (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ + | NR_ATTRIBUTE_DESTINATION_TXN_EVENT | NR_ATTRIBUTE_DESTINATION_SPAN) + + /* + * If the string is empty, CLM specs say don't add it. + * nr_attributes_agent_add_string is okay with an empty string attribute. + * Already checked function for strempty no need to check again, but will need + * to check filepath and namespace. + */ + + nr_attributes_agent_add_string(segment->attributes, CLM_ATTRIBUTE_DESTINATION, + "code.function", function); + + if (!nr_strempty(filepath)) { + nr_attributes_agent_add_string(segment->attributes, + CLM_ATTRIBUTE_DESTINATION, "code.filepath", + filepath); + } + + if (!nr_strempty(namespace)) { + nr_attributes_agent_add_string(segment->attributes, + CLM_ATTRIBUTE_DESTINATION, "code.namespace", + namespace); + } + + nr_attributes_agent_add_long(segment->attributes, CLM_ATTRIBUTE_DESTINATION, + "code.lineno", metadata->function_lineno); +} + +#endif /* * Purpose : Create a metric name from the given metadata. * @@ -1050,7 +1206,7 @@ static void nr_php_execute_metadata_metric( const char* function_name; const char* scope_name; -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ scope_name = metadata->scope ? ZSTR_VAL(metadata->scope) : NULL; function_name = metadata->function ? ZSTR_VAL(metadata->function) : NULL; #else @@ -1067,9 +1223,10 @@ static void nr_php_execute_metadata_metric( * * Params : 1. A pointer to the metadata. */ -static void nr_php_execute_metadata_release( +static inline void nr_php_execute_metadata_release( nr_php_execute_metadata_t* metadata) { -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO + if (NULL != metadata->scope) { zend_string_release(metadata->scope); metadata->scope = NULL; @@ -1079,6 +1236,12 @@ static void nr_php_execute_metadata_release( zend_string_release(metadata->function); metadata->function = NULL; } + + if (NULL != metadata->filepath) { + zend_string_release(metadata->filepath); + metadata->filepath = NULL; + } + #else metadata->op_array = NULL; #endif /* PHP7 */ @@ -1126,6 +1289,16 @@ static inline void nr_php_execute_segment_end( || stacked->error) { nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); nr_php_execute_segment_add_metric(s, metadata, create_metric); + + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, exit and don't create any metrics. + */ +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP >= PHP7 */ + if (NRINI(code_level_metrics_enabled)) { + nr_php_execute_segment_add_code_level_metrics(s, metadata); + } +#endif nr_segment_end(&s); } else { nr_php_stacked_segment_deinit(stacked TSRMLS_CC); @@ -1142,10 +1315,10 @@ static inline void nr_php_execute_segment_end( static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { int zcaught = 0; nrtime_t txn_start_time; - nr_php_execute_metadata_t metadata; + nr_php_execute_metadata_t metadata = {0}; nr_segment_t stacked = {0}; - nr_segment_t* segment; - nruserfn_t* wraprec; + nr_segment_t* segment = NULL; + nruserfn_t* wraprec = NULL; NRTXNGLOBAL(execute_count) += 1; @@ -1207,7 +1380,6 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { txn_start_time = nr_txn_start_time(NRPRG(txn)); segment = nr_php_stacked_segment_init(&stacked TSRMLS_CC); - zcaught = nr_zend_call_orig_execute_special(wraprec, segment, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); @@ -1226,7 +1398,6 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { } nr_php_execute_segment_end(segment, &metadata, create_metric TSRMLS_CC); - nr_php_execute_metadata_release(&metadata); if (nrunlikely(zcaught)) { @@ -1252,7 +1423,7 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { zval* exception_zval = NULL; nr_status_t status; -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ /* * On PHP 7, EG(exception) is stored as a zend_object, and is only * wrapped in a zval when it actually needs to be. @@ -1289,7 +1460,6 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { } nr_php_execute_segment_end(segment, &metadata, false TSRMLS_CC); - nr_php_execute_metadata_release(&metadata); if (nrunlikely(zcaught)) { @@ -1446,7 +1616,7 @@ void nr_php_execute_internal(zend_execute_data* execute_data, return; } -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ func = execute_data->func; #else func = execute_data->function_state.function; @@ -1488,7 +1658,7 @@ void nr_php_execute_internal(zend_execute_data* execute_data, nr_segment_set_timing(segment, segment->start_time, duration); if (duration >= NR_PHP_PROCESS_GLOBALS(expensive_min)) { - nr_php_execute_metadata_t metadata; + nr_php_execute_metadata_t metadata = {0}; nr_php_execute_metadata_init(&metadata, (zend_op_array*)func); diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 8c3dcba81..d54d48aea 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -304,6 +304,7 @@ nr_php_ini_attribute_config_t */ nrinibool_t custom_events_enabled; /* newrelic.custom_insights_events.enabled */ +nriniuint_t custom_events_max_samples_stored; /* newrelic.custom_events.max_samples_stored */ nrinibool_t synthetics_enabled; /* newrelic.synthetics.enabled */ nrinibool_t phpunit_events_enabled; /* newrelic.phpunit_events.enabled */ @@ -478,6 +479,12 @@ nrinibool_t nriniuint_t log_forwarding_log_level; /* newrelic.application_logging.forwarding.log_level */ +/* + * Configuration option to toggle code level metrics collection. + */ +nrinibool_t + code_level_metrics_enabled; /* newrelic.code_level_metrics.enabled */ + /* * pid and user_function_wrappers are used to store user function wrappers. * Storing this on a request level (as opposed to storing it on transaction diff --git a/agent/php_nrini.c b/agent/php_nrini.c index 14fedcb16..90a7d8906 100644 --- a/agent/php_nrini.c +++ b/agent/php_nrini.c @@ -1842,6 +1842,55 @@ static PHP_INI_MH(nr_log_forwarding_log_level_mh) { return FAILURE; } +static PHP_INI_MH(nr_custom_events_max_samples_stored_mh) { + nriniuint_t* p; + int val = NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED; + bool err = false; + nr_status_t parse_status = NR_SUCCESS; + +#ifndef ZTS + char* base = (char*)mh_arg2; +#else + char* base = (char*)ts_resource(*((int*)mh_arg2)); +#endif + + p = (nriniuint_t*)(base + (size_t)mh_arg1); + + (void)entry; + (void)mh_arg3; + NR_UNUSED_TSRMLS; + + /* + * -- An invalid value will result in the default value. + * -- A value < 0 will result in the default value + * -- A value > MAX will result in MAX value + */ + + p->where = 0; + + if (0 != NEW_VALUE_LEN) { + parse_status = nr_strtoi(&val, NEW_VALUE, 0); + if (0 > val || NR_FAILURE == parse_status) { + val = NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED; + err = true; + } else if (NR_MAX_CUSTOM_EVENTS_MAX_SAMPLES_STORED < val) { + val = NR_MAX_CUSTOM_EVENTS_MAX_SAMPLES_STORED; + err = true; + } + if (err) { + nrl_warning(NRL_INIT, + "Invalid custom_events.max_samples_stored " + "value \"%.8s\"; using " + "%d instead", + NEW_VALUE, val); + } + } + p->value = (zend_uint)val; + p->where = stage; + + return SUCCESS; +} + /* * Now for the actual INI entry table. Please note there are two types of INI * entry specification used. @@ -2173,6 +2222,7 @@ STD_PHP_INI_ENTRY_EX("newrelic.framework", zend_newrelic_globals, newrelic_globals, nr_framework_dh) + /* DEPRECATED */ STD_PHP_INI_ENTRY_EX("newrelic.cross_application_tracer.enabled", "0", @@ -2570,6 +2620,14 @@ STD_PHP_INI_ENTRY_EX("newrelic.custom_insights_events.enabled", zend_newrelic_globals, newrelic_globals, nr_enabled_disabled_dh) +STD_PHP_INI_ENTRY_EX("newrelic.custom_events.max_samples_stored", + NR_STR2(NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED), + NR_PHP_REQUEST, + nr_custom_events_max_samples_stored_mh, + custom_events_max_samples_stored, + zend_newrelic_globals, + newrelic_globals, + 0) /* * Synthetics @@ -2863,6 +2921,17 @@ STD_PHP_INI_ENTRY_EX( newrelic_globals, 0) +/* + * Code Level Metrics, initially off by default + */ +STD_PHP_INI_ENTRY_EX("newrelic.code_level_metrics.enabled", + "0", + NR_PHP_REQUEST, + nr_boolean_mh, + code_level_metrics_enabled, + zend_newrelic_globals, + newrelic_globals, + nr_enabled_disabled_dh) /* * Logging */ @@ -3093,10 +3162,11 @@ void zm_info_newrelic(void); /* ctags landing pad only */ PHP_MINFO_FUNCTION(newrelic) { php_info_print_table_start(); php_info_print_table_header(2, "New Relic RPM Monitoring", - NR_PHP_PROCESS_GLOBALS(enabled) ? "enabled" - : NR_PHP_PROCESS_GLOBALS(mpm_bad) - ? "disabled due to threaded MPM" - : "disabled"); + NR_PHP_PROCESS_GLOBALS(enabled) + ? "enabled" + : NR_PHP_PROCESS_GLOBALS(mpm_bad) + ? "disabled due to threaded MPM" + : "disabled"); php_info_print_table_row(2, "New Relic Version", nr_version_verbose()); php_info_print_table_end(); diff --git a/agent/php_txn.c b/agent/php_txn.c index d99585f11..1bda04be8 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -704,6 +704,7 @@ nr_status_t nr_php_txn_begin(const char* appnames, } opts.custom_events_enabled = (int)NRINI(custom_events_enabled); + opts.custom_events_max_samples_stored = NRINI(custom_events_max_samples_stored); opts.synthetics_enabled = (int)NRINI(synthetics_enabled); opts.instance_reporting_enabled = (int)NRINI(instance_reporting_enabled); opts.database_name_reporting_enabled @@ -781,10 +782,12 @@ nr_status_t nr_php_txn_begin(const char* appnames, info.span_queue_size = NRINI(span_queue_size); info.span_events_max_samples_stored = NRINI(span_events_max_samples_stored); - /* Need to initialize log max samples to value negotiated between that + /* Need to initialize custom and log event max samples to value negotiated between that * requested in the INI file and the value returned from the daaemon (based in * part on the collector connect response harvest limits) */ info.log_events_max_samples_stored = NRINI(log_events_max_samples_stored); + info.custom_events_max_samples_stored = NRINI(custom_events_max_samples_stored); + NRPRG(app) = nr_agent_find_or_add_app( nr_agent_applist, &info, /* diff --git a/agent/scripts/newrelic.ini.template b/agent/scripts/newrelic.ini.template index 71720f82d..05a78d204 100644 --- a/agent/scripts/newrelic.ini.template +++ b/agent/scripts/newrelic.ini.template @@ -638,10 +638,11 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; ; Must be one of the following values: ; cakephp, codeigniter, drupal, drupal8, joomla, kohana, laravel, -; magento, magento2, mediawiki, silex, slim, symfony1, symfony2, +; magento, magento2, mediawiki, slim, symfony2, symfony4, ; wordpress, yii, zend, zend2, no_framework ; -; Note that "drupal" covers only Drupal 6 and 7. +; Note that "drupal" covers only Drupal 6 and 7 and "symfony2" +; now only supports Symfony 3.x. ; ;newrelic.framework = "" @@ -833,6 +834,16 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; ;newrelic.custom_insights_events.enabled = true +; Setting: newrelic.custom_events.max_samples_stored +; Type : integer +; Scope : per-directory +; Default: 30000 +; Info : The default Custom Events reservoir limit in the agent is +; 30000 events per minute and the maximum allowed value +; is 100000 events per minute. +; +;newrelic.custom_events.max_samples_stored = 30000 + ; Setting: newrelic.labels ; Type : string (Use quotes) ; Scope : per-directory @@ -1206,3 +1217,14 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; on the APM Summary page. ; ;newrelic.application_logging.metrics.enabled = true + +; Setting: newrelic.code_level_metrics.enabled +; Type : boolean +; Scope : per-directory +; Default: false +; Info : Toggles whether the agent provides function name, function +; filepath, function namespace, and function lineno as +; attributes on reported spans +; +;newrelic.code_level_metrics.enabled = false + diff --git a/agent/tests/test_agent.c b/agent/tests/test_agent.c index a2b7bdfa5..509b4878b 100644 --- a/agent/tests/test_agent.c +++ b/agent/tests/test_agent.c @@ -579,6 +579,7 @@ static void test_nr_php_zend_function_lineno() { */ func.op_array.line_start = 4; + func.op_array.type = ZEND_USER_FUNCTION; tlib_pass_if_uint32_t_equal("Unexpected lineno name", 4, nr_php_zend_function_lineno(&func)); } diff --git a/axiom/cmd_appinfo_transmit.c b/axiom/cmd_appinfo_transmit.c index 7c9507f0e..1333e2eff 100644 --- a/axiom/cmd_appinfo_transmit.c +++ b/axiom/cmd_appinfo_transmit.c @@ -178,6 +178,8 @@ nr_flatbuffer_t* nr_appinfo_create_query(const char* agent_run_id, info->span_queue_size, 0); nr_flatbuffers_object_prepend_u64(fb, APP_SPAN_EVENTS_MAX_SAMPLES_STORED, info->span_events_max_samples_stored, 0); + nr_flatbuffers_object_prepend_u64(fb, APP_CUSTOM_EVENTS_MAX_SAMPLES_STORED, + info->custom_events_max_samples_stored, 0); nr_flatbuffers_object_prepend_u64(fb, APP_LOG_EVENTS_MAX_SAMPLES_STORED, info->log_events_max_samples_stored, 0); nr_flatbuffers_object_prepend_u16(fb, APP_TRACE_OBSERVER_PORT, @@ -413,6 +415,7 @@ void nr_cmd_appinfo_process_event_harvest_config(const nrobj_t* config, nr_app_limits_t* app_limits, nr_app_info_t info) { uint64_t harvest_log_limit; + uint64_t harvest_custom_limit; const nrobj_t* harvest_limits = nro_get_hash_hash(config, "harvest_limits", NULL); @@ -423,8 +426,6 @@ void nr_cmd_appinfo_process_event_harvest_config(const nrobj_t* config, * consistency, but the defaults are more or less inconsequential. */ app_limits->analytics_events = nr_cmd_appinfo_process_get_harvest_limit( harvest_limits, "analytic_event_data", NR_MAX_ANALYTIC_EVENTS); - app_limits->custom_events = nr_cmd_appinfo_process_get_harvest_limit( - harvest_limits, "custom_event_data", NR_MAX_CUSTOM_EVENTS); app_limits->error_events = nr_cmd_appinfo_process_get_harvest_limit( harvest_limits, "error_event_data", NR_MAX_ERRORS); app_limits->span_events = nr_cmd_appinfo_process_get_harvest_limit( @@ -433,8 +434,18 @@ void nr_cmd_appinfo_process_event_harvest_config(const nrobj_t* config, ? NR_MAX_SPAN_EVENTS_MAX_SAMPLES_STORED : info.span_events_max_samples_stored); + /* Need to select the smaller value between the agent INI config value - * and the value returned by the collector. */ + * and the value returned by the collector. This was necessary because + * the collector for some time returned '833' if sent a value of '0' + */ + harvest_custom_limit = nr_cmd_appinfo_process_get_harvest_limit( + harvest_limits, "custom_event_data", + NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED); + app_limits->custom_events = info.custom_events_max_samples_stored; + if (harvest_custom_limit < info.custom_events_max_samples_stored) + app_limits->custom_events = harvest_custom_limit; + harvest_log_limit = nr_cmd_appinfo_process_get_harvest_limit( harvest_limits, "log_event_data", info.log_events_max_samples_stored); app_limits->log_events = info.log_events_max_samples_stored; @@ -448,6 +459,15 @@ void nr_cmd_appinfo_process_event_harvest_config(const nrobj_t* config, "app_limits->log_events = %d", info.log_events_max_samples_stored, harvest_log_limit, app_limits->log_events); + + nrl_verbosedebug(NRL_AGENT, + "custom event limits: agent config = %" PRIu64 + ", harvest = %" PRIu64 + " final " + ", app_limits->log_events = %d", + info.custom_events_max_samples_stored, + harvest_custom_limit, + app_limits->custom_events); } int nr_cmd_appinfo_process_get_harvest_limit(const nrobj_t* limits, diff --git a/axiom/nr_app.c b/axiom/nr_app.c index 8d3d76230..bc5660ac0 100644 --- a/axiom/nr_app.c +++ b/axiom/nr_app.c @@ -277,6 +277,8 @@ static nrapp_t* create_new_app(const nr_app_info_t* info) { app->info.span_events_max_samples_stored = info->span_events_max_samples_stored; app->info.log_events_max_samples_stored = info->log_events_max_samples_stored; + app->info.custom_events_max_samples_stored + = info->custom_events_max_samples_stored; app->rnd = nr_random_create(); nr_random_seed_from_time(app->rnd); diff --git a/axiom/nr_app.h b/axiom/nr_app.h index cadd6501a..1fb780b7a 100644 --- a/axiom/nr_app.h +++ b/axiom/nr_app.h @@ -75,14 +75,16 @@ typedef struct _nr_app_info_t { char* trace_observer_host; /* 8T trace observer host */ uint16_t trace_observer_port; /* 8T trace observer port */ uint64_t span_queue_size; /* 8T span queue size (for the daemon) */ - uint64_t span_events_max_samples_stored; /* maximum number of spans (for the + uint64_t span_events_max_samples_stored; /* maximum number of spans per min (for the daemon) */ - uint64_t log_events_max_samples_stored; /* maximum number of log events (for + uint64_t log_events_max_samples_stored; /* maximum number of log events per min (for + the daemon) */ + uint64_t custom_events_max_samples_stored; /* maximum number of custom events per min (for the daemon) */ } nr_app_info_t; /* - * Calculated limits for event types. + * Calculated limits for event types per HARVEST. */ typedef struct _nr_app_limits_t { int analytics_events; diff --git a/axiom/nr_commands_private.h b/axiom/nr_commands_private.h index 176ab8610..b605b84db 100644 --- a/axiom/nr_commands_private.h +++ b/axiom/nr_commands_private.h @@ -82,7 +82,8 @@ enum { APP_SPAN_EVENTS_MAX_SAMPLES_STORED = 16, APP_METADATA = 17, APP_LOG_EVENTS_MAX_SAMPLES_STORED = 18, - APP_NUM_FIELDS = 19, + APP_CUSTOM_EVENTS_MAX_SAMPLES_STORED = 19, + APP_NUM_FIELDS = 20, }; /* Generated from: table AppReply */ diff --git a/axiom/nr_limits.h b/axiom/nr_limits.h index acea56c2f..3b3a44f3c 100644 --- a/axiom/nr_limits.h +++ b/axiom/nr_limits.h @@ -20,7 +20,12 @@ /* * The default maximum number of custom events in a transaction. */ -#define NR_MAX_CUSTOM_EVENTS 10000 +#define NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED 30000 + +/* + * The maximum number of custom events in a transaction. + */ +#define NR_MAX_CUSTOM_EVENTS_MAX_SAMPLES_STORED 100000 /* * Set the maximum number of errors in a transaction. diff --git a/axiom/nr_segment.c b/axiom/nr_segment.c index 34ea61540..4aa546c1c 100644 --- a/axiom/nr_segment.c +++ b/axiom/nr_segment.c @@ -404,7 +404,6 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { } trace_id = nr_txn_get_current_trace_id(segment->txn); - event = nr_span_event_create(); nr_span_event_set_guid(event, segment->id); nr_span_event_set_trace_id(event, trace_id); @@ -463,9 +462,7 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { agent_attributes = nr_attributes_agent_to_obj( segment->txn->attributes, NR_ATTRIBUTE_DESTINATION_TXN_EVENT); - nro_iteratehash(agent_attributes, add_agent_attribute_to_span_event, event); - nro_delete(agent_attributes); } @@ -505,6 +502,13 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { &event_and_counter); nro_delete(user_attributes); + /* + * Add segment agent attributes to span + */ + agent_attributes = nr_attributes_agent_to_obj( + segment->attributes, NR_ATTRIBUTE_DESTINATION_SPAN); + nro_iteratehash(agent_attributes, add_agent_attribute_to_span_event, event); + nro_delete(agent_attributes); } if (segment->attributes_txn_event) { user_attributes = nr_attributes_user_to_obj(segment->attributes_txn_event, @@ -1230,4 +1234,4 @@ bool nr_segment_attributes_user_txn_event_add(nr_segment_t* segment, nr_segment_set_priority_flag(segment, NR_SEGMENT_PRIORITY_ATTR); return (NR_SUCCESS == status); -} \ No newline at end of file +} diff --git a/axiom/nr_segment_traces.c b/axiom/nr_segment_traces.c index 93b63d537..846d89a55 100644 --- a/axiom/nr_segment_traces.c +++ b/axiom/nr_segment_traces.c @@ -228,7 +228,8 @@ static void nr_segment_iteration_pass_trace(nr_segment_t* segment, nrbuf_t* buf = userdata->trace.buf; int idx; nr_segment_t* parent = NULL; - nrobj_t* user_attributes; + nrobj_t* user_attributes = NULL; + nrobj_t* agent_attributes = NULL; uint64_t start_ms; uint64_t stop_ms; @@ -298,6 +299,13 @@ static void nr_segment_iteration_pass_trace(nr_segment_t* segment, segment->attributes, NR_ATTRIBUTE_DESTINATION_TXN_TRACE); add_attribute_hash_to_buffer(buf, user_attributes); nro_delete(user_attributes); + /* + * Add segment attributes to transaction trace. + */ + agent_attributes = nr_attributes_agent_to_obj( + segment->attributes, NR_ATTRIBUTE_DESTINATION_TXN_TRACE); + add_attribute_hash_to_buffer(buf, agent_attributes); + nro_delete(agent_attributes); } nr_buffer_add(buf, "}", 1); diff --git a/axiom/nr_txn.c b/axiom/nr_txn.c index d5b9fa45e..5c8be9f68 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -48,6 +48,10 @@ struct _nr_txn_attribute_t { uint32_t destinations; }; +#define NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT \ + (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ + | NR_ATTRIBUTE_DESTINATION_TXN_EVENT | NR_ATTRIBUTE_DESTINATION_SPAN) + #define NR_TXN_ATTRIBUTE_TRACE_ERROR_EVENT \ (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ | NR_ATTRIBUTE_DESTINATION_TXN_EVENT) @@ -55,10 +59,8 @@ struct _nr_txn_attribute_t { #define NR_TXN_ATTRIBUTE_TRACE_ERROR \ (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR) -#define NR_TXN_ATTR(X, NAME, DESTS) \ - const nr_txn_attribute_t* X = &(nr_txn_attribute_t) { \ - (NAME), (DESTS) \ - } +#define NR_TXN_ATTR(X, NAME, DESTS) \ + const nr_txn_attribute_t* X = &(nr_txn_attribute_t) { (NAME), (DESTS) } NR_TXN_ATTR(nr_txn_request_uri, "request.uri", @@ -2504,7 +2506,6 @@ nr_analytics_event_t* nr_error_to_event(const nrtxn_t* txn) { nro_set_hash_string(params, "spanId", nr_error_get_span_id(txn->error)); } } - agent_attributes = nr_attributes_agent_to_obj(txn->attributes, NR_ATTRIBUTE_DESTINATION_ERROR); user_attributes = nr_attributes_user_to_obj(txn->attributes, @@ -3451,4 +3452,4 @@ void nr_txn_record_log_event(nrtxn_t* txn, nr_txn_add_log_event(txn, log_level_name, log_message, timestamp, app); nr_txn_add_logging_metrics(txn, log_level_name); -} \ No newline at end of file +} diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index 6e6c5fcc6..0de8eef2e 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -58,6 +58,8 @@ typedef enum _nr_tt_recordsql_t { */ typedef struct _nrtxnopt_t { int custom_events_enabled; /* Whether or not to capture custom events */ + size_t custom_events_max_samples_stored; /* The maximum number of custom events + per transaction */ int synthetics_enabled; /* Whether or not to enable Synthetics support */ int instance_reporting_enabled; /* Whether to capture datastore instance host and port */ @@ -581,7 +583,6 @@ extern void nr_txn_set_string_attribute(nrtxn_t* txn, extern void nr_txn_set_long_attribute(nrtxn_t* txn, const nr_txn_attribute_t* attribute, long value); - /* * Purpose : Return the duration of the transaction. This function will return * 0 if the transaction has not yet finished or if the transaction diff --git a/axiom/nr_version.c b/axiom/nr_version.c index 9e610e28c..d50fa97fe 100644 --- a/axiom/nr_version.c +++ b/axiom/nr_version.c @@ -33,8 +33,9 @@ * cosmos 29Jun2022 (10.0) * dahlia 19Sep2022 (10.1) * echinacea 03Oct2022 (10.2) + * freesia 03Nov2022 (10.3) */ -#define NR_CODENAME "freesia" +#define NR_CODENAME "goldenrod" const char* nr_version(void) { return NR_STR2(NR_VERSION); diff --git a/axiom/tests/test_app_helpers.h b/axiom/tests/test_app_helpers.h index 98462ea99..5285fcdc9 100644 --- a/axiom/tests/test_app_helpers.h +++ b/axiom/tests/test_app_helpers.h @@ -15,7 +15,7 @@ static inline nr_app_limits_t default_app_limits(void) { return (nr_app_limits_t){ .analytics_events = NR_MAX_ANALYTIC_EVENTS, - .custom_events = NR_MAX_CUSTOM_EVENTS, + .custom_events = NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED, .error_events = NR_MAX_ERRORS, .span_events = NR_DEFAULT_SPAN_EVENTS_MAX_SAMPLES_STORED, .log_events = NR_DEFAULT_LOG_EVENTS_MAX_SAMPLES_STORED, diff --git a/axiom/tests/test_cmd_appinfo.c b/axiom/tests/test_cmd_appinfo.c index 71dbffd35..bd30dda15 100644 --- a/axiom/tests/test_cmd_appinfo.c +++ b/axiom/tests/test_cmd_appinfo.c @@ -74,6 +74,10 @@ static void test_create_empty_query(void) { tlib_pass_if_uint64_t_equal(__func__, 0, nr_flatbuffers_table_read_u64( &app, APP_LOG_EVENTS_MAX_SAMPLES_STORED, 0)); + + tlib_pass_if_uint64_t_equal(__func__, 0, + nr_flatbuffers_table_read_u64( + &app, APP_CUSTOM_EVENTS_MAX_SAMPLES_STORED, 0)); nr_flatbuffers_destroy(&query); } @@ -103,6 +107,7 @@ static void test_create_query(void) { info.span_queue_size = 10000; info.span_events_max_samples_stored = 1234; info.log_events_max_samples_stored = 2345; + info.custom_events_max_samples_stored = 345; query = nr_appinfo_create_query("12345", "this_host", &info); @@ -159,6 +164,9 @@ static void test_create_query(void) { tlib_pass_if_uint64_t_equal(__func__, info.log_events_max_samples_stored, nr_flatbuffers_table_read_u16( &app, APP_LOG_EVENTS_MAX_SAMPLES_STORED, 0)); + tlib_pass_if_uint64_t_equal(__func__, info.custom_events_max_samples_stored, + nr_flatbuffers_table_read_u16( + &app, APP_CUSTOM_EVENTS_MAX_SAMPLES_STORED, 0)); high_security = nr_flatbuffers_table_read_i8(&app, APP_FIELD_HIGH_SECURITY, 0); @@ -974,6 +982,9 @@ static void test_process_event_harvest_config(void) { info.span_events_max_samples_stored = NR_DEFAULT_SPAN_EVENTS_MAX_SAMPLES_STORED; info.log_events_max_samples_stored = NR_DEFAULT_LOG_EVENTS_MAX_SAMPLES_STORED; + info.custom_events_max_samples_stored + = NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED; + app_limits = app_limits_all_zero; nr_cmd_appinfo_process_event_harvest_config(NULL, &app_limits, info); tlib_pass_if_bytes_equal("a NULL config should enable all event types", @@ -1019,6 +1030,7 @@ static void test_process_get_harvest_limit(void) { nrobj_t* limits = nro_create_from_json( "{" "\"analytic_event_data\":833," + "\"span_event_data\":0," "\"custom_event_data\":0," "\"log_event_data\":0," "\"error_event_data\":null," @@ -1042,7 +1054,7 @@ static void test_process_get_harvest_limit(void) { tlib_pass_if_int_equal( "missing keys should return the default value", 100, - nr_cmd_appinfo_process_get_harvest_limit(limits, "span_event_data", 100)); + nr_cmd_appinfo_process_get_harvest_limit(limits, "missing_event_data", 100)); tlib_pass_if_int_equal("null values should return the default value", 100, nr_cmd_appinfo_process_get_harvest_limit( @@ -1065,9 +1077,18 @@ static void test_process_get_harvest_limit(void) { nr_cmd_appinfo_process_get_harvest_limit( limits, "custom_event_data", 100)); + tlib_pass_if_int_equal( + "zero integers for span_event_data should return zero", 0, + nr_cmd_appinfo_process_get_harvest_limit(limits, "span_event_data", 100)); + + tlib_pass_if_int_equal( + "zero integers for custom_event_data should return zero", 0, + nr_cmd_appinfo_process_get_harvest_limit(limits, "custom_event_data", 100)); + tlib_pass_if_int_equal( "zero integers for log_event_data should return zero", 0, nr_cmd_appinfo_process_get_harvest_limit(limits, "log_event_data", 100)); + nro_delete(array); nro_delete(limits); } diff --git a/axiom/tests/test_segment_helpers.h b/axiom/tests/test_segment_helpers.h index 06701c483..908432798 100644 --- a/axiom/tests/test_segment_helpers.h +++ b/axiom/tests/test_segment_helpers.h @@ -240,7 +240,7 @@ static NRUNUSED nrtxn_t* new_txn(int background) { app.rnd = NULL; app.limits = (nr_app_limits_t){ .analytics_events = NR_MAX_ANALYTIC_EVENTS, - .custom_events = NR_MAX_CUSTOM_EVENTS, + .custom_events = NR_DEFAULT_CUSTOM_EVENTS_MAX_SAMPLES_STORED, .error_events = NR_MAX_ERRORS, .span_events = NR_DEFAULT_SPAN_EVENTS_MAX_SAMPLES_STORED, }; diff --git a/protocol/flatbuffers/protocol.fbs b/protocol/flatbuffers/protocol.fbs index 895c4cb67..b04b624d2 100644 --- a/protocol/flatbuffers/protocol.fbs +++ b/protocol/flatbuffers/protocol.fbs @@ -34,6 +34,7 @@ table App { span_events_max_samples_stored: uint64; // added for PHP agent release 9.21 metadata: string; // pre-computed json, added for PHP agent release 10.0 log_events_max_samples_stored: uint64; // added for PHP agent release 10.1 + custom_events_max_samples_stored: uint64; // added for PHP agent release 10.4 } enum AppStatus : byte { Unknown = 0, Disconnected = 1, InvalidLicense = 2, diff --git a/src/flatbuffersdata/data_test.go b/src/flatbuffersdata/data_test.go index 23132ac84..145482157 100644 --- a/src/flatbuffersdata/data_test.go +++ b/src/flatbuffersdata/data_test.go @@ -179,7 +179,7 @@ func TestFlatbuffersTxnData(t *testing.T) { } out, err = harvest.CustomEvents.Data(id, now) - if nil != err || string(out) != `["12345",{"reservoir_size":10000,"events_seen":3},[[{"x":1}],[{"x":2}],[{"x":3}]]]` { + if nil != err || string(out) != `["12345",{"reservoir_size":100000,"events_seen":3},[[{"x":1}],[{"x":2}],[{"x":3}]]]` { t.Fatal(err, string(out)) } diff --git a/src/integration_runner/main.go b/src/integration_runner/main.go index f9b257555..853658f53 100644 --- a/src/integration_runner/main.go +++ b/src/integration_runner/main.go @@ -31,19 +31,24 @@ import ( ) var ( - flagAgent = flag.String("agent", "", "") - flagCGI = flag.String("cgi", "", "") - flagCollector = flag.String("collector", "", "the collector host") - flagLoglevel = flag.String("loglevel", "", "agent log level") - flagOutputDir = flag.String("output-dir", ".", "") - flagPattern = flag.String("pattern", "test_*", "shell pattern describing tests to run") - flagPHP = flag.String("php", "", "") - flagPort = flag.String("port", defaultPort(), "") - flagRetry = flag.Int("retry", 0, "maximum retry attempts") - flagTimeout = flag.Duration("timeout", 10*time.Second, "") - flagValgrind = flag.String("valgrind", "", "if given, this is the path to valgrind") - flagWorkers = flag.Int("threads", 1, "") - flagTime = flag.Bool("time", false, "time each test") + DefaultMaxCustomEvents = 30000 +) + +var ( + flagAgent = flag.String("agent", "", "") + flagCGI = flag.String("cgi", "", "") + flagCollector = flag.String("collector", "", "the collector host") + flagLoglevel = flag.String("loglevel", "", "agent log level") + flagOutputDir = flag.String("output-dir", ".", "") + flagPattern = flag.String("pattern", "test_*", "shell pattern describing tests to run") + flagPHP = flag.String("php", "", "") + flagPort = flag.String("port", defaultPort(), "") + flagRetry = flag.Int("retry", 0, "maximum retry attempts") + flagTimeout = flag.Duration("timeout", 10*time.Second, "") + flagValgrind = flag.String("valgrind", "", "if given, this is the path to valgrind") + flagWorkers = flag.Int("threads", 1, "") + flagTime = flag.Bool("time", false, "time each test") + flagMaxCustomEvents = flag.Int("max_custom_events", 30000, "value for newrelic.custom_events.max_samples_stored") // externalPort is the port on which we start a server to handle // external calls. @@ -139,10 +144,16 @@ var ( // Ensure that we get Javascript agent code in the reply map[string]interface{}{"newrelic.browser_monitoring.debug": false, "newrelic.browser_monitoring.loader": "rum"}, SecurityPolicyToken: "", + // Set log and customer event limits to non-zero values or else collector will return 0. + // Use default value for logging. + // Use max value for custom event so integration tests can exercise full range of values for max to record. AgentEventLimits: collector.EventConfigs{ LogEventConfig: collector.Event{ Limit: 10000, }, + CustomEventConfig: collector.Event{ + Limit: DefaultMaxCustomEvents, + }, }, } @@ -254,6 +265,9 @@ func main() { } TestApp.License = collector.LicenseKey(secrets.NewrelicLicenseKey) + // Set value that will be sent to collector for the max custom event samples + TestApp.AgentEventLimits.CustomEventConfig.Limit = *flagMaxCustomEvents + // Set the redirect collector from the flag, if given. if *flagCollector != "" { TestApp.RedirectCollector = *flagCollector diff --git a/src/newrelic/app_test.go b/src/newrelic/app_test.go index ec0c88fd7..dd5a0749f 100644 --- a/src/newrelic/app_test.go +++ b/src/newrelic/app_test.go @@ -363,6 +363,10 @@ func TestConnectPayloadEncoded(t *testing.T) { // propagate through and be sent to the collector info.AgentEventLimits.LogEventConfig.Limit = 4545 + // A valid custom event max samples stored value configured from the agent should + // propagate through and be sent to the collector + info.AgentEventLimits.CustomEventConfig.Limit = 1234 + pid := 123 expected := `[` + `{` + @@ -379,7 +383,7 @@ func TestConnectPayloadEncoded(t *testing.T) { `"metadata":{"NEW_RELIC_METADATA_ONE":"one","NEW_RELIC_METADATA_TWO":"two"},` + `"identifier":"one;two",` + `"utilization":{"metadata_version":1,"logical_processors":22,"total_ram_mib":1000,"hostname":"some_host"},` + - `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":10000,"span_event_data":2323,"log_event_data":4545}}` + + `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":1234,"span_event_data":2323,"log_event_data":4545}}` + `}` + `]` @@ -398,6 +402,10 @@ func TestConnectPayloadEncoded(t *testing.T) { // propagate defaults through and be sent to the collector info.AgentEventLimits.LogEventConfig.Limit = 45678 + // An invalid custom event max samples stored value configured from the agent should + // propagate defaults through and be sent to the collector + info.AgentEventLimits.CustomEventConfig.Limit = 456780 + pid = 123 expected = `[` + `{` + @@ -414,7 +422,9 @@ func TestConnectPayloadEncoded(t *testing.T) { `"metadata":{"NEW_RELIC_METADATA_ONE":"one","NEW_RELIC_METADATA_TWO":"two"},` + `"identifier":"one;two",` + `"utilization":{"metadata_version":1,"logical_processors":22,"total_ram_mib":1000,"hostname":"some_host"},` + - `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":10000,` + + `"event_harvest_config":{"report_period_ms":60000,` + + `"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,` + + `"custom_event_data":` + strconv.Itoa(limits.MaxCustomMaxEvents) + `,` + `"span_event_data":` + strconv.Itoa(limits.MaxSpanMaxEvents) + `,` + `"log_event_data":` + strconv.Itoa(limits.MaxLogMaxEvents) + `}}` + `}` + @@ -430,6 +440,7 @@ func TestConnectPayloadEncoded(t *testing.T) { // an empty string for the HostDisplayName should not produce JSON info.AgentEventLimits.SpanEventConfig.Limit = 1001 info.AgentEventLimits.LogEventConfig.Limit = 1002 + info.AgentEventLimits.CustomEventConfig.Limit = 1003 info.HostDisplayName = "" expected = `[` + `{` + @@ -445,7 +456,7 @@ func TestConnectPayloadEncoded(t *testing.T) { `"metadata":{"NEW_RELIC_METADATA_ONE":"one","NEW_RELIC_METADATA_TWO":"two"},` + `"identifier":"one;two",` + `"utilization":{"metadata_version":1,"logical_processors":22,"total_ram_mib":1000,"hostname":"some_host"},` + - `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":10000,"span_event_data":1001,"log_event_data":1002}}` + + `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":1003,"span_event_data":1001,"log_event_data":1002}}` + `}` + `]` @@ -472,7 +483,7 @@ func TestConnectPayloadEncoded(t *testing.T) { `"metadata":{},` + `"identifier":"one;two",` + `"utilization":{"metadata_version":1,"logical_processors":22,"total_ram_mib":1000,"hostname":"some_host"},` + - `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":10000,"span_event_data":1001,"log_event_data":1002}}` + + `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":1003,"span_event_data":1001,"log_event_data":1002}}` + `}` + `]` @@ -499,7 +510,7 @@ func TestConnectPayloadEncoded(t *testing.T) { `"metadata":{},` + `"identifier":"one;two",` + `"utilization":{"metadata_version":1,"logical_processors":22,"total_ram_mib":1000,"hostname":"some_host"},` + - `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":10000,"span_event_data":1001,"log_event_data":1002}}` + + `"event_harvest_config":{"report_period_ms":60000,"harvest_limits":{"error_event_data":100,"analytic_event_data":10000,"custom_event_data":1003,"span_event_data":1001,"log_event_data":1002}}` + `}` + `]` diff --git a/src/newrelic/collector/event_data.go b/src/newrelic/collector/event_data.go index 2478b320f..37563a5fd 100644 --- a/src/newrelic/collector/event_data.go +++ b/src/newrelic/collector/event_data.go @@ -260,7 +260,7 @@ func (daemonConfig *EventHarvestConfig) UnmarshalJSON(b []byte) error { err = getEventConfig( rawLimits.CustomEventData, daemonConfig.ReportPeriod, - limits.MaxCustomEvents, + limits.MaxCustomMaxEvents, limits.DefaultReportPeriod) if err != nil { log.Infof("Unexpected negative Custom event limit %d", rawLimits.CustomEventData) @@ -305,9 +305,10 @@ func (daemonConfig *EventHarvestConfig) UnmarshalJSON(b []byte) error { func NewHarvestLimits(agentLimits *EventConfigs) EventConfigs { // Check if we have agent limits to incorporate. - // Currently only max span events and max log events are configurable via the agent. + // Currently only max span, log, and custom events are configurable via the agent. spanEventLimit := limits.MaxSpanMaxEvents logEventLimit := limits.MaxLogMaxEvents + customEventLimit := limits.MaxCustomMaxEvents if agentLimits != nil { if (agentLimits.SpanEventConfig.Limit < limits.MaxSpanMaxEvents) && (agentLimits.SpanEventConfig.Limit >= 0) { @@ -317,6 +318,10 @@ func NewHarvestLimits(agentLimits *EventConfigs) EventConfigs { (agentLimits.LogEventConfig.Limit >= 0) { logEventLimit = agentLimits.LogEventConfig.Limit } + if (agentLimits.CustomEventConfig.Limit < limits.MaxCustomMaxEvents) && + (agentLimits.CustomEventConfig.Limit >= 0) { + customEventLimit = agentLimits.CustomEventConfig.Limit + } } return EventConfigs{ @@ -327,7 +332,7 @@ func NewHarvestLimits(agentLimits *EventConfigs) EventConfigs { Limit: limits.MaxTxnEvents, }, CustomEventConfig: Event{ - Limit: limits.MaxCustomEvents, + Limit: customEventLimit, }, SpanEventConfig: Event{ Limit: spanEventLimit, diff --git a/src/newrelic/commands.go b/src/newrelic/commands.go index f7a7379b9..a357def65 100644 --- a/src/newrelic/commands.go +++ b/src/newrelic/commands.go @@ -280,10 +280,11 @@ func UnmarshalAppInfo(tbl flatbuffers.Table) *AppInfo { info.initSettings(app.Settings()) // Of the four Event Limits (span, custom, analytic and error), - // only span events and log events are configurable from the agent. + // only span, log, and custom events are configurable from the agent. // If this changes in the future, the other values can be added here. info.AgentEventLimits.SpanEventConfig.Limit = int(app.SpanEventsMaxSamplesStored()) info.AgentEventLimits.LogEventConfig.Limit = int(app.LogEventsMaxSamplesStored()) + info.AgentEventLimits.CustomEventConfig.Limit = int(app.CustomEventsMaxSamplesStored()) return info } diff --git a/src/newrelic/limits/limits.go b/src/newrelic/limits/limits.go index 610a0f81f..90510fb06 100644 --- a/src/newrelic/limits/limits.go +++ b/src/newrelic/limits/limits.go @@ -34,7 +34,7 @@ const ( DefaultReportPeriod = 60 * time.Second MaxMetrics = 2 * 1000 MaxTxnEvents = 10 * 1000 - MaxCustomEvents = 10 * 1000 + MaxCustomMaxEvents = 100 * 1000 MaxErrorEvents = 100 MaxSpanMaxEvents = 10000 MaxLogMaxEvents = 20 * 1000 diff --git a/src/newrelic/processor.go b/src/newrelic/processor.go index 3b552b61d..011ca3b61 100644 --- a/src/newrelic/processor.go +++ b/src/newrelic/processor.go @@ -657,7 +657,7 @@ func harvestByType(ah *AppHarvest, args *harvestArgs, ht HarvestType, du_chan ch // This needs to be determined here, as even an empty harvest needs // to be overwritten with new containers for the next harvest. skip_data_usage := false - if (harvest.empty()) { + if harvest.empty() { skip_data_usage = true } // In many cases, all types are harvested diff --git a/src/newrelic/processor_test.go b/src/newrelic/processor_test.go index daa6826b2..bdeb150d7 100644 --- a/src/newrelic/processor_test.go +++ b/src/newrelic/processor_test.go @@ -208,6 +208,18 @@ func NewAppInfoWithLogEventLimit(limit int) AppInfo { return appInfo } +func NewAppInfoWithCustomEventLimit(limit int) AppInfo { + appInfo := sampleAppInfo + appInfo.AgentEventLimits = collector.EventConfigs{ + CustomEventConfig: collector.Event{ + Limit: limit, + ReportPeriod: 5000, + }, + } + + return appInfo +} + func TestProcessorHarvestDefaultData(t *testing.T) { m := NewMockedProcessor(2) @@ -431,7 +443,7 @@ func TestUsageHarvestExceedChannel(t *testing.T) { AppHarvest: m.p.harvests[idOne], ID: idOne, Type: HarvestTxnEvents, - Blocking: true, + Blocking: true, } /* collect txn data */ <-m.clientParams @@ -1396,7 +1408,7 @@ func TestShouldConnect(t *testing.T) { // runs a mocked test of resolution of agent harvest limit request and value returned by collector // Notes: -// eventType: "log_event_data" for log events (no others supported currently) +// eventType: "log_event_data" or "custom_event_data" (no others supported currently) // agentLimit: Harvest limit from agent (INI file) for a 60 second harvest period // collectorLimit: Harvest limit sent from collector for a 5 second harvest period // @@ -1409,13 +1421,18 @@ func runMockedCollectorHarvestLimitTest(t *testing.T, eventType string, agentLim // So the actual value used to compare to the collector // limit will be 1/12th (5s/60s) smaller var appInfo AppInfo = AppInfo{} + + // defaults that collector will send if no harvest limit requested in connect request logHarvestLimit := 833 + customHarvestLimit := 2500 switch eventType { case "log_event_data": logHarvestLimit = int(collectorLimit) appInfo = NewAppInfoWithLogEventLimit(int(agentLimit)) - + case "custom_event_data": + customHarvestLimit = int(collectorLimit) + appInfo = NewAppInfoWithCustomEventLimit(int(agentLimit)) default: t.Fatalf("%s: runMockedCollectorHarvestLimitTest() invalid eventType \"%s\" specified", testName, eventType) } @@ -1435,7 +1452,8 @@ func runMockedCollectorHarvestLimitTest(t *testing.T, eventType string, agentLim `","zip":"zap",` + `"span_event_harvest_config":{"report_period_ms":5000,"harvest_limit":166},` + `"event_harvest_config":{"report_period_ms":5000,` + - `"harvest_limits":{"analytics_event_data":833,"custom_event_data":833,"error_event_data":833,` + + `"harvest_limits":{"analytics_event_data":833, "error_event_data":833,` + + `"custom_event_data":` + strconv.Itoa(customHarvestLimit) + `,` + `"log_event_data":` + strconv.Itoa(logHarvestLimit) + `}}}` m.DoConnectConfiguredReply(t, mockedReply) @@ -1461,7 +1479,15 @@ func runMockedCollectorHarvestLimitTest(t *testing.T, eventType string, agentLim if scaledAgentLimit < collectorLimit { expectedLimit = scaledAgentLimit } - finalLimit := app.connectReply.EventHarvestConfig.EventConfigs.LogEventConfig.Limit + finalLimit := -1 + switch eventType { + case "log_event_data": + finalLimit = app.connectReply.EventHarvestConfig.EventConfigs.LogEventConfig.Limit + case "custom_event_data": + finalLimit = app.connectReply.EventHarvestConfig.EventConfigs.CustomEventConfig.Limit + default: + t.Fatalf("%s: runMockedCollectorHarvestLimitTest() invalid eventType \"%s\" specified", testName, eventType) + } if int(expectedLimit) != finalLimit { t.Errorf("\" %s \": Expected %d to be choosen but %d was instead", testName, expectedLimit, finalLimit) } @@ -1471,12 +1497,21 @@ func runMockedCollectorHarvestLimitTest(t *testing.T, eventType string, agentLim func TestConnectNegotiateLogEventLimits(t *testing.T) { - runMockedCollectorHarvestLimitTest(t, "log_event_data", 90*12, 100, "agent limit smaller than collector") - runMockedCollectorHarvestLimitTest(t, "log_event_data", 110*12, 100, "agent limit larger than collector") - runMockedCollectorHarvestLimitTest(t, "log_event_data", 100*12, 100, "agent limit equal to collector") - runMockedCollectorHarvestLimitTest(t, "log_event_data", 0, 100, "agent limit == 0, collector != 0") - runMockedCollectorHarvestLimitTest(t, "log_event_data", 100*12, 0, "agent limit != 0, collector == 0") + // these tests exist for the log events (TestConnectNegotiateLogEventsLimit()) because at some point + // the collector would return the default limit (833) if 0 was passed as the log limit. These tests + // exercise logic in the daemon to enforce the smaller agent value + runMockedCollectorHarvestLimitTest(t, "log_event_data", 90*12, 100, "agent log limit smaller than collector") + runMockedCollectorHarvestLimitTest(t, "log_event_data", 110*12, 100, "agent log limit larger than collector") + runMockedCollectorHarvestLimitTest(t, "log_event_data", 100*12, 100, "agent log limit equal to collector") + runMockedCollectorHarvestLimitTest(t, "log_event_data", 0, 100, "agent log limit == 0, collector != 0") + runMockedCollectorHarvestLimitTest(t, "log_event_data", 100*12, 0, "agent log limit != 0, collector == 0") + +} + +func TestConnectNegotiateCustomEventLimits(t *testing.T) { + runMockedCollectorHarvestLimitTest(t, "custom_event_data", 110*12, 100, "agent custom limit larger than collector") + runMockedCollectorHarvestLimitTest(t, "custom_event_data", 100*12, 100, "agent custom limit equal to collector") } func TestProcessLogEventLimit(t *testing.T) { diff --git a/src/newrelic/protocol/App.go b/src/newrelic/protocol/App.go index 7c9f732cf..eccafeb81 100644 --- a/src/newrelic/protocol/App.go +++ b/src/newrelic/protocol/App.go @@ -209,8 +209,20 @@ func (rcv *App) MutateLogEventsMaxSamplesStored(n uint64) bool { return rcv._tab.MutateUint64Slot(40, n) } +func (rcv *App) CustomEventsMaxSamplesStored() uint64 { + o := flatbuffers.UOffsetT(rcv._tab.Offset(42)) + if o != 0 { + return rcv._tab.GetUint64(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *App) MutateCustomEventsMaxSamplesStored(n uint64) bool { + return rcv._tab.MutateUint64Slot(42, n) +} + func AppStart(builder *flatbuffers.Builder) { - builder.StartObject(19) + builder.StartObject(20) } func AppAddLicense(builder *flatbuffers.Builder, license flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(license), 0) @@ -269,6 +281,9 @@ func AppAddMetadata(builder *flatbuffers.Builder, metadata flatbuffers.UOffsetT) func AppAddLogEventsMaxSamplesStored(builder *flatbuffers.Builder, logEventsMaxSamplesStored uint64) { builder.PrependUint64Slot(18, logEventsMaxSamplesStored, 0) } +func AppAddCustomEventsMaxSamplesStored(builder *flatbuffers.Builder, customEventsMaxSamplesStored uint64) { + builder.PrependUint64Slot(19, customEventsMaxSamplesStored, 0) +} func AppEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_0_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_0_limit.php new file mode 100644 index 000000000..5466c8452 --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_0_limit.php @@ -0,0 +1,29 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_100000_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_100000_limit.php new file mode 100644 index 000000000..1489d5fc0 --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_100000_limit.php @@ -0,0 +1,36 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_240_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_240_limit.php new file mode 100644 index 000000000..f01512dd0 --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_240_limit.php @@ -0,0 +1,36 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_30000_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_30000_limit.php new file mode 100644 index 000000000..a231ac30c --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_30000_limit.php @@ -0,0 +1,36 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_7000_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_7000_limit.php new file mode 100644 index 000000000..5d6339b1a --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_7000_limit.php @@ -0,0 +1,36 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toolarge_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toolarge_limit.php new file mode 100644 index 000000000..7ab609ac9 --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toolarge_limit.php @@ -0,0 +1,37 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toosmall_limit.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toosmall_limit.php new file mode 100644 index 000000000..b120dbe07 --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_invalid_toosmall_limit.php @@ -0,0 +1,37 @@ +$i)); +} \ No newline at end of file diff --git a/tests/event_limits/custom/test_custom_events_max_samples_stored_not_specified.php b/tests/event_limits/custom/test_custom_events_max_samples_stored_not_specified.php new file mode 100644 index 000000000..7b9ada200 --- /dev/null +++ b/tests/event_limits/custom/test_custom_events_max_samples_stored_not_specified.php @@ -0,0 +1,42 @@ +$i)); +} \ No newline at end of file diff --git a/tests/integration/attributes/test_transaction_closure_clm.php b/tests/integration/attributes/test_transaction_closure_clm.php new file mode 100644 index 000000000..cb6759465 --- /dev/null +++ b/tests/integration/attributes/test_transaction_closure_clm.php @@ -0,0 +1,166 @@ +edible = $edible; + $this->color = $color; + } + + public function isEdible() + { + return $this->edible; + } + + public function getColor() + { + echo "Yum\n"; + return $this->color; + } + } + + echo "two" . newrelic_add_custom_tracer("Foo\\Bar\\Vegetable::getColor"); + $veggie = new Vegetable(true, "blue"); + $veggie->getColor(); +} + diff --git a/tests/integration/attributes/test_transaction_namespace_clm.php b/tests/integration/attributes/test_transaction_namespace_clm.php new file mode 100644 index 000000000..5b8960354 --- /dev/null +++ b/tests/integration/attributes/test_transaction_namespace_clm.php @@ -0,0 +1,132 @@ +edible = $edible; + $this->color = $color; + } + + public function isEdible() + { + sleep(10); + return $this->edible; + } + + public function getColor() + { + echo "Yum\n"; + return $this->color; + } +} +newrelic_add_custom_tracer("Vegetable::getColor"); +$veggie = new Vegetable(true, "blue"); +$veggie->getColor(); diff --git a/tests/integration/attributes/test_transaction_namespace_len_clm.php b/tests/integration/attributes/test_transaction_namespace_len_clm.php new file mode 100644 index 000000000..5ed78f4dd --- /dev/null +++ b/tests/integration/attributes/test_transaction_namespace_len_clm.php @@ -0,0 +1,125 @@ +start = $start; + $this->lap = $lap; + } + + public function getLap() + { + echo "Beep\n"; + return $this->lap; + } +} +newrelic_add_custom_tracer("TheFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl::getLap"); +$pacer = new TheFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl(true, "0"); +$pacer->getLap(); diff --git a/tests/integration/attributes/test_transaction_nested_user_functions_clm.php b/tests/integration/attributes/test_transaction_nested_user_functions_clm.php new file mode 100644 index 000000000..b6bf788f9 --- /dev/null +++ b/tests/integration/attributes/test_transaction_nested_user_functions_clm.php @@ -0,0 +1,218 @@ +", + [ + [ + 0, + {}, + {}, + [ + "?? start time", "?? end time", "ROOT", "?? root attributes", + [ + [ + "?? start time", "?? end time", "`0", "?? node attributes", + [ + [ + "?? start time", "?? end time", "`1", + { + "code.lineno": 214, + "code.filepath": "__FILE__", + "code.function": "level_2" + }, + [ + [ + "?? start time", "?? end time", "`2", + { + "code.lineno": 210, + "code.filepath": "__FILE__", + "code.function": "level_1" + }, + [] + ] + ] + ] + ] + ] + ] + ], + { + "intrinsics": { + "totalTime": "??", + "cpu_time": "??", + "cpu_user_time": "??", + "cpu_sys_time": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??" + } + } + ], + [ + "OtherTransaction\/php__FILE__", + "Custom\/level_2", + "Custom\/level_1" + ] + ], + "?? txn guid", + "?? reserved", + "?? force persist", + "?? x-ray sessions", + null + ] + ] +] +*/ + +/* + * Normally super short duration functions are ignored. + * We'll force some to be noticed, while others should + * contrinue to be ignored. + */ +newrelic_add_custom_tracer("level_1"); +newrelic_add_custom_tracer("level_2"); + +function level_0() { + echo "level_0\n"; +} + +function level_1() { + level_0(); +} + +function level_2() { + level_1(); +} + +level_2(); diff --git a/tests/integration/attributes/test_transaction_non_web_clm.php b/tests/integration/attributes/test_transaction_non_web_clm.php new file mode 100644 index 000000000..a76fe5e7a --- /dev/null +++ b/tests/integration/attributes/test_transaction_non_web_clm.php @@ -0,0 +1,143 @@ +setFormatter($formatter); + + $logger->pushHandler($stdoutHandler); + + // insert delays between log messages to allow priority sampling + // to resolve that later messages have higher precedence + // since timestamps are only millisecond resolution + // without delays sometimes order in output will reflect + // all having the same timestamp. + $logger->debug("debug"); + usleep(10000); + +} + +test_logging(); \ No newline at end of file