From 71674c9d96f4027b4a49212343c791eecf57f338 Mon Sep 17 00:00:00 2001 From: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:55:52 -0500 Subject: [PATCH 1/6] chore: Bump version to 11.3.0 (#967) --- VERSION | 2 +- axiom/nr_version.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index b85c6c7b0..f628d2eaf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -11.2.0 +11.3.0 diff --git a/axiom/nr_version.c b/axiom/nr_version.c index e608278e9..2c950be4f 100644 --- a/axiom/nr_version.c +++ b/axiom/nr_version.c @@ -21,9 +21,8 @@ #endif /* - * Current version naming scheme is flowers + * Current version naming scheme is gemstones * - * dahlia 19Sep2022 (10.1) * echinacea 03Oct2022 (10.2) * freesia 03Nov2022 (10.3) * goldenrod 12Dec2022 (10.4) @@ -47,8 +46,9 @@ * yarrow 26Jun2024 (10.22) * zinnia 30Jul2024 (11.0) * amethyst 26Aug2024 (11.1) + * bowenite 30Sep2024 (11.2) */ -#define NR_CODENAME "bowenite" +#define NR_CODENAME "corundum" const char* nr_version(void) { return NR_STR2(NR_VERSION); From aa15a2d49ca7cbb3a7c8856102fa10dbca99f938 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Fri, 4 Oct 2024 09:02:44 -0600 Subject: [PATCH 2/6] feat(agent): Memcached instance metrics with host name (#958) These metrics can be used by the backend to create AWS relationship maps with memcached --------- Co-authored-by: Michal Nowacki --- agent/Makefile.frag | 1 + agent/config.m4 | 2 +- agent/php_internal_instrument.c | 59 ++++++++ agent/php_memcached.c | 34 +++++ agent/php_memcached.h | 35 +++++ agent/tests/test_memcached.c | 127 ++++++++++++++++++ .../memcached/test_add_servers.php | 39 ++++++ .../memcached/test_add_servers_bad.php | 68 ++++++++++ tests/integration/memcached/test_basic.php | 1 + .../memcached/test_basic_logging_off.php | 1 + tests/integration/memcached/test_by_key.php | 1 + tests/integration/memcached/test_cas.php7.php | 1 + .../memcached/test_cas_by_key.php7.php | 1 + tests/integration/memcached/test_concat.php | 1 + .../memcached/test_concat_by_key.php | 1 + .../test_concat_by_key_logging_off.php | 1 + tests/integration/memcached/test_multi.php | 1 + .../memcached/test_multi_by_key.php | 1 + tests/integration/memcached/test_socket.php | 30 +++++ 19 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 agent/php_memcached.c create mode 100644 agent/php_memcached.h create mode 100644 agent/tests/test_memcached.c create mode 100644 tests/integration/memcached/test_add_servers.php create mode 100644 tests/integration/memcached/test_add_servers_bad.php create mode 100644 tests/integration/memcached/test_socket.php diff --git a/agent/Makefile.frag b/agent/Makefile.frag index 648fc84b3..fbff46d69 100644 --- a/agent/Makefile.frag +++ b/agent/Makefile.frag @@ -93,6 +93,7 @@ TEST_BINARIES = \ tests/test_internal_instrument \ tests/test_hash \ tests/test_lib_aws_sdk_php \ + tests/test_memcached \ tests/test_mongodb \ tests/test_monolog \ tests/test_mysql \ diff --git a/agent/config.m4 b/agent/config.m4 index ee0859e30..5806caac5 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -215,7 +215,7 @@ if test "$PHP_NEWRELIC" = "yes"; then php_error.c php_execute.c php_explain.c php_explain_mysqli.c \ php_explain_pdo_mysql.c php_extension.c php_file_get_contents.c \ php_globals.c php_hash.c php_header.c php_httprequest_send.c \ - php_internal_instrument.c php_minit.c php_mshutdown.c php_mysql.c \ + php_internal_instrument.c php_memcached.c php_minit.c php_mshutdown.c php_mysql.c \ php_mysqli.c php_newrelic.c php_nrini.c php_observer.c php_output.c php_pdo.c \ php_pdo_mysql.c php_pdo_pgsql.c php_pgsql.c php_psr7.c php_redis.c \ php_rinit.c php_rshutdown.c php_samplers.c php_stack.c \ diff --git a/agent/php_internal_instrument.c b/agent/php_internal_instrument.c index 821379edb..ce6c089ec 100644 --- a/agent/php_internal_instrument.c +++ b/agent/php_internal_instrument.c @@ -13,8 +13,10 @@ #include "php_explain_mysqli.h" #include "php_file_get_contents.h" #include "php_globals.h" +#include "php_hash.h" #include "php_httprequest_send.h" #include "php_internal_instrument.h" +#include "php_memcached.h" #include "php_mysql.h" #include "php_mysqli.h" #include "php_pdo.h" @@ -1531,6 +1533,57 @@ NR_INNER_WRAPPER(memcache_function) { INTERNAL_FUNCTION_PARAM_PASSTHRU); } +NR_INNER_WRAPPER(memcached_add_server) { + char* host = NULL; + nr_string_len_t host_len = 0; + zend_long port = 0; + zend_long weight = 0; + int zcaught = 0; + + if (SUCCESS + == zend_parse_parameters_ex( + ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "s|ll", &host, + &host_len, &port, &weight) && + NULL != host) { + nr_php_memcached_create_instance_metric(host, port); + } + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, + INTERNAL_FUNCTION_PARAM_PASSTHRU); + if (zcaught) { + zend_bailout(); + /* NOTREACHED */ + } +} + +NR_INNER_WRAPPER(memcached_add_servers) { + zval* servers = NULL; + zval* server = NULL; + int zcaught = 0; + + if (SUCCESS + == zend_parse_parameters_ex( + ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "a", &servers)) { + if (NULL != servers && Z_TYPE_P(servers) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(servers), server) { + zval* host = nr_php_zend_hash_index_find(Z_ARRVAL_P(server), 0); + zval* port = nr_php_zend_hash_index_find(Z_ARRVAL_P(server), 1); + if (nr_php_is_zval_valid_string(host) && + nr_php_is_zval_valid_integer(port)) { + nr_php_memcached_create_instance_metric(Z_STRVAL_P(host), Z_LVAL_P(port)); + } + } + ZEND_HASH_FOREACH_END(); + } + } + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, + INTERNAL_FUNCTION_PARAM_PASSTHRU); + + if (zcaught) { + zend_bailout(); + /* NOTREACHED */ + } +} + /* * Handle * bool redis::connect ( string $host[, int $port = 6379 ... ] ) @@ -3098,6 +3151,8 @@ NR_OUTER_WRAPPER(memcached_set) NR_OUTER_WRAPPER(memcached_setbykey) NR_OUTER_WRAPPER(memcached_setmulti) NR_OUTER_WRAPPER(memcached_setmultibykey) +NR_OUTER_WRAPPER(memcached_addserver) +NR_OUTER_WRAPPER(memcached_addservers) NR_OUTER_WRAPPER(redis_append) NR_OUTER_WRAPPER(redis_bitcount) @@ -3511,6 +3566,10 @@ void nr_php_generate_internal_wrap_records(void) { memcache_function, 0, "set") NR_INTERNAL_WRAPREC("memcached::setmultibykey", memcached_setmultibykey, memcache_function, 0, "set") + NR_INTERNAL_WRAPREC("memcached::addserver", memcached_addserver, + memcached_add_server, 0, 0); + NR_INTERNAL_WRAPREC("memcached::addservers", memcached_addservers, + memcached_add_servers, 0, 0); NR_INTERNAL_WRAPREC("redis::connect", redis_connect, redis_connect, 0, "connect") diff --git a/agent/php_memcached.c b/agent/php_memcached.c new file mode 100644 index 000000000..ee13e1e73 --- /dev/null +++ b/agent/php_memcached.c @@ -0,0 +1,34 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "php_memcached.h" +#include "nr_datastore_instance.h" +#include "php_agent.h" + +nr_datastore_instance_t* nr_php_memcached_create_datastore_instance( + const char* host_or_socket, + zend_long port) { + nr_datastore_instance_t* instance = NULL; + if (port == 0) { // local socket + instance = nr_datastore_instance_create("localhost", host_or_socket, NULL); + } else { + char* port_str = nr_formatf("%ld", (long)port); + instance = nr_datastore_instance_create(host_or_socket, port_str, NULL); + nr_free(port_str); + } + return instance; +} + +void nr_php_memcached_create_instance_metric( + const char* host_or_socket, + zend_long port) { + nr_datastore_instance_t* instance + = nr_php_memcached_create_datastore_instance(host_or_socket, port); + char* instance_metric = nr_formatf("Datastore/instance/Memcached/%s/%s", + instance->host, instance->port_path_or_id); + nrm_force_add(NRPRG(txn)->unscoped_metrics, instance_metric, 0); + nr_datastore_instance_destroy(&instance); + nr_free(instance_metric); +} diff --git a/agent/php_memcached.h b/agent/php_memcached.h new file mode 100644 index 000000000..f449030ea --- /dev/null +++ b/agent/php_memcached.h @@ -0,0 +1,35 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef PHP_MEMCACHED_HDR +#define PHP_MEMCACHED_HDR + +#include "nr_datastore_instance.h" +#include "php_includes.h" + +/* + * Purpose : Create a datastore instance metadata for a Memcached server. + * + * Params : 1. The memcached host or socket name as given to Memcached::addServer(). + * 2. The memcached port as given as given to Memcached::addServer(). + * + * Returns: nr_datastore_instance_t* that the caller is responsible for freeing + */ +nr_datastore_instance_t* nr_php_memcached_create_datastore_instance( + const char* host_or_socket, + zend_long port); + +/* + * Purpose : Create a memcached instance metric + * + * Params : 1. The memcached host or socket name as given to Memcached::addServer(). + * 2. The memcached port as given as given to Memcached::addServer(). + */ +extern void nr_php_memcached_create_instance_metric( + const char* host_or_socket, + zend_long port); + + +#endif diff --git a/agent/tests/test_memcached.c b/agent/tests/test_memcached.c new file mode 100644 index 000000000..1f05c2443 --- /dev/null +++ b/agent/tests/test_memcached.c @@ -0,0 +1,127 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#include "tlib_php.h" +#include "tlib_datastore.h" + +#include "php_agent.h" +#include "php_memcached.h" +#include "util_system.h" + +tlib_parallel_info_t parallel_info + = {.suggested_nthreads = -1, .state_size = 0}; + +static char* system_host_name; + +static void test_create_datastore_instance(void) { + assert_datastore_instance_equals_destroy( + "named socket", + &((nr_datastore_instance_t){ + .host = system_host_name, + .database_name = "unknown", + .port_path_or_id = "/tmp/memcached.sock", + }), + nr_php_memcached_create_datastore_instance("/tmp/memcached.sock", 0)); + + assert_datastore_instance_equals_destroy( + "empty socket", + &((nr_datastore_instance_t){ + .host = system_host_name, + .database_name = "unknown", + .port_path_or_id = "unknown", + }), + nr_php_memcached_create_datastore_instance("", 0)); + + assert_datastore_instance_equals_destroy( + "empty host", + &((nr_datastore_instance_t){ + .host = system_host_name, + .database_name = "unknown", + .port_path_or_id = "unknown", + }), + nr_php_memcached_create_datastore_instance(NULL, 0)); + + assert_datastore_instance_equals_destroy( + "host.name socket", + &((nr_datastore_instance_t){ + .host = "host.name", + .database_name = "unknown", + .port_path_or_id = "11211", + }), + nr_php_memcached_create_datastore_instance("host.name", 11211)); + + assert_datastore_instance_equals_destroy( + "host and port", + &((nr_datastore_instance_t){ + .host = "unknown", + .database_name = "unknown", + .port_path_or_id = "6379", + }), + nr_php_memcached_create_datastore_instance("", 6379)); + + assert_datastore_instance_equals_destroy( + "NULL socket", + &((nr_datastore_instance_t){ + .host = "unknown", + .database_name = "unknown", + .port_path_or_id = "11211", + }), + nr_php_memcached_create_datastore_instance(NULL, 11211)); +} + +static void test_create_instance_metric(void) { + nrtxn_t* txn; + nrmetric_t* metric; + char* metric_str; + tlib_php_engine_create(""); + tlib_php_request_start(); + txn = NRPRG(txn); + + nr_php_memcached_create_instance_metric("host", 11211); + metric = nrm_find(txn->unscoped_metrics, "Datastore/instance/Memcached/host/11211"); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric("", 11211); + metric = nrm_find(txn->unscoped_metrics, "Datastore/instance/Memcached/unknown/11211"); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric(NULL, 7); + metric = nrm_find(txn->unscoped_metrics, "Datastore/instance/Memcached/unknown/7"); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric("path/to/sock", 0); + metric_str = nr_formatf("Datastore/instance/Memcached/%s/path/to/sock", system_host_name); + metric = nrm_find(txn->unscoped_metrics, metric_str); + nr_free(metric_str); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric("", 0); + metric_str = nr_formatf("Datastore/instance/Memcached/%s/unknown", system_host_name); + metric = nrm_find(txn->unscoped_metrics, metric_str); + nr_free(metric_str); + tlib_pass_if_not_null("metric found", metric); + + // restart the transaction because the next metric is the same as a previous metric + tlib_php_request_end(); + tlib_php_request_start(); + txn = NRPRG(txn); + + nr_php_memcached_create_instance_metric(NULL, 0); + metric_str = nr_formatf("Datastore/instance/Memcached/%s/unknown", system_host_name); + metric = nrm_find(txn->unscoped_metrics, metric_str); + nr_free(metric_str); + tlib_pass_if_not_null("metric found", metric); + + tlib_php_request_end(); + tlib_php_engine_destroy(); +} + +void test_main(void* p NRUNUSED) { + system_host_name = nr_system_get_hostname(); + + test_create_datastore_instance(); + test_create_instance_metric(); + + nr_free(system_host_name); +} diff --git a/tests/integration/memcached/test_add_servers.php b/tests/integration/memcached/test_add_servers.php new file mode 100644 index 000000000..41e9b5a63 --- /dev/null +++ b/tests/integration/memcached/test_add_servers.php @@ -0,0 +1,39 @@ + +*/ + +/*INI +*/ + +/*EXPECT_METRICS_EXIST +Datastore/instance/Memcached/host1/1, 1 +Datastore/instance/Memcached/host2/2, 1 +Datastore/instance/Memcached/host3/11211, 1 +Datastore/instance/Memcached/host4/1, 1 +*/ + +/*EXPECT_ERROR_EVENTS null */ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +$memcached = new Memcached(); +$memcached->addServers(array( + array("host1", 1), + array("host2", 2), + array("host3", 11211))); +$memcached->addServers(array()); +$memcached->addServers(array(array("host4", 1, "test field"))); +$memcached->quit(); diff --git a/tests/integration/memcached/test_add_servers_bad.php b/tests/integration/memcached/test_add_servers_bad.php new file mode 100644 index 000000000..c4887584f --- /dev/null +++ b/tests/integration/memcached/test_add_servers_bad.php @@ -0,0 +1,68 @@ + +*/ + +/*INI +*/ + +/*EXPECT_REGEX + +.*(PHP )?Warning:.*could not add entry.* + +.*(PHP )?Warning:.*could not add entry.* + +*/ + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": "??", + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "E_WARNING", + "error.message": "Memcached::addServers(): could not add entry #2 to the server list", + "transactionName": "OtherTransaction\/php__FILE__", + "duration": "??", + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + {} + ] + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +$memcached = new Memcached(); +$memcached->addServer(5, 5); +//$memcached->addServer("host", string); crashes PHP +$memcached->addServers(array(array(1))); +$memcached->addServers(array(array("host1"))); +$memcached->addServers(array(array(1, "host1"))); +//$memcahed->addServers("string"); crashes PHP +$memcached->quit(); diff --git a/tests/integration/memcached/test_basic.php b/tests/integration/memcached/test_basic.php index 6ccde57b7..1e24bfd7e 100644 --- a/tests/integration/memcached/test_basic.php +++ b/tests/integration/memcached/test_basic.php @@ -46,6 +46,7 @@ [{"name":"Datastore/allOther"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_basic_logging_off.php b/tests/integration/memcached/test_basic_logging_off.php index 521fc1d44..05e87a4c1 100644 --- a/tests/integration/memcached/test_basic_logging_off.php +++ b/tests/integration/memcached/test_basic_logging_off.php @@ -49,6 +49,7 @@ [{"name":"Datastore/allOther"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_by_key.php b/tests/integration/memcached/test_by_key.php index 006d6cd7d..ceb2758d8 100644 --- a/tests/integration/memcached/test_by_key.php +++ b/tests/integration/memcached/test_by_key.php @@ -48,6 +48,7 @@ [{"name":"Datastore/allOther"}, [11, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [11, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [11, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_cas.php7.php b/tests/integration/memcached/test_cas.php7.php index b69010e59..650bb22ec 100644 --- a/tests/integration/memcached/test_cas.php7.php +++ b/tests/integration/memcached/test_cas.php7.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_cas_by_key.php7.php b/tests/integration/memcached/test_cas_by_key.php7.php index f43a6dc29..10b073016 100644 --- a/tests/integration/memcached/test_cas_by_key.php7.php +++ b/tests/integration/memcached/test_cas_by_key.php7.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_concat.php b/tests/integration/memcached/test_concat.php index 092bd5683..0a526adcd 100644 --- a/tests/integration/memcached/test_concat.php +++ b/tests/integration/memcached/test_concat.php @@ -35,6 +35,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_concat_by_key.php b/tests/integration/memcached/test_concat_by_key.php index 7339eed12..2cb041974 100644 --- a/tests/integration/memcached/test_concat_by_key.php +++ b/tests/integration/memcached/test_concat_by_key.php @@ -35,6 +35,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_concat_by_key_logging_off.php b/tests/integration/memcached/test_concat_by_key_logging_off.php index d4e3f2d60..4d644a74d 100644 --- a/tests/integration/memcached/test_concat_by_key_logging_off.php +++ b/tests/integration/memcached/test_concat_by_key_logging_off.php @@ -38,6 +38,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_multi.php b/tests/integration/memcached/test_multi.php index 82328762a..78dbd2fa3 100644 --- a/tests/integration/memcached/test_multi.php +++ b/tests/integration/memcached/test_multi.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get"}, [4, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get", "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_multi_by_key.php b/tests/integration/memcached/test_multi_by_key.php index d5867c36d..5d4c2e9d3 100644 --- a/tests/integration/memcached/test_multi_by_key.php +++ b/tests/integration/memcached/test_multi_by_key.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get"}, [4, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get", "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_socket.php b/tests/integration/memcached/test_socket.php new file mode 100644 index 000000000..19a1787e9 --- /dev/null +++ b/tests/integration/memcached/test_socket.php @@ -0,0 +1,30 @@ + +*/ + +/*INI +*/ + +/*EXPECT_METRICS_EXIST +Datastore/instance/Memcached/__HOST__/my/socket, 1 +*/ + +/*EXPECT_ERROR_EVENTS null */ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +$memcached = new Memcached(); +$memcached->addServer("my/socket", 0); +$memcached->quit(); From f9530ed288f4ad47f10353590dd41ad877ac8800 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Mon, 7 Oct 2024 22:58:05 -0400 Subject: [PATCH 3/6] refactor(agent): improve magic file recognition performance (#970) Speed up package detection by performing a suffix match on the 'magic' file pattern with case insensitive string comparison instead of a substring search within a lowercased filename. This is possible because all of the 'magic' file search patterns patterns are right anchored. Fixup of e11b992c with changes from 24c1c656. --- agent/php_execute.c | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index 83ff4d5a9..e54dc7828 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -603,14 +603,17 @@ static size_t num_logging_frameworks typedef struct _nr_vuln_mgmt_table_t { const char* package_name; const char* file_to_check; + size_t file_to_check_len; nr_vuln_mgmt_enable_fn_t enable; } nr_vuln_mgmt_table_t; /* Note that all paths should be in lowercase. */ +// clang-format: off static const nr_vuln_mgmt_table_t vuln_mgmt_packages[] = { - {"Drupal", "drupal/component/dependencyinjection/container.php", nr_drupal_version}, - {"Wordpress", "wp-includes/version.php", nr_wordpress_version}, + {"Drupal", NR_PSTR("drupal/component/dependencyinjection/container.php"), nr_drupal_version}, + {"Wordpress", NR_PSTR("wp-includes/version.php"), nr_wordpress_version}, }; +// clang-format: on static const size_t num_packages = sizeof(vuln_mgmt_packages) / sizeof(nr_vuln_mgmt_table_t); @@ -990,28 +993,22 @@ static void nr_execute_handle_logging_framework(const char* filename, } } -#undef STR_AND_LEN - -static void nr_execute_handle_package(const char* filename) { - if (NULL == filename || 0 >= nr_strlen(filename)) { - nrl_verbosedebug(NRL_FRAMEWORK, "%s: The file name is NULL", - __func__); - return; - } - char* filename_lower = nr_string_to_lowercase(filename); +static void nr_execute_handle_package(const char* filename, + const size_t filename_len) { size_t i = 0; for (i = 0; i < num_packages; i++) { - if (nr_stridx(filename_lower, vuln_mgmt_packages[i].file_to_check) >= 0) { + if (nr_striendswith(STR_AND_LEN(filename), + STR_AND_LEN(vuln_mgmt_packages[i].file_to_check))) { if (NULL != vuln_mgmt_packages[i].enable) { vuln_mgmt_packages[i].enable(); } } } - - nr_free(filename_lower); } +#undef STR_AND_LEN + /* * Purpose : Detect library and framework usage from a PHP file. * @@ -1036,7 +1033,7 @@ static void nr_php_user_instrumentation_from_file(const char* filename, nr_execute_handle_autoload(filename, filename_len); nr_execute_handle_logging_framework(filename, filename_len TSRMLS_CC); if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_execute_handle_package(filename); + nr_execute_handle_package(filename, filename_len); } } From 7a5f848e99900715e5b2594a83b6abb9cb1d7cc1 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Tue, 15 Oct 2024 00:22:49 -0400 Subject: [PATCH 4/6] refactor(agent): improve magic file recognition performance (#975) Speed up magic file recognition performance by removing files from `libraries` and `logging_frameworks` that belong to old and un-instrumented packages. --- agent/php_execute.c | 55 ------------------- .../analog/test_supportability_metric.php | 25 --------- .../analog/analog/lib/Analog/Analog.php | 6 -- .../test_supportability_metric.php | 24 -------- .../cakephp-log/vendor/cakephp/log/Log.php | 6 -- 5 files changed, 116 deletions(-) delete mode 100644 tests/integration/logging/analog/test_supportability_metric.php delete mode 100644 tests/integration/logging/analog/vendor/analog/analog/lib/Analog/Analog.php delete mode 100644 tests/integration/logging/cakephp-log/test_supportability_metric.php delete mode 100644 tests/integration/logging/cakephp-log/vendor/cakephp/log/Log.php diff --git a/agent/php_execute.c b/agent/php_execute.c index e54dc7828..e8e0db9ca 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -523,59 +523,7 @@ static nr_library_table_t libraries[] = { */ {"Laminas_Http", NR_PSTR("laminas-http/src/client.php"), nr_laminas_http_enable}, - /* - * Other frameworks, detected only, but not specifically - * instrumented. We detect these as libraries so that we don't prevent - * detection of a supported framework or library later (since a transaction - * can only have one framework). - */ - {"Aura1", NR_PSTR("aura/framework/system.php"), NULL}, - {"Aura2", NR_PSTR("aura/di/src/containerinterface.php"), NULL}, - {"Aura3", NR_PSTR("aura/di/src/containerconfiginterface.php"), NULL}, {"CakePHP3", NR_PSTR("cakephp/src/core/functions.php"), NULL}, - {"Fuel", NR_PSTR("fuel/core/classes/fuel.php"), NULL}, - {"Lithium", NR_PSTR("lithium/core/libraries.php"), NULL}, - {"Phpbb", NR_PSTR("phpbb/request/request.php"), NULL}, - {"Phpixie2", NR_PSTR("phpixie/core/classes/phpixie/pixie.php"), NULL}, - {"Phpixie3", NR_PSTR("phpixie/framework.php"), NULL}, - {"React", NR_PSTR("react/event-loop/src/loopinterface.php"), NULL}, - {"SilverStripe", NR_PSTR("injector/silverstripeinjectioncreator.php"), NULL}, - {"SilverStripe4", NR_PSTR("silverstripeserviceconfigurationlocator.php"), NULL}, - {"Typo3", NR_PSTR("classes/typo3/flow/core/bootstrap.php"), NULL}, - {"Typo3", NR_PSTR("typo3/sysext/core/classes/core/bootstrap.php"), NULL}, - - /* - * Other CMS (content management systems), detected only, but - * not specifically instrumented. - */ - {"Moodle", NR_PSTR("moodlelib.php"), NULL}, - /* - * It is likely that this will never be found, since the CodeIgniter.php - * will get loaded first, and as such mark this transaction as belonging to - * CodeIgniter, and not Expession Engine. - */ - {"ExpressionEngine", NR_PSTR("system/expressionengine/config/config.php"), NULL}, - /* - * ExpressionEngine 5, however, has a very obvious file we can look for. - */ - {"ExpressionEngine5", NR_PSTR("expressionengine/boot/boot.php"), NULL}, - /* - * DokuWiki uses doku.php as an entry point, but has other files that are - * loaded directly that this won't pick up. That's probably OK for - * supportability metrics, but we'll add the most common name for the - * configuration file as well just in case. - */ - {"DokuWiki", NR_PSTR("doku.php"), NULL}, - {"DokuWiki", NR_PSTR("conf/dokuwiki.php"), NULL}, - - /* - * SugarCRM no longer has a community edition, so this likely only works - * with older versions. - */ - {"SugarCRM", NR_PSTR("sugarobjects/sugarconfig.php"), NULL}, - - {"Xoops", NR_PSTR("class/xoopsload.php"), NULL}, - {"E107", NR_PSTR("e107_handlers/e107_class.php"), NULL}, }; // clang-format: on @@ -590,9 +538,6 @@ static nr_library_table_t logging_frameworks[] = { /* laminas-log - Logging for PHP */ {"laminas-log", NR_PSTR("laminas-log/src/logger.php"), NULL}, /* cakephp-log - Logging for PHP */ - {"cakephp-log", NR_PSTR("cakephp/log/log.php"), NULL}, - /* Analog - Logging for PHP */ - {"Analog", NR_PSTR("analog/analog.php"), NULL}, }; // clang-format: on diff --git a/tests/integration/logging/analog/test_supportability_metric.php b/tests/integration/logging/analog/test_supportability_metric.php deleted file mode 100644 index f3cfb4477..000000000 --- a/tests/integration/logging/analog/test_supportability_metric.php +++ /dev/null @@ -1,25 +0,0 @@ - Date: Tue, 15 Oct 2024 13:08:33 -0600 Subject: [PATCH 5/6] fix(installer): tarball installer checks for ini in mods-available (#971) Debian uses a directory called "mods-available" to house ini files. It's `phpenmod` and `phpdismod` allow command-line management of extensions in directory. It also creates a symlink ini file to this "mods-available" directory in the normal conf.d directory. The issue is that this symlink is prefixed with a number (for example 20-newrelic.ini). This prefix prevents our tarball installer from recognizing that a newrelic.ini already exists, resulting in 2 ini files: a newrelic.ini and a 20-newrelic.ini. Our .deb package installer scouts out the "mods-available" directory and installs therein when possible. That means that if a customer installs via package but upgrades via tarball (not recommended), then their system gets the duplicates. This change makes our tarball installer scout for a potential previous installation in mods-available. Fixes https://github.com/newrelic/newrelic-php-agent/issues/399 --------- Co-authored-by: Michal Nowacki --- agent/newrelic-install.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/agent/newrelic-install.sh b/agent/newrelic-install.sh index d0959b252..9ef1e45fd 100755 --- a/agent/newrelic-install.sh +++ b/agent/newrelic-install.sh @@ -1371,6 +1371,25 @@ EOF if [ -d "${cfg_pfx}/fpm/conf.d" ]; then pi_inidir_dso="${cfg_pfx}/fpm/conf.d" fi + + # + # Debian can use a mods-available directory to store the ini files. + # It creates a symlink from the ini file in the conf.d directory that + # our installer can fail to find (because the symlink is prefixed with + # "20-" (notably the number can change based on configurations). + # While this install script will not install into the mods-available + # directory, our .deb installer can. Therefore, we want to detect if + # newrelic has previously been installed in the mods-available directory + # so that we do not create an additional ini file -- which would result in + # the conf.d directory having both newrelic.ini and 20-newrelic.ini. + # + + if [ -d "${cfg_pfx}/mods-available" -a -f "${cfg_pfx}/mods-available/newrelic.ini" ]; then + pi_inidir_cli="${cfg_pfx}/mods-available" + if [ -n "${pi_inidir_dso}" ]; then + pi_inidir_dso="${cfg_pfx}/mods-available" + fi + fi fi done From dc162a4eb72fa9faad2d982350e5362ed7cdd83b Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Fri, 18 Oct 2024 10:30:16 -0400 Subject: [PATCH 6/6] fix(agent): don't skip arguments when calling `mysqli::real_connect` (#976) Co-authored-by: Konstantin Kovshenin Co-authored-by: Amber Sistla --- agent/php_mysqli.c | 61 ++++--- docker-compose.yaml | 9 ++ .../mysqli/test_explain_connect_socket.php | 148 +++++++++++++++++ .../mysqli/test_explain_construct_socket.php | 150 ++++++++++++++++++ 4 files changed, 342 insertions(+), 26 deletions(-) create mode 100644 tests/integration/mysqli/test_explain_connect_socket.php create mode 100644 tests/integration/mysqli/test_explain_construct_socket.php diff --git a/agent/php_mysqli.c b/agent/php_mysqli.c index b17fd2491..24a321b02 100644 --- a/agent/php_mysqli.c +++ b/agent/php_mysqli.c @@ -398,42 +398,51 @@ static nr_status_t nr_php_mysqli_link_real_connect( zval* link, const nr_mysqli_metadata_link_t* metadata TSRMLS_DC) { zend_ulong argc = 0; + zend_ulong arg_required = 0; zval* argv[7] = {0}; zend_ulong i; zval* retval = NULL; -#define ADD_IF_INT_SET(args, argc, value) \ - if (value) { \ - args[argc] = nr_php_zval_alloc(); \ - ZVAL_LONG(args[argc], value); \ - argc++; \ - } - -#define ADD_IF_STR_SET(args, argc, value) \ - if (value) { \ - args[argc] = nr_php_zval_alloc(); \ - nr_php_zval_str(args[argc], value); \ - argc++; \ - } - - ADD_IF_STR_SET(argv, argc, +#define ADD_IF_INT_SET(null_ok, args, argc, value) \ + if (value) { \ + args[argc] = nr_php_zval_alloc(); \ + ZVAL_LONG(args[argc], value); \ + argc++; \ + } else if (true == null_ok) { \ + args[argc] = nr_php_zval_alloc(); \ + ZVAL_NULL(args[argc]); \ + argc++; \ + } + +#define ADD_IF_STR_SET(null_ok, args, argc, value) \ + if (value) { \ + args[argc] = nr_php_zval_alloc(); \ + nr_php_zval_str(args[argc], value); \ + argc++; \ + } else if (true == null_ok) { \ + args[argc] = nr_php_zval_alloc(); \ + ZVAL_NULL(args[argc]); \ + argc++; \ + } + + ADD_IF_STR_SET(false, argv, argc, nr_php_mysqli_strip_persistent_prefix(metadata->host)); - ADD_IF_STR_SET(argv, argc, metadata->user); - ADD_IF_STR_SET(argv, argc, metadata->password); + ADD_IF_STR_SET(false, argv, argc, metadata->user); + ADD_IF_STR_SET(false, argv, argc, metadata->password); /* * We can only add the remaining metadata fields if we already have three * arguments (host, user and password) above, lest we accidentally set the - * wrong positional argument to something it doesn't mean. + * wrong positional argument to something it doesn't mean. Note, prior + * to 7.4 not all args are nullable. */ + arg_required = argc; if (argc == 3) { - ADD_IF_STR_SET(argv, argc, metadata->database); - ADD_IF_INT_SET(argv, argc, metadata->port); - ADD_IF_STR_SET(argv, argc, metadata->socket); - ADD_IF_INT_SET(argv, argc, metadata->flags); - } - - retval = nr_php_call_user_func(link, "real_connect", argc, argv TSRMLS_CC); + ADD_IF_STR_SET(true, argv, argc, metadata->database); + ADD_IF_INT_SET(true, argv, argc, metadata->port); + ADD_IF_STR_SET(true, argv, argc, metadata->socket); + ADD_IF_INT_SET(false, argv, argc, metadata->flags); + } retval = nr_php_call_user_func(link, "real_connect", argc, argv TSRMLS_CC); for (i = 0; i < argc; i++) { nr_php_zval_free(&argv[i]); @@ -450,7 +459,7 @@ static nr_status_t nr_php_mysqli_link_real_connect( * If we didn't specify the database in the connection parameters, we need to * call mysqli::select_db here. */ - if (metadata->database && (argc < 4)) { + if (metadata->database && (arg_required < 3)) { zval* database = nr_php_zval_alloc(); nr_php_zval_str(database, metadata->database); diff --git a/docker-compose.yaml b/docker-compose.yaml index 4d57753ff..2ea1d11d3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -24,6 +24,8 @@ services: retries: 3 start_period: 20s container_name: mysqldb + volumes: + - var-run-mysqld:/var/run/mysqld redisdb: image: redis restart: always @@ -56,6 +58,7 @@ services: MYSQL_USER: admin MYSQL_PASSWD: admin MYSQL_HOST: mysqldb + MYSQL_SOCKET: /var/run/mysqld/mysqld.sock PG_HOST: postgres PG_PORT: 5432 @@ -67,6 +70,7 @@ services: volumes: - ${AGENT_CODE:-$PWD}:/usr/local/src/newrelic-php-agent + - var-run-mysqld:/var/run/mysqld entrypoint: tail command: -f /dev/null container_name: nr-php @@ -83,6 +87,7 @@ services: MYSQL_USER: admin MYSQL_PASSWD: admin MYSQL_HOST: mysqldb + MYSQL_SOCKET: /var/run/mysqld/mysqld.sock PG_HOST: postgres PG_PORT: 5432 @@ -97,8 +102,12 @@ services: NEWRELIC_LICENSE_KEY: ${NEW_RELIC_LICENSE_KEY} volumes: - ${PWD}:/usr/src/myapp + - var-run-mysqld:/var/run/mysqld working_dir: /usr/src/myapp stdin_open: true tty: true container_name: agent-devenv profiles: ["dev"] + +volumes: + var-run-mysqld: diff --git a/tests/integration/mysqli/test_explain_connect_socket.php b/tests/integration/mysqli/test_explain_connect_socket.php new file mode 100644 index 000000000..0c56e3bc9 --- /dev/null +++ b/tests/integration/mysqli/test_explain_connect_socket.php @@ -0,0 +1,148 @@ +", + "?? SQL ID", + "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + "Datastore/statement/MySQL/tables/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_NAME", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in test_prepare called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/config.php'); + +function test_prepare($link) +{ + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name='STATISTICS'"; + + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $value)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + while (mysqli_stmt_fetch($stmt)) { + echo $value . "\n"; + } + + mysqli_stmt_close($stmt); +} + +$link = mysqli_connect('localhost', $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, null, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +test_prepare($link); +mysqli_close($link); diff --git a/tests/integration/mysqli/test_explain_construct_socket.php b/tests/integration/mysqli/test_explain_construct_socket.php new file mode 100644 index 000000000..20c6e1481 --- /dev/null +++ b/tests/integration/mysqli/test_explain_construct_socket.php @@ -0,0 +1,150 @@ +", + "?? SQL ID", + "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + "Datastore/statement/MySQL/tables/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_NAME", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in test_prepare called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/config.php'); + +function test_prepare($link) +{ + + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name='STATISTICS'"; + + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $value)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + while (mysqli_stmt_fetch($stmt)) { + echo $value . "\n"; + } + + mysqli_stmt_close($stmt); +} + +$link = new mysqli('localhost', $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, null, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +test_prepare($link); +mysqli_close($link);