diff --git a/agent/Makefile.frag b/agent/Makefile.frag index 8a4b6705c..3633379cb 100644 --- a/agent/Makefile.frag +++ b/agent/Makefile.frag @@ -265,6 +265,8 @@ endif TEST_LIBS := $(PHP_EMBED_LIBRARY) $(shell $(PHP_CONFIG) --libs) TEST_LDFLAGS := $(shell $(PHP_CONFIG) --ldflags) $(EXPORT_DYNAMIC) TEST_LDFLAGS += $(USER_LDFLAGS) +CROSS_AGENT_DIR := $(CURDIR)/../axiom/tests/cross_agent_tests +EXTRA_CFLAGS += -DCROSS_AGENT_TESTS_DIR="\"$(CROSS_AGENT_DIR)\"" # # Implicit rule to build test object files with the appropriate flags. diff --git a/agent/php_environment.c b/agent/php_environment.c index 1bb5cf3e1..60f2a2c74 100644 --- a/agent/php_environment.c +++ b/agent/php_environment.c @@ -511,6 +511,109 @@ static void nr_php_get_environment_variables(TSRMLS_D) { __func__, NR_PHP_PROCESS_GLOBALS(env_labels)); } +#define MAX_LINE_COUNT (1000) // Upper bound for number of lines to read +/* + * Purpose: + * Extract the 64-byte hexadecimal Docker cgroup ID from + * /proc/self/mountinfo + */ +char* nr_php_parse_v2_docker_id(const char* cgroup_fname) { + char* line_ptr = NULL; + char* retval = NULL; + bool found = false; + int line_count = 0; + FILE* fd = NULL; + size_t len = 0; + nr_regex_t* line_regex = NULL; + nr_regex_substrings_t* ss = NULL; + + if (NULL == cgroup_fname) { + return NULL; + } + + // check if file exists + if (SUCCESS != access(cgroup_fname, F_OK)) { + nrl_verbosedebug(NRL_AGENT, "%s: File not found: %s", __func__, + cgroup_fname); + return NULL; + } + + // open file + fd = fopen(cgroup_fname, "r"); + if (NULL == fd) { + nrl_warning(NRL_AGENT, "%s: Failed to open %s", __func__, cgroup_fname); + return NULL; + } + + // compile regex to extract target string from file line + line_regex = nr_regex_create("/docker/containers/([a-fA-F0-9]{64})/", 0, 0); + + if (NULL == line_regex) { + nrl_error(NRL_AGENT, "%s: Error: line regex creation failed", __func__); + fclose(fd); + return NULL; + } + + // clang-format off + /* + * Example /proc/self/mountinfo file structure: + * ... + * 795 787 254:1 /docker/containers/ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw + * 796 787 254:1 /docker/containers/ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw + * 797 787 254:1 /docker/containers/ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw + * ... + */ + + /* + * File parsing logic: + * 1. scan file line-by-line + * 2. regex search each line for '/docker/containers/' string followed by a string + * a. 64 bytes long (not including null terminator) + * b. comprised of only hexadecimal characters + * 3. extract the 64 byte substring following "/docker/containers/" + * 4. Assign the extracted & verified ID to the retval + * a. Example ID: ec807d5258c06c355c07e2acb700f9029d820afe5836d6a7e19764773dc790f5 + * 5. Set found = true and exit the loops + */ + // clang-format on + + while (FAILURE != getline(&line_ptr, &len, fd) && !found + && line_count++ < MAX_LINE_COUNT) { + ss = nr_regex_match_capture(line_regex, line_ptr, nr_strlen(line_ptr)); + if (NULL == ss) { + continue; + } + retval = nr_regex_substrings_get(ss, 1); + nr_regex_substrings_destroy(&ss); + found = true; + } + + nr_regex_destroy(&line_regex); + nr_free(line_ptr); + fclose(fd); + return retval; +} +#undef MAX_LINE_COUNT + +void nr_php_gather_v2_docker_id() { + char* dockerId = NULL; + + // check if docker_id global already set + if (NULL != NR_PHP_PROCESS_GLOBALS(docker_id)) { + nrl_verbosedebug(NRL_AGENT, "%s: Docker ID already set.", __func__); + return; + } + + dockerId = nr_php_parse_v2_docker_id("/proc/self/mountinfo"); + if (NULL != dockerId) { + NR_PHP_PROCESS_GLOBALS(docker_id) = dockerId; + nrl_verbosedebug(NRL_AGENT, "%s: Docker v2 ID: %s", __func__, dockerId); + } else { + nrl_warning(NRL_AGENT, "%s: Unable to read docker v2 container id", + __func__); + } +} + nrobj_t* nr_php_get_environment(TSRMLS_D) { nrobj_t* env; @@ -520,6 +623,7 @@ nrobj_t* nr_php_get_environment(TSRMLS_D) { nr_php_gather_dynamic_modules(env TSRMLS_CC); nr_php_gather_dispatcher_information(env); nr_php_get_environment_variables(TSRMLS_C); + nr_php_gather_v2_docker_id(); return env; } diff --git a/agent/php_environment.h b/agent/php_environment.h index 333e2b171..c219a8982 100644 --- a/agent/php_environment.h +++ b/agent/php_environment.h @@ -97,4 +97,22 @@ char* nr_php_process_environment_variable_to_string(const char* prefix, const char* kv_delimeter, const char* delimeter); +/* + * Purpose : Parse the /proc/self/mountinfo file for the Docker cgroup v2 ID. + * Assign the value (if found) to the docker_id global. + * + * Params : 1. The filepath of the mountinfo file to parse + * + * Returns : String with v2 ID or NULL if not detected. + * Caller takes ownership of the string. + */ +char* nr_php_parse_v2_docker_id(const char* cgroup_fname); + +/* + * Purpose : Attempt to detect Docker cgroup v2 ID and set the global + * environment variable if successful + * */ +void nr_php_gather_v2_docker_id(); + + #endif /* PHP_ENVIRONMENT_HDR */ diff --git a/agent/php_globals.c b/agent/php_globals.c index 68f64ef04..5bb372d33 100644 --- a/agent/php_globals.c +++ b/agent/php_globals.c @@ -41,6 +41,7 @@ static void nr_php_per_process_globals_dispose(void) { nro_delete(nr_php_per_process_globals.metadata); nr_free(nr_php_per_process_globals.env_labels); nr_free(nr_php_per_process_globals.apache_add); + nr_free(nr_php_per_process_globals.docker_id); nr_memset(&nr_php_per_process_globals, 0, sizeof(nr_php_per_process_globals)); } diff --git a/agent/php_globals.h b/agent/php_globals.h index 8ae65995e..2f5ba9a41 100644 --- a/agent/php_globals.h +++ b/agent/php_globals.h @@ -75,6 +75,7 @@ typedef struct _nrphpglobals_t { int apache_threaded; /* 1 if a threaded MPM is in use, 0 otherwise */ int preload_framework_library_detection; /* Enables preloading framework and library detection */ + char* docker_id; /* 64 byte hex docker ID parsed from /proc/self/mountinfo */ /* Original PHP callback pointer contents */ nrphperrfn_t orig_error_cb; diff --git a/agent/php_txn.c b/agent/php_txn.c index 0ebb8cd00..5cd0f75de 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -811,6 +811,7 @@ nr_status_t nr_php_txn_begin(const char* appnames, info.log_events_max_samples_stored = NRINI(log_events_max_samples_stored); info.custom_events_max_samples_stored = NRINI(custom_events_max_samples_stored); + info.docker_id = nr_strdup(NR_PHP_PROCESS_GLOBALS(docker_id)); NRPRG(app) = nr_agent_find_or_add_app( nr_agent_applist, &info, diff --git a/agent/tests/test_environment.c b/agent/tests/test_environment.c index f5cfe32c6..feb77a1a8 100644 --- a/agent/tests/test_environment.c +++ b/agent/tests/test_environment.c @@ -7,6 +7,7 @@ #include "php_agent.h" #include "php_environment.h" +#include "util_text.h" tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; @@ -420,12 +421,65 @@ static void test_nr_php_process_environment_variables_to_string(void) { test_multi_nr_php_process_environment_variable_to_string(); } -void test_main(void* p NRUNUSED) { -#if defined(ZTS) && !defined(PHP7) - void*** tsrm_ls = NULL; -#endif /* ZTS && !PHP7 */ +static void test_cross_agent_docker_v2(void) { + int i; + char* json; + nrobj_t* tests; + +#define DOCKER_V2_TESTS_PATH CROSS_AGENT_TESTS_DIR "/docker_container_id_v2/" + + json = nr_read_file_contents(DOCKER_V2_TESTS_PATH "cases.json", + 10 * 1000 * 1000); + tlib_pass_if_not_null(DOCKER_V2_TESTS_PATH "cases.json readable", json); + tests = nro_create_from_json(json); + nr_free(json); + + for (i = 1; i <= nro_getsize(tests); i++) { + const nrobj_t* test = NULL; + const char* filename = NULL; + const char* expectedID = NULL; + const nrobj_t* expectedMetrics = NULL; + char* full_filename = NULL; + char* detectedID = NULL; + + test = nro_get_array_hash(tests, i, NULL); + tlib_pass_if_true("test valid", NULL != test, "test=%p", test); + filename = nro_get_hash_string(test, "filename", NULL); + expectedID = nro_get_hash_string(test, "containerId", NULL); + expectedMetrics = nro_get_hash_array(test, "expectedMetrics", NULL); + /* not currently inspected so this avoids a compiler error */ + (void)expectedMetrics; + + tlib_pass_if_true("filname valid", NULL != filename, "filename=%p", + filename); + + full_filename + = nr_str_append(nr_strdup(DOCKER_V2_TESTS_PATH), filename, ""); + detectedID = nr_php_parse_v2_docker_id(full_filename); + nr_free(full_filename); + tlib_pass_if_str_equal("Match Docker cgroup v2 ID", expectedID, detectedID); + nr_free(detectedID); + } + + nro_delete(tests); +} + +static void test_docker_v2(void) { + char* detectedID = NULL; + + // handles bad values without problems + detectedID = nr_php_parse_v2_docker_id(NULL); + tlib_pass_if_null("NULL filename returns NULL", detectedID); - tlib_php_engine_create("" PTSRMLS_CC); + detectedID = nr_php_parse_v2_docker_id(""); + tlib_pass_if_null("Empty filename returns NULL", detectedID); + + detectedID = nr_php_parse_v2_docker_id("/dev/null"); + tlib_pass_if_null("/dev/null returns NULL", detectedID); +} + +void test_main(void* p NRUNUSED) { + tlib_php_engine_create(""); test_rocket_assignments(); @@ -433,5 +487,9 @@ void test_main(void* p NRUNUSED) { test_nr_php_process_environment_variables_to_string(); - tlib_php_engine_destroy(TSRMLS_C); + tlib_php_engine_destroy(); + + test_cross_agent_docker_v2(); + + test_docker_v2(); } diff --git a/axiom/cmd_appinfo_transmit.c b/axiom/cmd_appinfo_transmit.c index 1333e2eff..806a66f63 100644 --- a/axiom/cmd_appinfo_transmit.c +++ b/axiom/cmd_appinfo_transmit.c @@ -147,6 +147,7 @@ nr_flatbuffer_t* nr_appinfo_create_query(const char* agent_run_id, uint32_t host_name; uint32_t trace_observer_host; uint32_t metadata; + uint32_t docker_id; char* json_supported_security_policies; fb = nr_flatbuffers_create(0); @@ -171,6 +172,8 @@ nr_flatbuffer_t* nr_appinfo_create_query(const char* agent_run_id, supported_security_policies = nr_flatbuffers_prepend_string(fb, json_supported_security_policies); + docker_id = nr_flatbuffers_prepend_string(fb, info->docker_id); + metadata = nr_appinfo_prepend_metadata(info, fb); nr_flatbuffers_object_begin(fb, APP_NUM_FIELDS); @@ -206,6 +209,7 @@ nr_flatbuffer_t* nr_appinfo_create_query(const char* agent_run_id, agent_lang, 0); nr_flatbuffers_object_prepend_uoffset(fb, APP_FIELD_APPNAME, appname, 0); nr_flatbuffers_object_prepend_uoffset(fb, APP_FIELD_LICENSE, license, 0); + nr_flatbuffers_object_prepend_uoffset(fb, APP_DOCKER_ID, docker_id, 0); appinfo = nr_flatbuffers_object_end(fb); if (agent_run_id && *agent_run_id) { diff --git a/axiom/nr_app.c b/axiom/nr_app.c index bc5660ac0..d20a56f65 100644 --- a/axiom/nr_app.c +++ b/axiom/nr_app.c @@ -93,6 +93,7 @@ void nr_app_info_destroy_fields(nr_app_info_t* info) { nr_free(info->security_policies_token); nro_delete(info->supported_security_policies); nr_free(info->trace_observer_host); + nr_free(info->docker_id); } /* @@ -277,8 +278,9 @@ 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 + app->info.custom_events_max_samples_stored = info->custom_events_max_samples_stored; + app->info.docker_id = nr_strdup(info->docker_id); 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 1fb780b7a..d05a7f364 100644 --- a/axiom/nr_app.h +++ b/axiom/nr_app.h @@ -81,6 +81,7 @@ typedef struct _nr_app_info_t { the daemon) */ uint64_t custom_events_max_samples_stored; /* maximum number of custom events per min (for the daemon) */ + char* docker_id; /* Docker container ID */ } nr_app_info_t; /* diff --git a/axiom/nr_commands_private.h b/axiom/nr_commands_private.h index b605b84db..0c60bf503 100644 --- a/axiom/nr_commands_private.h +++ b/axiom/nr_commands_private.h @@ -83,7 +83,8 @@ enum { APP_METADATA = 17, APP_LOG_EVENTS_MAX_SAMPLES_STORED = 18, APP_CUSTOM_EVENTS_MAX_SAMPLES_STORED = 19, - APP_NUM_FIELDS = 20, + APP_DOCKER_ID = 20, + APP_NUM_FIELDS = 21, }; /* Generated from: table AppReply */ diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/README.md b/axiom/tests/cross_agent_tests/docker_container_id_v2/README.md new file mode 100644 index 000000000..ea6cc2503 --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/README.md @@ -0,0 +1,6 @@ +These tests cover parsing of Docker container IDs on Linux hosts out of +`/proc/self/mountinfo` (or `/proc//mountinfo` more generally). + +The `cases.json` file lists each filename in this directory containing +example `/proc/self/mountinfo` content, and the expected Docker container ID that +should be parsed from that file. diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/cases.json b/axiom/tests/cross_agent_tests/docker_container_id_v2/cases.json new file mode 100644 index 000000000..1f47d0c06 --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/cases.json @@ -0,0 +1,38 @@ +[ + { + "filename": "docker-20.10.16.txt", + "containerId": "84cf3472a20d1bfb4b50e48b6ff50d96dfcd812652d76dd907951e6f98997bce", + "expectedMetrics": null + }, + { + "filename": "docker-24.0.2.txt", + "containerId": "b0a24eed1b031271d8ba0784b8f354b3da892dfd08bbcf14dd7e8a1cf9292f65", + "expectedMetrics": null + }, + { + "filename": "empty.txt", + "containerId": null, + "expectedMetrics": null + }, + { + "filename": "invalid-characters.txt", + "containerId": null, + "expectedMetrics": null + }, + { + "filename": "docker-too-long.txt", + "containerId": null, + "expectedMetrics": null + }, + { + "filename": "invalid-length.txt", + "containerId": null, + "expectedMetrics": [ + { + "Supportability/utilization/docker/error": { + "callCount": 1 + } + } + ] + } +] \ No newline at end of file diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-20.10.16.txt b/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-20.10.16.txt new file mode 100644 index 000000000..ce2b1bedf --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-20.10.16.txt @@ -0,0 +1,24 @@ +519 413 0:152 / / rw,relatime master:180 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/YCID3333O5VYPYDNTQRZX4GI67:/var/lib/docker/overlay2/l/G7H4TULAFM2UBFRL7QFQPUNXY5:/var/lib/docker/overlay2/l/RLC4GCL75VGXXXYJJO57STHIYN:/var/lib/docker/overlay2/l/YOZKNWFAP6YX74XEKPHX4KG4UN:/var/lib/docker/overlay2/l/46EQ6YX5PQQZ4Z3WCSMQ6Z4YWI:/var/lib/docker/overlay2/l/KGKX3Z5ZMOCDWOFKBS2FSHMQMQ:/var/lib/docker/overlay2/l/CKFYAF4TXZD4RCE6RG6UNL5WVI,upperdir=/var/lib/docker/overlay2/358c429f7b04ee5a228b94efaebe3413a98fcc676b726f078fe875727e3bddd2/diff,workdir=/var/lib/docker/overlay2/358c429f7b04ee5a228b94efaebe3413a98fcc676b726f078fe875727e3bddd2/work +520 519 0:155 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +521 519 0:156 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +522 521 0:157 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +523 519 0:158 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +524 523 0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw +525 521 0:154 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +526 521 0:159 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +527 519 254:1 /docker/volumes/3237dea4f8022f1addd7b6f072a9c847eb3e5b8df0d599f462ba7040884d4618/_data /data rw,relatime master:28 - ext4 /dev/vda1 rw +528 519 254:1 /docker/containers/84cf3472a20d1bfb4b50e48b6ff50d96dfcd812652d76dd907951e6f98997bce/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw +529 519 254:1 /docker/containers/84cf3472a20d1bfb4b50e48b6ff50d96dfcd812652d76dd907951e6f98997bce/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw +530 519 254:1 /docker/containers/84cf3472a20d1bfb4b50e48b6ff50d96dfcd812652d76dd907951e6f98997bce/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw +414 521 0:157 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +415 520 0:155 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +416 520 0:155 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +417 520 0:155 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +418 520 0:155 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +419 520 0:155 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +420 520 0:160 / /proc/acpi ro,relatime - tmpfs tmpfs ro +421 520 0:156 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +422 520 0:156 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +423 520 0:156 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +424 520 0:156 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +425 523 0:161 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-24.0.2.txt b/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-24.0.2.txt new file mode 100644 index 000000000..1725e7726 --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-24.0.2.txt @@ -0,0 +1,21 @@ +1014 1013 0:269 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1019 1013 0:270 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1020 1019 0:271 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1021 1013 0:272 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +1022 1021 0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw +1023 1019 0:268 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1024 1019 0:273 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +1025 1013 254:1 /docker/containers/b0a24eed1b031271d8ba0784b8f354b3da892dfd08bbcf14dd7e8a1cf9292f65/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw,discard +1026 1013 254:1 /docker/containers/b0a24eed1b031271d8ba0784b8f354b3da892dfd08bbcf14dd7e8a1cf9292f65/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw,discard +1027 1013 254:1 /docker/containers/b0a24eed1b031271d8ba0784b8f354b3da892dfd08bbcf14dd7e8a1cf9292f65/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw,discard +717 1019 0:271 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +718 1014 0:269 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +719 1014 0:269 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +720 1014 0:269 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +721 1014 0:269 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 1014 0:269 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +726 1014 0:274 / /proc/acpi ro,relatime - tmpfs tmpfs ro +727 1014 0:270 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +728 1014 0:270 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +729 1014 0:270 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +730 1021 0:275 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-too-long.txt b/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-too-long.txt new file mode 100644 index 000000000..608eaf7a4 --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/docker-too-long.txt @@ -0,0 +1,21 @@ +1014 1013 0:269 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1019 1013 0:270 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1020 1019 0:271 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1021 1013 0:272 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +1022 1021 0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw +1023 1019 0:268 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1024 1019 0:273 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +1025 1013 254:1 /docker/containers/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw,discard +1026 1013 254:1 /docker/containers/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw,discard +1027 1013 254:1 /docker/containers/3ccfa00432798ff38f85839de1e396f771b4acbe9f4ddea0a761c39b9790a7821/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw,discard +717 1019 0:271 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +718 1014 0:269 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +719 1014 0:269 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +720 1014 0:269 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +721 1014 0:269 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 1014 0:269 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +726 1014 0:274 / /proc/acpi ro,relatime - tmpfs tmpfs ro +727 1014 0:270 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +728 1014 0:270 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +729 1014 0:270 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +730 1021 0:275 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/empty.txt b/axiom/tests/cross_agent_tests/docker_container_id_v2/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/invalid-characters.txt b/axiom/tests/cross_agent_tests/docker_container_id_v2/invalid-characters.txt new file mode 100644 index 000000000..b561475ac --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/invalid-characters.txt @@ -0,0 +1,21 @@ +1014 1013 0:269 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1019 1013 0:270 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1020 1019 0:271 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1021 1013 0:272 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +1022 1021 0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw +1023 1019 0:268 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1024 1019 0:273 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +1025 1013 254:1 /docker/containers/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw,discard +1026 1013 254:1 /docker/containers/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw,discard +1027 1013 254:1 /docker/containers/WRONGINCORRECTINVALIDCHARSERRONEOUSBADPHONYBROKEN2TERRIBLENOPE55/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw,discard +717 1019 0:271 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +718 1014 0:269 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +719 1014 0:269 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +720 1014 0:269 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +721 1014 0:269 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 1014 0:269 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +726 1014 0:274 / /proc/acpi ro,relatime - tmpfs tmpfs ro +727 1014 0:270 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +728 1014 0:270 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +729 1014 0:270 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +730 1021 0:275 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/axiom/tests/cross_agent_tests/docker_container_id_v2/invalid-length.txt b/axiom/tests/cross_agent_tests/docker_container_id_v2/invalid-length.txt new file mode 100644 index 000000000..a8987df70 --- /dev/null +++ b/axiom/tests/cross_agent_tests/docker_container_id_v2/invalid-length.txt @@ -0,0 +1,21 @@ +1014 1013 0:269 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +1019 1013 0:270 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +1020 1019 0:271 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1021 1013 0:272 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +1022 1021 0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw +1023 1019 0:268 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +1024 1019 0:273 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +1025 1013 254:1 /docker/containers/47cbd16b77c5/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw,discard +1026 1013 254:1 /docker/containers/47cbd16b77c5/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw,discard +1027 1013 254:1 /docker/containers/47cbd16b77c5/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw,discard +717 1019 0:271 /0 /dev/console rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +718 1014 0:269 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +719 1014 0:269 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +720 1014 0:269 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +721 1014 0:269 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +723 1014 0:269 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +726 1014 0:274 / /proc/acpi ro,relatime - tmpfs tmpfs ro +727 1014 0:270 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +728 1014 0:270 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +729 1014 0:270 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +730 1021 0:275 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/axiom/tests/test_cmd_appinfo.c b/axiom/tests/test_cmd_appinfo.c index bd30dda15..59f277bc9 100644 --- a/axiom/tests/test_cmd_appinfo.c +++ b/axiom/tests/test_cmd_appinfo.c @@ -54,6 +54,7 @@ static void test_create_empty_query(void) { test_pass_if_empty_vector(&app, APP_TRACE_OBSERVER_HOST); test_pass_if_empty_vector(&app, APP_FIELD_LABELS); test_pass_if_empty_vector(&app, APP_METADATA); + test_pass_if_empty_vector(&app, APP_DOCKER_ID); high_security = nr_flatbuffers_table_read_i8(&app, APP_FIELD_HIGH_SECURITY, 42); @@ -108,6 +109,8 @@ static void test_create_query(void) { info.span_events_max_samples_stored = 1234; info.log_events_max_samples_stored = 2345; info.custom_events_max_samples_stored = 345; + info.docker_id = nr_strdup( + "1056761e1f44969c959364a8e26e9345b37ccb91aef09a8173c90cf1d1d99156"); query = nr_appinfo_create_query("12345", "this_host", &info); @@ -172,6 +175,10 @@ static void test_create_query(void) { = nr_flatbuffers_table_read_i8(&app, APP_FIELD_HIGH_SECURITY, 0); tlib_pass_if_true(__func__, 1 == high_security, "high_security=%d", high_security); + tlib_pass_if_str_equal( + __func__, + "1056761e1f44969c959364a8e26e9345b37ccb91aef09a8173c90cf1d1d99156", + (const char*)nr_flatbuffers_table_read_bytes(&app, APP_DOCKER_ID)); nr_app_info_destroy_fields(&info); nr_flatbuffers_destroy(&query); diff --git a/protocol/flatbuffers/protocol.fbs b/protocol/flatbuffers/protocol.fbs index b04b624d2..b34081d84 100644 --- a/protocol/flatbuffers/protocol.fbs +++ b/protocol/flatbuffers/protocol.fbs @@ -35,6 +35,7 @@ table App { 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 + docker_id: string; // added for PHP agent release 10.14 } enum AppStatus : byte { Unknown = 0, Disconnected = 1, InvalidLicense = 2, diff --git a/src/flatbuffersdata/data.go b/src/flatbuffersdata/data.go index d05b15155..fd32ca4b1 100644 --- a/src/flatbuffersdata/data.go +++ b/src/flatbuffersdata/data.go @@ -34,6 +34,7 @@ func MarshalAppInfo(info *newrelic.AppInfo) ([]byte, error) { metadata := buf.CreateString(string(metadataJSON)) host := buf.CreateString(string(info.Hostname)) traceObserverHost := buf.CreateString(info.TraceObserverHost) + dockerId := buf.CreateString(info.DockerId) protocol.AppStart(buf) protocol.AppAddAgentLanguage(buf, lang) @@ -50,6 +51,7 @@ func MarshalAppInfo(info *newrelic.AppInfo) ([]byte, error) { protocol.AppAddTraceObserverPort(buf, info.TraceObserverPort) protocol.AppAddHighSecurity(buf, info.HighSecurity) + protocol.AppAddDockerId(buf, dockerId) appInfo := protocol.AppEnd(buf) diff --git a/src/newrelic/app.go b/src/newrelic/app.go index a29c919d1..9ba90d577 100644 --- a/src/newrelic/app.go +++ b/src/newrelic/app.go @@ -74,6 +74,7 @@ type AppInfo struct { TraceObserverPort uint16 SpanQueueSize uint64 AgentEventLimits collector.EventConfigs + DockerId string } func (info *AppInfo) String() string { @@ -244,6 +245,12 @@ func (info *AppInfo) ConnectPayloadInternal(pid int, util *utilization.Data) *Ra utilCopy := *util data.Util = &utilCopy data.Util.Hostname = data.Host + if info.DockerId != "" { + err := utilization.OverrideDockerId(data.Util, info.DockerId) + if err != nil { + log.Errorf("Failed to set Agent Docker ID: %s", err) + } + } } if len(info.Labels) > 0 { @@ -257,6 +264,10 @@ func (info *AppInfo) ConnectPayloadInternal(pid int, util *utilization.Data) *Ra data.Metadata = JSONString("{}") } + if data.Util != nil { + utilization.OverrideVendors(data.Util) + } + return data } diff --git a/src/newrelic/app_test.go b/src/newrelic/app_test.go index dd5a0749f..f2b49e35c 100644 --- a/src/newrelic/app_test.go +++ b/src/newrelic/app_test.go @@ -38,6 +38,7 @@ func TestConnectPayloadInternal(t *testing.T) { Metadata: JSONString(`{"NEW_RELIC_METADATA_ONE":"one","NEW_RELIC_METADATA_TWO":"two"}`), RedirectCollector: "collector.newrelic.com", Hostname: "some_host", + DockerId: "1056761e1f44969c959364a8e26e9345b37ccb91aef09a8173c90cf1d1d99156", } info.AgentEventLimits.SpanEventConfig.Limit = 2323 @@ -63,6 +64,8 @@ func TestConnectPayloadInternal(t *testing.T) { pid := 123 b := info.ConnectPayloadInternal(pid, util) + actualDockerId, _ := utilization.GetDockerId(b.Util) + // Compare the string integer and string portions of the structs // TestConnectEncodedJSON will do a full comparison after being encoded to bytes if b == nil { @@ -92,6 +95,9 @@ func TestConnectPayloadInternal(t *testing.T) { if b.Util.Hostname != expected.Util.Hostname { t.Errorf("expected: %s\nactual: %s", expected.Util.Hostname, b.Util.Hostname) } + if actualDockerId != info.DockerId { + t.Errorf("expected: %s\nactual: %s", info.DockerId, actualDockerId) + } } func TestConnectPayloadInternalHostname(t *testing.T) { @@ -184,6 +190,67 @@ func TestConnectPayloadInternalHostname(t *testing.T) { } } +func TestConnectPayloadInternalDocker(t *testing.T) { + util := &utilization.Data{} + info := &AppInfo{} + + // No docker ID, nil utilization data + info.DockerId = "" + + b := info.ConnectPayloadInternal(1, nil) + + result, err := utilization.GetDockerId(b.Util) + + if nil != b.Util { + t.Errorf("expected: %v\nactual: %v", nil, b.Util) + } + if "Util is nil" != err.Error() { + t.Errorf("expected: %s\nactual: %s", "Util is nil", err.Error()) + } + + // No Docker ID, utilization data + info.DockerId = "" + + b = info.ConnectPayloadInternal(1, util) + + result, err = utilization.GetDockerId(b.Util) + + if result != info.DockerId { + t.Errorf("expected: %s\nactual: %v", info.DockerId, result) + } + if "Vendors structure is empty" != err.Error() { + t.Errorf("expected: %s\nactual: %s", "Vendors structure is empty", err.Error()) + } + + // Docker ID, nil utilization data + info.DockerId = "1056761e1f44969c959364a8e26e9345b37ccb91aef09a8173c90cf1d1d99156" + + b = info.ConnectPayloadInternal(1, nil) + result, err = utilization.GetDockerId(b.Util) + + if nil != b.Util { + t.Errorf("expected: %v\nactual: %v", nil, b.Util) + } + if "Util is nil" != err.Error() { + t.Errorf("expected: %s\nactual: %s", "Util is nil", err.Error()) + } + + // Docker ID, utilization data + info.DockerId = "1056761e1f44969c959364a8e26e9345b37ccb91aef09a8173c90cf1d1d99156" + + b = info.ConnectPayloadInternal(1, util) + + result, err = utilization.GetDockerId(b.Util) + + if result != info.DockerId { + t.Errorf("expected: %s\nactual: %v", info.DockerId, result) + } + if nil != err { + t.Errorf("expected: %v\nactual: %v", nil, err) + } + +} + func TestPreconnectPayloadEncoded(t *testing.T) { preconnectPayload := &RawPreconnectPayload{SecurityPolicyToken: "ffff-eeee-eeee-dddd", HighSecurity: false} diff --git a/src/newrelic/commands.go b/src/newrelic/commands.go index a357def65..5e67d71e4 100644 --- a/src/newrelic/commands.go +++ b/src/newrelic/commands.go @@ -275,6 +275,7 @@ func UnmarshalAppInfo(tbl flatbuffers.Table) *AppInfo { TraceObserverPort: app.TraceObserverPort(), SpanQueueSize: app.SpanQueueSize(), HighSecurity: app.HighSecurity(), + DockerId: string(app.DockerId()), } info.initSettings(app.Settings()) diff --git a/src/newrelic/protocol/App.go b/src/newrelic/protocol/App.go index eccafeb81..ecb8689fb 100644 --- a/src/newrelic/protocol/App.go +++ b/src/newrelic/protocol/App.go @@ -221,8 +221,16 @@ func (rcv *App) MutateCustomEventsMaxSamplesStored(n uint64) bool { return rcv._tab.MutateUint64Slot(42, n) } +func (rcv *App) DockerId() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(44)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + func AppStart(builder *flatbuffers.Builder) { - builder.StartObject(20) + builder.StartObject(21) } func AppAddLicense(builder *flatbuffers.Builder, license flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(license), 0) @@ -284,6 +292,9 @@ func AppAddLogEventsMaxSamplesStored(builder *flatbuffers.Builder, logEventsMaxS func AppAddCustomEventsMaxSamplesStored(builder *flatbuffers.Builder, customEventsMaxSamplesStored uint64) { builder.PrependUint64Slot(19, customEventsMaxSamplesStored, 0) } +func AppAddDockerId(builder *flatbuffers.Builder, dockerId flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(20, flatbuffers.UOffsetT(dockerId), 0) +} func AppEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/src/newrelic/utilization/utilization_hash.go b/src/newrelic/utilization/utilization_hash.go index 2f28c6235..c357175cb 100644 --- a/src/newrelic/utilization/utilization_hash.go +++ b/src/newrelic/utilization/utilization_hash.go @@ -164,11 +164,6 @@ func Gather(config Config) *Data { // Override whatever needs to be overridden. uDat.Config = overrideFromConfig(config) - if uDat.Vendors.isEmpty() { - // Per spec, we MUST NOT send any vendors hash if it's empty. - uDat.Vendors = nil - } - return uDat } @@ -204,6 +199,44 @@ func GatherDockerID(util *Data) error { return nil } +func OverrideDockerId(util *Data, id string) error { + if nil == util { + return fmt.Errorf("util is nil") + } + if nil == util.Vendors { + util.Vendors = &vendors{} + } + util.Vendors.Docker = &docker{ID: id} + return nil +} + +func OverrideVendors(util *Data) { + if nil == util { + return + } + if util.Vendors.isEmpty() { + // Per spec, we MUST NOT send any vendors hash if it's empty. + util.Vendors = nil + } +} + +func GetDockerId(util *Data) (string, error) { + id := "" + if nil == util { + return id, fmt.Errorf("Util is nil") + } + if util.Vendors.isEmpty() { + return id, fmt.Errorf("Vendors structure is empty") + } + if nil == util.Vendors.Docker { + return id, fmt.Errorf("Docker structure is empty") + } + + id = util.Vendors.Docker.ID + + return id, nil +} + func GatherMemory(util *Data) error { ram, err := sysinfo.PhysicalMemoryBytes() if nil == err {