objectstore_metrics/lib.rs
1//! Metrics macros and DogStatsD initialization for Objectstore.
2//!
3//! This crate provides three things:
4//!
5//! 1. [`counter!`], [`gauge!`], and [`distribution!`] macros that preserve
6//! a concise call-site syntax (`"name": value, "tag" => tag_value`).
7//! 2. [`MetricsConfig`] and [`init`] for wiring up a DogStatsD exporter.
8//! 3. [`with_capturing_test_client`] for asserting on emitted metrics in tests.
9//!
10//! # Usage
11//!
12//! ```rust
13//! use std::time::Duration;
14//! use objectstore_metrics::{counter, distribution, gauge};
15//!
16//! let count = 42_u64;
17//! let elapsed = Duration::from_secs(1);
18//! let route = "api/v1";
19//!
20//! counter!("server.start": 1);
21//! gauge!("server.requests.in_flight": count);
22//! distribution!("server.requests.duration"@s: elapsed, "route" => route);
23//! ```
24//!
25//! # Unit annotations
26//!
27//! - `@s` converts a [`Duration`](std::time::Duration) to seconds via `.as_secs_f64()`.
28//! - `@b` converts the value via `as f64` (identity for byte counts).
29//! - No annotation also converts via `as f64`.
30
31mod mock;
32
33use std::collections::BTreeMap;
34
35use metrics_exporter_dogstatsd::{AggregationMode, DogStatsDBuilder};
36use serde::{Deserialize, Serialize};
37
38/// Re-exports used by macro expansion. Not part of the public API.
39#[doc(hidden)]
40pub mod _macro_support {
41 pub use metrics;
42}
43
44/// Error type for metrics initialization.
45#[derive(Debug, thiserror::Error)]
46pub enum Error {
47 /// Failed to build the DogStatsD exporter.
48 #[error("failed to initialize metrics exporter: {0}")]
49 Build(#[from] metrics_exporter_dogstatsd::BuildError),
50}
51
52/// Configuration for the DogStatsD metrics exporter.
53///
54/// When `addr` is `None`, metrics are no-ops (the global recorder is never installed).
55///
56/// # Environment Variables
57///
58/// - `OS__METRICS__ADDR` — StatsD address (e.g. `127.0.0.1:8125` or `unixgram:///tmp/statsd.sock`)
59/// - `OS__METRICS__PREFIX` — global metric name prefix
60/// - `OS__METRICS__BUFFER_SIZE` — maximum payload length in bytes
61/// - `OS__METRICS__TAGS__KEY=value` — per-key global tags
62#[derive(Clone, Debug, Deserialize, Serialize)]
63pub struct MetricsConfig {
64 /// Remote address to forward metrics to.
65 ///
66 /// When `None`, metrics are disabled (the global recorder is not installed and all
67 /// metric calls are no-ops).
68 ///
69 /// For UDP, the address must be in the format `<host>:<port>` (e.g. `127.0.0.1:8125`).
70 /// For Unix domain sockets, use the format `<scheme>://<path>`, where the scheme is
71 /// either `unix` (stream, `SOCK_STREAM`) or `unixgram` (datagram, `SOCK_DGRAM`).
72 ///
73 /// # Default
74 ///
75 /// `None` (metrics disabled)
76 ///
77 /// # Environment Variable
78 ///
79 /// `OS__METRICS__ADDR`
80 pub addr: Option<String>,
81
82 /// Global prefix prepended to every metric name.
83 ///
84 /// The prefix is prepended to every metric name, with a `.` separator added automatically.
85 ///
86 /// # Default
87 ///
88 /// `"objectstore"`
89 ///
90 /// # Environment Variable
91 ///
92 /// `OS__METRICS__PREFIX`
93 #[serde(default = "default_prefix")]
94 pub prefix: String,
95
96 /// Maximum payload length in bytes.
97 ///
98 /// Controls the maximum size per StatsD payload. Should match the Datadog Agent's
99 /// `dogstatsd_buffer_size` setting. If `None`, the exporter uses its default
100 /// (1432 bytes for UDP, 8192 bytes for Unix sockets).
101 ///
102 /// # Default
103 ///
104 /// `None` (exporter default)
105 ///
106 /// # Environment Variable
107 ///
108 /// `OS__METRICS__BUFFER_SIZE`
109 pub buffer_size: Option<usize>,
110
111 /// Global tags applied to all metrics.
112 ///
113 /// Key-value pairs attached to every emitted metric. Useful for identifying
114 /// environment, region, or other deployment-specific dimensions.
115 ///
116 /// # Default
117 ///
118 /// Empty (no tags)
119 ///
120 /// # Environment Variables
121 ///
122 /// Each tag is set individually:
123 /// - `OS__METRICS__TAGS__FOO=foo`
124 /// - `OS__METRICS__TAGS__BAR=bar`
125 ///
126 /// # YAML Example
127 ///
128 /// ```yaml
129 /// metrics:
130 /// tags:
131 /// foo: foo
132 /// bar: bar
133 /// ```
134 pub tags: BTreeMap<String, String>,
135}
136
137fn default_prefix() -> String {
138 "objectstore".to_owned()
139}
140
141impl Default for MetricsConfig {
142 fn default() -> Self {
143 Self {
144 addr: None,
145 prefix: "objectstore".to_owned(),
146 buffer_size: None,
147 tags: BTreeMap::new(),
148 }
149 }
150}
151
152/// Initializes the global DogStatsD metrics exporter.
153///
154/// Returns `Ok(())` immediately when `config.addr` is `None` — in that case the
155/// global recorder is never installed and all `metrics` calls are no-ops.
156pub fn init(config: &MetricsConfig) -> Result<(), Error> {
157 let Some(ref addr) = config.addr else {
158 return Ok(());
159 };
160
161 tracing::info!("reporting metrics to statsd at {addr}");
162
163 let global_labels: Vec<metrics::Label> = config
164 .tags
165 .iter()
166 .map(|(k, v)| metrics::Label::new(k.clone(), v.clone()))
167 .collect();
168
169 let mut builder = DogStatsDBuilder::default()
170 .with_remote_address(addr)?
171 .with_telemetry(true)
172 .with_aggregation_mode(AggregationMode::Aggressive)
173 .send_histograms_as_distributions(true)
174 .with_histogram_sampling(true)
175 .set_global_prefix(&config.prefix)
176 .with_global_labels(global_labels);
177
178 if let Some(buffer_size) = config.buffer_size {
179 builder = builder.with_maximum_payload_length(buffer_size)?;
180 }
181
182 builder.install()?;
183
184 Ok(())
185}
186
187pub use mock::with_capturing_test_client;
188
189// ---------------------------------------------------------------------------
190// Macros
191// ---------------------------------------------------------------------------
192
193/// Emits a counter metric.
194///
195/// # Syntax
196///
197/// ```rust
198/// use objectstore_metrics::counter;
199///
200/// counter!("name": 1u64);
201/// counter!("name": 1u64, "tag" => "value");
202/// counter!("name": 1u64, "tag1" => "val1", "tag2" => "val2");
203/// ```
204#[macro_export]
205macro_rules! counter {
206 ($name:literal : $value:expr $(, $tag:literal => $tv:expr)* $(,)?) => {
207 $crate::_macro_support::metrics::counter!(
208 $name
209 $(, $tag => $crate::__label_value!($tv))*
210 )
211 .increment($value as u64);
212 };
213}
214
215/// Emits a gauge metric.
216///
217/// # Syntax
218///
219/// ```rust
220/// use objectstore_metrics::gauge;
221///
222/// gauge!("name": 1.0_f64);
223/// gauge!("name"@b: 1024_u64);
224/// gauge!("name": 1.0_f64, "tag" => "value");
225/// ```
226///
227/// The `@b` unit annotation converts via `as f64` (identity for byte counts).
228#[macro_export]
229macro_rules! gauge {
230 ($name:literal @b : $value:expr $(, $tag:literal => $tv:expr)* $(,)?) => {
231 $crate::_macro_support::metrics::gauge!(
232 $name
233 $(, $tag => $crate::__label_value!($tv))*
234 )
235 .set($value as f64);
236 };
237 ($name:literal : $value:expr $(, $tag:literal => $tv:expr)* $(,)?) => {
238 $crate::_macro_support::metrics::gauge!(
239 $name
240 $(, $tag => $crate::__label_value!($tv))*
241 )
242 .set($value as f64);
243 };
244}
245
246/// Emits a distribution (histogram) metric.
247///
248/// # Syntax
249///
250/// ```rust
251/// use std::time::Duration;
252/// use objectstore_metrics::distribution;
253///
254/// distribution!("name": 1.0_f64);
255/// distribution!("name"@s: Duration::from_secs(1));
256/// distribution!("name"@b: 1024_u64);
257/// distribution!("name"@s: Duration::from_secs(1), "tag" => "value");
258/// ```
259///
260/// - `@s` converts a [`Duration`](std::time::Duration) to seconds via `.as_secs_f64()`.
261/// - `@b` converts the value via `as f64` (identity for byte counts).
262/// - No annotation converts via `as f64`.
263#[macro_export]
264macro_rules! distribution {
265 ($name:literal @s : $value:expr $(, $tag:literal => $tv:expr)* $(,)?) => {
266 $crate::_macro_support::metrics::histogram!(
267 $name
268 $(, $tag => $crate::__label_value!($tv))*
269 )
270 .record($value.as_secs_f64());
271 };
272 ($name:literal @b : $value:expr $(, $tag:literal => $tv:expr)* $(,)?) => {
273 $crate::_macro_support::metrics::histogram!(
274 $name
275 $(, $tag => $crate::__label_value!($tv))*
276 )
277 .record($value as f64);
278 };
279 ($name:literal : $value:expr $(, $tag:literal => $tv:expr)* $(,)?) => {
280 $crate::_macro_support::metrics::histogram!(
281 $name
282 $(, $tag => $crate::__label_value!($tv))*
283 )
284 .record($value as f64);
285 };
286}
287
288/// Converts a tag value expression to a string suitable for a [`metrics::Label`].
289///
290/// This handles `&str`, `String`, integer types, and anything with a `Display` impl
291/// by converting through `format!`. It relies on specialization-free dispatching:
292/// `&str` and `String` pass through, everything else uses `format!`.
293#[doc(hidden)]
294#[macro_export]
295macro_rules! __label_value {
296 ($e:expr) => {{
297 // Use a trait-based dispatch that works for &str, String, and Display types.
298 // The metrics crate accepts Into<SharedString> which covers &str, String, etc.
299 $crate::_macro_support::metrics::SharedString::from(format!("{}", $e))
300 }};
301}