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]