Skip to content

Commit 34180eb

Browse files
authored
feat(service): Add metrics for multipart upload operations (#485)
This adds latency metrics for all multipart operations, all tagged with `usecase`: `multipart.initiate.latency`, `multipart.upload_part.latency`, `multipart.list_parts.latency`, `multipart.abort.latency`, and `multipart.complete.latency`. Note that these are distributions but we can also chart them as counts/rates to count such requests. We also track two specific sizes: `multipart.upload_part.size` and `multipart.complete.part_count`. Additionally, we reuse the `put.size` which we use for normal PUTs to record the size of the assembled object upon `multipart_complete`. The reason we reuse that metric is that right now we only use it to create histograms for the size we're storing, so I think it's actually good to have them together. A new tag `upload_type` lets us distinguish between the kind of upload that recorded a `put.size` (`direct` or `multipart`). Close FS-360 Also includes #491, which introduces `objectstore_metrics::timer!` returning a `TimerGuard` and uses it where appropriate.
1 parent c655b5b commit 34180eb

3 files changed

Lines changed: 305 additions & 42 deletions

File tree

objectstore-metrics/src/lib.rs

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! This crate provides three things:
44
//!
5-
//! 1. [`count!`], [`gauge!`], and [`record!`] macros with rustfmt-friendly
5+
//! 1. [`count!`], [`gauge!`], [`record!`], and [`timer!`] macros with rustfmt-friendly
66
//! expression-based syntax.
77
//! 2. [`MetricsConfig`] and [`init`] for wiring up a DogStatsD exporter.
88
//! 3. [`with_capturing_test_client`] for asserting on emitted metrics in tests.
@@ -11,7 +11,7 @@
1111
//!
1212
//! ```rust
1313
//! use std::time::Duration;
14-
//! use objectstore_metrics::{count, gauge, record};
14+
//! use objectstore_metrics::{count, gauge, record, timer};
1515
//!
1616
//! let stored_size: u64 = 1024;
1717
//! let elapsed = Duration::from_secs(1);
@@ -82,6 +82,91 @@ impl AsF64 for std::time::Duration {
8282
}
8383
}
8484

85+
/// A guard that measures elapsed time and records it as a distribution metric.
86+
///
87+
/// Created by the [`timer!`] macro. Records with `success:true` when
88+
/// [`record()`](TimerGuard::record) is called, or `success:false` when dropped
89+
/// without calling `record()`.
90+
/// Call [`success()`](TimerGuard::success) to override this behavior and record
91+
/// with `success:true` even on drop.
92+
///
93+
/// Tags can be added after creation via [`tag()`](TimerGuard::tag).
94+
#[must_use = "timer! returns a guard that records the metric on guard.record() or on drop, bind it to a variable"]
95+
pub struct TimerGuard {
96+
start: std::time::Instant,
97+
name: &'static str,
98+
module_path: &'static str,
99+
labels: Vec<metrics::Label>,
100+
record_failure_on_drop: bool,
101+
recorded: bool,
102+
}
103+
104+
impl TimerGuard {
105+
#[doc(hidden)]
106+
pub fn new(name: &'static str, module_path: &'static str, labels: Vec<metrics::Label>) -> Self {
107+
Self {
108+
start: std::time::Instant::now(),
109+
name,
110+
module_path,
111+
labels,
112+
record_failure_on_drop: true,
113+
recorded: false,
114+
}
115+
}
116+
117+
/// Returns the time elapsed since the guard was created.
118+
pub fn elapsed(&self) -> std::time::Duration {
119+
self.start.elapsed()
120+
}
121+
122+
/// Adds a tag to the metric.
123+
pub fn tag(mut self, key: &'static str, value: impl Into<metrics::SharedString>) -> Self {
124+
self.labels.push(metrics::Label::new(key, value));
125+
self
126+
}
127+
128+
/// Changes the behavior of this guard to always record the metric
129+
/// with `success:true`, even on drop.
130+
pub fn success(mut self) -> Self {
131+
self.record_failure_on_drop = false;
132+
self
133+
}
134+
135+
/// Consumes the guard, recording the elapsed time with `success:true`.
136+
pub fn record(mut self) {
137+
self.emit("true");
138+
}
139+
140+
fn emit(&mut self, success: &'static str) {
141+
self.recorded = true;
142+
let mut labels = std::mem::take(&mut self.labels);
143+
labels.push(metrics::Label::new("success", success));
144+
let key = metrics::Key::from_parts(self.name, labels);
145+
let metadata = metrics::Metadata::new(
146+
self.module_path,
147+
metrics::Level::INFO,
148+
Some(self.module_path),
149+
);
150+
metrics::with_recorder(|rec| {
151+
rec.register_histogram(&key, &metadata)
152+
.record(AsF64::as_f64(self.start.elapsed()));
153+
});
154+
}
155+
}
156+
157+
impl Drop for TimerGuard {
158+
fn drop(&mut self) {
159+
if !self.recorded {
160+
let success = if self.record_failure_on_drop {
161+
"false"
162+
} else {
163+
"true"
164+
};
165+
self.emit(success);
166+
}
167+
}
168+
}
169+
85170
/// Re-exports used by macro expansion. Not part of the public API.
86171
#[doc(hidden)]
87172
pub mod _macro_support {
@@ -343,3 +428,49 @@ macro_rules! record {
343428
.record($crate::_macro_support::AsF64::as_f64($value));
344429
};
345430
}
431+
432+
/// Starts a timer that records elapsed time in fractional seconds as a
433+
/// distribution metric.
434+
///
435+
/// Returns a [`TimerGuard`] that captures `Instant::now()` at creation.
436+
/// Call [`.record()`](TimerGuard::record) to record the metric with the
437+
/// tag `success:true`, or let it drop to record with `success:false`.
438+
///
439+
/// If you want to override this behavior and record the metric with
440+
/// `success:true` even on drop, call [`.success()`](TimerGuard::success)
441+
/// on the guard.
442+
///
443+
/// Tags can also be added after creation via [`.tag()`](TimerGuard::tag),
444+
/// which is useful when some tag values depend on the outcome of the
445+
/// timed operation.
446+
///
447+
/// # Syntax
448+
///
449+
/// ```rust
450+
/// use objectstore_metrics::timer;
451+
///
452+
/// let guard = timer!("server.requests.duration");
453+
/// let guard = timer!("server.requests.duration", route = "/v1/test");
454+
/// // ... do work ...
455+
/// guard.record(); // records elapsed time with success:true
456+
/// ```
457+
///
458+
/// ```rust
459+
/// use objectstore_metrics::timer;
460+
///
461+
/// let guard = timer!("server.requests.duration", route = "/v1/test");
462+
/// // ... determine backend ...
463+
/// let guard = guard.tag("backend", "gcs");
464+
/// guard.record();
465+
/// ```
466+
///
467+
/// Tag keys are identifiers; tag values must implement `Into<SharedString>`.
468+
#[macro_export]
469+
macro_rules! timer {
470+
($name:literal $(, $tag:ident = $tv:expr)* $(,)?) => {{
471+
let labels = vec![
472+
$($crate::_macro_support::metrics::Label::new(stringify!($tag), $tv),)*
473+
];
474+
$crate::TimerGuard::new($name, module_path!(), labels)
475+
}};
476+
}

objectstore-metrics/src/mock.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,80 @@ mod tests {
208208
assert_eq!(captured.len(), 1);
209209
assert_eq!(captured[0], "test.latency:2|d|#route:/v1/test,method:GET");
210210
}
211+
212+
#[test]
213+
fn timer_record_emits_success_true() {
214+
let captured = with_capturing_test_client(|| {
215+
let guard = crate::timer!("test.timer");
216+
guard.record();
217+
});
218+
assert_eq!(captured.len(), 1);
219+
assert!(captured[0].starts_with("test.timer:"));
220+
assert!(captured[0].contains("|d|#success:true"));
221+
}
222+
223+
#[test]
224+
fn timer_drop_emits_success_false() {
225+
let captured = with_capturing_test_client(|| {
226+
let _guard = crate::timer!("test.timer");
227+
});
228+
assert_eq!(captured.len(), 1);
229+
assert!(captured[0].starts_with("test.timer:"));
230+
assert!(captured[0].contains("|d|#success:false"));
231+
}
232+
233+
#[test]
234+
fn timer_drop_with_success_emits_success_true() {
235+
let captured = with_capturing_test_client(|| {
236+
let _guard = crate::timer!("test.timer").success();
237+
});
238+
assert_eq!(captured.len(), 1);
239+
assert!(captured[0].starts_with("test.timer:"));
240+
assert!(captured[0].contains("|d|#success:true"));
241+
}
242+
243+
#[test]
244+
fn timer_with_tags() {
245+
let captured = with_capturing_test_client(|| {
246+
let guard = crate::timer!("test.timer", route = "/v1/test");
247+
guard.record();
248+
});
249+
assert_eq!(captured.len(), 1);
250+
assert!(captured[0].starts_with("test.timer:"));
251+
assert!(captured[0].contains("route:/v1/test"));
252+
assert!(captured[0].contains("success:true"));
253+
}
254+
255+
#[test]
256+
fn timer_drop_with_tags() {
257+
let captured = with_capturing_test_client(|| {
258+
let _guard = crate::timer!("test.timer", op = "put");
259+
});
260+
assert_eq!(captured.len(), 1);
261+
assert!(captured[0].contains("op:put"));
262+
assert!(captured[0].contains("success:false"));
263+
}
264+
265+
#[test]
266+
fn timer_deferred_tag_on_record() {
267+
let captured = with_capturing_test_client(|| {
268+
let guard = crate::timer!("test.timer", usecase = "test");
269+
guard.tag("backend", "gcs").record();
270+
});
271+
assert_eq!(captured.len(), 1);
272+
assert!(captured[0].contains("usecase:test"));
273+
assert!(captured[0].contains("backend:gcs"));
274+
assert!(captured[0].contains("success:true"));
275+
}
276+
277+
#[test]
278+
fn timer_deferred_tag_on_drop() {
279+
let captured = with_capturing_test_client(|| {
280+
let _guard = crate::timer!("test.timer", usecase = "test").tag("backend", "gcs");
281+
});
282+
assert_eq!(captured.len(), 1);
283+
assert!(captured[0].contains("usecase:test"));
284+
assert!(captured[0].contains("backend:gcs"));
285+
assert!(captured[0].contains("success:false"));
286+
}
211287
}

0 commit comments

Comments
 (0)