Skip to content

Commit d03f356

Browse files
authored
Buffered metrics (#391)
Fixes #369
1 parent d5edb71 commit d03f356

File tree

10 files changed

+495
-47
lines changed

10 files changed

+495
-47
lines changed

temporalio/bridge/Cargo.lock

+5-23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

temporalio/bridge/metric.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(
2727
) -> None:
2828
"""Initialize metric meter."""
2929
self._ref = ref
30-
self._default_attributes = MetricAttributes(ref.default_attributes)
30+
self._default_attributes = MetricAttributes(self, ref.default_attributes)
3131

3232
@property
3333
def default_attributes(self) -> MetricAttributes:
@@ -99,13 +99,19 @@ class MetricAttributes:
9999
"""Metric attributes using SDK Core."""
100100

101101
def __init__(
102-
self, ref: temporalio.bridge.temporal_sdk_bridge.MetricAttributesRef
102+
self,
103+
meter: MetricMeter,
104+
ref: temporalio.bridge.temporal_sdk_bridge.MetricAttributesRef,
103105
) -> None:
104106
"""Initialize attributes."""
107+
self._meter = meter
105108
self._ref = ref
106109

107110
def with_additional_attributes(
108111
self, new_attrs: Mapping[str, Union[str, int, float, bool]]
109112
) -> MetricAttributes:
110113
"""Create new attributes with new attributes appended."""
111-
return MetricAttributes(self._ref.with_additional_attributes(new_attrs))
114+
return MetricAttributes(
115+
self._meter,
116+
self._ref.with_additional_attributes(self._meter._ref, new_attrs),
117+
)

temporalio/bridge/runtime.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from __future__ import annotations
77

88
from dataclasses import dataclass
9-
from typing import Mapping, Optional, Type
9+
from typing import Any, Mapping, Optional, Sequence, Type
1010

1111
import temporalio.bridge.temporal_sdk_bridge
1212

@@ -25,6 +25,10 @@ def __init__(self, *, telemetry: TelemetryConfig) -> None:
2525
"""Create SDK Core runtime."""
2626
self._ref = temporalio.bridge.temporal_sdk_bridge.init_runtime(telemetry)
2727

28+
def retrieve_buffered_metrics(self) -> Sequence[Any]:
29+
"""Get buffered metrics."""
30+
return self._ref.retrieve_buffered_metrics()
31+
2832

2933
@dataclass(frozen=True)
3034
class LoggingConfig:
@@ -40,6 +44,7 @@ class MetricsConfig:
4044

4145
opentelemetry: Optional[OpenTelemetryConfig]
4246
prometheus: Optional[PrometheusConfig]
47+
buffered_with_size: int
4348
attach_service_name: bool
4449
global_tags: Optional[Mapping[str, str]]
4550
metric_prefix: Optional[str]

temporalio/bridge/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ fn temporal_sdk_bridge(py: Python, m: &PyModule) -> PyResult<()> {
2020
m.add_class::<metric::MetricCounterRef>()?;
2121
m.add_class::<metric::MetricHistogramRef>()?;
2222
m.add_class::<metric::MetricGaugeRef>()?;
23+
m.add_class::<metric::BufferedMetricUpdate>()?;
24+
m.add_class::<metric::BufferedMetric>()?;
2325
m.add_function(wrap_pyfunction!(new_metric_meter, m)?)?;
2426

2527
// Runtime stuff

temporalio/bridge/src/metric.rs

+157-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use std::any::Any;
12
use std::{collections::HashMap, sync::Arc};
23

3-
use pyo3::exceptions::PyTypeError;
44
use pyo3::prelude::*;
5-
use temporal_sdk_core_api::telemetry::metrics;
5+
use pyo3::{exceptions::PyTypeError, types::PyDict};
6+
use temporal_sdk_core_api::telemetry::metrics::{
7+
self, BufferInstrumentRef, CustomMetricAttributes, MetricEvent, NewAttributes,
8+
};
69

710
use crate::runtime;
811

@@ -139,14 +142,17 @@ impl MetricAttributesRef {
139142
fn with_additional_attributes<'p>(
140143
&self,
141144
py: Python<'p>,
145+
meter: &MetricMeterRef,
142146
new_attrs: HashMap<String, PyObject>,
143147
) -> PyResult<Self> {
144-
let mut attrs = self.attrs.clone();
145-
attrs.add_new_attrs(
146-
new_attrs
147-
.into_iter()
148-
.map(|(k, obj)| metric_key_value_from_py(py, k, obj))
149-
.collect::<PyResult<Vec<metrics::MetricKeyValue>>>()?,
148+
let attrs = meter.meter.inner.extend_attributes(
149+
self.attrs.clone(),
150+
NewAttributes {
151+
attributes: new_attrs
152+
.into_iter()
153+
.map(|(k, obj)| metric_key_value_from_py(py, k, obj))
154+
.collect::<PyResult<Vec<metrics::MetricKeyValue>>>()?,
155+
},
150156
);
151157
Ok(MetricAttributesRef { attrs })
152158
}
@@ -173,3 +179,146 @@ fn metric_key_value_from_py<'p>(
173179
};
174180
Ok(metrics::MetricKeyValue::new(k, val))
175181
}
182+
183+
// WARNING: This must match temporalio.runtime.BufferedMetricUpdate protocol
184+
#[pyclass]
185+
pub struct BufferedMetricUpdate {
186+
#[pyo3(get)]
187+
pub metric: Py<BufferedMetric>,
188+
#[pyo3(get)]
189+
pub value: u64,
190+
#[pyo3(get)]
191+
pub attributes: Py<PyDict>,
192+
}
193+
194+
// WARNING: This must match temporalio.runtime.BufferedMetric protocol
195+
#[pyclass]
196+
pub struct BufferedMetric {
197+
#[pyo3(get)]
198+
pub name: String,
199+
#[pyo3(get)]
200+
pub description: Option<String>,
201+
#[pyo3(get)]
202+
pub unit: Option<String>,
203+
#[pyo3(get)]
204+
pub kind: u8, // 0 - counter, 1 - gauge, 2 - histogram
205+
}
206+
207+
#[derive(Debug)]
208+
struct BufferedMetricAttributes(Py<PyDict>);
209+
210+
#[derive(Clone, Debug)]
211+
pub struct BufferedMetricRef(Py<BufferedMetric>);
212+
213+
impl BufferInstrumentRef for BufferedMetricRef {}
214+
215+
impl CustomMetricAttributes for BufferedMetricAttributes {
216+
fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
217+
self as Arc<dyn Any + Send + Sync>
218+
}
219+
}
220+
221+
pub fn convert_metric_events<'p>(
222+
py: Python<'p>,
223+
events: Vec<MetricEvent<BufferedMetricRef>>,
224+
) -> Vec<BufferedMetricUpdate> {
225+
events
226+
.into_iter()
227+
.filter_map(|e| convert_metric_event(py, e))
228+
.collect()
229+
}
230+
231+
fn convert_metric_event<'p>(
232+
py: Python<'p>,
233+
event: MetricEvent<BufferedMetricRef>,
234+
) -> Option<BufferedMetricUpdate> {
235+
match event {
236+
// Create the metric and put it on the lazy ref
237+
MetricEvent::Create {
238+
params,
239+
populate_into,
240+
kind,
241+
} => {
242+
let buffered_ref = BufferedMetricRef(
243+
Py::new(
244+
py,
245+
BufferedMetric {
246+
name: params.name.to_string(),
247+
description: Some(params.description)
248+
.filter(|s| !s.is_empty())
249+
.map(|s| s.to_string()),
250+
unit: Some(params.unit)
251+
.filter(|s| !s.is_empty())
252+
.map(|s| s.to_string()),
253+
kind: match kind {
254+
metrics::MetricKind::Counter => 0,
255+
metrics::MetricKind::Gauge => 1,
256+
metrics::MetricKind::Histogram => 2,
257+
},
258+
},
259+
)
260+
.expect("Unable to create buffered metric"),
261+
);
262+
populate_into.set(Arc::new(buffered_ref)).unwrap();
263+
None
264+
}
265+
// Create the attributes and put it on the lazy ref
266+
MetricEvent::CreateAttributes {
267+
populate_into,
268+
append_from,
269+
attributes,
270+
} => {
271+
// Create the dictionary (as copy from existing if needed)
272+
let new_attrs_ref: Py<PyDict> = match append_from {
273+
Some(existing) => existing
274+
.get()
275+
.clone()
276+
.as_any()
277+
.downcast::<BufferedMetricAttributes>()
278+
.expect("Unable to downcast to expected buffered metric attributes")
279+
.0
280+
.as_ref(py)
281+
.copy()
282+
.expect("Failed to copy metric attribute dictionary")
283+
.into(),
284+
None => PyDict::new(py).into(),
285+
};
286+
// Add attributes
287+
let new_attrs = new_attrs_ref.as_ref(py);
288+
for kv in attributes.into_iter() {
289+
match kv.value {
290+
metrics::MetricValue::String(v) => new_attrs.set_item(kv.key, v),
291+
metrics::MetricValue::Int(v) => new_attrs.set_item(kv.key, v),
292+
metrics::MetricValue::Float(v) => new_attrs.set_item(kv.key, v),
293+
metrics::MetricValue::Bool(v) => new_attrs.set_item(kv.key, v),
294+
}
295+
.expect("Unable to set metric key/value on dictionary");
296+
}
297+
// Put on lazy ref
298+
populate_into
299+
.set(Arc::new(BufferedMetricAttributes(new_attrs_ref)))
300+
.expect("Unable to set buffered metric attributes on reference");
301+
None
302+
}
303+
// Convert to Python metric event
304+
MetricEvent::Update {
305+
instrument,
306+
attributes,
307+
update,
308+
} => Some(BufferedMetricUpdate {
309+
metric: instrument.get().clone().0.clone(),
310+
value: match update {
311+
metrics::MetricUpdateVal::Delta(v) => v,
312+
metrics::MetricUpdateVal::Value(v) => v,
313+
},
314+
attributes: attributes
315+
.get()
316+
.clone()
317+
.as_any()
318+
.downcast::<BufferedMetricAttributes>()
319+
.expect("Unable to downcast to expected buffered metric attributes")
320+
.0
321+
.clone(),
322+
}),
323+
}
324+
}

0 commit comments

Comments
 (0)