From 1573d3a4df544cc7c9ac145106dd44fc5c064c26 Mon Sep 17 00:00:00 2001 From: Chad Retz Date: Fri, 19 Apr 2024 07:51:04 -0500 Subject: [PATCH] Support duration and float metrics (#223) Fixes #209 --- .../CustomMetricMeter.cs | 35 ++- .../WorkflowActivation/WorkflowActivation.cs | 20 +- src/Temporalio/Bridge/Cargo.lock | 6 +- src/Temporalio/Bridge/CustomMetricMeter.cs | 115 ++++++++-- src/Temporalio/Bridge/Interop/Interop.cs | 71 ++++-- .../Bridge/{MetricInteger.cs => Metric.cs} | 48 +++- src/Temporalio/Bridge/OptionsExtensions.cs | 5 +- .../Bridge/include/temporal-sdk-bridge.h | 68 ++++-- src/Temporalio/Bridge/sdk-core | 2 +- src/Temporalio/Bridge/src/metric.rs | 204 ++++++++++++----- src/Temporalio/Bridge/src/runtime.rs | 8 +- src/Temporalio/Common/MetricMeter.cs | 7 +- src/Temporalio/Common/MetricMeterBridge.cs | 215 +++++++++++++++--- .../Runtime/CustomMetricMeterOptions.cs | 40 ++++ src/Temporalio/Runtime/ICustomMetricMeter.cs | 17 +- src/Temporalio/Runtime/MetricsOptions.cs | 6 + .../Runtime/OpenTelemetryOptions.cs | 6 + src/Temporalio/Runtime/PrometheusOptions.cs | 6 + .../CustomMetricMeterTests.cs | 97 ++++++++ .../Runtime/TemporalRuntimeTests.cs | 14 +- tests/Temporalio.Tests/TestUtils.cs | 29 +-- .../Worker/WorkflowWorkerTests.cs | 91 +++++++- 22 files changed, 900 insertions(+), 210 deletions(-) rename src/Temporalio/Bridge/{MetricInteger.cs => Metric.cs} (52%) create mode 100644 src/Temporalio/Runtime/CustomMetricMeterOptions.cs diff --git a/src/Temporalio.Extensions.DiagnosticSource/CustomMetricMeter.cs b/src/Temporalio.Extensions.DiagnosticSource/CustomMetricMeter.cs index c70bab7d..3f71fdaf 100644 --- a/src/Temporalio.Extensions.DiagnosticSource/CustomMetricMeter.cs +++ b/src/Temporalio.Extensions.DiagnosticSource/CustomMetricMeter.cs @@ -13,6 +13,12 @@ namespace Temporalio.Extensions.DiagnosticSource /// Implementation of for a that can be /// set on to record metrics to the meter. /// + /// + /// By default all histograms are set as a long of milliseconds unless + /// is set to FloatSeconds. + /// Similarly, if the unit for a histogram is "duration", it is changed to "ms" unless that same + /// setting is set, at which point the unit is changed to "s". + /// public class CustomMetricMeter : ICustomMetricMeter { /// @@ -35,8 +41,22 @@ public ICustomMetricCounter CreateCounter( /// public ICustomMetricHistogram CreateHistogram( string name, string? unit, string? description) - where T : struct => - new CustomMetricHistogram(Meter.CreateHistogram(name, unit, description)); + where T : struct + { + // Have to convert TimeSpan to something .NET meter can work with. For this to even + // happen, a user would have had to set custom options to report as time span. + if (typeof(T) == typeof(TimeSpan)) + { + // If unit is "duration", change to "ms since we're converting here + if (unit == "duration") + { + unit = "ms"; + } + return (new CustomMetricHistogramTimeSpan( + Meter.CreateHistogram(name, unit, description)) as ICustomMetricHistogram)!; + } + return new CustomMetricHistogram(Meter.CreateHistogram(name, unit, description)); + } /// public ICustomMetricGauge CreateGauge( @@ -75,6 +95,17 @@ public void Record(T value, object tags) => underlying.Record(value, ((Tags)tags).TagList); } + private sealed class CustomMetricHistogramTimeSpan : ICustomMetricHistogram + { + private readonly Histogram underlying; + + internal CustomMetricHistogramTimeSpan(Histogram underlying) => + this.underlying = underlying; + + public void Record(TimeSpan value, object tags) => + underlying.Record((long)value.TotalMilliseconds, ((Tags)tags).TagList); + } + #pragma warning disable CA1001 // We are disposing the lock on destruction since this can't be disposable private sealed class CustomMetricGauge : ICustomMetricGauge #pragma warning restore CA1001 diff --git a/src/Temporalio/Bridge/Api/WorkflowActivation/WorkflowActivation.cs b/src/Temporalio/Bridge/Api/WorkflowActivation/WorkflowActivation.cs index a81de8d2..e04e58dd 100644 --- a/src/Temporalio/Bridge/Api/WorkflowActivation/WorkflowActivation.cs +++ b/src/Temporalio/Bridge/Api/WorkflowActivation/WorkflowActivation.cs @@ -193,6 +193,7 @@ static WorkflowActivationReflection() { /// * Signal and update handlers should be invoked before workflow routines are iterated. That is to /// say before the users' main workflow function and anything spawned by it is allowed to continue. /// * Queries always go last (and, in fact, always come in their own activation) + /// * Evictions also always come in their own activation /// /// The downside of this reordering is that a signal or update handler may not observe that some /// other event had already happened (ex: an activity completed) when it is first invoked, though it @@ -204,11 +205,9 @@ static WorkflowActivationReflection() { /// /// ## Evictions /// - /// Activations that contain only a `remove_from_cache` job should not cause the workflow code - /// to be invoked and may be responded to with an empty command list. Eviction jobs may also - /// appear with other jobs, but will always appear last in the job list. In this case it is - /// expected that the workflow code will be invoked, and the response produced as normal, but - /// the caller should evict the run after doing so. + /// Evictions appear as an activations that contains only a `remove_from_cache` job. Such activations + /// should not cause the workflow code to be invoked and may be responded to with an empty command + /// list. /// internal sealed partial class WorkflowActivation : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -853,7 +852,8 @@ public WorkflowActivationJob Clone() { /// Field number for the "query_workflow" field. public const int QueryWorkflowFieldNumber = 5; /// - /// A request to query the workflow was received. + /// A request to query the workflow was received. It is guaranteed that queries (one or more) + /// always come in their own activation after other mutating jobs. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -1005,11 +1005,9 @@ public WorkflowActivationJob Clone() { /// Field number for the "remove_from_cache" field. public const int RemoveFromCacheFieldNumber = 50; /// - /// Remove the workflow identified by the [WorkflowActivation] containing this job from the cache - /// after performing the activation. - /// - /// If other job variant are present in the list, this variant will be the last job in the - /// job list. The string value is a reason for eviction. + /// Remove the workflow identified by the [WorkflowActivation] containing this job from the + /// cache after performing the activation. It is guaranteed that this will be the only job + /// in the activation if present. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] diff --git a/src/Temporalio/Bridge/Cargo.lock b/src/Temporalio/Bridge/Cargo.lock index 0986ce85..4603b144 100644 --- a/src/Temporalio/Bridge/Cargo.lock +++ b/src/Temporalio/Bridge/Cargo.lock @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.6" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -2388,11 +2388,13 @@ dependencies = [ "anyhow", "async-trait", "backoff", + "base64", "derive_builder", "derive_more", "futures", "futures-retry", "http 0.2.11", + "hyper 0.14.28", "once_cell", "parking_lot", "prost-types", diff --git a/src/Temporalio/Bridge/CustomMetricMeter.cs b/src/Temporalio/Bridge/CustomMetricMeter.cs index dd5639a7..3d5a586e 100644 --- a/src/Temporalio/Bridge/CustomMetricMeter.cs +++ b/src/Temporalio/Bridge/CustomMetricMeter.cs @@ -10,22 +10,29 @@ namespace Temporalio.Bridge internal class CustomMetricMeter { private readonly Temporalio.Runtime.ICustomMetricMeter meter; + private readonly Temporalio.Runtime.CustomMetricMeterOptions options; private readonly List handles = new(); /// /// Initializes a new instance of the class. /// /// Meter implementation. - public unsafe CustomMetricMeter(Temporalio.Runtime.ICustomMetricMeter meter) + /// Options. + public unsafe CustomMetricMeter( + Temporalio.Runtime.ICustomMetricMeter meter, + Temporalio.Runtime.CustomMetricMeterOptions options) { this.meter = meter; + this.options = options; // Create metric meter struct var interopMeter = new Interop.CustomMetricMeter() { - metric_integer_new = FunctionPointer(CreateMetric), - metric_integer_free = FunctionPointer(FreeMetric), - metric_integer_update = FunctionPointer(UpdateMetric), + metric_new = FunctionPointer(CreateMetric), + metric_free = FunctionPointer(FreeMetric), + metric_record_integer = FunctionPointer(RecordMetricInteger), + metric_record_float = FunctionPointer(RecordMetricFloat), + metric_record_duration = FunctionPointer(RecordMetricDuration), attributes_new = FunctionPointer(CreateAttributes), attributes_free = FunctionPointer(FreeAttributes), meter_free = FunctionPointer(Free), @@ -58,38 +65,69 @@ private static unsafe string GetString(byte* bytes, UIntPtr size) => Interop.ByteArrayRef name, Interop.ByteArrayRef description, Interop.ByteArrayRef unit, - Interop.MetricIntegerKind kind) + Interop.MetricKind kind) { - Temporalio.Runtime.ICustomMetric metric; + GCHandle metric; var nameStr = GetString(name); var unitStr = GetStringOrNull(unit); var descStr = GetStringOrNull(description); switch (kind) { - case Interop.MetricIntegerKind.Counter: - metric = meter.CreateCounter(nameStr, unitStr, descStr); + case Interop.MetricKind.CounterInteger: + metric = GCHandle.Alloc(meter.CreateCounter(nameStr, unitStr, descStr)); break; - case Interop.MetricIntegerKind.Histogram: - metric = meter.CreateHistogram(nameStr, unitStr, descStr); + case Interop.MetricKind.HistogramInteger: + metric = GCHandle.Alloc(meter.CreateHistogram(nameStr, unitStr, descStr)); break; - case Interop.MetricIntegerKind.Gauge: - metric = meter.CreateGauge(nameStr, unitStr, descStr); + case Interop.MetricKind.HistogramFloat: + metric = GCHandle.Alloc(meter.CreateHistogram(nameStr, unitStr, descStr)); + break; + case Interop.MetricKind.HistogramDuration: + switch (options.HistogramDurationFormat) + { + case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.IntegerMilliseconds: + // Change unit from "duration" to "ms" since we're converting to ms + if (unitStr == "duration") + { + unitStr = "ms"; + } + metric = GCHandle.Alloc(meter.CreateHistogram(nameStr, unitStr, descStr)); + break; + case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.FloatSeconds: + // Change unit from "duration" to "s" since we're converting to s + if (unitStr == "duration") + { + unitStr = "s"; + } + metric = GCHandle.Alloc(meter.CreateHistogram(nameStr, unitStr, descStr)); + break; + case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.TimeSpan: + metric = GCHandle.Alloc(meter.CreateHistogram(nameStr, unitStr, descStr)); + break; + default: + throw new InvalidOperationException($"Unknown format: {options.HistogramDurationFormat}"); + } + break; + case Interop.MetricKind.GaugeInteger: + metric = GCHandle.Alloc(meter.CreateGauge(nameStr, unitStr, descStr)); + break; + case Interop.MetricKind.GaugeFloat: + metric = GCHandle.Alloc(meter.CreateGauge(nameStr, unitStr, descStr)); break; default: throw new InvalidOperationException($"Unknown kind: {kind}"); } // Return pointer - return GCHandle.ToIntPtr(GCHandle.Alloc(metric)).ToPointer(); + return GCHandle.ToIntPtr(metric).ToPointer(); } private unsafe void FreeMetric(void* metric) => GCHandle.FromIntPtr(new(metric)).Free(); - private unsafe void UpdateMetric(void* metric, ulong value, void* attributes) + private unsafe void RecordMetricInteger(void* metric, ulong value, void* attributes) { var metricObject = (Temporalio.Runtime.ICustomMetric)GCHandle.FromIntPtr(new(metric)).Target!; var tags = GCHandle.FromIntPtr(new(attributes)).Target!; - // We trust that value will never be over Int64.MaxValue - var metricValue = unchecked((long)value); + var metricValue = value > long.MaxValue ? long.MaxValue : unchecked((long)value); switch (metricObject) { case Temporalio.Runtime.ICustomMetricCounter counter: @@ -104,6 +142,51 @@ private unsafe void UpdateMetric(void* metric, ulong value, void* attributes) } } + private unsafe void RecordMetricFloat(void* metric, double value, void* attributes) + { + var metricObject = (Temporalio.Runtime.ICustomMetric)GCHandle.FromIntPtr(new(metric)).Target!; + var tags = GCHandle.FromIntPtr(new(attributes)).Target!; + switch (metricObject) + { + case Temporalio.Runtime.ICustomMetricHistogram histogram: + histogram.Record(value, tags); + break; + case Temporalio.Runtime.ICustomMetricGauge gauge: + gauge.Set(value, tags); + break; + } + } + + private unsafe void RecordMetricDuration(void* metric, ulong valueMs, void* attributes) + { + var metricObject = GCHandle.FromIntPtr(new(metric)).Target!; + var tags = GCHandle.FromIntPtr(new(attributes)).Target!; + var metricValue = valueMs > long.MaxValue ? long.MaxValue : unchecked((long)valueMs); + // We don't want to throw out of here, so we just fall through if anything doesn't match + // expected types (which should never happen since we controlled creation) + switch (options.HistogramDurationFormat) + { + case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.IntegerMilliseconds: + if (metricObject is Temporalio.Runtime.ICustomMetricHistogram histLong) + { + histLong.Record(metricValue, tags); + } + break; + case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.FloatSeconds: + if (metricObject is Temporalio.Runtime.ICustomMetricHistogram histDouble) + { + histDouble.Record(metricValue / 1000.0, tags); + } + break; + case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.TimeSpan: + if (metricObject is Temporalio.Runtime.ICustomMetricHistogram histTimeSpan) + { + histTimeSpan.Record(TimeSpan.FromMilliseconds(metricValue), tags); + } + break; + } + } + private unsafe void* CreateAttributes( void* appendFrom, Interop.CustomMetricAttribute* attributes, UIntPtr attributesSize) { diff --git a/src/Temporalio/Bridge/Interop/Interop.cs b/src/Temporalio/Bridge/Interop/Interop.cs index 1befa803..8944ceab 100644 --- a/src/Temporalio/Bridge/Interop/Interop.cs +++ b/src/Temporalio/Bridge/Interop/Interop.cs @@ -20,11 +20,14 @@ internal enum MetricAttributeValueType Bool, } - internal enum MetricIntegerKind + internal enum MetricKind { - Counter = 1, - Histogram, - Gauge, + CounterInteger = 1, + HistogramInteger, + HistogramFloat, + HistogramDuration, + GaugeInteger, + GaugeFloat, } internal enum OpenTelemetryMetricTemporality @@ -57,11 +60,11 @@ internal partial struct ForwardedLog { } - internal partial struct MetricAttributes + internal partial struct Metric { } - internal partial struct MetricInteger + internal partial struct MetricAttributes { } @@ -243,7 +246,7 @@ internal partial struct MetricAttribute public MetricAttributeValueType value_type; } - internal partial struct MetricIntegerOptions + internal partial struct MetricOptions { [NativeTypeName("struct ByteArrayRef")] public ByteArrayRef name; @@ -254,8 +257,8 @@ internal partial struct MetricIntegerOptions [NativeTypeName("struct ByteArrayRef")] public ByteArrayRef unit; - [NativeTypeName("enum MetricIntegerKind")] - public MetricIntegerKind kind; + [NativeTypeName("enum MetricKind")] + public MetricKind kind; } internal unsafe partial struct RuntimeOrFail @@ -292,6 +295,9 @@ internal partial struct OpenTelemetryOptions [NativeTypeName("enum OpenTelemetryMetricTemporality")] public OpenTelemetryMetricTemporality metric_temporality; + + [NativeTypeName("bool")] + public byte durations_as_seconds; } internal partial struct PrometheusOptions @@ -304,17 +310,26 @@ internal partial struct PrometheusOptions [NativeTypeName("bool")] public byte unit_suffix; + + [NativeTypeName("bool")] + public byte durations_as_seconds; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: NativeTypeName("const void *")] - internal unsafe delegate void* CustomMetricMeterMetricIntegerNewCallback([NativeTypeName("struct ByteArrayRef")] ByteArrayRef name, [NativeTypeName("struct ByteArrayRef")] ByteArrayRef description, [NativeTypeName("struct ByteArrayRef")] ByteArrayRef unit, [NativeTypeName("enum MetricIntegerKind")] MetricIntegerKind kind); + internal unsafe delegate void* CustomMetricMeterMetricNewCallback([NativeTypeName("struct ByteArrayRef")] ByteArrayRef name, [NativeTypeName("struct ByteArrayRef")] ByteArrayRef description, [NativeTypeName("struct ByteArrayRef")] ByteArrayRef unit, [NativeTypeName("enum MetricKind")] MetricKind kind); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal unsafe delegate void CustomMetricMeterMetricFreeCallback([NativeTypeName("const void *")] void* metric); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal unsafe delegate void CustomMetricMeterMetricIntegerFreeCallback([NativeTypeName("const void *")] void* metric); + internal unsafe delegate void CustomMetricMeterMetricRecordIntegerCallback([NativeTypeName("const void *")] void* metric, [NativeTypeName("uint64_t")] ulong value, [NativeTypeName("const void *")] void* attributes); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal unsafe delegate void CustomMetricMeterMetricIntegerUpdateCallback([NativeTypeName("const void *")] void* metric, [NativeTypeName("uint64_t")] ulong value, [NativeTypeName("const void *")] void* attributes); + internal unsafe delegate void CustomMetricMeterMetricRecordFloatCallback([NativeTypeName("const void *")] void* metric, double value, [NativeTypeName("const void *")] void* attributes); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal unsafe delegate void CustomMetricMeterMetricRecordDurationCallback([NativeTypeName("const void *")] void* metric, [NativeTypeName("uint64_t")] ulong value_ms, [NativeTypeName("const void *")] void* attributes); internal unsafe partial struct CustomMetricAttributeValueString { @@ -368,14 +383,20 @@ internal partial struct CustomMetricAttribute internal partial struct CustomMetricMeter { - [NativeTypeName("CustomMetricMeterMetricIntegerNewCallback")] - public IntPtr metric_integer_new; + [NativeTypeName("CustomMetricMeterMetricNewCallback")] + public IntPtr metric_new; + + [NativeTypeName("CustomMetricMeterMetricFreeCallback")] + public IntPtr metric_free; + + [NativeTypeName("CustomMetricMeterMetricRecordIntegerCallback")] + public IntPtr metric_record_integer; - [NativeTypeName("CustomMetricMeterMetricIntegerFreeCallback")] - public IntPtr metric_integer_free; + [NativeTypeName("CustomMetricMeterMetricRecordFloatCallback")] + public IntPtr metric_record_float; - [NativeTypeName("CustomMetricMeterMetricIntegerUpdateCallback")] - public IntPtr metric_integer_update; + [NativeTypeName("CustomMetricMeterMetricRecordDurationCallback")] + public IntPtr metric_record_duration; [NativeTypeName("CustomMetricMeterAttributesNewCallback")] public IntPtr attributes_new; @@ -628,14 +649,20 @@ internal static unsafe partial class Methods public static extern void metric_attributes_free([NativeTypeName("struct MetricAttributes *")] MetricAttributes* attrs); [DllImport("temporal_sdk_bridge", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - [return: NativeTypeName("struct MetricInteger *")] - public static extern MetricInteger* metric_integer_new([NativeTypeName("const struct MetricMeter *")] MetricMeter* meter, [NativeTypeName("const struct MetricIntegerOptions *")] MetricIntegerOptions* options); + [return: NativeTypeName("struct Metric *")] + public static extern Metric* metric_new([NativeTypeName("const struct MetricMeter *")] MetricMeter* meter, [NativeTypeName("const struct MetricOptions *")] MetricOptions* options); + + [DllImport("temporal_sdk_bridge", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void metric_free([NativeTypeName("struct Metric *")] Metric* metric); + + [DllImport("temporal_sdk_bridge", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void metric_record_integer([NativeTypeName("const struct Metric *")] Metric* metric, [NativeTypeName("uint64_t")] ulong value, [NativeTypeName("const struct MetricAttributes *")] MetricAttributes* attrs); [DllImport("temporal_sdk_bridge", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern void metric_integer_free([NativeTypeName("struct MetricInteger *")] MetricInteger* metric); + public static extern void metric_record_float([NativeTypeName("const struct Metric *")] Metric* metric, double value, [NativeTypeName("const struct MetricAttributes *")] MetricAttributes* attrs); [DllImport("temporal_sdk_bridge", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - public static extern void metric_integer_record([NativeTypeName("const struct MetricInteger *")] MetricInteger* metric, [NativeTypeName("uint64_t")] ulong value, [NativeTypeName("const struct MetricAttributes *")] MetricAttributes* attrs); + public static extern void metric_record_duration([NativeTypeName("const struct Metric *")] Metric* metric, [NativeTypeName("uint64_t")] ulong value_ms, [NativeTypeName("const struct MetricAttributes *")] MetricAttributes* attrs); [DllImport("temporal_sdk_bridge", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [return: NativeTypeName("struct Random *")] diff --git a/src/Temporalio/Bridge/MetricInteger.cs b/src/Temporalio/Bridge/Metric.cs similarity index 52% rename from src/Temporalio/Bridge/MetricInteger.cs rename to src/Temporalio/Bridge/Metric.cs index d23da53f..50a18836 100644 --- a/src/Temporalio/Bridge/MetricInteger.cs +++ b/src/Temporalio/Bridge/Metric.cs @@ -4,23 +4,23 @@ namespace Temporalio.Bridge { /// - /// Core-owned metric for integers. + /// Core-owned metric. /// - internal class MetricInteger : SafeHandle + internal class Metric : SafeHandle { - private readonly unsafe Interop.MetricInteger* ptr; + private readonly unsafe Interop.Metric* ptr; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Core meter. /// Metric kind. /// Metric name. /// Metric unit. /// Metric description. - public MetricInteger( + public Metric( MetricMeter meter, - Interop.MetricIntegerKind kind, + Interop.MetricKind kind, string name, string? unit, string? description) @@ -30,14 +30,14 @@ public MetricInteger( { unsafe { - var options = new Interop.MetricIntegerOptions() + var options = new Interop.MetricOptions() { name = scope.ByteArray(name), description = scope.ByteArray(description ?? string.Empty), unit = scope.ByteArray(unit ?? string.Empty), kind = kind, }; - ptr = Interop.Methods.metric_integer_new(meter.Ptr, scope.Pointer(options)); + ptr = Interop.Methods.metric_new(meter.Ptr, scope.Pointer(options)); SetHandle((IntPtr)ptr); } } @@ -51,18 +51,44 @@ public MetricInteger( /// /// Value to record. /// Attributes to set. - public void Record(ulong value, MetricAttributes attributes) + public void RecordInteger(ulong value, MetricAttributes attributes) { unsafe { - Interop.Methods.metric_integer_record(ptr, value, attributes.Ptr); + Interop.Methods.metric_record_integer(ptr, value, attributes.Ptr); + } + } + + /// + /// Record a value for the metric. + /// + /// Value to record. + /// Attributes to set. + public void RecordFloat(double value, MetricAttributes attributes) + { + unsafe + { + Interop.Methods.metric_record_float(ptr, value, attributes.Ptr); + } + } + + /// + /// Record a value for the metric. + /// + /// Value to record. + /// Attributes to set. + public void RecordDuration(ulong valueMs, MetricAttributes attributes) + { + unsafe + { + Interop.Methods.metric_record_duration(ptr, valueMs, attributes.Ptr); } } /// protected override unsafe bool ReleaseHandle() { - Interop.Methods.metric_integer_free(ptr); + Interop.Methods.metric_free(ptr); return true; } } diff --git a/src/Temporalio/Bridge/OptionsExtensions.cs b/src/Temporalio/Bridge/OptionsExtensions.cs index 77173bc9..0746fe94 100644 --- a/src/Temporalio/Bridge/OptionsExtensions.cs +++ b/src/Temporalio/Bridge/OptionsExtensions.cs @@ -92,6 +92,7 @@ public static unsafe Interop.OpenTelemetryOptions ToInteropOptions( ? 0 : options.MetricsExportInterval.Value.TotalMilliseconds), metric_temporality = temporality, + durations_as_seconds = (byte)(options.UseSecondsForDuration ? 1 : 0), }; } @@ -114,6 +115,7 @@ public static unsafe Interop.PrometheusOptions ToInteropOptions( bind_address = scope.ByteArray(options.BindAddress), counters_total_suffix = (byte)(options.HasCounterTotalSuffix ? 1 : 0), unit_suffix = (byte)(options.HasUnitSuffix ? 1 : 0), + durations_as_seconds = (byte)(options.UseSecondsForDuration ? 1 : 0), }; } @@ -181,7 +183,8 @@ public static unsafe Interop.MetricsOptions ToInteropOptions( else if (options.CustomMetricMeter != null) { // This object pins itself in memory and is only freed on the Rust side - customMeter = new CustomMetricMeter(options.CustomMetricMeter).Ptr; + customMeter = new CustomMetricMeter( + options.CustomMetricMeter, options.CustomMetricMeterOptions ?? new()).Ptr; } else { diff --git a/src/Temporalio/Bridge/include/temporal-sdk-bridge.h b/src/Temporalio/Bridge/include/temporal-sdk-bridge.h index 92d911f0..e206b9b7 100644 --- a/src/Temporalio/Bridge/include/temporal-sdk-bridge.h +++ b/src/Temporalio/Bridge/include/temporal-sdk-bridge.h @@ -20,11 +20,14 @@ typedef enum MetricAttributeValueType { Bool, } MetricAttributeValueType; -typedef enum MetricIntegerKind { - Counter = 1, - Histogram, - Gauge, -} MetricIntegerKind; +typedef enum MetricKind { + CounterInteger = 1, + HistogramInteger, + HistogramFloat, + HistogramDuration, + GaugeInteger, + GaugeFloat, +} MetricKind; typedef enum OpenTelemetryMetricTemporality { Cumulative = 1, @@ -46,9 +49,9 @@ typedef struct EphemeralServer EphemeralServer; typedef struct ForwardedLog ForwardedLog; -typedef struct MetricAttributes MetricAttributes; +typedef struct Metric Metric; -typedef struct MetricInteger MetricInteger; +typedef struct MetricAttributes MetricAttributes; typedef struct MetricMeter MetricMeter; @@ -162,12 +165,12 @@ typedef struct MetricAttribute { enum MetricAttributeValueType value_type; } MetricAttribute; -typedef struct MetricIntegerOptions { +typedef struct MetricOptions { struct ByteArrayRef name; struct ByteArrayRef description; struct ByteArrayRef unit; - enum MetricIntegerKind kind; -} MetricIntegerOptions; + enum MetricKind kind; +} MetricOptions; /** * If fail is not null, it must be manually freed when done. Runtime is always @@ -198,25 +201,35 @@ typedef struct OpenTelemetryOptions { MetadataRef headers; uint32_t metric_periodicity_millis; enum OpenTelemetryMetricTemporality metric_temporality; + bool durations_as_seconds; } OpenTelemetryOptions; typedef struct PrometheusOptions { struct ByteArrayRef bind_address; bool counters_total_suffix; bool unit_suffix; + bool durations_as_seconds; } PrometheusOptions; -typedef const void *(*CustomMetricMeterMetricIntegerNewCallback)(struct ByteArrayRef name, - struct ByteArrayRef description, - struct ByteArrayRef unit, - enum MetricIntegerKind kind); +typedef const void *(*CustomMetricMeterMetricNewCallback)(struct ByteArrayRef name, + struct ByteArrayRef description, + struct ByteArrayRef unit, + enum MetricKind kind); -typedef void (*CustomMetricMeterMetricIntegerFreeCallback)(const void *metric); +typedef void (*CustomMetricMeterMetricFreeCallback)(const void *metric); -typedef void (*CustomMetricMeterMetricIntegerUpdateCallback)(const void *metric, +typedef void (*CustomMetricMeterMetricRecordIntegerCallback)(const void *metric, uint64_t value, const void *attributes); +typedef void (*CustomMetricMeterMetricRecordFloatCallback)(const void *metric, + double value, + const void *attributes); + +typedef void (*CustomMetricMeterMetricRecordDurationCallback)(const void *metric, + uint64_t value_ms, + const void *attributes); + typedef struct CustomMetricAttributeValueString { const uint8_t *data; size_t size; @@ -250,9 +263,11 @@ typedef void (*CustomMetricMeterMeterFreeCallback)(const struct CustomMetricMete * invoked on. */ typedef struct CustomMetricMeter { - CustomMetricMeterMetricIntegerNewCallback metric_integer_new; - CustomMetricMeterMetricIntegerFreeCallback metric_integer_free; - CustomMetricMeterMetricIntegerUpdateCallback metric_integer_update; + CustomMetricMeterMetricNewCallback metric_new; + CustomMetricMeterMetricFreeCallback metric_free; + CustomMetricMeterMetricRecordIntegerCallback metric_record_integer; + CustomMetricMeterMetricRecordFloatCallback metric_record_float; + CustomMetricMeterMetricRecordDurationCallback metric_record_duration; CustomMetricMeterAttributesNewCallback attributes_new; CustomMetricMeterAttributesFreeCallback attributes_free; CustomMetricMeterMeterFreeCallback meter_free; @@ -438,15 +453,22 @@ struct MetricAttributes *metric_attributes_new_append(const struct MetricMeter * void metric_attributes_free(struct MetricAttributes *attrs); -struct MetricInteger *metric_integer_new(const struct MetricMeter *meter, - const struct MetricIntegerOptions *options); +struct Metric *metric_new(const struct MetricMeter *meter, const struct MetricOptions *options); -void metric_integer_free(struct MetricInteger *metric); +void metric_free(struct Metric *metric); -void metric_integer_record(const struct MetricInteger *metric, +void metric_record_integer(const struct Metric *metric, uint64_t value, const struct MetricAttributes *attrs); +void metric_record_float(const struct Metric *metric, + double value, + const struct MetricAttributes *attrs); + +void metric_record_duration(const struct Metric *metric, + uint64_t value_ms, + const struct MetricAttributes *attrs); + struct Random *random_new(uint64_t seed); void random_free(struct Random *random); diff --git a/src/Temporalio/Bridge/sdk-core b/src/Temporalio/Bridge/sdk-core index 764a88b7..409e74ec 160000 --- a/src/Temporalio/Bridge/sdk-core +++ b/src/Temporalio/Bridge/sdk-core @@ -1 +1 @@ -Subproject commit 764a88b7f2db180696c728dc81cc1d1c5aa1f01e +Subproject commit 409e74ec8e80ae4c1f9043e8b413b1371b65f946 diff --git a/src/Temporalio/Bridge/src/metric.rs b/src/Temporalio/Bridge/src/metric.rs index d49a263c..d76ed2d3 100644 --- a/src/Temporalio/Bridge/src/metric.rs +++ b/src/Temporalio/Bridge/src/metric.rs @@ -1,4 +1,4 @@ -use std::{any::Any, sync::Arc}; +use std::{any::Any, sync::Arc, time::Duration}; use temporal_sdk_core_api::telemetry::metrics; @@ -126,68 +126,103 @@ fn metric_attribute_to_key_value(attr: &MetricAttribute) -> metrics::MetricKeyVa } #[repr(C)] -pub struct MetricIntegerOptions { +pub struct MetricOptions { name: ByteArrayRef, description: ByteArrayRef, unit: ByteArrayRef, - kind: MetricIntegerKind, + kind: MetricKind, } #[repr(C)] -pub enum MetricIntegerKind { - Counter = 1, - Histogram, - Gauge, +pub enum MetricKind { + CounterInteger = 1, + HistogramInteger, + HistogramFloat, + HistogramDuration, + GaugeInteger, + GaugeFloat } -pub enum MetricInteger { - Counter(Arc), - Histogram(Arc), - Gauge(Arc), +pub enum Metric { + CounterInteger(Arc), + HistogramInteger(Arc), + HistogramFloat(Arc), + HistogramDuration(Arc), + GaugeInteger(Arc), + GaugeFloat(Arc) } #[no_mangle] -pub extern "C" fn metric_integer_new( +pub extern "C" fn metric_new( meter: *const MetricMeter, - options: *const MetricIntegerOptions, -) -> *mut MetricInteger { + options: *const MetricOptions, +) -> *mut Metric { let meter = unsafe { &*meter }; let options = unsafe { &*options }; Box::into_raw(Box::new(match options.kind { - MetricIntegerKind::Counter => { - MetricInteger::Counter(meter.core.inner.counter(options.into())) - } - MetricIntegerKind::Histogram => { - MetricInteger::Histogram(meter.core.inner.histogram(options.into())) - } - MetricIntegerKind::Gauge => MetricInteger::Gauge(meter.core.inner.gauge(options.into())), + MetricKind::CounterInteger => Metric::CounterInteger(meter.core.inner.counter(options.into())), + MetricKind::HistogramInteger => Metric::HistogramInteger(meter.core.inner.histogram(options.into())), + MetricKind::HistogramFloat => Metric::HistogramFloat(meter.core.inner.histogram_f64(options.into())), + MetricKind::HistogramDuration =>Metric::HistogramDuration(meter.core.inner.histogram_duration(options.into())), + MetricKind::GaugeInteger => Metric::GaugeInteger(meter.core.inner.gauge(options.into())), + MetricKind::GaugeFloat => Metric::GaugeFloat(meter.core.inner.gauge_f64(options.into())), })) } #[no_mangle] -pub extern "C" fn metric_integer_free(metric: *mut MetricInteger) { +pub extern "C" fn metric_free(metric: *mut Metric) { unsafe { let _ = Box::from_raw(metric); } } #[no_mangle] -pub extern "C" fn metric_integer_record( - metric: *const MetricInteger, +pub extern "C" fn metric_record_integer( + metric: *const Metric, value: u64, attrs: *const MetricAttributes, ) { let metric = unsafe { &*metric }; let attrs = unsafe { &*attrs }; match metric { - MetricInteger::Counter(counter) => counter.add(value, &attrs.core), - MetricInteger::Histogram(histogram) => histogram.record(value, &attrs.core), - MetricInteger::Gauge(gauge) => gauge.record(value, &attrs.core), + Metric::CounterInteger(counter) => counter.add(value, &attrs.core), + Metric::HistogramInteger(histogram) => histogram.record(value, &attrs.core), + Metric::GaugeInteger(gauge) => gauge.record(value, &attrs.core), + _ => panic!("Not an integer type"), } } -impl From<&MetricIntegerOptions> for metrics::MetricParameters { - fn from(options: &MetricIntegerOptions) -> Self { +#[no_mangle] +pub extern "C" fn metric_record_float( + metric: *const Metric, + value: f64, + attrs: *const MetricAttributes, +) { + let metric = unsafe { &*metric }; + let attrs = unsafe { &*attrs }; + match metric { + Metric::HistogramFloat(histogram) => histogram.record(value, &attrs.core), + Metric::GaugeFloat(gauge) => gauge.record(value, &attrs.core), + _ => panic!("Not a float type"), + } +} + +#[no_mangle] +pub extern "C" fn metric_record_duration( + metric: *const Metric, + value_ms: u64, + attrs: *const MetricAttributes, +) { + let metric = unsafe { &*metric }; + let attrs = unsafe { &*attrs }; + match metric { + Metric::HistogramDuration(histogram) => histogram.record(Duration::from_millis(value_ms), &attrs.core), + _ => panic!("Not a duration type"), + } +} + +impl From<&MetricOptions> for metrics::MetricParameters { + fn from(options: &MetricOptions) -> Self { metrics::MetricParametersBuilder::default() .name(options.name.to_string()) .description(options.description.to_string()) @@ -197,18 +232,24 @@ impl From<&MetricIntegerOptions> for metrics::MetricParameters { } } -type CustomMetricMeterMetricIntegerNewCallback = unsafe extern "C" fn( +type CustomMetricMeterMetricNewCallback = unsafe extern "C" fn( name: ByteArrayRef, description: ByteArrayRef, unit: ByteArrayRef, - kind: MetricIntegerKind, + kind: MetricKind, ) -> *const libc::c_void; -type CustomMetricMeterMetricIntegerFreeCallback = unsafe extern "C" fn(metric: *const libc::c_void); +type CustomMetricMeterMetricFreeCallback = unsafe extern "C" fn(metric: *const libc::c_void); -type CustomMetricMeterMetricIntegerUpdateCallback = +type CustomMetricMeterMetricRecordIntegerCallback = unsafe extern "C" fn(metric: *const libc::c_void, value: u64, attributes: *const libc::c_void); +type CustomMetricMeterMetricRecordFloatCallback = + unsafe extern "C" fn(metric: *const libc::c_void, value: f64, attributes: *const libc::c_void); + +type CustomMetricMeterMetricRecordDurationCallback = + unsafe extern "C" fn(metric: *const libc::c_void, value_ms: u64, attributes: *const libc::c_void); + type CustomMetricMeterAttributesNewCallback = unsafe extern "C" fn( append_from: *const libc::c_void, attributes: *const CustomMetricAttribute, @@ -226,9 +267,11 @@ type CustomMetricMeterMeterFreeCallback = unsafe extern "C" fn(meter: *const Cus /// invoked on. #[repr(C)] pub struct CustomMetricMeter { - pub metric_integer_new: CustomMetricMeterMetricIntegerNewCallback, - pub metric_integer_free: CustomMetricMeterMetricIntegerFreeCallback, - pub metric_integer_update: CustomMetricMeterMetricIntegerUpdateCallback, + pub metric_new: CustomMetricMeterMetricNewCallback, + pub metric_free: CustomMetricMeterMetricFreeCallback, + pub metric_record_integer: CustomMetricMeterMetricRecordIntegerCallback, + pub metric_record_float: CustomMetricMeterMetricRecordFloatCallback, + pub metric_record_duration: CustomMetricMeterMetricRecordDurationCallback, pub attributes_new: CustomMetricMeterAttributesNewCallback, pub attributes_free: CustomMetricMeterAttributesFreeCallback, pub meter_free: CustomMetricMeterMeterFreeCallback, @@ -279,15 +322,27 @@ impl metrics::CoreMeter for CustomMetricMeterRef { } fn counter(&self, params: metrics::MetricParameters) -> Arc { - Arc::new(self.new_metric_integer(params, MetricIntegerKind::Counter)) + Arc::new(self.new_metric(params, MetricKind::CounterInteger)) } fn histogram(&self, params: metrics::MetricParameters) -> Arc { - Arc::new(self.new_metric_integer(params, MetricIntegerKind::Histogram)) + Arc::new(self.new_metric(params, MetricKind::HistogramInteger)) + } + + fn histogram_f64(&self, params: metrics::MetricParameters) -> Arc { + Arc::new(self.new_metric(params, MetricKind::HistogramFloat)) + } + + fn histogram_duration(&self, params: metrics::MetricParameters) -> Arc { + Arc::new(self.new_metric(params, MetricKind::HistogramDuration)) } fn gauge(&self, params: metrics::MetricParameters) -> Arc { - Arc::new(self.new_metric_integer(params, MetricIntegerKind::Gauge)) + Arc::new(self.new_metric(params, MetricKind::GaugeInteger)) + } + + fn gauge_f64(&self, params: metrics::MetricParameters) -> Arc { + Arc::new(self.new_metric(params, MetricKind::GaugeFloat)) } } @@ -363,20 +418,20 @@ impl CustomMetricMeterRef { } } - fn new_metric_integer( + fn new_metric( &self, params: metrics::MetricParameters, - kind: MetricIntegerKind, - ) -> CustomMetricInteger { + kind: MetricKind, + ) -> CustomMetric { unsafe { let meter = &*(self.meter_impl.0); - let metric = (meter.metric_integer_new)( + let metric = (meter.metric_new)( ByteArrayRef::from_str(¶ms.name), ByteArrayRef::from_str(¶ms.description), ByteArrayRef::from_str(¶ms.unit), kind, ); - CustomMetricInteger { + CustomMetric { meter_impl: self.meter_impl.clone(), metric, } @@ -421,19 +476,19 @@ impl Drop for CustomMetricAttributes { } } -struct CustomMetricInteger { +struct CustomMetric { meter_impl: Arc, metric: *const libc::c_void, } -unsafe impl Send for CustomMetricInteger {} -unsafe impl Sync for CustomMetricInteger {} +unsafe impl Send for CustomMetric {} +unsafe impl Sync for CustomMetric {} -impl metrics::Counter for CustomMetricInteger { +impl metrics::Counter for CustomMetric { fn add(&self, value: u64, attributes: &metrics::MetricAttributes) { unsafe { let meter = &*(self.meter_impl.0); - (meter.metric_integer_update)( + (meter.metric_record_integer)( self.metric, value, raw_custom_metric_attributes(attributes), @@ -442,11 +497,11 @@ impl metrics::Counter for CustomMetricInteger { } } -impl metrics::Histogram for CustomMetricInteger { +impl metrics::Histogram for CustomMetric { fn record(&self, value: u64, attributes: &metrics::MetricAttributes) { unsafe { let meter = &*(self.meter_impl.0); - (meter.metric_integer_update)( + (meter.metric_record_integer)( self.metric, value, raw_custom_metric_attributes(attributes), @@ -455,11 +510,50 @@ impl metrics::Histogram for CustomMetricInteger { } } -impl metrics::Gauge for CustomMetricInteger { +impl metrics::HistogramF64 for CustomMetric { + fn record(&self, value: f64, attributes: &metrics::MetricAttributes) { + unsafe { + let meter = &*(self.meter_impl.0); + (meter.metric_record_float)( + self.metric, + value, + raw_custom_metric_attributes(attributes), + ); + } + } +} + +impl metrics::HistogramDuration for CustomMetric { + fn record(&self, value: Duration, attributes: &metrics::MetricAttributes) { + unsafe { + let meter = &*(self.meter_impl.0); + (meter.metric_record_duration)( + self.metric, + value.as_millis().try_into().unwrap_or(u64::MAX), + raw_custom_metric_attributes(attributes), + ); + } + } +} + +impl metrics::Gauge for CustomMetric { fn record(&self, value: u64, attributes: &metrics::MetricAttributes) { unsafe { let meter = &*(self.meter_impl.0); - (meter.metric_integer_update)( + (meter.metric_record_integer)( + self.metric, + value, + raw_custom_metric_attributes(attributes), + ); + } + } +} + +impl metrics::GaugeF64 for CustomMetric { + fn record(&self, value: f64, attributes: &metrics::MetricAttributes) { + unsafe { + let meter = &*(self.meter_impl.0); + (meter.metric_record_float)( self.metric, value, raw_custom_metric_attributes(attributes), @@ -480,11 +574,11 @@ fn raw_custom_metric_attributes(attributes: &metrics::MetricAttributes) -> *cons } } -impl Drop for CustomMetricInteger { +impl Drop for CustomMetric { fn drop(&mut self) { unsafe { let meter = &*(self.meter_impl.0); - (meter.metric_integer_free)(self.metric); + (meter.metric_free)(self.metric); } } } diff --git a/src/Temporalio/Bridge/src/runtime.rs b/src/Temporalio/Bridge/src/runtime.rs index 0fbd5cab..36a4148b 100644 --- a/src/Temporalio/Bridge/src/runtime.rs +++ b/src/Temporalio/Bridge/src/runtime.rs @@ -90,6 +90,7 @@ pub struct OpenTelemetryOptions { headers: MetadataRef, metric_periodicity_millis: u32, metric_temporality: OpenTelemetryMetricTemporality, + durations_as_seconds: bool, } #[repr(C)] @@ -103,6 +104,7 @@ pub struct PrometheusOptions { bind_address: ByteArrayRef, counters_total_suffix: bool, unit_suffix: bool, + durations_as_seconds: bool, } #[derive(Clone)] @@ -378,7 +380,8 @@ fn create_meter( OpenTelemetryMetricTemporality::Cumulative => MetricTemporality::Cumulative, OpenTelemetryMetricTemporality::Delta => MetricTemporality::Delta, }) - .global_tags(options.global_tags.to_string_map_on_newlines()); + .global_tags(options.global_tags.to_string_map_on_newlines()) + .use_seconds_for_durations(otel_options.durations_as_seconds); if otel_options.metric_periodicity_millis > 0 { build.metric_periodicity(Duration::from_millis( otel_options.metric_periodicity_millis.into(), @@ -397,7 +400,8 @@ fn create_meter( .socket_addr(SocketAddr::from_str(prom_options.bind_address.to_str())?) .global_tags(options.global_tags.to_string_map_on_newlines()) .counters_total_suffix(prom_options.counters_total_suffix) - .unit_suffix(prom_options.unit_suffix); + .unit_suffix(prom_options.unit_suffix) + .use_seconds_for_durations(prom_options.durations_as_seconds); Ok(start_prometheus_metric_exporter(build.build()?)?.meter) } else if let Some(custom_meter) = custom_meter { Ok(Arc::new(custom_meter)) diff --git a/src/Temporalio/Common/MetricMeter.cs b/src/Temporalio/Common/MetricMeter.cs index 6a714ff8..2377778a 100644 --- a/src/Temporalio/Common/MetricMeter.cs +++ b/src/Temporalio/Common/MetricMeter.cs @@ -25,7 +25,8 @@ public abstract MetricCounter CreateCounter( /// Create a new histogram. Performance is better if this histogram is reused instead of /// recreating it. /// - /// Type of value for the metric. Currently this must be an integer + /// Type of value for the metric. Currently this must be an integer, + /// float, or type. /// type. /// Name for the histogram. /// Unit for the histogram if any. @@ -39,8 +40,8 @@ public abstract MetricHistogram CreateHistogram( /// Create a new gauge. Performance is better if this gauge is reused instead of recreating /// it. /// - /// Type of value for the metric. Currently this must be an integer - /// type. + /// Type of value for the metric. Currently this must be an integer or + /// float type. /// Name for the gauge. /// Unit for the gauge if any. /// Description for the gauge if any. diff --git a/src/Temporalio/Common/MetricMeterBridge.cs b/src/Temporalio/Common/MetricMeterBridge.cs index a6e7f18e..51b539a2 100644 --- a/src/Temporalio/Common/MetricMeterBridge.cs +++ b/src/Temporalio/Common/MetricMeterBridge.cs @@ -25,10 +25,14 @@ public override MetricCounter CreateCounter( string name, string? unit = null, string? description = null) where T : struct { - AssertValidMetricType(); + if (!IsInteger()) + { + throw new ArgumentException($"Invalid metric type of {typeof(T)}, must be integer"); + } return new Counter( new(name, unit, description), - new(meter, Bridge.Interop.MetricIntegerKind.Counter, name, unit, description), + new(meter, Bridge.Interop.MetricKind.CounterInteger, name, unit, description), + Bridge.Interop.MetricKind.CounterInteger, attributes); } @@ -37,10 +41,27 @@ public override MetricHistogram CreateHistogram( string name, string? unit = null, string? description = null) where T : struct { - AssertValidMetricType(); + Bridge.Interop.MetricKind kind; + if (IsInteger()) + { + kind = Bridge.Interop.MetricKind.HistogramInteger; + } + else if (IsFloat()) + { + kind = Bridge.Interop.MetricKind.HistogramFloat; + } + else if (typeof(T) == typeof(TimeSpan)) + { + kind = Bridge.Interop.MetricKind.HistogramDuration; + } + else + { + throw new ArgumentException($"Invalid metric type of {typeof(T)}, must be integer, float, or TimeSpan"); + } return new Histogram( new(name, unit, description), - new(meter, Bridge.Interop.MetricIntegerKind.Histogram, name, unit, description), + new(meter, kind, name, unit, description), + kind, attributes); } @@ -49,10 +70,23 @@ public override MetricGauge CreateGauge( string name, string? unit = null, string? description = null) where T : struct { - AssertValidMetricType(); + Bridge.Interop.MetricKind kind; + if (IsInteger()) + { + kind = Bridge.Interop.MetricKind.GaugeInteger; + } + else if (IsFloat()) + { + kind = Bridge.Interop.MetricKind.GaugeFloat; + } + else + { + throw new ArgumentException($"Invalid metric type of {typeof(T)}, must be integer or float"); + } return new Gauge( new(name, unit, description), - new(meter, Bridge.Interop.MetricIntegerKind.Gauge, name, unit, description), + new(meter, kind, name, unit, description), + kind, attributes); } @@ -75,7 +109,7 @@ public override MetricMeter WithTags(IEnumerable> t return MetricMeterNoop.Instance; }); - private static void AssertValidMetricType() + private static bool IsInteger() { switch (Type.GetTypeCode(typeof(T))) { @@ -87,12 +121,12 @@ private static void AssertValidMetricType() case TypeCode.UInt16: case TypeCode.UInt32: case TypeCode.UInt64: - return; + return true; } - throw new ArgumentException($"Metric type must be an integer numeric, got: {typeof(T)}"); + return false; } - private static ulong GetMetricValue(T value) + private static ulong GetIntegerValue(T value) where T : struct { switch (value) @@ -134,99 +168,220 @@ private static ulong GetMetricValue(T value) } } - private static void RecordMetric( - Bridge.MetricInteger metric, + private static void RecordInteger( + Bridge.Metric metric, + Bridge.MetricAttributes attributes, + T value, + IEnumerable>? extraTags) + where T : struct + { + var ulongValue = GetIntegerValue(value); + if (extraTags is { } extraAttrs) + { + using (var withExtras = attributes.Append(extraAttrs)) + { + metric.RecordInteger(ulongValue, withExtras); + } + } + else + { + metric.RecordInteger(ulongValue, attributes); + } + } + + private static bool IsFloat() + { + switch (Type.GetTypeCode(typeof(T))) + { + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + return true; + } + return false; + } + + private static double GetFloatValue(T value) + where T : struct + { + switch (value) + { + case float v: + if (v < 0) + { + throw new ArgumentException("Value cannot be negative"); + } + return (double)v; + case double v: + if (v < 0) + { + throw new ArgumentException("Value cannot be negative"); + } + return (double)v; + case decimal v: + if (v < 0) + { + throw new ArgumentException("Value cannot be negative"); + } + return decimal.ToDouble(v); + default: + throw new ArgumentException($"Only float metric types supported, but got {value.GetType()}"); + } + } + + private static void RecordFloat( + Bridge.Metric metric, + Bridge.MetricAttributes attributes, + T value, + IEnumerable>? extraTags) + where T : struct + { + var floatValue = GetFloatValue(value); + if (extraTags is { } extraAttrs) + { + using (var withExtras = attributes.Append(extraAttrs)) + { + metric.RecordFloat(floatValue, withExtras); + } + } + else + { + metric.RecordFloat(floatValue, attributes); + } + } + + private static void RecordDuration( + Bridge.Metric metric, Bridge.MetricAttributes attributes, T value, IEnumerable>? extraTags) where T : struct { - var ulongValue = GetMetricValue(value); + var ms = value is TimeSpan v ? v.TotalMilliseconds : + throw new ArgumentException($"Only TimeSpan types supported, but got {value.GetType()}"); + if (ms < 0) + { + throw new ArgumentException("Value cannot be negative"); + } if (extraTags is { } extraAttrs) { using (var withExtras = attributes.Append(extraAttrs)) { - metric.Record(ulongValue, withExtras); + metric.RecordDuration((ulong)ms, withExtras); } } else { - metric.Record(ulongValue, attributes); + metric.RecordDuration((ulong)ms, attributes); } } private class Counter : MetricCounter where T : struct { - private readonly Bridge.MetricInteger metric; + private readonly Bridge.Metric metric; + private readonly Bridge.Interop.MetricKind kind; private readonly Bridge.MetricAttributes attributes; internal Counter( MetricDetails details, - Bridge.MetricInteger metric, + Bridge.Metric metric, + Bridge.Interop.MetricKind kind, Bridge.MetricAttributes attributes) : base(details) { this.metric = metric; + this.kind = kind; this.attributes = attributes; } public override void Add( - T value, IEnumerable>? extraTags = null) => - RecordMetric(metric, attributes, value, extraTags); + T value, IEnumerable>? extraTags = null) + { + RecordInteger(metric, attributes, value, extraTags); + } public override MetricCounter WithTags(IEnumerable> tags) => - new Counter(Details, metric, attributes.Append(tags)); + new Counter(Details, metric, kind, attributes.Append(tags)); } private class Histogram : MetricHistogram where T : struct { - private readonly Bridge.MetricInteger metric; + private readonly Bridge.Metric metric; + private readonly Bridge.Interop.MetricKind kind; private readonly Bridge.MetricAttributes attributes; internal Histogram( MetricDetails details, - Bridge.MetricInteger metric, + Bridge.Metric metric, + Bridge.Interop.MetricKind kind, Bridge.MetricAttributes attributes) : base(details) { this.metric = metric; + this.kind = kind; this.attributes = attributes; } public override void Record( - T value, IEnumerable>? extraTags = null) => - RecordMetric(metric, attributes, value, extraTags); + T value, IEnumerable>? extraTags = null) + { + switch (kind) + { + case Bridge.Interop.MetricKind.HistogramInteger: + RecordInteger(metric, attributes, value, extraTags); + break; + case Bridge.Interop.MetricKind.HistogramFloat: + RecordFloat(metric, attributes, value, extraTags); + break; + case Bridge.Interop.MetricKind.HistogramDuration: + RecordDuration(metric, attributes, value, extraTags); + break; + } + } public override MetricHistogram WithTags(IEnumerable> tags) => - new Histogram(Details, metric, attributes.Append(tags)); + new Histogram(Details, metric, kind, attributes.Append(tags)); } private class Gauge : MetricGauge where T : struct { - private readonly Bridge.MetricInteger metric; + private readonly Bridge.Metric metric; + private readonly Bridge.Interop.MetricKind kind; private readonly Bridge.MetricAttributes attributes; internal Gauge( MetricDetails details, - Bridge.MetricInteger metric, + Bridge.Metric metric, + Bridge.Interop.MetricKind kind, Bridge.MetricAttributes attributes) : base(details) { this.metric = metric; + this.kind = kind; this.attributes = attributes; } #pragma warning disable CA1716 // We are ok with using the "Set" name even though "set" is in lang public override void Set( - T value, IEnumerable>? extraTags = null) => - RecordMetric(metric, attributes, value, extraTags); + T value, IEnumerable>? extraTags = null) + { + switch (kind) + { + case Bridge.Interop.MetricKind.GaugeInteger: + RecordInteger(metric, attributes, value, extraTags); + break; + case Bridge.Interop.MetricKind.GaugeFloat: + RecordFloat(metric, attributes, value, extraTags); + break; + } + } #pragma warning restore CA1716 public override MetricGauge WithTags(IEnumerable> tags) => - new Gauge(Details, metric, attributes.Append(tags)); + new Gauge(Details, metric, kind, attributes.Append(tags)); } } } \ No newline at end of file diff --git a/src/Temporalio/Runtime/CustomMetricMeterOptions.cs b/src/Temporalio/Runtime/CustomMetricMeterOptions.cs new file mode 100644 index 00000000..c49a0f51 --- /dev/null +++ b/src/Temporalio/Runtime/CustomMetricMeterOptions.cs @@ -0,0 +1,40 @@ +namespace Temporalio.Runtime +{ + /// + /// Options for custom metric meter. + /// + public class CustomMetricMeterOptions + { + /// + /// Format for duration values in metrics. + /// + public enum DurationFormat + { + /// + /// Format the value as long milliseconds. + /// + IntegerMilliseconds, + + /// + /// Format the value as double seconds. + /// + FloatSeconds, + + /// + /// Format the value as . + /// + TimeSpan, + } + + /// + /// Gets or sets how histogram duration values are given to the interface. + /// + public DurationFormat HistogramDurationFormat { get; set; } = DurationFormat.IntegerMilliseconds; + + /// + /// Create a shallow copy of these options. + /// + /// A shallow copy of these options and any transitive options fields. + public virtual object Clone() => MemberwiseClone(); + } +} \ No newline at end of file diff --git a/src/Temporalio/Runtime/ICustomMetricMeter.cs b/src/Temporalio/Runtime/ICustomMetricMeter.cs index 11a4d119..20ad74be 100644 --- a/src/Temporalio/Runtime/ICustomMetricMeter.cs +++ b/src/Temporalio/Runtime/ICustomMetricMeter.cs @@ -11,7 +11,7 @@ public interface ICustomMetricMeter /// Create a metric counter. /// /// The type of counter value. Currently this is always - /// long. + /// long, but the types can change in the future. /// Name for the metric. /// Unit for the metric if any. /// Description for the metric if any. @@ -23,12 +23,19 @@ ICustomMetricCounter CreateCounter( /// /// Create a metric histogram. /// - /// The type of histogram value. Currently this is always - /// long. + /// The type of histogram value. Currently this can be long, + /// double, or , but the types can change in the + /// future. /// Name for the metric. /// Unit for the metric if any. /// Description for the metric if any. /// Histogram to be called with updates. + /// + /// By default all histograms are set as a long of milliseconds unless + /// is set to FloatSeconds. + /// Similarly, if the unit for a histogram is "duration", it is changed to "ms" unless that + /// same setting is set, at which point the unit is changed to "s". + /// ICustomMetricHistogram CreateHistogram( string name, string? unit, string? description) where T : struct; @@ -36,8 +43,8 @@ ICustomMetricHistogram CreateHistogram( /// /// Create a metric gauge. /// - /// The type of gauge value. Currently this is always - /// long. + /// The type of gauge value. Currently this can be long or + /// double, but the types can change in the future. /// Name for the metric. /// Unit for the metric if any. /// Description for the metric if any. diff --git a/src/Temporalio/Runtime/MetricsOptions.cs b/src/Temporalio/Runtime/MetricsOptions.cs index 079fedda..eeed0f8a 100644 --- a/src/Temporalio/Runtime/MetricsOptions.cs +++ b/src/Temporalio/Runtime/MetricsOptions.cs @@ -44,6 +44,12 @@ public MetricsOptions() /// public ICustomMetricMeter? CustomMetricMeter { get; set; } + /// + /// Gets or sets the custom metric meter options. Only applies if + /// is not null. + /// + public CustomMetricMeterOptions? CustomMetricMeterOptions { get; set; } + /// /// Gets or sets a value indicating whether the service name is set on metrics. /// diff --git a/src/Temporalio/Runtime/OpenTelemetryOptions.cs b/src/Temporalio/Runtime/OpenTelemetryOptions.cs index eb7ca6bc..d826da0d 100644 --- a/src/Temporalio/Runtime/OpenTelemetryOptions.cs +++ b/src/Temporalio/Runtime/OpenTelemetryOptions.cs @@ -51,6 +51,12 @@ public OpenTelemetryOptions(string url) /// public OpenTelemetryMetricTemporality MetricTemporality { get; set; } = OpenTelemetryMetricTemporality.Cumulative; + /// + /// Gets or sets a value indicating whether duration values will be emitted as float + /// seconds. If false, it is integer milliseconds. + /// + public bool UseSecondsForDuration { get; set; } + /// /// Create a shallow copy of these options. /// diff --git a/src/Temporalio/Runtime/PrometheusOptions.cs b/src/Temporalio/Runtime/PrometheusOptions.cs index 749bd390..d35a05ae 100644 --- a/src/Temporalio/Runtime/PrometheusOptions.cs +++ b/src/Temporalio/Runtime/PrometheusOptions.cs @@ -35,6 +35,12 @@ public PrometheusOptions() /// public bool HasUnitSuffix { get; set; } + /// + /// Gets or sets a value indicating whether duration values will be emitted as float + /// seconds. If false, it is integer milliseconds. + /// + public bool UseSecondsForDuration { get; set; } + /// /// Create a shallow copy of these options. /// diff --git a/tests/Temporalio.Tests/Extensions/DiagnosticSource/CustomMetricMeterTests.cs b/tests/Temporalio.Tests/Extensions/DiagnosticSource/CustomMetricMeterTests.cs index 94e73d7d..62cfb1cf 100644 --- a/tests/Temporalio.Tests/Extensions/DiagnosticSource/CustomMetricMeterTests.cs +++ b/tests/Temporalio.Tests/Extensions/DiagnosticSource/CustomMetricMeterTests.cs @@ -257,4 +257,101 @@ m.Instrument is ObservableGauge && m.Value == 100 && m.Tags["worker_type"].Equals("LocalActivityWorker")); } + + [Workflow] + public class FloatsAndDurationsMetricsWorkflow + { + [WorkflowRun] + public async Task RunAsync() + { + Workflow.MetricMeter.CreateHistogram("histogram-float").Record(1.23); + Workflow.MetricMeter.CreateHistogram("histogram-duration").Record( + new TimeSpan(0, 0, 4, 5, 6)); + Workflow.MetricMeter.CreateGauge("gauge-float").Set(7.89); + } + } + + [Theory] + [InlineData(CustomMetricMeterOptions.DurationFormat.IntegerMilliseconds)] + [InlineData(CustomMetricMeterOptions.DurationFormat.FloatSeconds)] + [InlineData(CustomMetricMeterOptions.DurationFormat.TimeSpan)] + public async Task CustomMetricMeter_Workflow_FloatsAndDurations( + CustomMetricMeterOptions.DurationFormat durationFormat) + { + // Create meter + using var meter = new Meter("test-meter"); + + // Create/start listener + using var meterListener = new MeterListener(); + meterListener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == "test-meter") + { + listener.EnableMeasurementEvents(instrument); + } + }; + var metrics = new ConcurrentQueue<(Instrument Instrument, object Value, Dictionary Tags)>(); + meterListener.SetMeasurementEventCallback((inst, value, tags, state) => + metrics.Enqueue((inst, value, new(tags.ToArray().Select( + kv => KeyValuePair.Create(kv.Key, kv.Value!)))))); + meterListener.SetMeasurementEventCallback((inst, value, tags, state) => + metrics.Enqueue((inst, value, new(tags.ToArray().Select( + kv => KeyValuePair.Create(kv.Key, kv.Value!)))))); + meterListener.Start(); + + // Create runtime/client with meter + var runtime = new TemporalRuntime(new() + { + Telemetry = new() + { + Metrics = new() + { + CustomMetricMeter = new CustomMetricMeter(meter), + CustomMetricMeterOptions = new() { HistogramDurationFormat = durationFormat }, + }, + }, + }); + var client = await TemporalClient.ConnectAsync( + new() + { + TargetHost = Client.Connection.Options.TargetHost, + Namespace = Client.Options.Namespace, + Runtime = runtime, + }); + + // Run workflow + using var worker = new TemporalWorker( + client, + new TemporalWorkerOptions($"tq-{Guid.NewGuid()}") + { Interceptors = new[] { new XunitExceptionInterceptor() } }. + AddWorkflow()); + await worker.ExecuteAsync(() => + client.ExecuteWorkflowAsync( + (FloatsAndDurationsMetricsWorkflow wf) => wf.RunAsync(), + new(id: $"workflow-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!))); + + // Record and wait until the three metrics show up + meterListener.RecordObservableInstruments(); + var (histFloat, histDur, gaugeFloat) = await AssertMore.EventuallyAsync(async () => + (Assert.Single(metrics, m => m.Instrument.Name == "histogram-float"), + Assert.Single(metrics, m => m.Instrument.Name == "histogram-duration"), + Assert.Single(metrics, m => m.Instrument.Name == "gauge-float"))); + + Assert.Equal(1.23, histFloat.Value); + if (durationFormat == CustomMetricMeterOptions.DurationFormat.FloatSeconds) + { + Assert.Single(metrics, m => + m.Instrument.Name == "temporal_workflow_task_execution_latency" && + m.Instrument.Unit == "s"); + Assert.Equal(new TimeSpan(0, 0, 4, 5, 6).TotalSeconds, histDur.Value); + } + else + { + Assert.Single(metrics, m => + m.Instrument.Name == "temporal_workflow_task_execution_latency" && + m.Instrument.Unit == "ms"); + Assert.Equal((long)new TimeSpan(0, 0, 4, 5, 6).TotalMilliseconds, histDur.Value); + } + Assert.Equal(7.89, gaugeFloat.Value); + } } \ No newline at end of file diff --git a/tests/Temporalio.Tests/Runtime/TemporalRuntimeTests.cs b/tests/Temporalio.Tests/Runtime/TemporalRuntimeTests.cs index c82d2b1e..05885c90 100644 --- a/tests/Temporalio.Tests/Runtime/TemporalRuntimeTests.cs +++ b/tests/Temporalio.Tests/Runtime/TemporalRuntimeTests.cs @@ -97,15 +97,15 @@ public async Task Runtime_CustomMetricMeter_WorksProperly() (metrics[0].Name, metrics[0].Unit, metrics[0].Description)); var metricValues = metrics[0].Values.ToList(); Assert.Equal(4, metricValues.Count); - Assert.Equal(123, metricValues[0].Value); + Assert.Equal(123L, metricValues[0].Value); Assert.Equal( new Dictionary() { { "service_name", "temporal-core-sdk" } }, metricValues[0].Tags); - Assert.Equal(234, metricValues[1].Value); + Assert.Equal(234L, metricValues[1].Value); Assert.Equal( new Dictionary() { { "service_name", "temporal-core-sdk" } }, metricValues[1].Tags); - Assert.Equal(345, metricValues[2].Value); + Assert.Equal(345L, metricValues[2].Value); Assert.Equal( new Dictionary() { @@ -116,7 +116,7 @@ public async Task Runtime_CustomMetricMeter_WorksProperly() { "bool-tag", false }, }, metricValues[2].Tags); - Assert.Equal(456, metricValues[3].Value); + Assert.Equal(456L, metricValues[3].Value); Assert.Equal( new Dictionary() { @@ -134,7 +134,7 @@ public async Task Runtime_CustomMetricMeter_WorksProperly() (metrics[1].Name, metrics[1].Unit, metrics[1].Description)); metricValues = metrics[1].Values.ToList(); Assert.Single(metricValues); - Assert.Equal(567, metricValues[0].Value); + Assert.Equal(567L, metricValues[0].Value); Assert.Equal( new Dictionary() { { "service_name", "temporal-core-sdk" } }, metricValues[0].Tags); @@ -144,7 +144,7 @@ public async Task Runtime_CustomMetricMeter_WorksProperly() (metrics[2].Name, metrics[2].Unit, metrics[2].Description)); metricValues = metrics[2].Values.ToList(); Assert.Single(metricValues); - Assert.Equal(678, metricValues[0].Value); + Assert.Equal(678L, metricValues[0].Value); Assert.Equal( new Dictionary() { { "service_name", "temporal-core-sdk" }, { "string-tag", "histval" } }, @@ -155,7 +155,7 @@ public async Task Runtime_CustomMetricMeter_WorksProperly() (metrics[3].Name, metrics[3].Unit, metrics[3].Description)); metricValues = metrics[3].Values.ToList(); Assert.Single(metricValues); - Assert.Equal(789, metricValues[0].Value); + Assert.Equal(789L, metricValues[0].Value); Assert.Equal( new Dictionary() { { "service_name", "temporal-core-sdk" }, { "string-tag", "gaugeval" } }, diff --git a/tests/Temporalio.Tests/TestUtils.cs b/tests/Temporalio.Tests/TestUtils.cs index a0c199aa..2a9512e9 100644 --- a/tests/Temporalio.Tests/TestUtils.cs +++ b/tests/Temporalio.Tests/TestUtils.cs @@ -137,17 +137,17 @@ public class CaptureMetricMeter : ICustomMetricMeter public ICustomMetricCounter CreateCounter( string name, string? unit, string? description) where T : struct => - CreateMetric>(name, unit, description); + CreateMetric>(name, unit, description); public ICustomMetricHistogram CreateHistogram( string name, string? unit, string? description) where T : struct => - CreateMetric>(name, unit, description); + CreateMetric>(name, unit, description); public ICustomMetricGauge CreateGauge( string name, string? unit, string? description) where T : struct => - CreateMetric>(name, unit, description); + CreateMetric>(name, unit, description); public object CreateTags( object? appendFrom, IReadOnlyCollection> tags) @@ -163,26 +163,27 @@ public object CreateTags( } private TMetric CreateMetric(string name, string? unit, string? description) + where TValue : struct { - if (typeof(TValue) != typeof(long)) - { - throw new InvalidOperationException($"Expected long type, got {typeof(TValue)}"); - } - var metric = new CaptureMetric(name, unit, description); + var metric = new CaptureMetric(name, unit, description); Metrics.Enqueue(metric); return (TMetric)(object)metric; } } - public record CaptureMetric(string Name, string? Unit, string? Description) - : ICustomMetricCounter, ICustomMetricHistogram, ICustomMetricGauge + public abstract record CaptureMetric(string Name, string? Unit, string? Description) { - public ConcurrentQueue<(long Value, Dictionary Tags)> Values { get; } = new(); + public ConcurrentQueue<(object Value, Dictionary Tags)> Values { get; } = new(); + } - public void Add(long value, object tags) => Values.Enqueue((value, (Dictionary)tags)); + public record CaptureMetric(string Name, string? Unit, string? Description) + : CaptureMetric(Name, Unit, Description), ICustomMetricCounter, ICustomMetricHistogram, ICustomMetricGauge + where T : struct + { + public void Add(T value, object tags) => Values.Enqueue((value, (Dictionary)tags)); - public void Record(long value, object tags) => Values.Enqueue((value, (Dictionary)tags)); + public void Record(T value, object tags) => Values.Enqueue((value, (Dictionary)tags)); - public void Set(long value, object tags) => Values.Enqueue((value, (Dictionary)tags)); + public void Set(T value, object tags) => Values.Enqueue((value, (Dictionary)tags)); } } \ No newline at end of file diff --git a/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs b/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs index 667d6316..0f1f6039 100644 --- a/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs +++ b/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs @@ -3399,10 +3399,10 @@ await client.ExecuteWorkflowAsync( v.Tags.Contains(new("task_queue", taskQueue)) && v.Tags.Contains(new("workflow_type", "CustomMetricsWorkflow")) && !v.Tags.ContainsKey("my-workflow-extra-tag") && - v.Value == 56); + (long)v.Value == 56); Assert.Single(metric.Values, v => v.Tags.Contains(new("my-workflow-extra-tag", 1234L)) && - v.Value == 78); + (long)v.Value == 78); // Activity counter metric = Assert.Single(meter.Metrics, m => m.Name == "my-activity-counter" && @@ -3413,15 +3413,96 @@ await client.ExecuteWorkflowAsync( v.Tags.Contains(new("task_queue", taskQueue)) && v.Tags.Contains(new("activity_type", "DoActivity")) && !v.Tags.ContainsKey("my-activity-extra-tag") && - v.Value == 12); + (long)v.Value == 12); Assert.Single(metric.Values, v => v.Tags.Contains(new("my-activity-extra-tag", 12.34D)) && - v.Value == 34); + (long)v.Value == 34); // Check Temporal metric metric = Assert.Single(meter.Metrics, m => m.Name == "some-prefix_workflow_completed"); Assert.Single(metric.Values, v => v.Tags.Contains(new("workflow_type", "CustomMetricsWorkflow")) && - v.Value == 1); + (long)v.Value == 1); + } + + [Fact] + public async Task ExecuteWorkflowAsync_CustomMetrics_FloatsAndDurations() + { + var timeSpan = new TimeSpan(2, 0, 0, 3, 4); + async Task DoStuffAsync(CustomMetricMeterOptions.DurationFormat durationFormat) + { + var meter = new TestUtils.CaptureMetricMeter(); + var runtime = new TemporalRuntime(new() + { + Telemetry = new() + { + Metrics = new() + { + CustomMetricMeter = meter, + CustomMetricMeterOptions = new() { HistogramDurationFormat = durationFormat }, + }, + }, + }); + var client = await TemporalClient.ConnectAsync( + new() + { + TargetHost = Client.Connection.Options.TargetHost, + Namespace = Client.Options.Namespace, + Runtime = runtime, + }); + var taskQueue = string.Empty; + await ExecuteWorkerAsync( + async worker => + { + taskQueue = worker.Options.TaskQueue!; + await client.ExecuteWorkflowAsync( + (SimpleWorkflow wf) => wf.RunAsync("Temporal"), + new(id: $"workflow-{Guid.NewGuid()}", taskQueue)); + }, + client: client); + // Also, add some manual types beyond the defaults tested in other tests + runtime.MetricMeter.CreateHistogram("my-histogram-float").Record(1.23); + runtime.MetricMeter.CreateHistogram("my-histogram-duration").Record(timeSpan); + runtime.MetricMeter.CreateGauge("my-gauge-float").Set(4.56); + return meter; + } + + // Do stuff with ms duration format, check metrics + var meter = await DoStuffAsync(CustomMetricMeterOptions.DurationFormat.IntegerMilliseconds); + Assert.Single( + Assert.Single(meter.Metrics, v => + v.Name == "temporal_workflow_task_execution_latency" && v.Unit == "ms").Values, + v => v.Value is long val); + Assert.Single( + Assert.Single(meter.Metrics, v => v.Name == "my-histogram-float").Values, + v => v.Value is double val && val == 1.23); + Assert.Single( + Assert.Single(meter.Metrics, v => v.Name == "my-histogram-duration").Values, + v => v.Value is long val && val == (long)timeSpan.TotalMilliseconds); + Assert.Single( + Assert.Single(meter.Metrics, v => v.Name == "my-gauge-float").Values, + v => v.Value is double val && val == 4.56); + + // Do it again with seconds + meter = await DoStuffAsync(CustomMetricMeterOptions.DurationFormat.FloatSeconds); + // Took less than 5s + Assert.Single( + Assert.Single(meter.Metrics, v => + v.Name == "temporal_workflow_task_execution_latency" && v.Unit == "s").Values, + v => v.Value is double val && val < 5); + Assert.Single( + Assert.Single(meter.Metrics, v => v.Name == "my-histogram-duration").Values, + v => v.Value is double val && val == timeSpan.TotalSeconds); + + // Do it again with TimeSpan + meter = await DoStuffAsync(CustomMetricMeterOptions.DurationFormat.TimeSpan); + // Took less than 5s + Assert.Single( + Assert.Single(meter.Metrics, v => + v.Name == "temporal_workflow_task_execution_latency" && v.Unit == "duration").Values, + v => v.Value is TimeSpan val && val < TimeSpan.FromSeconds(5)); + Assert.Single( + Assert.Single(meter.Metrics, v => v.Name == "my-histogram-duration").Values, + v => v.Value is TimeSpan val && val == timeSpan); } [Workflow]