Skip to content

Commit 1573d3a

Browse files
authored
Support duration and float metrics (#223)
Fixes #209
1 parent 740dfa2 commit 1573d3a

File tree

22 files changed

+900
-210
lines changed

22 files changed

+900
-210
lines changed

src/Temporalio.Extensions.DiagnosticSource/CustomMetricMeter.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ namespace Temporalio.Extensions.DiagnosticSource
1313
/// Implementation of <see cref="ICustomMetricMeter" /> for a <see cref="Meter" /> that can be
1414
/// set on <see cref="MetricsOptions.CustomMetricMeter" /> to record metrics to the meter.
1515
/// </summary>
16+
/// <remarks>
17+
/// By default all histograms are set as a <c>long</c> of milliseconds unless
18+
/// <see cref="MetricsOptions.CustomMetricMeterOptions"/> is set to <c>FloatSeconds</c>.
19+
/// Similarly, if the unit for a histogram is "duration", it is changed to "ms" unless that same
20+
/// setting is set, at which point the unit is changed to "s".
21+
/// </remarks>
1622
public class CustomMetricMeter : ICustomMetricMeter
1723
{
1824
/// <summary>
@@ -35,8 +41,22 @@ public ICustomMetricCounter<T> CreateCounter<T>(
3541
/// <inheritdoc />
3642
public ICustomMetricHistogram<T> CreateHistogram<T>(
3743
string name, string? unit, string? description)
38-
where T : struct =>
39-
new CustomMetricHistogram<T>(Meter.CreateHistogram<T>(name, unit, description));
44+
where T : struct
45+
{
46+
// Have to convert TimeSpan to something .NET meter can work with. For this to even
47+
// happen, a user would have had to set custom options to report as time span.
48+
if (typeof(T) == typeof(TimeSpan))
49+
{
50+
// If unit is "duration", change to "ms since we're converting here
51+
if (unit == "duration")
52+
{
53+
unit = "ms";
54+
}
55+
return (new CustomMetricHistogramTimeSpan(
56+
Meter.CreateHistogram<long>(name, unit, description)) as ICustomMetricHistogram<T>)!;
57+
}
58+
return new CustomMetricHistogram<T>(Meter.CreateHistogram<T>(name, unit, description));
59+
}
4060

4161
/// <inheritdoc />
4262
public ICustomMetricGauge<T> CreateGauge<T>(
@@ -75,6 +95,17 @@ public void Record(T value, object tags) =>
7595
underlying.Record(value, ((Tags)tags).TagList);
7696
}
7797

98+
private sealed class CustomMetricHistogramTimeSpan : ICustomMetricHistogram<TimeSpan>
99+
{
100+
private readonly Histogram<long> underlying;
101+
102+
internal CustomMetricHistogramTimeSpan(Histogram<long> underlying) =>
103+
this.underlying = underlying;
104+
105+
public void Record(TimeSpan value, object tags) =>
106+
underlying.Record((long)value.TotalMilliseconds, ((Tags)tags).TagList);
107+
}
108+
78109
#pragma warning disable CA1001 // We are disposing the lock on destruction since this can't be disposable
79110
private sealed class CustomMetricGauge<T> : ICustomMetricGauge<T>
80111
#pragma warning restore CA1001

src/Temporalio/Bridge/Api/WorkflowActivation/WorkflowActivation.cs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ static WorkflowActivationReflection() {
193193
/// * Signal and update handlers should be invoked before workflow routines are iterated. That is to
194194
/// say before the users' main workflow function and anything spawned by it is allowed to continue.
195195
/// * Queries always go last (and, in fact, always come in their own activation)
196+
/// * Evictions also always come in their own activation
196197
///
197198
/// The downside of this reordering is that a signal or update handler may not observe that some
198199
/// other event had already happened (ex: an activity completed) when it is first invoked, though it
@@ -204,11 +205,9 @@ static WorkflowActivationReflection() {
204205
///
205206
/// ## Evictions
206207
///
207-
/// Activations that contain only a `remove_from_cache` job should not cause the workflow code
208-
/// to be invoked and may be responded to with an empty command list. Eviction jobs may also
209-
/// appear with other jobs, but will always appear last in the job list. In this case it is
210-
/// expected that the workflow code will be invoked, and the response produced as normal, but
211-
/// the caller should evict the run after doing so.
208+
/// Evictions appear as an activations that contains only a `remove_from_cache` job. Such activations
209+
/// should not cause the workflow code to be invoked and may be responded to with an empty command
210+
/// list.
212211
/// </summary>
213212
internal sealed partial class WorkflowActivation : pb::IMessage<WorkflowActivation>
214213
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
@@ -853,7 +852,8 @@ public WorkflowActivationJob Clone() {
853852
/// <summary>Field number for the "query_workflow" field.</summary>
854853
public const int QueryWorkflowFieldNumber = 5;
855854
/// <summary>
856-
/// A request to query the workflow was received.
855+
/// A request to query the workflow was received. It is guaranteed that queries (one or more)
856+
/// always come in their own activation after other mutating jobs.
857857
/// </summary>
858858
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
859859
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
@@ -1005,11 +1005,9 @@ public WorkflowActivationJob Clone() {
10051005
/// <summary>Field number for the "remove_from_cache" field.</summary>
10061006
public const int RemoveFromCacheFieldNumber = 50;
10071007
/// <summary>
1008-
/// Remove the workflow identified by the [WorkflowActivation] containing this job from the cache
1009-
/// after performing the activation.
1010-
///
1011-
/// If other job variant are present in the list, this variant will be the last job in the
1012-
/// job list. The string value is a reason for eviction.
1008+
/// Remove the workflow identified by the [WorkflowActivation] containing this job from the
1009+
/// cache after performing the activation. It is guaranteed that this will be the only job
1010+
/// in the activation if present.
10131011
/// </summary>
10141012
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
10151013
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

src/Temporalio/Bridge/Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Temporalio/Bridge/CustomMetricMeter.cs

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,29 @@ namespace Temporalio.Bridge
1010
internal class CustomMetricMeter
1111
{
1212
private readonly Temporalio.Runtime.ICustomMetricMeter meter;
13+
private readonly Temporalio.Runtime.CustomMetricMeterOptions options;
1314
private readonly List<GCHandle> handles = new();
1415

1516
/// <summary>
1617
/// Initializes a new instance of the <see cref="CustomMetricMeter" /> class.
1718
/// </summary>
1819
/// <param name="meter">Meter implementation.</param>
19-
public unsafe CustomMetricMeter(Temporalio.Runtime.ICustomMetricMeter meter)
20+
/// <param name="options">Options.</param>
21+
public unsafe CustomMetricMeter(
22+
Temporalio.Runtime.ICustomMetricMeter meter,
23+
Temporalio.Runtime.CustomMetricMeterOptions options)
2024
{
2125
this.meter = meter;
26+
this.options = options;
2227

2328
// Create metric meter struct
2429
var interopMeter = new Interop.CustomMetricMeter()
2530
{
26-
metric_integer_new = FunctionPointer<Interop.CustomMetricMeterMetricIntegerNewCallback>(CreateMetric),
27-
metric_integer_free = FunctionPointer<Interop.CustomMetricMeterMetricIntegerFreeCallback>(FreeMetric),
28-
metric_integer_update = FunctionPointer<Interop.CustomMetricMeterMetricIntegerUpdateCallback>(UpdateMetric),
31+
metric_new = FunctionPointer<Interop.CustomMetricMeterMetricNewCallback>(CreateMetric),
32+
metric_free = FunctionPointer<Interop.CustomMetricMeterMetricFreeCallback>(FreeMetric),
33+
metric_record_integer = FunctionPointer<Interop.CustomMetricMeterMetricRecordIntegerCallback>(RecordMetricInteger),
34+
metric_record_float = FunctionPointer<Interop.CustomMetricMeterMetricRecordFloatCallback>(RecordMetricFloat),
35+
metric_record_duration = FunctionPointer<Interop.CustomMetricMeterMetricRecordDurationCallback>(RecordMetricDuration),
2936
attributes_new = FunctionPointer<Interop.CustomMetricMeterAttributesNewCallback>(CreateAttributes),
3037
attributes_free = FunctionPointer<Interop.CustomMetricMeterAttributesFreeCallback>(FreeAttributes),
3138
meter_free = FunctionPointer<Interop.CustomMetricMeterMeterFreeCallback>(Free),
@@ -58,38 +65,69 @@ private static unsafe string GetString(byte* bytes, UIntPtr size) =>
5865
Interop.ByteArrayRef name,
5966
Interop.ByteArrayRef description,
6067
Interop.ByteArrayRef unit,
61-
Interop.MetricIntegerKind kind)
68+
Interop.MetricKind kind)
6269
{
63-
Temporalio.Runtime.ICustomMetric<long> metric;
70+
GCHandle metric;
6471
var nameStr = GetString(name);
6572
var unitStr = GetStringOrNull(unit);
6673
var descStr = GetStringOrNull(description);
6774
switch (kind)
6875
{
69-
case Interop.MetricIntegerKind.Counter:
70-
metric = meter.CreateCounter<long>(nameStr, unitStr, descStr);
76+
case Interop.MetricKind.CounterInteger:
77+
metric = GCHandle.Alloc(meter.CreateCounter<long>(nameStr, unitStr, descStr));
7178
break;
72-
case Interop.MetricIntegerKind.Histogram:
73-
metric = meter.CreateHistogram<long>(nameStr, unitStr, descStr);
79+
case Interop.MetricKind.HistogramInteger:
80+
metric = GCHandle.Alloc(meter.CreateHistogram<long>(nameStr, unitStr, descStr));
7481
break;
75-
case Interop.MetricIntegerKind.Gauge:
76-
metric = meter.CreateGauge<long>(nameStr, unitStr, descStr);
82+
case Interop.MetricKind.HistogramFloat:
83+
metric = GCHandle.Alloc(meter.CreateHistogram<double>(nameStr, unitStr, descStr));
84+
break;
85+
case Interop.MetricKind.HistogramDuration:
86+
switch (options.HistogramDurationFormat)
87+
{
88+
case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.IntegerMilliseconds:
89+
// Change unit from "duration" to "ms" since we're converting to ms
90+
if (unitStr == "duration")
91+
{
92+
unitStr = "ms";
93+
}
94+
metric = GCHandle.Alloc(meter.CreateHistogram<long>(nameStr, unitStr, descStr));
95+
break;
96+
case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.FloatSeconds:
97+
// Change unit from "duration" to "s" since we're converting to s
98+
if (unitStr == "duration")
99+
{
100+
unitStr = "s";
101+
}
102+
metric = GCHandle.Alloc(meter.CreateHistogram<double>(nameStr, unitStr, descStr));
103+
break;
104+
case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.TimeSpan:
105+
metric = GCHandle.Alloc(meter.CreateHistogram<TimeSpan>(nameStr, unitStr, descStr));
106+
break;
107+
default:
108+
throw new InvalidOperationException($"Unknown format: {options.HistogramDurationFormat}");
109+
}
110+
break;
111+
case Interop.MetricKind.GaugeInteger:
112+
metric = GCHandle.Alloc(meter.CreateGauge<long>(nameStr, unitStr, descStr));
113+
break;
114+
case Interop.MetricKind.GaugeFloat:
115+
metric = GCHandle.Alloc(meter.CreateGauge<double>(nameStr, unitStr, descStr));
77116
break;
78117
default:
79118
throw new InvalidOperationException($"Unknown kind: {kind}");
80119
}
81120
// Return pointer
82-
return GCHandle.ToIntPtr(GCHandle.Alloc(metric)).ToPointer();
121+
return GCHandle.ToIntPtr(metric).ToPointer();
83122
}
84123

85124
private unsafe void FreeMetric(void* metric) => GCHandle.FromIntPtr(new(metric)).Free();
86125

87-
private unsafe void UpdateMetric(void* metric, ulong value, void* attributes)
126+
private unsafe void RecordMetricInteger(void* metric, ulong value, void* attributes)
88127
{
89128
var metricObject = (Temporalio.Runtime.ICustomMetric<long>)GCHandle.FromIntPtr(new(metric)).Target!;
90129
var tags = GCHandle.FromIntPtr(new(attributes)).Target!;
91-
// We trust that value will never be over Int64.MaxValue
92-
var metricValue = unchecked((long)value);
130+
var metricValue = value > long.MaxValue ? long.MaxValue : unchecked((long)value);
93131
switch (metricObject)
94132
{
95133
case Temporalio.Runtime.ICustomMetricCounter<long> counter:
@@ -104,6 +142,51 @@ private unsafe void UpdateMetric(void* metric, ulong value, void* attributes)
104142
}
105143
}
106144

145+
private unsafe void RecordMetricFloat(void* metric, double value, void* attributes)
146+
{
147+
var metricObject = (Temporalio.Runtime.ICustomMetric<double>)GCHandle.FromIntPtr(new(metric)).Target!;
148+
var tags = GCHandle.FromIntPtr(new(attributes)).Target!;
149+
switch (metricObject)
150+
{
151+
case Temporalio.Runtime.ICustomMetricHistogram<double> histogram:
152+
histogram.Record(value, tags);
153+
break;
154+
case Temporalio.Runtime.ICustomMetricGauge<double> gauge:
155+
gauge.Set(value, tags);
156+
break;
157+
}
158+
}
159+
160+
private unsafe void RecordMetricDuration(void* metric, ulong valueMs, void* attributes)
161+
{
162+
var metricObject = GCHandle.FromIntPtr(new(metric)).Target!;
163+
var tags = GCHandle.FromIntPtr(new(attributes)).Target!;
164+
var metricValue = valueMs > long.MaxValue ? long.MaxValue : unchecked((long)valueMs);
165+
// We don't want to throw out of here, so we just fall through if anything doesn't match
166+
// expected types (which should never happen since we controlled creation)
167+
switch (options.HistogramDurationFormat)
168+
{
169+
case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.IntegerMilliseconds:
170+
if (metricObject is Temporalio.Runtime.ICustomMetricHistogram<long> histLong)
171+
{
172+
histLong.Record(metricValue, tags);
173+
}
174+
break;
175+
case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.FloatSeconds:
176+
if (metricObject is Temporalio.Runtime.ICustomMetricHistogram<double> histDouble)
177+
{
178+
histDouble.Record(metricValue / 1000.0, tags);
179+
}
180+
break;
181+
case Temporalio.Runtime.CustomMetricMeterOptions.DurationFormat.TimeSpan:
182+
if (metricObject is Temporalio.Runtime.ICustomMetricHistogram<TimeSpan> histTimeSpan)
183+
{
184+
histTimeSpan.Record(TimeSpan.FromMilliseconds(metricValue), tags);
185+
}
186+
break;
187+
}
188+
}
189+
107190
private unsafe void* CreateAttributes(
108191
void* appendFrom, Interop.CustomMetricAttribute* attributes, UIntPtr attributesSize)
109192
{

0 commit comments

Comments
 (0)