From ba2cee43a6f36bb0e5ab427024710199be65ceee Mon Sep 17 00:00:00 2001 From: Basundhara Chakrabarty Date: Sun, 5 Jul 2026 21:21:40 +0000 Subject: [PATCH 1/2] dynamic_modules: expose stat tags and tag-extracted names in the stats sink ABI Signed-off-by: Basundhara Chakrabarty --- source/extensions/dynamic_modules/abi/abi.h | 109 ++++++++ source/extensions/dynamic_modules/abi_impl.cc | 72 +++++ .../dynamic_modules/sdk/rust/src/lib_test.rs | 183 +++++++++++++ .../sdk/rust/src/stats_sink.rs | 255 +++++++++++++++--- .../stat_sinks/dynamic_modules/abi_impl.cc | 143 ++++++++++ .../stat_sink/abi_impl_test.cc | 114 ++++++++ 6 files changed, 834 insertions(+), 42 deletions(-) diff --git a/source/extensions/dynamic_modules/abi/abi.h b/source/extensions/dynamic_modules/abi/abi.h index 6ce9459e559d2..833b613ec0102 100644 --- a/source/extensions/dynamic_modules/abi/abi.h +++ b/source/extensions/dynamic_modules/abi/abi.h @@ -13657,6 +13657,115 @@ bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout( char* name_buffer, size_t name_buffer_capacity, size_t* name_size, char* value_buffer, size_t value_buffer_capacity, size_t* value_size); +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name writes the + * tag-extracted name of a counter at the given index. The tag-extracted name is the stat name with + * the tag values removed (for example "cluster.foo.bar" with a "cluster_name" tag extracted becomes + * "cluster.bar"), so a module can reconstruct the same name and labels that Envoy's built-in stat + * formatters produce. Pair this with the tag callbacks below. + * + * @param snapshot_envoy_ptr is the opaque snapshot handle. + * @param index is the index of the counter (0-based). + * @param name_buffer is the module-owned buffer that receives the tag-extracted name. No null + * terminator is written. May be null only if name_buffer_capacity is 0. + * @param name_buffer_capacity is the capacity of name_buffer in bytes. + * @param name_size is set to the full length of the name, with the same truncation contract as the + * name callbacks above. Must not be null. + * @return true if the index is valid, false otherwise. When false, no outputs are written. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + char* name_buffer, size_t name_buffer_capacity, size_t* name_size); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count returns the number of + * tags on the counter at the given index. + * + * @param snapshot_envoy_ptr is the opaque snapshot handle. + * @param index is the index of the counter (0-based). + * @param tag_count is set to the number of tags on the counter. Must not be null. + * @return true if the index is valid, false otherwise. When false, tag_count is not written. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t* tag_count); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag writes the name and value of a + * single tag on the counter at the given index into module-provided buffers. + * + * @param snapshot_envoy_ptr is the opaque snapshot handle. + * @param index is the index of the counter (0-based). + * @param tag_index is the index of the tag (0-based, less than the count reported by + * envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count). + * @param name_buffer is the module-owned buffer that receives the tag name. No null terminator is + * written. May be null only if name_buffer_capacity is 0. + * @param name_buffer_capacity is the capacity of name_buffer in bytes. + * @param name_size is set to the full length of the tag name, with the same truncation contract as + * the name callbacks above. Must not be null. + * @param value_buffer is the module-owned buffer that receives the tag value. No null terminator is + * written. May be null only if value_buffer_capacity is 0. + * @param value_buffer_capacity is the capacity of value_buffer in bytes. + * @param value_size is set to the full length of the tag value, with the same truncation contract. + * Must not be null. + * @return true if both index and tag_index are valid, false otherwise. When false, no outputs are + * written. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t tag_index, char* name_buffer, size_t name_buffer_capacity, size_t* name_size, + char* value_buffer, size_t value_buffer_capacity, size_t* value_size); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name is the gauge + * counterpart of the counter tag-extracted-name callback above. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + char* name_buffer, size_t name_buffer_capacity, size_t* name_size); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count is the gauge counterpart of + * the counter tag-count callback above. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t* tag_count); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag is the gauge counterpart of the + * counter tag callback above. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t tag_index, char* name_buffer, size_t name_buffer_capacity, size_t* name_size, + char* value_buffer, size_t value_buffer_capacity, size_t* value_size); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name is the text + * readout counterpart of the counter tag-extracted-name callback above. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + char* name_buffer, size_t name_buffer_capacity, size_t* name_size); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count is the text readout + * counterpart of the counter tag-count callback above. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t* tag_count); + +/** + * envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag is the text readout + * counterpart of the counter tag callback above. + */ +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t tag_index, char* name_buffer, size_t name_buffer_capacity, size_t* name_size, + char* value_buffer, size_t value_buffer_capacity, size_t* value_size); + /** * envoy_dynamic_module_callback_stat_sink_config_define_gauge creates a gauge with the given name * that the module can update later via envoy_dynamic_module_callback_stat_sink_config_set_gauge. diff --git a/source/extensions/dynamic_modules/abi_impl.cc b/source/extensions/dynamic_modules/abi_impl.cc index b788c2c4ba022..488a791d81cef 100644 --- a/source/extensions/dynamic_modules/abi_impl.cc +++ b/source/extensions/dynamic_modules/abi_impl.cc @@ -3821,6 +3821,78 @@ __attribute__((weak)) bool envoy_dynamic_module_callback_stat_sink_snapshot_get_ return false; } +__attribute__((weak)) bool +envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, char*, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool +envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t, char*, size_t, size_t*, + char*, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool +envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, char*, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t, char*, size_t, size_t*, + char*, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool +envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, char*, size_t, size_t*) { + IS_ENVOY_BUG( + "envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool +envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count: " + "not implemented in this context"); + return false; +} + +__attribute__((weak)) bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t, char*, size_t, size_t*, + char*, size_t, size_t*) { + IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag: " + "not implemented in this context"); + return false; +} + __attribute__((weak)) envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_stat_sink_config_define_gauge( envoy_dynamic_module_type_stat_sink_config_envoy_ptr, envoy_dynamic_module_type_module_buffer, diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs index 55431de029d30..7a7312316f54e 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs @@ -6736,6 +6736,12 @@ const STUB_COUNTERS: [(&str, u64, u64); 2] = [("counter_0", 10, 5), ("counter_1" const STUB_GAUGES: [(&str, u64); 1] = [("gauge_0", 42)]; const STUB_TEXT_READOUTS: [(&str, &str); 1] = [("text_0", "value_0")]; +// Tag-extracted names and tags, indexed to match STUB_COUNTERS. counter_0 carries two tags, +// counter_1 carries none, exercising both the empty and multi-tag paths. +const STUB_COUNTER_TAG_EXTRACTED_NAMES: [&str; 2] = ["cluster.rq_total", "counter_1"]; +const STUB_COUNTER_TAGS: [&[(&str, &str)]; 2] = + [&[("envoy.cluster_name", "foo"), ("envoy.response_code", "200")], &[]]; + // Counts stub_write invocations on the calling thread so tests can assert the grow-and-retry // behavior (each ABI getter call writes its name once, plus a value for text readouts). It is // thread-local so it stays correct even if tests run in parallel. @@ -6800,6 +6806,155 @@ pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_counter( true } +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + name_buffer: *mut std::ffi::c_char, + name_buffer_capacity: usize, + name_size: *mut usize, +) -> bool { + if index >= STUB_COUNTER_TAG_EXTRACTED_NAMES.len() { + return false; + } + unsafe { + stub_write( + STUB_COUNTER_TAG_EXTRACTED_NAMES[index], + name_buffer, + name_buffer_capacity, + name_size, + ); + } + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + tag_count: *mut usize, +) -> bool { + if index >= STUB_COUNTER_TAGS.len() { + return false; + } + unsafe { + *tag_count = STUB_COUNTER_TAGS[index].len(); + } + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + tag_index: usize, + name_buffer: *mut std::ffi::c_char, + name_buffer_capacity: usize, + name_size: *mut usize, + value_buffer: *mut std::ffi::c_char, + value_buffer_capacity: usize, + value_size: *mut usize, +) -> bool { + if index >= STUB_COUNTER_TAGS.len() || tag_index >= STUB_COUNTER_TAGS[index].len() { + return false; + } + let (name, value) = STUB_COUNTER_TAGS[index][tag_index]; + unsafe { + stub_write(name, name_buffer, name_buffer_capacity, name_size); + stub_write(value, value_buffer, value_buffer_capacity, value_size); + } + true +} + +// Gauge and text-readout tag callbacks share the counter tag code paths, so the harness stubs +// them as empty (no tags) purely to satisfy linkage; the counter stubs above carry the tag data +// the tests exercise. +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + name_buffer: *mut std::ffi::c_char, + name_buffer_capacity: usize, + name_size: *mut usize, +) -> bool { + if index >= STUB_GAUGES.len() { + return false; + } + unsafe { stub_write(STUB_GAUGES[index].0, name_buffer, name_buffer_capacity, name_size) }; + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + tag_count: *mut usize, +) -> bool { + if index >= STUB_GAUGES.len() { + return false; + } + unsafe { *tag_count = 0 }; + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + _index: usize, + _tag_index: usize, + _name_buffer: *mut std::ffi::c_char, + _name_buffer_capacity: usize, + _name_size: *mut usize, + _value_buffer: *mut std::ffi::c_char, + _value_buffer_capacity: usize, + _value_size: *mut usize, +) -> bool { + false +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + name_buffer: *mut std::ffi::c_char, + name_buffer_capacity: usize, + name_size: *mut usize, +) -> bool { + if index >= STUB_TEXT_READOUTS.len() { + return false; + } + unsafe { stub_write(STUB_TEXT_READOUTS[index].0, name_buffer, name_buffer_capacity, name_size) }; + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + index: usize, + tag_count: *mut usize, +) -> bool { + if index >= STUB_TEXT_READOUTS.len() { + return false; + } + unsafe { *tag_count = 0 }; + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, + _index: usize, + _tag_index: usize, + _name_buffer: *mut std::ffi::c_char, + _name_buffer_capacity: usize, + _name_size: *mut usize, + _value_buffer: *mut std::ffi::c_char, + _value_buffer_capacity: usize, + _value_size: *mut usize, +) -> bool { + false +} + #[no_mangle] pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_count( _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, @@ -6958,6 +7113,34 @@ fn test_metric_snapshot_reads_all_entry_types() { assert_eq!(text_value.as_slice(), b"value_0"); } +#[test] +fn test_metric_snapshot_reads_tags() { + let mut dummy = 0u8; + let snapshot = stats_sink::MetricSnapshot::new(&mut dummy as *mut _ as *mut std::ffi::c_void); + + let mut name = Vec::new(); + let mut value = Vec::new(); + + // counter_0: tag-extracted name plus two tags. + assert!(snapshot.counter_tag_extracted_name(0, &mut name)); + assert_eq!(name.as_slice(), b"cluster.rq_total"); + assert_eq!(snapshot.counter_tag_count(0), Some(2)); + assert!(snapshot.counter_tag(0, 0, &mut name, &mut value)); + assert_eq!(name.as_slice(), b"envoy.cluster_name"); + assert_eq!(value.as_slice(), b"foo"); + assert!(snapshot.counter_tag(0, 1, &mut name, &mut value)); + assert_eq!(name.as_slice(), b"envoy.response_code"); + assert_eq!(value.as_slice(), b"200"); + + // counter_1: no tags. + assert_eq!(snapshot.counter_tag_count(1), Some(0)); + + // Out-of-range metric and tag indices return None/false. + assert_eq!(snapshot.counter_tag_count(2), None); + assert!(!snapshot.counter_tag(0, 2, &mut name, &mut value)); + assert!(!snapshot.counter_tag(2, 0, &mut name, &mut value)); +} + #[test] fn test_metric_snapshot_reuses_buffer_without_reallocating() { let mut dummy = 0u8; diff --git a/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs b/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs index 8b43a6ffa6211..6fec008c7bd20 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs @@ -104,48 +104,175 @@ impl<'a> MetricSnapshot<'a> { /// Returns `true` on success with both buffers holding exactly their bytes. Returns `false` and /// leaves both buffers unchanged when the index is out of range. pub fn text_readout(&self, index: usize, name: &mut Vec, value: &mut Vec) -> bool { - // Both outputs come from a single call, and either may be truncated independently, so grow - // whichever did not fit and retry until both fit. The snapshot is stable for the duration of - // the flush, so this converges in at most two iterations. As with `fill_buffer`, a - // zero-capacity `Vec` passes a dangling-but-non-null pointer that Envoy leaves untouched. - loop { - let name_capacity = name.capacity(); - let value_capacity = value.capacity(); - let mut name_size: usize = 0; - let mut value_size: usize = 0; - let found = unsafe { - abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout( - self.envoy_ptr, - index, - name.as_mut_ptr() as *mut c_char, - name_capacity, - &mut name_size, - value.as_mut_ptr() as *mut c_char, - value_capacity, - &mut value_size, - ) - }; - if !found { - return false; - } - if name_size <= name_capacity && value_size <= value_capacity { - // SAFETY: Envoy's snprintf-style contract guarantees that when each `size <= capacity` it - // wrote exactly that many bytes into the buffer, so no uninitialized bytes are exposed. - unsafe { - name.set_len(name_size); - value.set_len(value_size); - } - return true; - } - if name_size > name_capacity { - name.clear(); - name.reserve(name_size); - } - if value_size > value_capacity { - value.clear(); - value.reserve(value_size); - } - } + fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, + value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout( + self.envoy_ptr, + index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }) + } + + /// Reads the tag-extracted name of the counter at `index` into `name` (the stat name with tag + /// values removed). Returns `None` and leaves `name` unchanged when the index is out of range. + pub fn counter_tag_extracted_name(&self, index: usize, name: &mut Vec) -> bool { + fill_buffer(name, |ptr, capacity, size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + self.envoy_ptr, + index, + ptr, + capacity, + size, + ) + }) + } + + /// The number of tags on the counter at `index`, or `None` when the index is out of range. + pub fn counter_tag_count(&self, index: usize) -> Option { + let mut count: usize = 0; + let found = unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + self.envoy_ptr, + index, + &mut count, + ) + }; + found.then_some(count) + } + + /// Reads tag `tag_index` of the counter at `index` into `name` and `value`. Returns `false` and + /// leaves both buffers unchanged when either index is out of range. + pub fn counter_tag( + &self, + index: usize, + tag_index: usize, + name: &mut Vec, + value: &mut Vec, + ) -> bool { + fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, + value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + self.envoy_ptr, + index, + tag_index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }) + } + + /// Reads the tag-extracted name of the gauge at `index` into `name`. See + /// [`counter_tag_extracted_name`](Self::counter_tag_extracted_name). + pub fn gauge_tag_extracted_name(&self, index: usize, name: &mut Vec) -> bool { + fill_buffer(name, |ptr, capacity, size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( + self.envoy_ptr, + index, + ptr, + capacity, + size, + ) + }) + } + + /// The number of tags on the gauge at `index`, or `None` when the index is out of range. + pub fn gauge_tag_count(&self, index: usize) -> Option { + let mut count: usize = 0; + let found = unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( + self.envoy_ptr, + index, + &mut count, + ) + }; + found.then_some(count) + } + + /// Reads tag `tag_index` of the gauge at `index` into `name` and `value`. See + /// [`counter_tag`](Self::counter_tag). + pub fn gauge_tag( + &self, + index: usize, + tag_index: usize, + name: &mut Vec, + value: &mut Vec, + ) -> bool { + fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, + value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + self.envoy_ptr, + index, + tag_index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }) + } + + /// Reads the tag-extracted name of the text readout at `index` into `name`. See + /// [`counter_tag_extracted_name`](Self::counter_tag_extracted_name). + pub fn text_readout_tag_extracted_name(&self, index: usize, name: &mut Vec) -> bool { + fill_buffer(name, |ptr, capacity, size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + self.envoy_ptr, + index, + ptr, + capacity, + size, + ) + }) + } + + /// The number of tags on the text readout at `index`, or `None` when the index is out of range. + pub fn text_readout_tag_count(&self, index: usize) -> Option { + let mut count: usize = 0; + let found = unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count( + self.envoy_ptr, + index, + &mut count, + ) + }; + found.then_some(count) + } + + /// Reads tag `tag_index` of the text readout at `index` into `name` and `value`. See + /// [`counter_tag`](Self::counter_tag). + pub fn text_readout_tag( + &self, + index: usize, + tag_index: usize, + name: &mut Vec, + value: &mut Vec, + ) -> bool { + fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, + value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + self.envoy_ptr, + index, + tag_index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }) } /// Copies the whole snapshot into an [`OwnedMetricSnapshot`]. @@ -270,6 +397,50 @@ where } } +/// Fills a name and value buffer from a single call that may truncate either independently. Grows +/// whichever did not fit and retries until both fit, which converges in at most two iterations +/// since the snapshot is stable for the flush. Returns `false` and leaves both buffers unchanged +/// when the fill reports the entry does not exist. +fn fill_two_buffers(name: &mut Vec, value: &mut Vec, mut fill: F) -> bool +where + F: FnMut(*mut c_char, usize, &mut usize, *mut c_char, usize, &mut usize) -> bool, +{ + loop { + let name_capacity = name.capacity(); + let value_capacity = value.capacity(); + let mut name_size: usize = 0; + let mut value_size: usize = 0; + let found = fill( + name.as_mut_ptr() as *mut c_char, + name_capacity, + &mut name_size, + value.as_mut_ptr() as *mut c_char, + value_capacity, + &mut value_size, + ); + if !found { + return false; + } + if name_size <= name_capacity && value_size <= value_capacity { + // SAFETY: Envoy's snprintf-style contract guarantees that when each `size <= capacity` it + // wrote exactly that many bytes into the buffer, so no uninitialized bytes are exposed. + unsafe { + name.set_len(name_size); + value.set_len(value_size); + } + return true; + } + if name_size > name_capacity { + name.clear(); + name.reserve(name_size); + } + if value_size > value_capacity { + value.clear(); + value.reserve(value_size); + } + } +} + /// A stats sink implemented by a dynamic module. /// /// A single instance is created per sink configuration and shared across threads. [`on_flush`] diff --git a/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc b/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc index 215dfdc38b6b8..0d8a1e7bc6818 100644 --- a/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc +++ b/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc @@ -40,6 +40,82 @@ void copyToModuleBuffer(absl::string_view src, char* buffer, size_t capacity, si *size_out = src.size(); } +// Resolves the Metric at index from a snapshot collection, or nullptr when out of range. The +// counter collection holds CounterSnapshot (metric under counter_), while the gauge and text +// readout collections hold metric reference wrappers directly; these overloads hide that shape +// difference from the tag helpers below. +const Envoy::Stats::Metric* +metricAt(const std::vector& counters, size_t index) { + return index < counters.size() ? &counters[index].counter_.get() : nullptr; +} +template +const Envoy::Stats::Metric* metricAt(const MetricRefs& metrics, size_t index) { + return index < metrics.size() ? &metrics[index].get() : nullptr; +} + +// Serializes the metric's tag-extracted name directly into the module buffer. Uses the borrowed +// tagExtractedStatName() and serializeToBuffer, so no intermediate std::string is composed (the +// same allocation-free path as the counter/gauge name callbacks). Returns false when index is out +// of range. +template +bool getTagExtractedName(const MetricRefs& metrics, size_t index, char* name_buffer, + size_t name_buffer_capacity, size_t* name_size) { + const Envoy::Stats::Metric* metric = metricAt(metrics, index); + if (metric == nullptr) { + return false; + } + *name_size = metric->constSymbolTable().serializeToBuffer(metric->tagExtractedStatName(), + name_buffer, name_buffer_capacity); + return true; +} + +// Reports the number of tags on the metric at index. Counts via iterateTagStatNames so the tag +// StatNames are not materialized into a std::string vector. Returns false when index is out of +// range. +template +bool getTagCount(const MetricRefs& metrics, size_t index, size_t* tag_count) { + const Envoy::Stats::Metric* metric = metricAt(metrics, index); + if (metric == nullptr) { + return false; + } + size_t count = 0; + metric->iterateTagStatNames([&count](Envoy::Stats::StatName, Envoy::Stats::StatName) { + ++count; + return true; + }); + *tag_count = count; + return true; +} + +// Serializes the name and value of one tag directly into the module buffers. Iterates the borrowed +// tag StatNames and serializeToBuffers the requested pair, so no std::string is composed. Returns +// false when either index is out of range. +template +bool getTag(const MetricRefs& metrics, size_t index, size_t tag_index, char* name_buffer, + size_t name_buffer_capacity, size_t* name_size, char* value_buffer, + size_t value_buffer_capacity, size_t* value_size) { + const Envoy::Stats::Metric* metric = metricAt(metrics, index); + if (metric == nullptr) { + return false; + } + const Envoy::Stats::SymbolTable& symbol_table = metric->constSymbolTable(); + size_t current = 0; + bool found = false; + metric->iterateTagStatNames( + [&](Envoy::Stats::StatName tag_name, Envoy::Stats::StatName tag_value) { + if (current == tag_index) { + *name_size = symbol_table.serializeToBuffer(tag_name, name_buffer, name_buffer_capacity); + *value_size = + symbol_table.serializeToBuffer(tag_value, value_buffer, value_buffer_capacity); + found = true; + return false; + } + ++current; + return true; + }); + return found; +} + } // namespace extern "C" { @@ -110,6 +186,73 @@ bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout( return true; } +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + char* name_buffer, size_t name_buffer_capacity, size_t* name_size) { + return getTagExtractedName(toFlushContext(snapshot_envoy_ptr)->snapshot_.counters(), index, + name_buffer, name_buffer_capacity, name_size); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t* tag_count) { + return getTagCount(toFlushContext(snapshot_envoy_ptr)->snapshot_.counters(), index, tag_count); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t tag_index, char* name_buffer, size_t name_buffer_capacity, size_t* name_size, + char* value_buffer, size_t value_buffer_capacity, size_t* value_size) { + return getTag(toFlushContext(snapshot_envoy_ptr)->snapshot_.counters(), index, tag_index, + name_buffer, name_buffer_capacity, name_size, value_buffer, value_buffer_capacity, + value_size); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + char* name_buffer, size_t name_buffer_capacity, size_t* name_size) { + return getTagExtractedName(toFlushContext(snapshot_envoy_ptr)->snapshot_.gauges(), index, + name_buffer, name_buffer_capacity, name_size); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t* tag_count) { + return getTagCount(toFlushContext(snapshot_envoy_ptr)->snapshot_.gauges(), index, tag_count); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t tag_index, char* name_buffer, size_t name_buffer_capacity, size_t* name_size, + char* value_buffer, size_t value_buffer_capacity, size_t* value_size) { + return getTag(toFlushContext(snapshot_envoy_ptr)->snapshot_.gauges(), index, tag_index, + name_buffer, name_buffer_capacity, name_size, value_buffer, value_buffer_capacity, + value_size); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + char* name_buffer, size_t name_buffer_capacity, size_t* name_size) { + return getTagExtractedName(toFlushContext(snapshot_envoy_ptr)->snapshot_.textReadouts(), index, + name_buffer, name_buffer_capacity, name_size); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t* tag_count) { + return getTagCount(toFlushContext(snapshot_envoy_ptr)->snapshot_.textReadouts(), index, + tag_count); +} + +bool envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr snapshot_envoy_ptr, size_t index, + size_t tag_index, char* name_buffer, size_t name_buffer_capacity, size_t* name_size, + char* value_buffer, size_t value_buffer_capacity, size_t* value_size) { + return getTag(toFlushContext(snapshot_envoy_ptr)->snapshot_.textReadouts(), index, tag_index, + name_buffer, name_buffer_capacity, name_size, value_buffer, value_buffer_capacity, + value_size); +} + envoy_dynamic_module_type_metrics_result envoy_dynamic_module_callback_stat_sink_config_define_gauge( envoy_dynamic_module_type_stat_sink_config_envoy_ptr config_envoy_ptr, diff --git a/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc b/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc index 066b6e9910ff5..58f18b8c8b6eb 100644 --- a/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc +++ b/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc @@ -163,6 +163,120 @@ TEST_F(DynamicModuleStatsSinkAbiTest, GetCounterOutOfRange) { EXPECT_EQ('Z', name_buffer[0]); } +// ============================================================================= +// Tag callbacks +// ============================================================================= + +TEST_F(DynamicModuleStatsSinkAbiTest, GetCounterTagExtractedNameAndTags) { + c0_.name_ = "cluster.foo.rq_total"; + c0_.setTagExtractedName("cluster.rq_total"); + c0_.setTags({{"envoy.cluster_name", "foo"}, {"envoy.response_code", "200"}}); + snapshot_.counters_.push_back({/*delta=*/1, c0_}); + + char name_buffer[256]; + size_t name_size = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + snapshotHandle(), 0, name_buffer, sizeof(name_buffer), &name_size)); + EXPECT_EQ("cluster.rq_total", written(name_buffer, name_size, sizeof(name_buffer))); + + size_t tag_count = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + snapshotHandle(), 0, &tag_count)); + EXPECT_EQ(2u, tag_count); + + struct Expected { + const char* name; + const char* value; + } expected[] = {{"envoy.cluster_name", "foo"}, {"envoy.response_code", "200"}}; + for (size_t i = 0; i < 2; i++) { + char tag_name[256]; + char tag_value[256]; + size_t tag_name_size = 0; + size_t tag_value_size = 0; + ASSERT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + snapshotHandle(), 0, i, tag_name, sizeof(tag_name), &tag_name_size, tag_value, + sizeof(tag_value), &tag_value_size)); + EXPECT_EQ(expected[i].name, written(tag_name, tag_name_size, sizeof(tag_name))); + EXPECT_EQ(expected[i].value, written(tag_value, tag_value_size, sizeof(tag_value))); + } +} + +// A counter with no tags reports zero and no tag is readable. +TEST_F(DynamicModuleStatsSinkAbiTest, GetCounterTagCountZero) { + c0_.name_ = "no_tags"; + snapshot_.counters_.push_back({/*delta=*/1, c0_}); + + size_t tag_count = 12345; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + snapshotHandle(), 0, &tag_count)); + EXPECT_EQ(0u, tag_count); + + char name_buffer[16]; + char value_buffer[16]; + size_t name_size = 0; + size_t value_size = 0; + EXPECT_FALSE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + snapshotHandle(), 0, 0, name_buffer, sizeof(name_buffer), &name_size, value_buffer, + sizeof(value_buffer), &value_size)); +} + +// Out-of-range metric and tag indices return false without writing outputs. +TEST_F(DynamicModuleStatsSinkAbiTest, GetCounterTagOutOfRange) { + c0_.name_ = "cluster.foo.rq_total"; + c0_.setTags({{"envoy.cluster_name", "foo"}}); + snapshot_.counters_.push_back({/*delta=*/1, c0_}); + + char name_buffer[256] = {'Z'}; + char value_buffer[256] = {'Z'}; + size_t name_size = 12345; + size_t value_size = 6789; + // Tag index past the end. + EXPECT_FALSE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + snapshotHandle(), 0, 1, name_buffer, sizeof(name_buffer), &name_size, value_buffer, + sizeof(value_buffer), &value_size)); + // Metric index past the end. + EXPECT_FALSE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + snapshotHandle(), 5, 0, name_buffer, sizeof(name_buffer), &name_size, value_buffer, + sizeof(value_buffer), &value_size)); + size_t tag_count = 42; + EXPECT_FALSE(envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( + snapshotHandle(), 5, &tag_count)); + EXPECT_EQ(12345u, name_size); + EXPECT_EQ(6789u, value_size); + EXPECT_EQ(42u, tag_count); + EXPECT_EQ('Z', name_buffer[0]); +} + +// Gauge and text-readout tag callbacks share the counter code path; a gauge spot check confirms +// the wiring reaches the right snapshot collection. +TEST_F(DynamicModuleStatsSinkAbiTest, GetGaugeTagExtractedNameAndTags) { + g0_.name_ = "cluster.bar.cx_active"; + g0_.setTagExtractedName("cluster.cx_active"); + g0_.setTags({{"envoy.cluster_name", "bar"}}); + snapshot_.gauges_.push_back(g0_); + + char name_buffer[256]; + size_t name_size = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( + snapshotHandle(), 0, name_buffer, sizeof(name_buffer), &name_size)); + EXPECT_EQ("cluster.cx_active", written(name_buffer, name_size, sizeof(name_buffer))); + + size_t tag_count = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( + snapshotHandle(), 0, &tag_count)); + EXPECT_EQ(1u, tag_count); + + char tag_name[256]; + char tag_value[256]; + size_t tag_name_size = 0; + size_t tag_value_size = 0; + ASSERT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + snapshotHandle(), 0, 0, tag_name, sizeof(tag_name), &tag_name_size, tag_value, + sizeof(tag_value), &tag_value_size)); + EXPECT_EQ("envoy.cluster_name", written(tag_name, tag_name_size, sizeof(tag_name))); + EXPECT_EQ("bar", written(tag_value, tag_value_size, sizeof(tag_value))); +} + // ============================================================================= // Gauge callbacks // ============================================================================= From 493b3bd3d2f23b67287572c727c170f8a0a60be8 Mon Sep 17 00:00:00 2001 From: Basundhara Chakrabarty Date: Wed, 8 Jul 2026 05:46:18 +0000 Subject: [PATCH 2/2] dynamic_modules: test stats sink tag callbacks end to end and fix a doc comment Signed-off-by: Basundhara Chakrabarty --- ..._modules__added-stats-sink-tag-getters.rst | 4 + source/extensions/dynamic_modules/abi_impl.cc | 3 +- .../dynamic_modules/sdk/rust/src/lib_test.rs | 125 +++++++++++++----- .../sdk/rust/src/stats_sink.rs | 124 +++++++++-------- .../stat_sinks/dynamic_modules/abi_impl.cc | 23 ++-- .../dynamic_modules/abi_impl_test.cc | 29 ++++ .../stat_sink/abi_impl_test.cc | 34 ++++- .../stat_sink/integration_test.cc | 27 ++-- .../rust/stat_sink_integration_test.rs | 38 ++++++ 9 files changed, 294 insertions(+), 113 deletions(-) create mode 100644 changelogs/current/new_features/dynamic_modules__added-stats-sink-tag-getters.rst diff --git a/changelogs/current/new_features/dynamic_modules__added-stats-sink-tag-getters.rst b/changelogs/current/new_features/dynamic_modules__added-stats-sink-tag-getters.rst new file mode 100644 index 0000000000000..b228103c88b1a --- /dev/null +++ b/changelogs/current/new_features/dynamic_modules__added-stats-sink-tag-getters.rst @@ -0,0 +1,4 @@ +Added stats sink snapshot getters that expose each metric's tag-extracted name and its tags +(name/value pairs) for counters, gauges, and text readouts, so a dynamic module can reconstruct +the dimensional metric names Envoy's built-in formatters produce. Available through the Rust SDK +``MetricSnapshot`` tag accessors. diff --git a/source/extensions/dynamic_modules/abi_impl.cc b/source/extensions/dynamic_modules/abi_impl.cc index 488a791d81cef..9caa95f1cae9a 100644 --- a/source/extensions/dynamic_modules/abi_impl.cc +++ b/source/extensions/dynamic_modules/abi_impl.cc @@ -3829,8 +3829,7 @@ envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( return false; } -__attribute__((weak)) bool -envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( +__attribute__((weak)) bool envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count( envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, size_t, size_t*) { IS_ENVOY_BUG("envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count: " "not implemented in this context"); diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs index 7a7312316f54e..8b0c7d4904aa8 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs @@ -6739,8 +6739,21 @@ const STUB_TEXT_READOUTS: [(&str, &str); 1] = [("text_0", "value_0")]; // Tag-extracted names and tags, indexed to match STUB_COUNTERS. counter_0 carries two tags, // counter_1 carries none, exercising both the empty and multi-tag paths. const STUB_COUNTER_TAG_EXTRACTED_NAMES: [&str; 2] = ["cluster.rq_total", "counter_1"]; -const STUB_COUNTER_TAGS: [&[(&str, &str)]; 2] = - [&[("envoy.cluster_name", "foo"), ("envoy.response_code", "200")], &[]]; +const STUB_COUNTER_TAGS: [&[(&str, &str)]; 2] = [ + &[ + ("envoy.cluster_name", "foo"), + ("envoy.response_code", "200"), + ], + &[], +]; + +// Tag data for the single stub gauge and text readout, matching STUB_GAUGES / STUB_TEXT_READOUTS. +// Both carry one tag so the gauge and text-readout tag wrappers are exercised distinctly from the +// counter ones, guarding against a wrapper wired to the wrong ABI callback. +const STUB_GAUGE_TAG_EXTRACTED_NAMES: [&str; 1] = ["cluster.cx_active"]; +const STUB_GAUGE_TAGS: [&[(&str, &str)]; 1] = [&[("envoy.cluster_name", "bar")]]; +const STUB_TEXT_READOUT_TAG_EXTRACTED_NAMES: [&str; 1] = ["control_plane.identifier"]; +const STUB_TEXT_READOUT_TAGS: [&[(&str, &str)]; 1] = [&[("envoy.control_plane", "xds")]]; // Counts stub_write invocations on the calling thread so tests can assert the grow-and-retry // behavior (each ABI getter call writes its name once, plus a value for text readouts). It is @@ -6866,9 +6879,8 @@ pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_t true } -// Gauge and text-readout tag callbacks share the counter tag code paths, so the harness stubs -// them as empty (no tags) purely to satisfy linkage; the counter stubs above carry the tag data -// the tests exercise. +// The gauge and text-readout tag callbacks share the counter tag code paths, but each stub serves +// its own canned data so the corresponding SDK wrappers are exercised distinctly. #[no_mangle] pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name( _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, @@ -6877,10 +6889,17 @@ pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag name_buffer_capacity: usize, name_size: *mut usize, ) -> bool { - if index >= STUB_GAUGES.len() { + if index >= STUB_GAUGE_TAG_EXTRACTED_NAMES.len() { return false; } - unsafe { stub_write(STUB_GAUGES[index].0, name_buffer, name_buffer_capacity, name_size) }; + unsafe { + stub_write( + STUB_GAUGE_TAG_EXTRACTED_NAMES[index], + name_buffer, + name_buffer_capacity, + name_size, + ); + } true } @@ -6890,26 +6909,34 @@ pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag index: usize, tag_count: *mut usize, ) -> bool { - if index >= STUB_GAUGES.len() { + if index >= STUB_GAUGE_TAGS.len() { return false; } - unsafe { *tag_count = 0 }; + unsafe { *tag_count = STUB_GAUGE_TAGS[index].len() }; true } #[no_mangle] pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, - _index: usize, - _tag_index: usize, - _name_buffer: *mut std::ffi::c_char, - _name_buffer_capacity: usize, - _name_size: *mut usize, - _value_buffer: *mut std::ffi::c_char, - _value_buffer_capacity: usize, - _value_size: *mut usize, + index: usize, + tag_index: usize, + name_buffer: *mut std::ffi::c_char, + name_buffer_capacity: usize, + name_size: *mut usize, + value_buffer: *mut std::ffi::c_char, + value_buffer_capacity: usize, + value_size: *mut usize, ) -> bool { - false + if index >= STUB_GAUGE_TAGS.len() || tag_index >= STUB_GAUGE_TAGS[index].len() { + return false; + } + let (name, value) = STUB_GAUGE_TAGS[index][tag_index]; + unsafe { + stub_write(name, name_buffer, name_buffer_capacity, name_size); + stub_write(value, value_buffer, value_buffer_capacity, value_size); + } + true } #[no_mangle] @@ -6920,10 +6947,17 @@ pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_text_read name_buffer_capacity: usize, name_size: *mut usize, ) -> bool { - if index >= STUB_TEXT_READOUTS.len() { + if index >= STUB_TEXT_READOUT_TAG_EXTRACTED_NAMES.len() { return false; } - unsafe { stub_write(STUB_TEXT_READOUTS[index].0, name_buffer, name_buffer_capacity, name_size) }; + unsafe { + stub_write( + STUB_TEXT_READOUT_TAG_EXTRACTED_NAMES[index], + name_buffer, + name_buffer_capacity, + name_size, + ); + } true } @@ -6933,26 +6967,34 @@ pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_text_read index: usize, tag_count: *mut usize, ) -> bool { - if index >= STUB_TEXT_READOUTS.len() { + if index >= STUB_TEXT_READOUT_TAGS.len() { return false; } - unsafe { *tag_count = 0 }; + unsafe { *tag_count = STUB_TEXT_READOUT_TAGS[index].len() }; true } #[no_mangle] pub extern "C" fn envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( _snapshot: abi::envoy_dynamic_module_type_stat_sink_snapshot_envoy_ptr, - _index: usize, - _tag_index: usize, - _name_buffer: *mut std::ffi::c_char, - _name_buffer_capacity: usize, - _name_size: *mut usize, - _value_buffer: *mut std::ffi::c_char, - _value_buffer_capacity: usize, - _value_size: *mut usize, + index: usize, + tag_index: usize, + name_buffer: *mut std::ffi::c_char, + name_buffer_capacity: usize, + name_size: *mut usize, + value_buffer: *mut std::ffi::c_char, + value_buffer_capacity: usize, + value_size: *mut usize, ) -> bool { - false + if index >= STUB_TEXT_READOUT_TAGS.len() || tag_index >= STUB_TEXT_READOUT_TAGS[index].len() { + return false; + } + let (name, value) = STUB_TEXT_READOUT_TAGS[index][tag_index]; + unsafe { + stub_write(name, name_buffer, name_buffer_capacity, name_size); + stub_write(value, value_buffer, value_buffer_capacity, value_size); + } + true } #[no_mangle] @@ -7139,6 +7181,27 @@ fn test_metric_snapshot_reads_tags() { assert_eq!(snapshot.counter_tag_count(2), None); assert!(!snapshot.counter_tag(0, 2, &mut name, &mut value)); assert!(!snapshot.counter_tag(2, 0, &mut name, &mut value)); + + // gauge_0: one tag. Exercises the gauge tag wrappers, which are distinct entry points from the + // counter ones. + assert!(snapshot.gauge_tag_extracted_name(0, &mut name)); + assert_eq!(name.as_slice(), b"cluster.cx_active"); + assert_eq!(snapshot.gauge_tag_count(0), Some(1)); + assert!(snapshot.gauge_tag(0, 0, &mut name, &mut value)); + assert_eq!(name.as_slice(), b"envoy.cluster_name"); + assert_eq!(value.as_slice(), b"bar"); + assert_eq!(snapshot.gauge_tag_count(1), None); + assert!(!snapshot.gauge_tag(0, 1, &mut name, &mut value)); + + // text_0: one tag. Exercises the text-readout tag wrappers. + assert!(snapshot.text_readout_tag_extracted_name(0, &mut name)); + assert_eq!(name.as_slice(), b"control_plane.identifier"); + assert_eq!(snapshot.text_readout_tag_count(0), Some(1)); + assert!(snapshot.text_readout_tag(0, 0, &mut name, &mut value)); + assert_eq!(name.as_slice(), b"envoy.control_plane"); + assert_eq!(value.as_slice(), b"xds"); + assert_eq!(snapshot.text_readout_tag_count(1), None); + assert!(!snapshot.text_readout_tag(0, 1, &mut name, &mut value)); } #[test] diff --git a/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs b/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs index 6fec008c7bd20..f291685b2d9e5 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/stats_sink.rs @@ -104,23 +104,26 @@ impl<'a> MetricSnapshot<'a> { /// Returns `true` on success with both buffers holding exactly their bytes. Returns `false` and /// leaves both buffers unchanged when the index is out of range. pub fn text_readout(&self, index: usize, name: &mut Vec, value: &mut Vec) -> bool { - fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, - value_size| unsafe { - abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout( - self.envoy_ptr, - index, - name_ptr, - name_cap, - name_size, - value_ptr, - value_cap, - value_size, - ) - }) + fill_two_buffers( + name, + value, + |name_ptr, name_cap, name_size, value_ptr, value_cap, value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout( + self.envoy_ptr, + index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }, + ) } /// Reads the tag-extracted name of the counter at `index` into `name` (the stat name with tag - /// values removed). Returns `None` and leaves `name` unchanged when the index is out of range. + /// values removed). Returns `false` and leaves `name` unchanged when the index is out of range. pub fn counter_tag_extracted_name(&self, index: usize, name: &mut Vec) -> bool { fill_buffer(name, |ptr, capacity, size| unsafe { abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( @@ -155,20 +158,23 @@ impl<'a> MetricSnapshot<'a> { name: &mut Vec, value: &mut Vec, ) -> bool { - fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, - value_size| unsafe { - abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( - self.envoy_ptr, - index, - tag_index, - name_ptr, - name_cap, - name_size, - value_ptr, - value_cap, - value_size, - ) - }) + fill_two_buffers( + name, + value, + |name_ptr, name_cap, name_size, value_ptr, value_cap, value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag( + self.envoy_ptr, + index, + tag_index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }, + ) } /// Reads the tag-extracted name of the gauge at `index` into `name`. See @@ -207,20 +213,23 @@ impl<'a> MetricSnapshot<'a> { name: &mut Vec, value: &mut Vec, ) -> bool { - fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, - value_size| unsafe { - abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( - self.envoy_ptr, - index, - tag_index, - name_ptr, - name_cap, - name_size, - value_ptr, - value_cap, - value_size, - ) - }) + fill_two_buffers( + name, + value, + |name_ptr, name_cap, name_size, value_ptr, value_cap, value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag( + self.envoy_ptr, + index, + tag_index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }, + ) } /// Reads the tag-extracted name of the text readout at `index` into `name`. See @@ -259,20 +268,23 @@ impl<'a> MetricSnapshot<'a> { name: &mut Vec, value: &mut Vec, ) -> bool { - fill_two_buffers(name, value, |name_ptr, name_cap, name_size, value_ptr, value_cap, - value_size| unsafe { - abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( - self.envoy_ptr, - index, - tag_index, - name_ptr, - name_cap, - name_size, - value_ptr, - value_cap, - value_size, - ) - }) + fill_two_buffers( + name, + value, + |name_ptr, name_cap, name_size, value_ptr, value_cap, value_size| unsafe { + abi::envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + self.envoy_ptr, + index, + tag_index, + name_ptr, + name_cap, + name_size, + value_ptr, + value_cap, + value_size, + ) + }, + ) } /// Copies the whole snapshot into an [`OwnedMetricSnapshot`]. diff --git a/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc b/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc index 0d8a1e7bc6818..02bc60f98ce9f 100644 --- a/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc +++ b/source/extensions/stat_sinks/dynamic_modules/abi_impl.cc @@ -101,18 +101,17 @@ bool getTag(const MetricRefs& metrics, size_t index, size_t tag_index, char* nam const Envoy::Stats::SymbolTable& symbol_table = metric->constSymbolTable(); size_t current = 0; bool found = false; - metric->iterateTagStatNames( - [&](Envoy::Stats::StatName tag_name, Envoy::Stats::StatName tag_value) { - if (current == tag_index) { - *name_size = symbol_table.serializeToBuffer(tag_name, name_buffer, name_buffer_capacity); - *value_size = - symbol_table.serializeToBuffer(tag_value, value_buffer, value_buffer_capacity); - found = true; - return false; - } - ++current; - return true; - }); + metric->iterateTagStatNames([&](Envoy::Stats::StatName tag_name, + Envoy::Stats::StatName tag_value) { + if (current == tag_index) { + *name_size = symbol_table.serializeToBuffer(tag_name, name_buffer, name_buffer_capacity); + *value_size = symbol_table.serializeToBuffer(tag_value, value_buffer, value_buffer_capacity); + found = true; + return false; + } + ++current; + return true; + }); return found; } diff --git a/test/extensions/dynamic_modules/abi_impl_test.cc b/test/extensions/dynamic_modules/abi_impl_test.cc index 0aaa21cf1223f..30eb22c4332d2 100644 --- a/test/extensions/dynamic_modules/abi_impl_test.cc +++ b/test/extensions/dynamic_modules/abi_impl_test.cc @@ -1377,6 +1377,35 @@ WEAK_STUB(StatSinkSnapshotGetTextReadout, envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout(nullptr, 0, nullptr, 0, nullptr, nullptr, 0, nullptr)) +WEAK_STUB(StatSinkSnapshotGetCounterTagExtractedName, + envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_extracted_name( + nullptr, 0, nullptr, 0, nullptr)) +WEAK_STUB(StatSinkSnapshotGetCounterTagCount, + envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag_count(nullptr, 0, + nullptr)) +WEAK_STUB(StatSinkSnapshotGetCounterTag, + envoy_dynamic_module_callback_stat_sink_snapshot_get_counter_tag(nullptr, 0, 0, nullptr, + 0, nullptr, nullptr, 0, + nullptr)) +WEAK_STUB(StatSinkSnapshotGetGaugeTagExtractedName, + envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_extracted_name(nullptr, 0, + nullptr, 0, + nullptr)) +WEAK_STUB(StatSinkSnapshotGetGaugeTagCount, + envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count(nullptr, 0, nullptr)) +WEAK_STUB(StatSinkSnapshotGetGaugeTag, + envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag(nullptr, 0, 0, nullptr, 0, + nullptr, nullptr, 0, + nullptr)) +WEAK_STUB(StatSinkSnapshotGetTextReadoutTagExtractedName, + envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + nullptr, 0, nullptr, 0, nullptr)) +WEAK_STUB(StatSinkSnapshotGetTextReadoutTagCount, + envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count(nullptr, 0, + nullptr)) +WEAK_STUB(StatSinkSnapshotGetTextReadoutTag, + envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + nullptr, 0, 0, nullptr, 0, nullptr, nullptr, 0, nullptr)) WEAK_STUB(StatSinkConfigDefineGauge, envoy_dynamic_module_callback_stat_sink_config_define_gauge(nullptr, {nullptr, 0}, nullptr)) diff --git a/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc b/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc index 58f18b8c8b6eb..4796015458b45 100644 --- a/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc +++ b/test/extensions/dynamic_modules/stat_sink/abi_impl_test.cc @@ -262,8 +262,8 @@ TEST_F(DynamicModuleStatsSinkAbiTest, GetGaugeTagExtractedNameAndTags) { EXPECT_EQ("cluster.cx_active", written(name_buffer, name_size, sizeof(name_buffer))); size_t tag_count = 0; - EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count( - snapshotHandle(), 0, &tag_count)); + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_gauge_tag_count(snapshotHandle(), + 0, &tag_count)); EXPECT_EQ(1u, tag_count); char tag_name[256]; @@ -277,6 +277,36 @@ TEST_F(DynamicModuleStatsSinkAbiTest, GetGaugeTagExtractedNameAndTags) { EXPECT_EQ("bar", written(tag_value, tag_value_size, sizeof(tag_value))); } +// The text-readout tag callbacks reach the third snapshot collection; a spot check confirms the +// wiring, mirroring the gauge check above. +TEST_F(DynamicModuleStatsSinkAbiTest, GetTextReadoutTagExtractedNameAndTags) { + t0_.name_ = "control_plane.foo.identifier"; + t0_.setTagExtractedName("control_plane.identifier"); + t0_.setTags({{"envoy.control_plane", "foo"}}); + snapshot_.text_readouts_.push_back(t0_); + + char name_buffer[256]; + size_t name_size = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_extracted_name( + snapshotHandle(), 0, name_buffer, sizeof(name_buffer), &name_size)); + EXPECT_EQ("control_plane.identifier", written(name_buffer, name_size, sizeof(name_buffer))); + + size_t tag_count = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag_count( + snapshotHandle(), 0, &tag_count)); + EXPECT_EQ(1u, tag_count); + + char tag_name[256]; + char tag_value[256]; + size_t tag_name_size = 0; + size_t tag_value_size = 0; + ASSERT_TRUE(envoy_dynamic_module_callback_stat_sink_snapshot_get_text_readout_tag( + snapshotHandle(), 0, 0, tag_name, sizeof(tag_name), &tag_name_size, tag_value, + sizeof(tag_value), &tag_value_size)); + EXPECT_EQ("envoy.control_plane", written(tag_name, tag_name_size, sizeof(tag_name))); + EXPECT_EQ("foo", written(tag_value, tag_value_size, sizeof(tag_value))); +} + // ============================================================================= // Gauge callbacks // ============================================================================= diff --git a/test/extensions/dynamic_modules/stat_sink/integration_test.cc b/test/extensions/dynamic_modules/stat_sink/integration_test.cc index 37ec54882829b..0d18435eea89a 100644 --- a/test/extensions/dynamic_modules/stat_sink/integration_test.cc +++ b/test/extensions/dynamic_modules/stat_sink/integration_test.cc @@ -88,16 +88,23 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(DynamicModulesStatsSinkIntegrationTest, BasicFlush) { // The "found gauge server.uptime" marker proves the module decoded a gauge name through the // buffer-based snapshot API. - EXPECT_LOG_CONTAINS_ALL_OF(Envoy::ExpectedLogMessages({ - {"info", "stat sink integration test: config_new called"}, - {"info", "stat sink integration test: flush called"}, - {"info", "stat sink integration test: found gauge server.uptime"}, - }), - { - addStatSinkAndInitialize(); - timeSystem().realSleepDoNotUseWithoutScrutiny( - std::chrono::milliseconds(500)); - }); + Envoy::ExpectedLogMessages expected{ + {"info", "stat sink integration test: config_new called"}, + {"info", "stat sink integration test: flush called"}, + {"info", "stat sink integration test: found gauge server.uptime"}, + }; + if (language() == "rust") { + // Only the Rust SDK exposes the tag callbacks. The "reconstructed tagged gauge" marker proves + // the module read the tag-extracted name and the "envoy.cluster_name" tag of the always-present + // cluster.membership_total gauge and rebuilt the dimensional name a Prometheus-style sink would + // emit. + expected.push_back({"info", "stat sink integration test: reconstructed tagged gauge " + "cluster.membership_total envoy.cluster_name=cluster_0"}); + } + EXPECT_LOG_CONTAINS_ALL_OF(expected, { + addStatSinkAndInitialize(); + timeSystem().realSleepDoNotUseWithoutScrutiny(std::chrono::milliseconds(500)); + }); } TEST_P(DynamicModulesStatsSinkIntegrationTest, FlushAfterTraffic) { diff --git a/test/extensions/dynamic_modules/test_data/rust/stat_sink_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/stat_sink_integration_test.rs index df3ec43d4e77d..f7355c94e2a3f 100644 --- a/test/extensions/dynamic_modules/test_data/rust/stat_sink_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/stat_sink_integration_test.rs @@ -63,16 +63,49 @@ impl StatSink for TestStatSink { // pattern the API enables (for example writing each name to a socket). let mut name = Vec::new(); let mut value = Vec::new(); + let mut tag_name = Vec::new(); + let mut tag_value = Vec::new(); + // Exercise the counter tag callbacks against every counter in the live snapshot. Every tag + // index the count reports must resolve; a mismatch means the count and the per-tag reader + // disagree. + let mut counter_tags_consistent = true; for index in 0..snapshot.counter_count() { let _ = snapshot.counter(index, &mut name); + if !snapshot.counter_tag_extracted_name(index, &mut name) { + counter_tags_consistent = false; + continue; + } + let Some(tag_count) = snapshot.counter_tag_count(index) else { + counter_tags_consistent = false; + continue; + }; + for tag_index in 0..tag_count { + if !snapshot.counter_tag(index, tag_index, &mut tag_name, &mut tag_value) { + counter_tags_consistent = false; + } + } } // Decode every gauge name and look for the always-present "server.uptime" gauge, which proves a // name round-trips byte-for-byte through the buffer API end to end. let mut found_uptime = false; + // Reconstruct the dimensional name of a tagged gauge from its tag-extracted name and tags, the + // way a Prometheus-style sink would. "cluster.cluster_0.membership_total" carries a single + // "envoy.cluster_name" tag whose value is the cluster name, so the tag callbacks must yield + // tag-extracted name "cluster.membership_total" and tag ("envoy.cluster_name", "cluster_0"). + let mut found_tagged_gauge = false; for index in 0..snapshot.gauge_count() { if snapshot.gauge(index, &mut name).is_some() && name.as_slice() == b"server.uptime" { found_uptime = true; } + if snapshot.gauge_tag_extracted_name(index, &mut name) + && name.as_slice() == b"cluster.membership_total" + && snapshot.gauge_tag_count(index) == Some(1) + && snapshot.gauge_tag(index, 0, &mut tag_name, &mut tag_value) + && tag_name.as_slice() == b"envoy.cluster_name" + && tag_value.as_slice() == b"cluster_0" + { + found_tagged_gauge = true; + } } for index in 0..snapshot.text_readout_count() { let _ = snapshot.text_readout(index, &mut name, &mut value); @@ -80,6 +113,11 @@ impl StatSink for TestStatSink { if found_uptime { envoy_log_info!("stat sink integration test: found gauge server.uptime"); } + // A tagged gauge reconstructed from the tag callbacks, and every counter's tag count agreed + // with its per-tag reads: the tag ABI round-trips end to end against a live snapshot. + if found_tagged_gauge && counter_tags_consistent { + envoy_log_info!("stat sink integration test: reconstructed tagged gauge cluster.membership_total envoy.cluster_name=cluster_0"); + } envoy_log_info!( "stat sink integration test: flush called counters={} gauges={}", snapshot.counter_count(),