Skip to content

Commit 50c2033

Browse files
authored
Support float and duration metrics (#508)
Fixes #493
1 parent b45447e commit 50c2033

File tree

10 files changed

+650
-238
lines changed

10 files changed

+650
-238
lines changed

temporalio/bridge/metric.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,46 @@ def record(self, value: int, attrs: MetricAttributes) -> None:
7575
self._ref.record(value, attrs._ref)
7676

7777

78+
class MetricHistogramFloat:
79+
"""Metric histogram using SDK Core."""
80+
81+
def __init__(
82+
self,
83+
meter: MetricMeter,
84+
name: str,
85+
description: Optional[str],
86+
unit: Optional[str],
87+
) -> None:
88+
"""Initialize histogram."""
89+
self._ref = meter._ref.new_histogram_float(name, description, unit)
90+
91+
def record(self, value: float, attrs: MetricAttributes) -> None:
92+
"""Record value on histogram."""
93+
if value < 0:
94+
raise ValueError("Metric value must be non-negative value")
95+
self._ref.record(value, attrs._ref)
96+
97+
98+
class MetricHistogramDuration:
99+
"""Metric histogram using SDK Core."""
100+
101+
def __init__(
102+
self,
103+
meter: MetricMeter,
104+
name: str,
105+
description: Optional[str],
106+
unit: Optional[str],
107+
) -> None:
108+
"""Initialize histogram."""
109+
self._ref = meter._ref.new_histogram_duration(name, description, unit)
110+
111+
def record(self, value_ms: int, attrs: MetricAttributes) -> None:
112+
"""Record value on histogram."""
113+
if value_ms < 0:
114+
raise ValueError("Metric value must be non-negative value")
115+
self._ref.record(value_ms, attrs._ref)
116+
117+
78118
class MetricGauge:
79119
"""Metric gauge using SDK Core."""
80120

@@ -95,6 +135,26 @@ def set(self, value: int, attrs: MetricAttributes) -> None:
95135
self._ref.set(value, attrs._ref)
96136

97137

138+
class MetricGaugeFloat:
139+
"""Metric gauge using SDK Core."""
140+
141+
def __init__(
142+
self,
143+
meter: MetricMeter,
144+
name: str,
145+
description: Optional[str],
146+
unit: Optional[str],
147+
) -> None:
148+
"""Initialize gauge."""
149+
self._ref = meter._ref.new_gauge_float(name, description, unit)
150+
151+
def set(self, value: float, attrs: MetricAttributes) -> None:
152+
"""Set value on gauge."""
153+
if value < 0:
154+
raise ValueError("Metric value must be non-negative value")
155+
self._ref.set(value, attrs._ref)
156+
157+
98158
class MetricAttributes:
99159
"""Metric attributes using SDK Core."""
100160

temporalio/bridge/runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ def __init__(self, *, telemetry: TelemetryConfig) -> None:
2727
"""Create SDK Core runtime."""
2828
self._ref = temporalio.bridge.temporal_sdk_bridge.init_runtime(telemetry)
2929

30-
def retrieve_buffered_metrics(self) -> Sequence[Any]:
30+
def retrieve_buffered_metrics(self, durations_as_seconds: bool) -> Sequence[Any]:
3131
"""Get buffered metrics."""
32-
return self._ref.retrieve_buffered_metrics()
32+
return self._ref.retrieve_buffered_metrics(durations_as_seconds)
3333

3434
def write_test_info_log(self, message: str, extra_data: str) -> None:
3535
"""Write a test core log at INFO level."""

temporalio/bridge/src/client.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::str::FromStr;
55
use std::time::Duration;
66
use temporal_client::{
77
ClientKeepAliveConfig as CoreClientKeepAliveConfig, ClientOptions, ClientOptionsBuilder,
8-
ConfiguredClient, HealthService, OperatorService, RetryClient, RetryConfig,
9-
TemporalServiceClientWithMetrics, TestService, TlsConfig, WorkflowService, HttpConnectProxyOptions,
8+
ConfiguredClient, HealthService, HttpConnectProxyOptions, OperatorService, RetryClient,
9+
RetryConfig, TemporalServiceClientWithMetrics, TestService, TlsConfig, WorkflowService,
1010
};
1111
use tonic::metadata::MetadataKey;
1212
use url::Url;
@@ -467,4 +467,4 @@ impl From<ClientHttpConnectProxyConfig> for HttpConnectProxyOptions {
467467
basic_auth: conf.basic_auth,
468468
}
469469
}
470-
}
470+
}

temporalio/bridge/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ fn temporal_sdk_bridge(py: Python, m: &PyModule) -> PyResult<()> {
1919
m.add_class::<metric::MetricAttributesRef>()?;
2020
m.add_class::<metric::MetricCounterRef>()?;
2121
m.add_class::<metric::MetricHistogramRef>()?;
22+
m.add_class::<metric::MetricHistogramFloatRef>()?;
23+
m.add_class::<metric::MetricHistogramDurationRef>()?;
2224
m.add_class::<metric::MetricGaugeRef>()?;
25+
m.add_class::<metric::MetricGaugeFloatRef>()?;
2326
m.add_class::<metric::BufferedMetricUpdate>()?;
2427
m.add_class::<metric::BufferedMetric>()?;
2528
m.add_function(wrap_pyfunction!(new_metric_meter, m)?)?;

temporalio/bridge/src/metric.rs

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::any::Any;
2+
use std::time::Duration;
23
use std::{collections::HashMap, sync::Arc};
34

45
use pyo3::prelude::*;
@@ -32,11 +33,26 @@ pub struct MetricHistogramRef {
3233
histogram: Arc<dyn metrics::Histogram>,
3334
}
3435

36+
#[pyclass]
37+
pub struct MetricHistogramFloatRef {
38+
histogram: Arc<dyn metrics::HistogramF64>,
39+
}
40+
41+
#[pyclass]
42+
pub struct MetricHistogramDurationRef {
43+
histogram: Arc<dyn metrics::HistogramDuration>,
44+
}
45+
3546
#[pyclass]
3647
pub struct MetricGaugeRef {
3748
gauge: Arc<dyn metrics::Gauge>,
3849
}
3950

51+
#[pyclass]
52+
pub struct MetricGaugeFloatRef {
53+
gauge: Arc<dyn metrics::GaugeF64>,
54+
}
55+
4056
pub fn new_metric_meter(runtime_ref: &runtime::RuntimeRef) -> Option<MetricMeterRef> {
4157
runtime_ref
4258
.runtime
@@ -84,6 +100,36 @@ impl MetricMeterRef {
84100
}
85101
}
86102

103+
fn new_histogram_float(
104+
&self,
105+
name: String,
106+
description: Option<String>,
107+
unit: Option<String>,
108+
) -> MetricHistogramFloatRef {
109+
MetricHistogramFloatRef {
110+
histogram: self.meter.inner.histogram_f64(build_metric_parameters(
111+
name,
112+
description,
113+
unit,
114+
)),
115+
}
116+
}
117+
118+
fn new_histogram_duration(
119+
&self,
120+
name: String,
121+
description: Option<String>,
122+
unit: Option<String>,
123+
) -> MetricHistogramDurationRef {
124+
MetricHistogramDurationRef {
125+
histogram: self.meter.inner.histogram_duration(build_metric_parameters(
126+
name,
127+
description,
128+
unit,
129+
)),
130+
}
131+
}
132+
87133
fn new_gauge(
88134
&self,
89135
name: String,
@@ -97,6 +143,20 @@ impl MetricMeterRef {
97143
.gauge(build_metric_parameters(name, description, unit)),
98144
}
99145
}
146+
147+
fn new_gauge_float(
148+
&self,
149+
name: String,
150+
description: Option<String>,
151+
unit: Option<String>,
152+
) -> MetricGaugeFloatRef {
153+
MetricGaugeFloatRef {
154+
gauge: self
155+
.meter
156+
.inner
157+
.gauge_f64(build_metric_parameters(name, description, unit)),
158+
}
159+
}
100160
}
101161

102162
#[pymethods]
@@ -113,13 +173,35 @@ impl MetricHistogramRef {
113173
}
114174
}
115175

176+
#[pymethods]
177+
impl MetricHistogramFloatRef {
178+
fn record(&self, value: f64, attrs_ref: &MetricAttributesRef) {
179+
self.histogram.record(value, &attrs_ref.attrs);
180+
}
181+
}
182+
183+
#[pymethods]
184+
impl MetricHistogramDurationRef {
185+
fn record(&self, value_ms: u64, attrs_ref: &MetricAttributesRef) {
186+
self.histogram
187+
.record(Duration::from_millis(value_ms), &attrs_ref.attrs);
188+
}
189+
}
190+
116191
#[pymethods]
117192
impl MetricGaugeRef {
118193
fn set(&self, value: u64, attrs_ref: &MetricAttributesRef) {
119194
self.gauge.record(value, &attrs_ref.attrs);
120195
}
121196
}
122197

198+
#[pymethods]
199+
impl MetricGaugeFloatRef {
200+
fn set(&self, value: f64, attrs_ref: &MetricAttributesRef) {
201+
self.gauge.record(value, &attrs_ref.attrs);
202+
}
203+
}
204+
123205
fn build_metric_parameters(
124206
name: String,
125207
description: Option<String>,
@@ -192,16 +274,18 @@ pub struct BufferedMetricUpdate {
192274
}
193275

194276
#[derive(Clone)]
195-
pub struct BufferedMetricUpdateValue(metrics::MetricUpdateVal);
277+
pub enum BufferedMetricUpdateValue {
278+
U64(u64),
279+
U128(u128),
280+
F64(f64),
281+
}
196282

197283
impl IntoPy<PyObject> for BufferedMetricUpdateValue {
198284
fn into_py(self, py: Python) -> PyObject {
199-
match self.0 {
200-
metrics::MetricUpdateVal::Delta(v) => v.into_py(py),
201-
metrics::MetricUpdateVal::DeltaF64(v) => v.into_py(py),
202-
metrics::MetricUpdateVal::Value(v) => v.into_py(py),
203-
metrics::MetricUpdateVal::ValueF64(v) => v.into_py(py),
204-
metrics::MetricUpdateVal::Duration(v) => v.as_millis().into_py(py),
285+
match self {
286+
BufferedMetricUpdateValue::U64(v) => v.into_py(py),
287+
BufferedMetricUpdateValue::U128(v) => v.into_py(py),
288+
BufferedMetricUpdateValue::F64(v) => v.into_py(py),
205289
}
206290
}
207291
}
@@ -236,16 +320,18 @@ impl CustomMetricAttributes for BufferedMetricAttributes {
236320
pub fn convert_metric_events<'p>(
237321
py: Python<'p>,
238322
events: Vec<MetricEvent<BufferedMetricRef>>,
323+
durations_as_seconds: bool,
239324
) -> Vec<BufferedMetricUpdate> {
240325
events
241326
.into_iter()
242-
.filter_map(|e| convert_metric_event(py, e))
327+
.filter_map(|e| convert_metric_event(py, e, durations_as_seconds))
243328
.collect()
244329
}
245330

246331
fn convert_metric_event<'p>(
247332
py: Python<'p>,
248333
event: MetricEvent<BufferedMetricRef>,
334+
durations_as_seconds: bool,
249335
) -> Option<BufferedMetricUpdate> {
250336
match event {
251337
// Create the metric and put it on the lazy ref
@@ -262,9 +348,19 @@ fn convert_metric_event<'p>(
262348
description: Some(params.description)
263349
.filter(|s| !s.is_empty())
264350
.map(|s| s.to_string()),
265-
unit: Some(params.unit)
266-
.filter(|s| !s.is_empty())
267-
.map(|s| s.to_string()),
351+
unit: if matches!(kind, metrics::MetricKind::HistogramDuration)
352+
&& params.unit == "duration"
353+
{
354+
if durations_as_seconds {
355+
Some("s".to_owned())
356+
} else {
357+
Some("ms".to_owned())
358+
}
359+
} else if params.unit.is_empty() {
360+
None
361+
} else {
362+
Some(params.unit.to_string())
363+
},
268364
kind: match kind {
269365
metrics::MetricKind::Counter => 0,
270366
metrics::MetricKind::Gauge | metrics::MetricKind::GaugeF64 => 1,
@@ -324,7 +420,18 @@ fn convert_metric_event<'p>(
324420
update,
325421
} => Some(BufferedMetricUpdate {
326422
metric: instrument.get().clone().0.clone(),
327-
value: BufferedMetricUpdateValue(update),
423+
value: match update {
424+
metrics::MetricUpdateVal::Duration(v) if durations_as_seconds => {
425+
BufferedMetricUpdateValue::F64(v.as_secs_f64())
426+
}
427+
metrics::MetricUpdateVal::Duration(v) => {
428+
BufferedMetricUpdateValue::U128(v.as_millis())
429+
}
430+
metrics::MetricUpdateVal::Delta(v) => BufferedMetricUpdateValue::U64(v),
431+
metrics::MetricUpdateVal::DeltaF64(v) => BufferedMetricUpdateValue::F64(v),
432+
metrics::MetricUpdateVal::Value(v) => BufferedMetricUpdateValue::U64(v),
433+
metrics::MetricUpdateVal::ValueF64(v) => BufferedMetricUpdateValue::F64(v),
434+
},
328435
attributes: attributes
329436
.get()
330437
.clone()

temporalio/bridge/src/runtime.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,19 @@ impl Drop for Runtime {
205205

206206
#[pymethods]
207207
impl RuntimeRef {
208-
fn retrieve_buffered_metrics<'p>(&self, py: Python<'p>) -> Vec<BufferedMetricUpdate> {
208+
fn retrieve_buffered_metrics<'p>(
209+
&self,
210+
py: Python<'p>,
211+
durations_as_seconds: bool,
212+
) -> Vec<BufferedMetricUpdate> {
209213
convert_metric_events(
210214
py,
211215
self.runtime
212216
.metrics_call_buffer
213217
.as_ref()
214218
.expect("Attempting to retrieve buffered metrics without buffer")
215219
.retrieve(),
220+
durations_as_seconds,
216221
)
217222
}
218223

0 commit comments

Comments
 (0)