diff --git a/dotnet-engine/Yggdrasil.Engine.Tests/YggdrasilImpactMetricsTest.cs b/dotnet-engine/Yggdrasil.Engine.Tests/YggdrasilImpactMetricsTest.cs index d2aa4ca..7ee353a 100644 --- a/dotnet-engine/Yggdrasil.Engine.Tests/YggdrasilImpactMetricsTest.cs +++ b/dotnet-engine/Yggdrasil.Engine.Tests/YggdrasilImpactMetricsTest.cs @@ -1,16 +1,9 @@ -using System; -using System.Text.Json; +using System.Collections.Generic; using NUnit.Framework; using Yggdrasil; -using Yggdrasil.Test; public class YggdrasilImpactMetricsTest { - private JsonSerializerOptions options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - [Test] public void DefineCounter_Throws_Only_When_Invalid() { @@ -20,4 +13,77 @@ public void DefineCounter_Throws_Only_When_Invalid() Assert.Throws(() => yggdrasilEngine.DefineCounter(null!, "Measures time spent on server doing things")); Assert.Throws(() => yggdrasilEngine.DefineCounter(null!, null!)); } + + [Test] + public void IncCounter_Throws_Only_When_Invalid() + { + var yggdrasilEngine = new YggdrasilEngine(); + yggdrasilEngine.DefineCounter("requests_total", "Total requests"); + + Assert.DoesNotThrow(() => yggdrasilEngine.IncCounter("requests_total")); + Assert.DoesNotThrow(() => yggdrasilEngine.IncCounter("requests_total", 5)); + Assert.DoesNotThrow(() => yggdrasilEngine.IncCounter("requests_total", 3, new Dictionary { { "env", "test" } })); + + Assert.Throws(() => yggdrasilEngine.IncCounter(null!)); + } + + [Test] + public void DefineGauge_Throws_Only_When_Invalid() + { + var yggdrasilEngine = new YggdrasilEngine(); + Assert.DoesNotThrow(() => yggdrasilEngine.DefineGauge("cpu_usage", "CPU usage")); + Assert.Throws(() => yggdrasilEngine.DefineGauge("cpu_usage", null!)); + Assert.Throws(() => yggdrasilEngine.DefineGauge(null!, "CPU usage")); + Assert.Throws(() => yggdrasilEngine.DefineGauge(null!, null!)); + } + + [Test] + public void SetGauge_Throws_Only_When_Invalid() + { + var yggdrasilEngine = new YggdrasilEngine(); + yggdrasilEngine.DefineGauge("queue_depth", "Queue depth"); + + Assert.DoesNotThrow(() => yggdrasilEngine.SetGauge("queue_depth", 10.5)); + Assert.DoesNotThrow(() => yggdrasilEngine.SetGauge("queue_depth", 5.25, new Dictionary { { "env", "prod" } })); + Assert.Throws(() => yggdrasilEngine.SetGauge(null!, 1.0)); + } + + [Test] + public void DefineHistogram_Throws_Only_When_Invalid() + { + var yggdrasilEngine = new YggdrasilEngine(); + Assert.DoesNotThrow(() => yggdrasilEngine.DefineHistogram("request_duration", "Request duration")); + Assert.DoesNotThrow(() => yggdrasilEngine.DefineHistogram("request_duration_custom", "Request duration custom", new[] { 0.1, 0.5, 1.0, 5.0 })); + + Assert.Throws(() => yggdrasilEngine.DefineHistogram("request_duration", null!)); + Assert.Throws(() => yggdrasilEngine.DefineHistogram(null!, "Request duration")); + Assert.Throws(() => yggdrasilEngine.DefineHistogram(null!, null!)); + } + + [Test] + public void ObserveHistogram_Throws_Only_When_Invalid() + { + var yggdrasilEngine = new YggdrasilEngine(); + yggdrasilEngine.DefineHistogram("request_duration", "Request duration", new[] { 0.1, 0.5, 1.0, 5.0 }); + + Assert.DoesNotThrow(() => yggdrasilEngine.ObserveHistogram("request_duration", 0.05)); + Assert.DoesNotThrow(() => yggdrasilEngine.ObserveHistogram("request_duration", 0.75, new Dictionary { { "env", "test" } })); + Assert.Throws(() => yggdrasilEngine.ObserveHistogram(null!, 1.0)); + } + + [Test] + public void ImpactMetrics_Methods_Can_Be_Used_Together() + { + var yggdrasilEngine = new YggdrasilEngine(); + + Assert.DoesNotThrow(() => + { + yggdrasilEngine.DefineCounter("test_counter", "Test counter"); + yggdrasilEngine.IncCounter("test_counter", 10); + yggdrasilEngine.DefineGauge("test_gauge", "Test gauge"); + yggdrasilEngine.SetGauge("test_gauge", 42); + yggdrasilEngine.DefineHistogram("test_histogram", "Test histogram", new[] { 0.1, 0.5, 1.0 }); + yggdrasilEngine.ObserveHistogram("test_histogram", 0.25); + }); + } } diff --git a/dotnet-engine/Yggdrasil.Engine/Flat.cs b/dotnet-engine/Yggdrasil.Engine/Flat.cs index 61d2ba8..86ad37c 100644 --- a/dotnet-engine/Yggdrasil.Engine/Flat.cs +++ b/dotnet-engine/Yggdrasil.Engine/Flat.cs @@ -17,6 +17,11 @@ static Flat() built_in_strategies = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_built_in_strategies")); get_metrics = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_get_metrics")); define_counter = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_define_counter")); + inc_counter = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_inc_counter")); + define_gauge = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_define_gauge")); + set_gauge = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_set_gauge")); + define_histogram = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_define_histogram")); + observe_histogram = Marshal.GetDelegateForFunctionPointer(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_observe_histogram")); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -41,6 +46,16 @@ static Flat() [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate Buf DefineCounterDelegate(IntPtr enginePtr, IntPtr messagePtr, nuint messageLen); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate Buf IncCounterDelegate(IntPtr enginePtr, IntPtr messagePtr, nuint messageLen); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate Buf DefineGaugeDelegate(IntPtr enginePtr, IntPtr messagePtr, nuint messageLen); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate Buf SetGaugeDelegate(IntPtr enginePtr, IntPtr messagePtr, nuint messageLen); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate Buf DefineHistogramDelegate(IntPtr enginePtr, IntPtr messagePtr, nuint messageLen); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate Buf ObserveHistogramDelegate(IntPtr enginePtr, IntPtr messagePtr, nuint messageLen); private static readonly TakeStateDelegate take_state; private static readonly FreeBufferDelegate free_buffer; @@ -51,6 +66,11 @@ static Flat() private static readonly GetMetricsDelegate get_metrics; private static readonly DefineCounterDelegate define_counter; + private static readonly IncCounterDelegate inc_counter; + private static readonly DefineGaugeDelegate define_gauge; + private static readonly SetGaugeDelegate set_gauge; + private static readonly DefineHistogramDelegate define_histogram; + private static readonly ObserveHistogramDelegate observe_histogram; public static Buf TakeState(IntPtr ptr, string json) { @@ -86,6 +106,7 @@ public static Buf CheckVariant(IntPtr ptr, byte[] message) handle.Free(); } } + public static Buf ListKnownToggles(IntPtr ptr) { return list_known_toggles(ptr); @@ -116,6 +137,81 @@ public static Buf DefineCounter(IntPtr ptr, byte[] message) } } + public static Buf IncCounter(IntPtr ptr, byte[] message) + { + nuint len = (nuint)message.Length; + GCHandle handle = GCHandle.Alloc(message, GCHandleType.Pinned); + try + { + IntPtr msgPtr = handle.AddrOfPinnedObject(); + return inc_counter(ptr, msgPtr, len); + } + finally + { + handle.Free(); + } + } + + public static Buf DefineGauge(IntPtr ptr, byte[] message) + { + nuint len = (nuint)message.Length; + GCHandle handle = GCHandle.Alloc(message, GCHandleType.Pinned); + try + { + IntPtr msgPtr = handle.AddrOfPinnedObject(); + return define_gauge(ptr, msgPtr, len); + } + finally + { + handle.Free(); + } + } + + public static Buf SetGauge(IntPtr ptr, byte[] message) + { + nuint len = (nuint)message.Length; + GCHandle handle = GCHandle.Alloc(message, GCHandleType.Pinned); + try + { + IntPtr msgPtr = handle.AddrOfPinnedObject(); + return set_gauge(ptr, msgPtr, len); + } + finally + { + handle.Free(); + } + } + + public static Buf DefineHistogram(IntPtr ptr, byte[] message) + { + nuint len = (nuint)message.Length; + GCHandle handle = GCHandle.Alloc(message, GCHandleType.Pinned); + try + { + IntPtr msgPtr = handle.AddrOfPinnedObject(); + return define_histogram(ptr, msgPtr, len); + } + finally + { + handle.Free(); + } + } + + public static Buf ObserveHistogram(IntPtr ptr, byte[] message) + { + nuint len = (nuint)message.Length; + GCHandle handle = GCHandle.Alloc(message, GCHandleType.Pinned); + try + { + IntPtr msgPtr = handle.AddrOfPinnedObject(); + return observe_histogram(ptr, msgPtr, len); + } + finally + { + handle.Free(); + } + } + public static void FreeBuf(Buf buf) { free_buffer(buf); diff --git a/dotnet-engine/Yggdrasil.Engine/Flatbuffers.cs b/dotnet-engine/Yggdrasil.Engine/Flatbuffers.cs index db4dc33..c44decd 100644 --- a/dotnet-engine/Yggdrasil.Engine/Flatbuffers.cs +++ b/dotnet-engine/Yggdrasil.Engine/Flatbuffers.cs @@ -64,6 +64,118 @@ public static byte[] CreateDefineCounterBuffer(FlatBufferBuilder builder, string return builder.SizedByteArray(); } + public static byte[] CreateIncCounterBuffer(FlatBufferBuilder builder, string name, long value, IDictionary? labels = null) + { + var nameOffset = builder.CreateString(name); + var labelsOffset = CreateSampleLabelsVector(builder, labels); + + IncCounter.StartIncCounter(builder); + IncCounter.AddName(builder, nameOffset); + IncCounter.AddValue(builder, value); + if (labelsOffset.HasValue) + { + IncCounter.AddLabels(builder, labelsOffset.Value); + } + + var incCounterMessage = IncCounter.EndIncCounter(builder); + builder.Finish(incCounterMessage.Value); + return builder.SizedByteArray(); + } + + public static byte[] CreateDefineGaugeBuffer(FlatBufferBuilder builder, string name, string help) + { + var nameOffset = builder.CreateString(name); + var helpOffset = builder.CreateString(help); + + DefineGauge.StartDefineGauge(builder); + DefineGauge.AddName(builder, nameOffset); + DefineGauge.AddHelp(builder, helpOffset); + + var defineGaugeMessage = DefineGauge.EndDefineGauge(builder); + builder.Finish(defineGaugeMessage.Value); + return builder.SizedByteArray(); + } + + public static byte[] CreateSetGaugeBuffer(FlatBufferBuilder builder, string name, double value, IDictionary? labels = null) + { + var nameOffset = builder.CreateString(name); + var labelsOffset = CreateSampleLabelsVector(builder, labels); + + SetGauge.StartSetGauge(builder); + SetGauge.AddName(builder, nameOffset); + SetGauge.AddValue(builder, value); + if (labelsOffset.HasValue) + { + SetGauge.AddLabels(builder, labelsOffset.Value); + } + + var setGaugeMessage = SetGauge.EndSetGauge(builder); + builder.Finish(setGaugeMessage.Value); + return builder.SizedByteArray(); + } + + public static byte[] CreateDefineHistogramBuffer(FlatBufferBuilder builder, string name, string help, IEnumerable? buckets = null) + { + var nameOffset = builder.CreateString(name); + var helpOffset = builder.CreateString(help); + var bucketArray = (buckets ?? Enumerable.Empty()).ToArray(); + var bucketsOffset = bucketArray.Length > 0 + ? DefineHistogram.CreateBucketsVector(builder, bucketArray) + : default(VectorOffset); + + DefineHistogram.StartDefineHistogram(builder); + DefineHistogram.AddName(builder, nameOffset); + DefineHistogram.AddHelp(builder, helpOffset); + if (bucketArray.Length > 0) + { + DefineHistogram.AddBuckets(builder, bucketsOffset); + } + + var defineHistogramMessage = DefineHistogram.EndDefineHistogram(builder); + builder.Finish(defineHistogramMessage.Value); + return builder.SizedByteArray(); + } + + public static byte[] CreateObserveHistogramBuffer(FlatBufferBuilder builder, string name, double value, IDictionary? labels = null) + { + var nameOffset = builder.CreateString(name); + var labelsOffset = CreateSampleLabelsVector(builder, labels); + + ObserveHistogram.StartObserveHistogram(builder); + ObserveHistogram.AddName(builder, nameOffset); + ObserveHistogram.AddValue(builder, value); + if (labelsOffset.HasValue) + { + ObserveHistogram.AddLabels(builder, labelsOffset.Value); + } + + var observeHistogramMessage = ObserveHistogram.EndObserveHistogram(builder); + builder.Finish(observeHistogramMessage.Value); + return builder.SizedByteArray(); + } + + private static VectorOffset? CreateSampleLabelsVector(FlatBufferBuilder builder, IDictionary? labels) + { + if (labels == null || labels.Count == 0) + { + return null; + } + + var labelEntries = new Offset[labels.Count]; + var index = 0; + foreach (var kvp in labels) + { + labelEntries[index] = SampleLabelEntry.CreateSampleLabelEntry( + builder, + builder.CreateString(kvp.Key), + builder.CreateString(kvp.Value) + ); + index++; + } + + return IncCounter.CreateLabelsVector(builder, labelEntries); + } + internal static VectorOffset CreatePropertiesVector(FlatBufferBuilder builder, Context context) { var propertyEntries = new Offset[context.Properties?.Count ?? 0]; @@ -238,4 +350,4 @@ private static Dictionary GetVariantCounts(ToggleStats stats) .ToDictionary(x => x.Key, v => v.Value); } -} \ No newline at end of file +} diff --git a/dotnet-engine/Yggdrasil.Engine/YggdrasilEngine.cs b/dotnet-engine/Yggdrasil.Engine/YggdrasilEngine.cs index 82b02b6..f30ba7e 100644 --- a/dotnet-engine/Yggdrasil.Engine/YggdrasilEngine.cs +++ b/dotnet-engine/Yggdrasil.Engine/YggdrasilEngine.cs @@ -90,6 +90,46 @@ public void DefineCounter(string name, string help) finally { Flat.FreeBuf(buf); } } + public void IncCounter(string name, long value = 1, IDictionary? labels = null) + { + var messageBuffer = Flatbuffers.CreateIncCounterBuffer(new FlatBufferBuilder(128), name, value, labels); + var buf = Flat.IncCounter(state, messageBuffer); + try { Flatbuffers.ParseVoidAndThrow(buf); } + finally { Flat.FreeBuf(buf); } + } + + public void DefineGauge(string name, string help) + { + var messageBuffer = Flatbuffers.CreateDefineGaugeBuffer(new FlatBufferBuilder(128), name, help); + var buf = Flat.DefineGauge(state, messageBuffer); + try { Flatbuffers.ParseVoidAndThrow(buf); } + finally { Flat.FreeBuf(buf); } + } + + public void SetGauge(string name, double value, IDictionary? labels = null) + { + var messageBuffer = Flatbuffers.CreateSetGaugeBuffer(new FlatBufferBuilder(128), name, value, labels); + var buf = Flat.SetGauge(state, messageBuffer); + try { Flatbuffers.ParseVoidAndThrow(buf); } + finally { Flat.FreeBuf(buf); } + } + + public void DefineHistogram(string name, string help, IEnumerable? buckets = null) + { + var messageBuffer = Flatbuffers.CreateDefineHistogramBuffer(new FlatBufferBuilder(128), name, help, buckets); + var buf = Flat.DefineHistogram(state, messageBuffer); + try { Flatbuffers.ParseVoidAndThrow(buf); } + finally { Flat.FreeBuf(buf); } + } + + public void ObserveHistogram(string name, double value, IDictionary? labels = null) + { + var messageBuffer = Flatbuffers.CreateObserveHistogramBuffer(new FlatBufferBuilder(128), name, value, labels); + var buf = Flat.ObserveHistogram(state, messageBuffer); + try { Flatbuffers.ParseVoidAndThrow(buf); } + finally { Flat.FreeBuf(buf); } + } + public ICollection ListKnownToggles() { var buf = Flat.ListKnownToggles(state); diff --git a/yggdrasilffi/src/flat/mod.rs b/yggdrasilffi/src/flat/mod.rs index 2e8b5ab..9f98e47 100644 --- a/yggdrasilffi/src/flat/mod.rs +++ b/yggdrasilffi/src/flat/mod.rs @@ -9,10 +9,11 @@ use flatbuffers::root; use std::borrow::Cow; use std::ffi::{c_char, c_void}; -use unleash_yggdrasil::impact_metrics::MetricOptions; +use unleash_yggdrasil::impact_metrics::{BucketMetricOptions, MetricLabels, MetricOptions}; use crate::flat::messaging::yggdrasil::messaging::{ - CollectMetricsResponse, DefineCounter, VoidResponse, + CollectMetricsResponse, DefineCounter, DefineGauge, DefineHistogram, IncCounter, + ObserveHistogram, SetGauge, VoidResponse, }; use crate::flat::serialisation::{Buf, MetricMeasurement, TakeStateResult}; use crate::{get_json, ManagedEngine, RawPointerDataType}; @@ -190,8 +191,8 @@ pub unsafe extern "C" fn flat_check_enabled( let engine = recover_lock(&lock); let enabled = engine.check_enabled(&context); - let impression_data = engine.should_emit_impression_event(&context.toggle_name); - engine.count_toggle(&context.toggle_name, enabled.unwrap_or(false)); + let impression_data = engine.should_emit_impression_event(context.toggle_name); + engine.count_toggle(context.toggle_name, enabled.unwrap_or(false)); Ok(Some(ResponseMessage { message: enabled, @@ -239,10 +240,10 @@ pub unsafe extern "C" fn flat_check_variant( let engine = recover_lock(&lock); let base_variant = engine.check_variant(&context); let toggle_enabled = engine.check_enabled(&context).unwrap_or_default(); - let impression_data = engine.should_emit_impression_event(&context.toggle_name); - engine.count_toggle(&context.toggle_name, toggle_enabled); + let impression_data = engine.should_emit_impression_event(context.toggle_name); + engine.count_toggle(context.toggle_name, toggle_enabled); if let Some(v) = base_variant.clone() { - engine.count_variant(&context.toggle_name, &v.name); + engine.count_variant(context.toggle_name, &v.name); } let message = base_variant.map(|variant| variant.to_enriched_response(toggle_enabled)); Ok(Some(ResponseMessage { @@ -345,6 +346,222 @@ pub unsafe extern "C" fn flat_define_counter( VoidResponse::build_response(result) } +fn parse_labels( + labels: Option< + flatbuffers::Vector< + '_, + flatbuffers::ForwardsUOffset>, + >, + >, +) -> MetricLabels { + labels + .map(|entries| { + entries + .iter() + .map(|entry| { + ( + entry.key().to_owned(), + entry.value().unwrap_or_default().to_owned(), + ) + }) + .collect() + }) + .unwrap_or_default() +} + +/// Increments a counter metric by a given value. +/// +/// # Safety +/// +/// passing an invalid engine_ptr, message_ptr, or improper message_len will cause UB +/// the returned Buf should be freed by calling flat_buf_free, otherwise you're leaking memory +#[no_mangle] +pub unsafe extern "C" fn flat_inc_counter( + engine_ptr: *mut c_void, + message_ptr: u64, + message_len: u64, +) -> Buf { + let result = guard_result::<(), _>(|| { + let bytes = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + let inc_counter_message = + root::(bytes).map_err(|e| FlatError::InvalidBuffer(e.to_string()))?; + + let guard = get_engine(engine_ptr)?; + let engine = recover_lock(&guard); + + let Some(name) = inc_counter_message.name() else { + return Err(FlatError::MissingRequiredParameter("name".to_owned())); + }; + + let value = inc_counter_message.value(); + let labels = parse_labels(inc_counter_message.labels()); + if labels.is_empty() { + engine.inc_counter_by(name, value); + } else { + engine.inc_counter_with_labels(name, value, &labels); + } + + Ok(Some(())) + }); + + VoidResponse::build_response(result) +} + +/// Defines a gauge metric with the given name and help text. +/// +/// # Safety +/// +/// passing an invalid engine_ptr, message_ptr, or improper message_len will cause UB +/// the returned Buf should be freed by calling flat_buf_free, otherwise you're leaking memory +#[no_mangle] +pub unsafe extern "C" fn flat_define_gauge( + engine_ptr: *mut c_void, + message_ptr: u64, + message_len: u64, +) -> Buf { + let result = guard_result::<(), _>(|| { + let bytes = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + let define_gauge_message = + root::(bytes).map_err(|e| FlatError::InvalidBuffer(e.to_string()))?; + + let guard = get_engine(engine_ptr)?; + let engine = recover_lock(&guard); + + let Some(name) = define_gauge_message.name() else { + return Err(FlatError::MissingRequiredParameter("name".to_owned())); + }; + + let Some(help) = define_gauge_message.help() else { + return Err(FlatError::MissingRequiredParameter("help".to_owned())); + }; + + engine.define_gauge(MetricOptions::new(name, help)); + Ok(Some(())) + }); + + VoidResponse::build_response(result) +} + +/// Sets a gauge metric to a given value. +/// +/// # Safety +/// +/// passing an invalid engine_ptr, message_ptr, or improper message_len will cause UB +/// the returned Buf should be freed by calling flat_buf_free, otherwise you're leaking memory +#[no_mangle] +pub unsafe extern "C" fn flat_set_gauge( + engine_ptr: *mut c_void, + message_ptr: u64, + message_len: u64, +) -> Buf { + let result = guard_result::<(), _>(|| { + let bytes = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + let set_gauge_message = + root::(bytes).map_err(|e| FlatError::InvalidBuffer(e.to_string()))?; + + let guard = get_engine(engine_ptr)?; + let engine = recover_lock(&guard); + + let Some(name) = set_gauge_message.name() else { + return Err(FlatError::MissingRequiredParameter("name".to_owned())); + }; + + let value = set_gauge_message.value(); + let labels = parse_labels(set_gauge_message.labels()); + if labels.is_empty() { + engine.set_gauge(name, value); + } else { + engine.set_gauge_with_labels(name, value, &labels); + } + + Ok(Some(())) + }); + + VoidResponse::build_response(result) +} + +/// Defines a histogram metric with optional buckets. +/// +/// # Safety +/// +/// passing an invalid engine_ptr, message_ptr, or improper message_len will cause UB +/// the returned Buf should be freed by calling flat_buf_free, otherwise you're leaking memory +#[no_mangle] +pub unsafe extern "C" fn flat_define_histogram( + engine_ptr: *mut c_void, + message_ptr: u64, + message_len: u64, +) -> Buf { + let result = guard_result::<(), _>(|| { + let bytes = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + let define_histogram_message = + root::(bytes).map_err(|e| FlatError::InvalidBuffer(e.to_string()))?; + + let guard = get_engine(engine_ptr)?; + let engine = recover_lock(&guard); + + let Some(name) = define_histogram_message.name() else { + return Err(FlatError::MissingRequiredParameter("name".to_owned())); + }; + + let Some(help) = define_histogram_message.help() else { + return Err(FlatError::MissingRequiredParameter("help".to_owned())); + }; + + let buckets = define_histogram_message + .buckets() + .map(|entries| entries.iter().collect::>()) + .unwrap_or_default(); + engine.define_histogram(BucketMetricOptions::new(name, help, buckets)); + Ok(Some(())) + }); + + VoidResponse::build_response(result) +} + +/// Observes a value on a histogram metric. +/// +/// # Safety +/// +/// passing an invalid engine_ptr, message_ptr, or improper message_len will cause UB +/// the returned Buf should be freed by calling flat_buf_free, otherwise you're leaking memory +#[no_mangle] +pub unsafe extern "C" fn flat_observe_histogram( + engine_ptr: *mut c_void, + message_ptr: u64, + message_len: u64, +) -> Buf { + let result = guard_result::<(), _>(|| { + let bytes = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + let observe_histogram_message = + root::(bytes).map_err(|e| FlatError::InvalidBuffer(e.to_string()))?; + + let guard = get_engine(engine_ptr)?; + let engine = recover_lock(&guard); + + let Some(name) = observe_histogram_message.name() else { + return Err(FlatError::MissingRequiredParameter("name".to_owned())); + }; + + let value = observe_histogram_message.value(); + let labels = parse_labels(observe_histogram_message.labels()); + if labels.is_empty() { + engine.observe_histogram(name, value); + } else { + engine.observe_histogram_with_labels(name, value, &labels); + } + + Ok(Some(())) + }); + + VoidResponse::build_response(result) +} + /// Collects and returns metrics and impact metrics /// /// # Safety