Skip to content

Commit

Permalink
feat: Send Docker cgroup V2 ID (#756)
Browse files Browse the repository at this point in the history
Implements the following:
- Parse the Docker cgroup V2 ID from the Agent container contained in
the file `/proc/self/mountinfo`
- Send the Docker ID from agent to Daemon
- Send Agent Docker ID from Daemon to Collector in the connect message

---------

Co-authored-by: Michael Fulbright <[email protected]>
  • Loading branch information
bduranleau-nr and mfulb authored Nov 9, 2023
1 parent cbe8303 commit 37532b3
Show file tree
Hide file tree
Showing 27 changed files with 492 additions and 14 deletions.
2 changes: 2 additions & 0 deletions agent/Makefile.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
104 changes: 104 additions & 0 deletions agent/php_environment.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
18 changes: 18 additions & 0 deletions agent/php_environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
1 change: 1 addition & 0 deletions agent/php_globals.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
1 change: 1 addition & 0 deletions agent/php_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions agent/php_txn.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
70 changes: 64 additions & 6 deletions agent/tests/test_environment.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -420,18 +421,75 @@ 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();

test_nr_php_process_environment_variables_to_nrobj();

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();
}
4 changes: 4 additions & 0 deletions axiom/cmd_appinfo_transmit.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion axiom/nr_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/*
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions axiom/nr_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
Expand Down
3 changes: 2 additions & 1 deletion axiom/nr_commands_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
These tests cover parsing of Docker container IDs on Linux hosts out of
`/proc/self/mountinfo` (or `/proc/<pid>/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.
38 changes: 38 additions & 0 deletions axiom/tests/cross_agent_tests/docker_container_id_v2/cases.json
Original file line number Diff line number Diff line change
@@ -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
}
}
]
}
]
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 37532b3

Please sign in to comment.