Skip to content

Commit beb6a71

Browse files
authored
Update use of published_at_ns in process context C/C++ implementation + some small fixes (#66)
**What does this PR do?** This PR collects a number of fixes to the process context C/C++ implementation. I recommend reviewing it commit-by-commit (and let me know if this is too annoying to review, I can break it down further). The changes around "MFD_NOEXEC_SEAL on failure" and "Change publish synchronization to use timestamp instead of signature" I haven't yet pushed the change to the upstream spec, I'll do that next, but I wanted to have a working version first :) **Motivation:** Improve implementation/spec :) **Additional Notes:** N/A **How to test the change?** The built-in decoder and otel_process_ctx_dump.sh have been updated to match.
1 parent 85aa47f commit beb6a71

File tree

4 files changed

+144
-74
lines changed

4 files changed

+144
-74
lines changed

process-context/c-and-cpp/example_ctx.c

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ void burn_cpu_for(int seconds) {
3939
getchar();
4040
}
4141

42+
static void print_key_value_pairs(const char *label, const char **pairs) {
43+
if (pairs && pairs[0] != NULL) {
44+
printf(", %s=", label);
45+
for (int i = 0; pairs[i] != NULL; i += 2) {
46+
if (i > 0) printf(",");
47+
printf("%s:%s", pairs[i], pairs[i + 1]);
48+
}
49+
} else {
50+
printf(", %s=(none)", label);
51+
}
52+
}
53+
4254
bool read_and_print_ctx(const char* prefix) {
4355
otel_process_ctx_read_result result = otel_process_ctx_read();
4456

@@ -60,27 +72,25 @@ bool read_and_print_ctx(const char* prefix) {
6072
result.data.telemetry_sdk_version
6173
);
6274

63-
if (result.data.resources && result.data.resources[0] != NULL) {
64-
printf(", resources=");
65-
for (int i = 0; result.data.resources[i] != NULL; i += 2) {
66-
if (i > 0) printf(",");
67-
printf("%s=%s", result.data.resources[i], result.data.resources[i + 1]);
68-
}
69-
} else {
70-
printf(", resources=(none)");
71-
}
75+
print_key_value_pairs("resource_attributes", result.data.resource_attributes);
76+
print_key_value_pairs("extra_attributes", result.data.extra_attributes);
7277
printf("\n");
7378

7479
otel_process_ctx_read_drop(&result);
7580
return true;
7681
}
7782

78-
const char *resources[] = {
83+
const char *resource_attributes[] = {
7984
"resource.key1", "resource.value1",
8085
"resource.key2", "resource.value2",
8186
NULL
8287
};
8388

89+
const char *extra_attributes[] = {
90+
"example_extra_attribute_foo", "example_extra_attribute_foo_value",
91+
NULL
92+
};
93+
8494
int update_and_fork(void) {
8595
printf("Burning CPU for 5 seconds...\n");
8696
burn_cpu_for(5);
@@ -94,7 +104,7 @@ int update_and_fork(void) {
94104
.telemetry_sdk_language = "c",
95105
.telemetry_sdk_version = "1.2.3",
96106
.telemetry_sdk_name = "example_ctx.c",
97-
.resources = resources
107+
.resource_attributes = resource_attributes
98108
};
99109

100110
otel_process_ctx_result result = otel_process_ctx_publish(&update_data);
@@ -119,7 +129,7 @@ int update_and_fork(void) {
119129
.telemetry_sdk_language = "c",
120130
.telemetry_sdk_version = "1.2.3",
121131
.telemetry_sdk_name = "example_ctx.c",
122-
.resources = NULL
132+
.resource_attributes = NULL
123133
};
124134

125135
result = otel_process_ctx_publish(&child_data);
@@ -172,7 +182,8 @@ int main(int argc, char* argv[]) {
172182
.telemetry_sdk_language = "c",
173183
.telemetry_sdk_version = "1.2.3",
174184
.telemetry_sdk_name = "example_ctx.c",
175-
.resources = resources
185+
.resource_attributes = resource_attributes,
186+
.extra_attributes = extra_attributes
176187
};
177188

178189
otel_process_ctx_result result = otel_process_ctx_publish(&data);

process-context/c-and-cpp/otel_process_ctx.c

Lines changed: 112 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ static const otel_process_ctx_data empty_data = {
4242
.telemetry_sdk_language = NULL,
4343
.telemetry_sdk_version = NULL,
4444
.telemetry_sdk_name = NULL,
45-
.resources = NULL
45+
.resource_attributes = NULL,
46+
.extra_attributes = NULL
4647
};
4748

4849
#if (defined(OTEL_PROCESS_CTX_NOOP) && OTEL_PROCESS_CTX_NOOP) || !defined(__linux__)
@@ -75,10 +76,10 @@ static const otel_process_ctx_data empty_data = {
7576
* An outside-of-process reader will read this struct + otel_process_payload to get the data.
7677
*/
7778
typedef struct __attribute__((packed, aligned(8))) {
78-
char otel_process_ctx_signature[8]; // Always OTEL_CTX_SIGNATURE
79+
char otel_process_ctx_signature[8]; // Always "OTEL_CTX"
7980
uint32_t otel_process_ctx_version; // Always > 0, incremented when the data structure changes, currently v2
8081
uint32_t otel_process_payload_size; // Always > 0, size of storage
81-
uint64_t otel_process_ctx_published_at_ns; // Always > 0, timestamp from when the context was published in nanoseconds since epoch
82+
uint64_t otel_process_ctx_published_at_ns; // Timestamp from when the context was published in nanoseconds since epoch. 0 during updates.
8283
char *otel_process_payload; // Always non-null, points to the storage for the data; expected to be a protobuf map of string key/value pairs, null-terminated
8384
} otel_process_ctx_mapping;
8485

@@ -144,6 +145,10 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da
144145
const ssize_t mapping_size = sizeof(otel_process_ctx_mapping);
145146
published_state.publisher_pid = getpid(); // This allows us to detect in forks that we shouldn't touch the mapping
146147
int fd = memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL);
148+
if (fd < 0) {
149+
// MFD_NOEXEC_SEAL is a newer flag; older kernels reject unknown flags, so let's retry without it
150+
fd = memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING);
151+
}
147152
bool failed_to_close_fd = false;
148153
if (fd >= 0) {
149154
// Try to create mapping from memfd
@@ -178,24 +183,24 @@ otel_process_ctx_result otel_process_ctx_publish(const otel_process_ctx_data *da
178183
}
179184

180185
// Step: Populate the mapping
181-
// The payload and any extra fields must come first and not be reordered with the signature by the compiler.
186+
// The payload and any extra fields must come first and not be reordered with the published_at_ns by the compiler.
182187
*published_state.mapping = (otel_process_ctx_mapping) {
183-
.otel_process_ctx_signature = {0}, // Set in "Step: Populate the signature into the mapping" below
188+
.otel_process_ctx_signature = { 'O', 'T', 'E', 'L', '_', 'C', 'T', 'X' },
184189
.otel_process_ctx_version = 2,
185190
.otel_process_payload_size = payload_size,
186-
.otel_process_ctx_published_at_ns = published_at_ns,
191+
.otel_process_ctx_published_at_ns = 0, // Set in "Step: Populate the published_at_ns into the mapping" below
187192
.otel_process_payload = published_state.payload
188193
};
189194

190-
// Step: Synchronization - Mapping has been filled and is missing signature
191-
// Make sure the initialization of the mapping + payload above does not get reordered with setting the signature below. Setting
192-
// the signature is what tells an outside reader that the context is fully published.
195+
// Step: Synchronization - Mapping has been filled and is missing published_at_ns
196+
// Make sure the initialization of the mapping + payload above does not get reordered with setting the published_at_ns below. Setting
197+
// the published_at_ns is what tells an outside reader that the context is fully published.
193198
atomic_thread_fence(memory_order_seq_cst);
194199

195-
// Step: Populate the signature into the mapping
196-
// The signature must come last and not be reordered with the fields above by the compiler. After this step, external readers
197-
// can read the signature and know that the payload is ready to be read.
198-
memcpy(published_state.mapping->otel_process_ctx_signature, OTEL_CTX_SIGNATURE, sizeof(published_state.mapping->otel_process_ctx_signature));
200+
// Step: Populate the published_at_ns into the mapping
201+
// The published_at_ns must come last and not be reordered with the fields above by the compiler. After this step, external readers
202+
// can read the published_at_ns and know that the payload is ready to be read.
203+
published_state.mapping->otel_process_ctx_published_at_ns = published_at_ns;
199204

200205
// Step: Attempt to name the mapping so outside readers can:
201206
// * Find it by name
@@ -242,6 +247,11 @@ static otel_process_ctx_result otel_process_ctx_update(uint64_t published_at_ns,
242247
return (otel_process_ctx_result) {.success = false, .error_message = "Unexpected: otel_process_ctx_data is NULL or context is not published (" __FILE__ ":" ADD_QUOTES(__LINE__) ")"};
243248
}
244249

250+
if (published_at_ns == published_state.mapping->otel_process_ctx_published_at_ns) {
251+
// Advance published_at_ns to allow readers to detect the update
252+
published_at_ns++;
253+
}
254+
245255
// Step: Prepare the new payload to be published
246256
// The payload SHOULD be ready and valid before trying to actually update the mapping.
247257
uint32_t payload_size = 0;
@@ -345,8 +355,8 @@ static void write_protobuf_tag(char **ptr, uint8_t field_number) {
345355
*(*ptr)++ = (char)((field_number << 3) | 2); // Field type is always 2 (LEN)
346356
}
347357

348-
static void write_attribute(char **ptr, const char *key, const char *value) {
349-
write_protobuf_tag(ptr, 1); // Resource.attributes (field 1)
358+
static void write_attribute(char **ptr, uint8_t field_number, const char *key, const char *value) {
359+
write_protobuf_tag(ptr, field_number);
350360
write_protobuf_varint(ptr, protobuf_otel_keyvalue_string_size(key, value));
351361

352362
// KeyValue
@@ -381,15 +391,20 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
381391
otel_process_ctx_result validation_result = validate_and_calculate_protobuf_payload_size(&pairs_size, (const char **) pairs);
382392
if (!validation_result.success) return validation_result;
383393

384-
size_t resources_pairs_size = 0;
385-
if (data.resources != NULL) {
386-
validation_result = validate_and_calculate_protobuf_payload_size(&resources_pairs_size, data.resources);
394+
size_t resource_attributes_pairs_size = 0;
395+
if (data.resource_attributes != NULL) {
396+
validation_result = validate_and_calculate_protobuf_payload_size(&resource_attributes_pairs_size, data.resource_attributes);
387397
if (!validation_result.success) return validation_result;
388398
}
389399

390-
size_t resource_size = pairs_size + resources_pairs_size;
391-
// ProcessContext wrapper: tag (1 byte) + resource length varint + resource content
392-
size_t total_size = 1 + protobuf_varint_size(resource_size) + resource_size;
400+
size_t extra_attributes_pairs_size = 0;
401+
if (data.extra_attributes != NULL) {
402+
validation_result = validate_and_calculate_protobuf_payload_size(&extra_attributes_pairs_size, data.extra_attributes);
403+
if (!validation_result.success) return validation_result;
404+
}
405+
406+
size_t resource_size = pairs_size + resource_attributes_pairs_size;
407+
size_t total_size = protobuf_record_size(resource_size) + extra_attributes_pairs_size;
393408

394409
char *encoded = (char *) calloc(total_size, 1);
395410
if (!encoded) {
@@ -402,11 +417,15 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
402417
write_protobuf_varint(&ptr, resource_size);
403418

404419
for (size_t i = 0; pairs[i * 2] != NULL; i++) {
405-
write_attribute(&ptr, pairs[i * 2], pairs[i * 2 + 1]);
420+
write_attribute(&ptr, 1, pairs[i * 2], pairs[i * 2 + 1]);
406421
}
407422

408-
for (size_t i = 0; data.resources != NULL && data.resources[i * 2] != NULL; i++) {
409-
write_attribute(&ptr, data.resources[i * 2], data.resources[i * 2 + 1]);
423+
for (size_t i = 0; data.resource_attributes != NULL && data.resource_attributes[i * 2] != NULL; i++) {
424+
write_attribute(&ptr, 1, data.resource_attributes[i * 2], data.resource_attributes[i * 2 + 1]);
425+
}
426+
427+
for (size_t i = 0; data.extra_attributes != NULL && data.extra_attributes[i * 2] != NULL; i++) {
428+
write_attribute(&ptr, 2, data.extra_attributes[i * 2], data.extra_attributes[i * 2 + 1]);
410429
}
411430

412431
*out = encoded;
@@ -439,7 +458,7 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
439458
if (!fp) return result;
440459

441460
while (fgets(line, sizeof(line), fp)) {
442-
bool is_process_ctx = strstr(line, "[anon_shmem:OTEL_CTX]") != NULL || strstr(line, "/memfd:OTEL_CTX") != NULL;
461+
bool is_process_ctx = strstr(line, "[anon_shmem:OTEL_CTX]") != NULL || strstr(line, "[anon:OTEL_CTX]") != NULL || strstr(line, "/memfd:OTEL_CTX") != NULL;
443462
if (is_process_ctx) {
444463
result = (otel_process_ctx_mapping *)parse_mapping_start(line);
445464
break;
@@ -495,6 +514,34 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
495514
return wire_type == 2; // We only need the LEN wire type for now
496515
}
497516

517+
// Reads an OTel KeyValue message (key string + AnyValue-wrapped string) into the provided buffers.
518+
static bool read_protobuf_keyvalue(char **ptr, char *end_ptr, char *key_buffer, char *value_buffer) {
519+
bool key_found = false;
520+
bool value_found = false;
521+
522+
while (*ptr < end_ptr) {
523+
uint8_t kv_field;
524+
if (!read_protobuf_tag(ptr, end_ptr, &kv_field)) return false;
525+
526+
if (kv_field == 1) { // KeyValue.key
527+
if (!read_protobuf_string(ptr, end_ptr, key_buffer)) return false;
528+
key_found = true;
529+
} else if (kv_field == 2) { // KeyValue.value (AnyValue)
530+
uint16_t _any_len; // Unused, but we still need to consume + validate the varint
531+
if (!read_protobuf_varint(ptr, end_ptr, &_any_len)) return false;
532+
uint8_t any_field;
533+
if (!read_protobuf_tag(ptr, end_ptr, &any_field)) return false;
534+
535+
if (any_field == 1) { // AnyValue.string_value
536+
if (!read_protobuf_string(ptr, end_ptr, value_buffer)) return false;
537+
value_found = true;
538+
}
539+
}
540+
}
541+
542+
return key_found && value_found;
543+
}
544+
498545
// Simplified protobuf decoder to match the exact encoder above. If the protobuf data doesn't match the encoder, this will
499546
// return false.
500547
static bool otel_process_ctx_decode_payload(char *payload, uint32_t payload_size, otel_process_ctx_data *data_out, char *key_buffer, char *value_buffer) {
@@ -514,8 +561,13 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
514561

515562
size_t resource_index = 0;
516563
size_t resource_capacity = 201; // Allocate space for 100 pairs + NULL terminator entry
517-
data_out->resources = (const char **) calloc(resource_capacity, sizeof(char *));
518-
if (data_out->resources == NULL) return false;
564+
data_out->resource_attributes = (const char **) calloc(resource_capacity, sizeof(char *));
565+
if (data_out->resource_attributes == NULL) return false;
566+
567+
size_t extra_attributes_index = 0;
568+
size_t extra_attributes_capacity = 201; // Allocate space for 100 pairs + NULL terminator entry
569+
data_out->extra_attributes = (const char **) calloc(extra_attributes_capacity, sizeof(char *));
570+
if (data_out->extra_attributes == NULL) return false;
519571

520572
while (ptr < resource_end) {
521573
uint8_t field_number;
@@ -526,31 +578,7 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
526578
char *kv_end = ptr + kv_len;
527579
if (kv_end > resource_end) return false;
528580

529-
bool key_found = false;
530-
bool value_found = false;
531-
532-
// Parse KeyValue
533-
while (ptr < kv_end) {
534-
uint8_t kv_field;
535-
if (!read_protobuf_tag(&ptr, kv_end, &kv_field)) return false;
536-
537-
if (kv_field == 1) { // KeyValue.key
538-
if (!read_protobuf_string(&ptr, kv_end, key_buffer)) return false;
539-
key_found = true;
540-
} else if (kv_field == 2) { // KeyValue.value (AnyValue)
541-
uint16_t _any_len; // Unused, but we still need to consume + validate the varint
542-
if (!read_protobuf_varint(&ptr, kv_end, &_any_len)) return false;
543-
uint8_t any_field;
544-
if (!read_protobuf_tag(&ptr, kv_end, &any_field)) return false;
545-
546-
if (any_field == 1) { // AnyValue.string_value
547-
if (!read_protobuf_string(&ptr, kv_end, value_buffer)) return false;
548-
value_found = true;
549-
}
550-
}
551-
}
552-
553-
if (!key_found || !value_found) return false;
581+
if (!read_protobuf_keyvalue(&ptr, kv_end, key_buffer, value_buffer)) return false;
554582

555583
char *value = strdup(value_buffer);
556584
if (!value) return false;
@@ -571,12 +599,35 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
571599
free(value);
572600
return false;
573601
}
574-
data_out->resources[resource_index] = key;
575-
data_out->resources[resource_index + 1] = value;
602+
data_out->resource_attributes[resource_index] = key;
603+
data_out->resource_attributes[resource_index + 1] = value;
576604
resource_index += 2;
577605
}
578606
}
579607

608+
while (ptr < end_ptr) {
609+
uint8_t extra_ctx_field;
610+
if (!read_protobuf_tag(&ptr, end_ptr, &extra_ctx_field) || extra_ctx_field != 2) return false;
611+
612+
uint16_t kv_len;
613+
if (!read_protobuf_varint(&ptr, end_ptr, &kv_len)) return false;
614+
char *kv_end = ptr + kv_len;
615+
if (kv_end > end_ptr) return false;
616+
617+
if (!read_protobuf_keyvalue(&ptr, kv_end, key_buffer, value_buffer)) return false;
618+
619+
char *key = strdup(key_buffer);
620+
char *value = strdup(value_buffer);
621+
if (!key || !value || extra_attributes_index + 2 >= extra_attributes_capacity) {
622+
free(key);
623+
free(value);
624+
return false;
625+
}
626+
data_out->extra_attributes[extra_attributes_index] = key;
627+
data_out->extra_attributes[extra_attributes_index + 1] = value;
628+
extra_attributes_index += 2;
629+
}
630+
580631
// Validate all required fields were found
581632
return data_out->deployment_environment_name != NULL &&
582633
data_out->service_instance_id != NULL &&
@@ -595,9 +646,13 @@ static otel_process_ctx_result otel_process_ctx_encode_protobuf_payload(char **o
595646
if (data.telemetry_sdk_language) free((void *)data.telemetry_sdk_language);
596647
if (data.telemetry_sdk_version) free((void *)data.telemetry_sdk_version);
597648
if (data.telemetry_sdk_name) free((void *)data.telemetry_sdk_name);
598-
if (data.resources) {
599-
for (int i = 0; data.resources[i] != NULL; i++) free((void *)data.resources[i]);
600-
free((void *)data.resources);
649+
if (data.resource_attributes) {
650+
for (int i = 0; data.resource_attributes[i] != NULL; i++) free((void *)data.resource_attributes[i]);
651+
free((void *)data.resource_attributes);
652+
}
653+
if (data.extra_attributes) {
654+
for (int i = 0; data.extra_attributes[i] != NULL; i++) free((void *)data.extra_attributes[i]);
655+
free((void *)data.extra_attributes);
601656
}
602657
}
603658

process-context/c-and-cpp/otel_process_ctx.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,14 @@ typedef struct {
5454
const char *telemetry_sdk_version;
5555
// https://opentelemetry.io/docs/specs/semconv/registry/attributes/telemetry/#telemetry-sdk-name
5656
const char *telemetry_sdk_name;
57-
// Additional key/value pairs as resources https://opentelemetry.io/docs/specs/otel/resource/sdk/
58-
// Can be NULL if no resources are needed; if non-NULL, this array MUST be terminated with a NULL entry.
57+
// Additional key/value pairs as resource attributes https://opentelemetry.io/docs/specs/otel/resource/sdk/
58+
// Can be NULL if no resource attributes are needed; if non-NULL, this array MUST be terminated with a NULL entry.
5959
// Every even entry is a key, every odd entry is a value (E.g. "key1", "value1", "key2", "value2", NULL).
60-
const char **resources;
60+
const char **resource_attributes;
61+
// Additional key/value pairs as extra attributes (ProcessContext.extra_attributes in process_context.proto)
62+
// Can be NULL if no extra attributes are needed; if non-NULL, this array MUST be terminated with a NULL entry.
63+
// Every even entry is a key, every odd entry is a value (E.g. "key1", "value1", "key2", "value2", NULL).
64+
const char **extra_attributes;
6165
} otel_process_ctx_data;
6266

6367
/** Number of entries in the `otel_process_ctx_data` struct. Can be used to easily detect when the struct is updated. */

process-context/c-and-cpp/otel_process_ctx_dump.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if ! [[ "$pid" =~ ^[0-9]+$ ]]; then
2727
fi
2828

2929
# Find the mapping by name
30-
if ! line="$(grep -F -m 1 -e '[anon_shmem:OTEL_CTX]' -e '/memfd:OTEL_CTX' "/proc/$pid/maps")"; then
30+
if ! line="$(grep -F -m 1 -e '[anon_shmem:OTEL_CTX]' -e '/memfd:OTEL_CTX' -e '[anon:OTEL_CTX]' "/proc/$pid/maps")"; then
3131
echo "No OTEL_CTX context found." >&2
3232
exit 1
3333
fi

0 commit comments

Comments
 (0)