Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 74 additions & 8 deletions dotnet-engine/Yggdrasil.Engine.Tests/YggdrasilImpactMetricsTest.cs
Original file line number Diff line number Diff line change
@@ -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()
{
Expand All @@ -20,4 +13,77 @@ public void DefineCounter_Throws_Only_When_Invalid()
Assert.Throws<YggdrasilEngineException>(() => yggdrasilEngine.DefineCounter(null!, "Measures time spent on server doing things"));
Assert.Throws<YggdrasilEngineException>(() => 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<string, string> { { "env", "test" } }));

Assert.Throws<YggdrasilEngineException>(() => 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<YggdrasilEngineException>(() => yggdrasilEngine.DefineGauge("cpu_usage", null!));
Assert.Throws<YggdrasilEngineException>(() => yggdrasilEngine.DefineGauge(null!, "CPU usage"));
Assert.Throws<YggdrasilEngineException>(() => 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<string, string> { { "env", "prod" } }));
Assert.Throws<YggdrasilEngineException>(() => 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<YggdrasilEngineException>(() => yggdrasilEngine.DefineHistogram("request_duration", null!));
Assert.Throws<YggdrasilEngineException>(() => yggdrasilEngine.DefineHistogram(null!, "Request duration"));
Assert.Throws<YggdrasilEngineException>(() => 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<string, string> { { "env", "test" } }));
Assert.Throws<YggdrasilEngineException>(() => 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);
});
}
}
96 changes: 96 additions & 0 deletions dotnet-engine/Yggdrasil.Engine/Flat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ static Flat()
built_in_strategies = Marshal.GetDelegateForFunctionPointer<BuiltInStrategiesDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_built_in_strategies"));
get_metrics = Marshal.GetDelegateForFunctionPointer<GetMetricsDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_get_metrics"));
define_counter = Marshal.GetDelegateForFunctionPointer<DefineCounterDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_define_counter"));
inc_counter = Marshal.GetDelegateForFunctionPointer<IncCounterDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_inc_counter"));
define_gauge = Marshal.GetDelegateForFunctionPointer<DefineGaugeDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_define_gauge"));
set_gauge = Marshal.GetDelegateForFunctionPointer<SetGaugeDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_set_gauge"));
define_histogram = Marshal.GetDelegateForFunctionPointer<DefineHistogramDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_define_histogram"));
observe_histogram = Marshal.GetDelegateForFunctionPointer<ObserveHistogramDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "flat_observe_histogram"));
}

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
Expand All @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
114 changes: 113 additions & 1 deletion dotnet-engine/Yggdrasil.Engine/Flatbuffers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>? 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<string, string>? 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<double>? buckets = null)
{
var nameOffset = builder.CreateString(name);
var helpOffset = builder.CreateString(help);
var bucketArray = (buckets ?? Enumerable.Empty<double>()).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<string, string>? 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<string, string>? labels)
{
if (labels == null || labels.Count == 0)
{
return null;
}

var labelEntries = new Offset<SampleLabelEntry>[labels.Count];
var index = 0;
foreach (var kvp in labels)
{
labelEntries[index] = SampleLabelEntry.CreateSampleLabelEntry(
builder,
builder.CreateString(kvp.Key),
builder.CreateString(kvp.Value)
);
index++;
}

Comment on lines +155 to +175
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateSampleLabelsVector builds the [SampleLabelEntry] vector by calling IncCounter.CreateLabelsVector(...), which unnecessarily couples label serialization to the presence/name of the IncCounter table. Consider building the vector directly with FlatBufferBuilder.StartVector/AddOffset (or introducing a dedicated helper) so label encoding remains independent of any specific message type.

Copilot uses AI. Check for mistakes.
return IncCounter.CreateLabelsVector(builder, labelEntries);
Comment on lines 157 to 176
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SampleLabelEntry.key is marked as a FlatBuffers (key) field (see enabled-message.fbs), which means vectors of SampleLabelEntry are expected to be sorted by key for generated LabelsByKey(...) / __lookup_by_key helpers to work correctly. CreateSampleLabelsVector currently preserves the IDictionary enumeration order, so any by-key lookups on these vectors may return incorrect results. Sort the label entries by key (or use the FlatBuffers helper for creating sorted vectors of tables) before creating the vector.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateSampleLabelsVector returns IncCounter.CreateLabelsVector(...), which unnecessarily couples this generic label-builder to the IncCounter schema/type. This makes the code harder to maintain if IncCounter’s generated API changes, and it’s confusing when the same helper is used for SetGauge/ObserveHistogram labels. Prefer building the vector directly via the FlatBufferBuilder, or use the CreateLabelsVector method on the specific message type being built.

Suggested change
return IncCounter.CreateLabelsVector(builder, labelEntries);
return builder.CreateVectorOfTables(labelEntries);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method uses IncCounter.CreateLabelsVector to create label vectors for all message types (IncCounter, SetGauge, ObserveHistogram). While this works because all generated FlatBuffer types have identical CreateLabelsVector methods, it's semantically confusing and reduces code clarity. Consider using a more neutral type like SetGauge.CreateLabelsVector or documenting why IncCounter is used here.

Suggested change
return IncCounter.CreateLabelsVector(builder, labelEntries);
return SetGauge.CreateLabelsVector(builder, labelEntries);

Copilot uses AI. Check for mistakes.
}

internal static VectorOffset CreatePropertiesVector(FlatBufferBuilder builder, Context context)
{
var propertyEntries = new Offset<PropertyEntry>[context.Properties?.Count ?? 0];
Expand Down Expand Up @@ -238,4 +350,4 @@ private static Dictionary<string, long> GetVariantCounts(ToggleStats stats)
.ToDictionary(x => x.Key, v => v.Value);
}

}
}
40 changes: 40 additions & 0 deletions dotnet-engine/Yggdrasil.Engine/YggdrasilEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,46 @@ public void DefineCounter(string name, string help)
finally { Flat.FreeBuf(buf); }
}

public void IncCounter(string name, long value = 1, IDictionary<string, string>? 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<string, string>? 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<double>? 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<string, string>? 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<FeatureDefinition> ListKnownToggles()
{
var buf = Flat.ListKnownToggles(state);
Expand Down
Loading
Loading