Skip to content

Commit 4599b85

Browse files
authored
Metric meter support (#177)
1 parent ecaab5c commit 4599b85

File tree

19 files changed

+964
-79
lines changed

19 files changed

+964
-79
lines changed

temporalio/.rubocop.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ Lint/MissingSuper:
3030
AllowedParentClasses:
3131
- Temporalio::Activity
3232

33+
# Allow tests to nest methods
34+
Lint/NestedMethodDefinition:
35+
Exclude:
36+
- test/**/*
37+
3338
# The default is too small and triggers simply setting lots of values on a proto
3439
Metrics/AbcSize:
3540
Max: 200

temporalio/ext/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use magnus::{prelude::*, value::Lazy, Error, ExceptionClass, RModule, Ruby};
22

33
mod client;
44
mod client_rpc_generated;
5+
mod metric;
56
mod runtime;
67
mod testing;
78
mod util;
@@ -49,6 +50,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
4950
Lazy::force(&ROOT_ERR, ruby);
5051

5152
client::init(ruby)?;
53+
metric::init(ruby)?;
5254
runtime::init(ruby)?;
5355
testing::init(ruby)?;
5456
worker::init(ruby)?;

temporalio/ext/src/metric.rs

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
use std::{sync::Arc, time::Duration};
2+
3+
use magnus::{
4+
class, function, method,
5+
prelude::*,
6+
r_hash::ForEach,
7+
value::{IntoId, Qfalse, Qtrue},
8+
DataTypeFunctions, Error, Float, Integer, RHash, RString, Ruby, Symbol, TryConvert, TypedData,
9+
Value,
10+
};
11+
use temporal_sdk_core_api::telemetry::metrics;
12+
13+
use crate::{error, id, runtime::Runtime, ROOT_MOD};
14+
15+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
16+
let root_mod = ruby.get_inner(&ROOT_MOD);
17+
18+
let class = root_mod.define_class("Metric", class::object())?;
19+
class.define_singleton_method("new", function!(Metric::new, 6))?;
20+
class.define_method("record_value", method!(Metric::record_value, 2))?;
21+
22+
let inner_class = class.define_class("Meter", class::object())?;
23+
inner_class.define_singleton_method("new", function!(MetricMeter::new, 1))?;
24+
inner_class.define_method(
25+
"default_attributes",
26+
method!(MetricMeter::default_attributes, 0),
27+
)?;
28+
29+
let inner_class = class.define_class("Attributes", class::object())?;
30+
inner_class.define_method(
31+
"with_additional",
32+
method!(MetricAttributes::with_additional, 1),
33+
)?;
34+
35+
Ok(())
36+
}
37+
38+
#[derive(DataTypeFunctions, TypedData)]
39+
#[magnus(class = "Temporalio::Internal::Bridge::Metric", free_immediately)]
40+
pub struct Metric {
41+
instrument: Arc<dyn Instrument>,
42+
}
43+
44+
impl Metric {
45+
pub fn new(
46+
meter: &MetricMeter,
47+
metric_type: Symbol,
48+
name: String,
49+
description: Option<String>,
50+
unit: Option<String>,
51+
value_type: Symbol,
52+
) -> Result<Metric, Error> {
53+
let ruby = Ruby::get().expect("Ruby not available");
54+
let counter = id!("counter");
55+
let histogram = id!("histogram");
56+
let gauge = id!("gauge");
57+
let integer = id!("integer");
58+
let float = id!("float");
59+
let duration = id!("duration");
60+
61+
let params = build_metric_parameters(name, description, unit);
62+
let metric_type = metric_type.into_id_with(&ruby);
63+
let value_type = value_type.into_id_with(&ruby);
64+
let instrument: Arc<dyn Instrument> = if metric_type == counter {
65+
if value_type != integer {
66+
return Err(error!(
67+
"Unrecognized value type for counter, must be :integer"
68+
));
69+
}
70+
Arc::new(meter.core.inner.counter(params))
71+
} else if metric_type == histogram {
72+
if value_type == integer {
73+
Arc::new(meter.core.inner.histogram(params))
74+
} else if value_type == float {
75+
Arc::new(meter.core.inner.histogram_f64(params))
76+
} else if value_type == duration {
77+
Arc::new(meter.core.inner.histogram_duration(params))
78+
} else {
79+
return Err(error!(
80+
"Unrecognized value type for histogram, must be :integer, :float, or :duration"
81+
));
82+
}
83+
} else if metric_type == gauge {
84+
if value_type == integer {
85+
Arc::new(meter.core.inner.gauge(params))
86+
} else if value_type == float {
87+
Arc::new(meter.core.inner.gauge_f64(params))
88+
} else {
89+
return Err(error!(
90+
"Unrecognized value type for gauge, must be :integer or :float"
91+
));
92+
}
93+
} else {
94+
return Err(error!(
95+
"Unrecognized instrument type, must be :counter, :histogram, or :gauge"
96+
));
97+
};
98+
Ok(Metric { instrument })
99+
}
100+
101+
pub fn record_value(&self, value: Value, attrs: &MetricAttributes) -> Result<(), Error> {
102+
self.instrument.record_value(value, &attrs.core)
103+
}
104+
}
105+
106+
#[derive(DataTypeFunctions, TypedData)]
107+
#[magnus(
108+
class = "Temporalio::Internal::Bridge::Metric::Meter",
109+
free_immediately
110+
)]
111+
pub struct MetricMeter {
112+
core: metrics::TemporalMeter,
113+
default_attributes: metrics::MetricAttributes,
114+
}
115+
116+
impl MetricMeter {
117+
pub fn new(runtime: &Runtime) -> Result<Option<MetricMeter>, Error> {
118+
Ok(runtime
119+
.handle
120+
.core
121+
.telemetry()
122+
.get_metric_meter()
123+
.map(|core| {
124+
let default_attributes = core.inner.new_attributes(core.default_attribs.clone());
125+
MetricMeter {
126+
core,
127+
default_attributes,
128+
}
129+
}))
130+
}
131+
132+
pub fn default_attributes(&self) -> Result<MetricAttributes, Error> {
133+
Ok(MetricAttributes {
134+
core: self.default_attributes.clone(),
135+
core_meter: self.core.clone(),
136+
})
137+
}
138+
}
139+
140+
#[derive(DataTypeFunctions, TypedData)]
141+
#[magnus(
142+
class = "Temporalio::Internal::Bridge::Metric::Attributes",
143+
free_immediately
144+
)]
145+
pub struct MetricAttributes {
146+
core: metrics::MetricAttributes,
147+
core_meter: metrics::TemporalMeter,
148+
}
149+
150+
impl MetricAttributes {
151+
pub fn with_additional(&self, attrs: RHash) -> Result<MetricAttributes, Error> {
152+
let attributes = metric_key_values(attrs)?;
153+
let core = self
154+
.core_meter
155+
.inner
156+
.extend_attributes(self.core.clone(), metrics::NewAttributes { attributes });
157+
Ok(MetricAttributes {
158+
core,
159+
core_meter: self.core_meter.clone(),
160+
})
161+
}
162+
}
163+
164+
trait Instrument: Send + Sync {
165+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error>;
166+
}
167+
168+
impl Instrument for Arc<dyn metrics::Counter> {
169+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error> {
170+
self.add(TryConvert::try_convert(value)?, attrs);
171+
Ok(())
172+
}
173+
}
174+
175+
impl Instrument for Arc<dyn metrics::Histogram> {
176+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error> {
177+
self.record(TryConvert::try_convert(value)?, attrs);
178+
Ok(())
179+
}
180+
}
181+
182+
impl Instrument for Arc<dyn metrics::HistogramF64> {
183+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error> {
184+
self.record(TryConvert::try_convert(value)?, attrs);
185+
Ok(())
186+
}
187+
}
188+
189+
impl Instrument for Arc<dyn metrics::HistogramDuration> {
190+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error> {
191+
let secs = f64::try_convert(value)?;
192+
if secs < 0.0 {
193+
return Err(error!("Duration cannot be negative"));
194+
}
195+
self.record(Duration::from_secs_f64(secs), attrs);
196+
Ok(())
197+
}
198+
}
199+
200+
impl Instrument for Arc<dyn metrics::Gauge> {
201+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error> {
202+
self.record(TryConvert::try_convert(value)?, attrs);
203+
Ok(())
204+
}
205+
}
206+
207+
impl Instrument for Arc<dyn metrics::GaugeF64> {
208+
fn record_value(&self, value: Value, attrs: &metrics::MetricAttributes) -> Result<(), Error> {
209+
self.record(TryConvert::try_convert(value)?, attrs);
210+
Ok(())
211+
}
212+
}
213+
214+
fn build_metric_parameters(
215+
name: String,
216+
description: Option<String>,
217+
unit: Option<String>,
218+
) -> metrics::MetricParameters {
219+
let mut build = metrics::MetricParametersBuilder::default();
220+
build.name(name);
221+
if let Some(description) = description {
222+
build.description(description);
223+
}
224+
if let Some(unit) = unit {
225+
build.unit(unit);
226+
}
227+
// Should be nothing that would fail validation here
228+
build.build().unwrap()
229+
}
230+
231+
fn metric_key_values(hash: RHash) -> Result<Vec<metrics::MetricKeyValue>, Error> {
232+
let mut vals = Vec::with_capacity(hash.len());
233+
hash.foreach(|k: Value, v: Value| {
234+
vals.push(metric_key_value(k, v));
235+
Ok(ForEach::Continue)
236+
})?;
237+
vals.into_iter()
238+
.collect::<Result<Vec<metrics::MetricKeyValue>, Error>>()
239+
}
240+
241+
fn metric_key_value(k: Value, v: Value) -> Result<metrics::MetricKeyValue, Error> {
242+
// Attribute key can be string or symbol
243+
let key = if let Some(k) = RString::from_value(k) {
244+
k.to_string()?
245+
} else if let Some(k) = Symbol::from_value(k) {
246+
k.name()?.to_string()
247+
} else {
248+
return Err(error!(
249+
"Invalid value type for attribute key, must be String or Symbol"
250+
));
251+
};
252+
253+
// Value can be string, bool, int, or float
254+
let val = if let Some(v) = RString::from_value(v) {
255+
metrics::MetricValue::String(v.to_string()?)
256+
} else if Qtrue::from_value(v).is_some() {
257+
metrics::MetricValue::Bool(true)
258+
} else if Qfalse::from_value(v).is_some() {
259+
metrics::MetricValue::Bool(false)
260+
} else if let Some(v) = Integer::from_value(v) {
261+
metrics::MetricValue::Int(v.to_i64()?)
262+
} else if let Some(v) = Float::from_value(v) {
263+
metrics::MetricValue::Float(v.to_f64())
264+
} else {
265+
return Err(error!(
266+
"Invalid value type for attribute value, must be String, Integer, Float, or boolean"
267+
));
268+
};
269+
Ok(metrics::MetricKeyValue::new(key, val))
270+
}

temporalio/lib/temporalio/activity/context.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ def _scoped_logger_info
101101
}.freeze
102102
end
103103

104-
# TODO(cretz): metric meter
104+
# @return [Metric::Meter] Metric meter to create metrics on, with some activity-specific attributes already set.
105+
def metric_meter
106+
raise NotImplementedError
107+
end
105108
end
106109
end
107110
end

0 commit comments

Comments
 (0)