QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
···66// Semi-public modules - needed by binary but with limited exposure
77pub mod cache; // Only create_redis_pool exposed
88pub mod handle_resolver_task; // Factory functions and TaskConfig exposed
99+pub mod metrics; // Metrics publishing trait and implementations
910pub mod queue; // Queue adapter system with trait and factory functions
1011pub mod sqlite_schema; // SQLite schema management functions exposed
1112pub mod task_manager; // Only spawn_cancellable_task exposed
+476
src/metrics.rs
···11+use crate::config::Config;
22+use async_trait::async_trait;
33+use cadence::{BufferedUdpMetricSink, Counted, CountedExt, Gauged, Metric, QueuingMetricSink, StatsdClient, Timed};
44+use std::net::UdpSocket;
55+use std::sync::Arc;
66+use thiserror::Error;
77+use tracing::{debug, error};
88+99+/// Trait for publishing metrics with counter and gauge support
1010+/// Designed for minimal compatibility with cadence-style metrics
1111+#[async_trait]
1212+pub trait MetricsPublisher: Send + Sync {
1313+ /// Increment a counter by 1
1414+ async fn incr(&self, key: &str);
1515+1616+ /// Increment a counter by a specific value
1717+ async fn count(&self, key: &str, value: u64);
1818+1919+ /// Increment a counter with tags
2020+ async fn incr_with_tags(&self, key: &str, tags: &[(&str, &str)]);
2121+2222+ /// Increment a counter by a specific value with tags
2323+ async fn count_with_tags(&self, key: &str, value: u64, tags: &[(&str, &str)]);
2424+2525+ /// Record a gauge value
2626+ async fn gauge(&self, key: &str, value: u64);
2727+2828+ /// Record a gauge value with tags
2929+ async fn gauge_with_tags(&self, key: &str, value: u64, tags: &[(&str, &str)]);
3030+3131+ /// Record a timing in milliseconds
3232+ async fn time(&self, key: &str, millis: u64);
3333+3434+ /// Record a timing with tags
3535+ async fn time_with_tags(&self, key: &str, millis: u64, tags: &[(&str, &str)]);
3636+}
3737+3838+/// No-op implementation for development and testing
3939+#[derive(Debug, Clone, Default)]
4040+pub struct NoOpMetricsPublisher;
4141+4242+impl NoOpMetricsPublisher {
4343+ pub fn new() -> Self {
4444+ Self
4545+ }
4646+}
4747+4848+#[async_trait]
4949+impl MetricsPublisher for NoOpMetricsPublisher {
5050+ async fn incr(&self, _key: &str) {
5151+ // No-op
5252+ }
5353+5454+ async fn count(&self, _key: &str, _value: u64) {
5555+ // No-op
5656+ }
5757+5858+ async fn incr_with_tags(&self, _key: &str, _tags: &[(&str, &str)]) {
5959+ // No-op
6060+ }
6161+6262+ async fn count_with_tags(&self, _key: &str, _value: u64, _tags: &[(&str, &str)]) {
6363+ // No-op
6464+ }
6565+6666+ async fn gauge(&self, _key: &str, _value: u64) {
6767+ // No-op
6868+ }
6969+7070+ async fn gauge_with_tags(&self, _key: &str, _value: u64, _tags: &[(&str, &str)]) {
7171+ // No-op
7272+ }
7373+7474+ async fn time(&self, _key: &str, _millis: u64) {
7575+ // No-op
7676+ }
7777+7878+ async fn time_with_tags(&self, _key: &str, _millis: u64, _tags: &[(&str, &str)]) {
7979+ // No-op
8080+ }
8181+}
8282+8383+/// Statsd-backed metrics publisher using cadence
8484+pub struct StatsdMetricsPublisher {
8585+ client: StatsdClient,
8686+ default_tags: Vec<(String, String)>,
8787+}
8888+8989+impl StatsdMetricsPublisher {
9090+ /// Create a new StatsdMetricsPublisher with default configuration
9191+ pub fn new(host: &str, prefix: &str) -> Result<Self, Box<dyn std::error::Error>> {
9292+ Self::new_with_tags(host, prefix, vec![])
9393+ }
9494+9595+ /// Create a new StatsdMetricsPublisher with default tags
9696+ pub fn new_with_tags(
9797+ host: &str,
9898+ prefix: &str,
9999+ default_tags: Vec<(String, String)>
100100+ ) -> Result<Self, Box<dyn std::error::Error>> {
101101+ tracing::info!("Creating StatsdMetricsPublisher: host={}, prefix={}, tags={:?}", host, prefix, default_tags);
102102+103103+ let socket = UdpSocket::bind("0.0.0.0:0")?;
104104+ socket.set_nonblocking(true)?;
105105+106106+ let buffered_sink = BufferedUdpMetricSink::from(host, socket)?;
107107+ let queuing_sink = QueuingMetricSink::builder()
108108+ .with_error_handler(move |error| {
109109+ error!("Failed to send metric via sink: {}", error);
110110+ })
111111+ .build(buffered_sink);
112112+ let client = StatsdClient::from_sink(prefix, queuing_sink);
113113+114114+ tracing::info!("StatsdMetricsPublisher created successfully");
115115+ Ok(Self { client, default_tags })
116116+ }
117117+118118+ /// Create from an existing StatsdClient
119119+ pub fn from_client(client: StatsdClient) -> Self {
120120+ Self::from_client_with_tags(client, vec![])
121121+ }
122122+123123+ /// Create from an existing StatsdClient with default tags
124124+ pub fn from_client_with_tags(client: StatsdClient, default_tags: Vec<(String, String)>) -> Self {
125125+ Self { client, default_tags }
126126+ }
127127+128128+ /// Apply default tags to a builder
129129+ fn apply_default_tags<'a, M>(&'a self, mut builder: cadence::MetricBuilder<'a, 'a, M>) -> cadence::MetricBuilder<'a, 'a, M>
130130+ where
131131+ M: Metric + From<String>,
132132+ {
133133+ for (k, v) in &self.default_tags {
134134+ builder = builder.with_tag(k.as_str(), v.as_str());
135135+ }
136136+ builder
137137+ }
138138+}
139139+140140+#[async_trait]
141141+impl MetricsPublisher for StatsdMetricsPublisher {
142142+ async fn incr(&self, key: &str) {
143143+ debug!("Sending metric incr: {}", key);
144144+ if self.default_tags.is_empty() {
145145+ match self.client.incr(key) {
146146+ Ok(_) => debug!("Successfully sent metric: {}", key),
147147+ Err(e) => error!("Failed to send metric {}: {}", key, e),
148148+ }
149149+ } else {
150150+ let builder = self.client.incr_with_tags(key);
151151+ let builder = self.apply_default_tags(builder);
152152+ let _ = builder.send();
153153+ debug!("Sent metric with tags: {}", key);
154154+ }
155155+ }
156156+157157+ async fn count(&self, key: &str, value: u64) {
158158+ if self.default_tags.is_empty() {
159159+ let _ = self.client.count(key, value);
160160+ } else {
161161+ let builder = self.client.count_with_tags(key, value);
162162+ let builder = self.apply_default_tags(builder);
163163+ let _ = builder.send();
164164+ }
165165+ }
166166+167167+ async fn incr_with_tags(&self, key: &str, tags: &[(&str, &str)]) {
168168+ let mut builder = self.client.incr_with_tags(key);
169169+ builder = self.apply_default_tags(builder);
170170+ for (k, v) in tags {
171171+ builder = builder.with_tag(k, v);
172172+ }
173173+ let _ = builder.send();
174174+ }
175175+176176+ async fn count_with_tags(&self, key: &str, value: u64, tags: &[(&str, &str)]) {
177177+ let mut builder = self.client.count_with_tags(key, value);
178178+ builder = self.apply_default_tags(builder);
179179+ for (k, v) in tags {
180180+ builder = builder.with_tag(k, v);
181181+ }
182182+ let _ = builder.send();
183183+ }
184184+185185+ async fn gauge(&self, key: &str, value: u64) {
186186+ debug!("Sending metric gauge: {} = {}", key, value);
187187+ if self.default_tags.is_empty() {
188188+ match self.client.gauge(key, value) {
189189+ Ok(_) => debug!("Successfully sent gauge: {} = {}", key, value),
190190+ Err(e) => error!("Failed to send gauge {} = {}: {}", key, value, e),
191191+ }
192192+ } else {
193193+ let builder = self.client.gauge_with_tags(key, value);
194194+ let builder = self.apply_default_tags(builder);
195195+ builder.send();
196196+ debug!("Sent gauge with tags: {} = {}", key, value);
197197+ }
198198+ }
199199+200200+ async fn gauge_with_tags(&self, key: &str, value: u64, tags: &[(&str, &str)]) {
201201+ let mut builder = self.client.gauge_with_tags(key, value);
202202+ builder = self.apply_default_tags(builder);
203203+ for (k, v) in tags {
204204+ builder = builder.with_tag(k, v);
205205+ }
206206+ let _ = builder.send();
207207+ }
208208+209209+ async fn time(&self, key: &str, millis: u64) {
210210+ if self.default_tags.is_empty() {
211211+ let _ = self.client.time(key, millis);
212212+ } else {
213213+ let builder = self.client.time_with_tags(key, millis);
214214+ let builder = self.apply_default_tags(builder);
215215+ let _ = builder.send();
216216+ }
217217+ }
218218+219219+ async fn time_with_tags(&self, key: &str, millis: u64, tags: &[(&str, &str)]) {
220220+ let mut builder = self.client.time_with_tags(key, millis);
221221+ builder = self.apply_default_tags(builder);
222222+ for (k, v) in tags {
223223+ builder = builder.with_tag(k, v);
224224+ }
225225+ let _ = builder.send();
226226+ }
227227+}
228228+229229+/// Type alias for shared metrics publisher
230230+pub type SharedMetricsPublisher = Arc<dyn MetricsPublisher>;
231231+232232+/// Metrics-specific errors
233233+#[derive(Debug, Error)]
234234+pub enum MetricsError {
235235+ /// Failed to create metrics publisher
236236+ #[error("error-quickdid-metrics-1 Failed to create metrics publisher: {0}")]
237237+ CreationFailed(String),
238238+239239+ /// Invalid configuration for metrics
240240+ #[error("error-quickdid-metrics-2 Invalid metrics configuration: {0}")]
241241+ InvalidConfig(String),
242242+}
243243+244244+/// Create a metrics publisher based on configuration
245245+///
246246+/// Returns either a no-op publisher or a StatsD publisher based on the
247247+/// `metrics_adapter` configuration value.
248248+///
249249+/// ## Example
250250+///
251251+/// ```rust,no_run
252252+/// use quickdid::config::Config;
253253+/// use quickdid::metrics::create_metrics_publisher;
254254+///
255255+/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
256256+/// let config = Config::from_env()?;
257257+/// let metrics = create_metrics_publisher(&config)?;
258258+///
259259+/// // Use the metrics publisher
260260+/// metrics.incr("request.count").await;
261261+/// # Ok(())
262262+/// # }
263263+/// ```
264264+pub fn create_metrics_publisher(config: &Config) -> Result<SharedMetricsPublisher, MetricsError> {
265265+ match config.metrics_adapter.as_str() {
266266+ "noop" => {
267267+ Ok(Arc::new(NoOpMetricsPublisher::new()))
268268+ }
269269+ "statsd" => {
270270+ let host = config.metrics_statsd_host.as_ref()
271271+ .ok_or_else(|| MetricsError::InvalidConfig(
272272+ "METRICS_STATSD_HOST is required when using statsd adapter".to_string()
273273+ ))?;
274274+275275+ // Parse tags from comma-separated key:value pairs
276276+ let default_tags = if let Some(tags_str) = &config.metrics_tags {
277277+ tags_str
278278+ .split(',')
279279+ .filter_map(|tag| {
280280+ let parts: Vec<&str> = tag.trim().split(':').collect();
281281+ if parts.len() == 2 {
282282+ Some((parts[0].to_string(), parts[1].to_string()))
283283+ } else {
284284+ error!("Invalid tag format: {}", tag);
285285+ None
286286+ }
287287+ })
288288+ .collect()
289289+ } else {
290290+ vec![]
291291+ };
292292+293293+ let publisher = StatsdMetricsPublisher::new_with_tags(
294294+ host,
295295+ &config.metrics_prefix,
296296+ default_tags
297297+ ).map_err(|e| MetricsError::CreationFailed(e.to_string()))?;
298298+299299+ Ok(Arc::new(publisher))
300300+ }
301301+ _ => {
302302+ Err(MetricsError::InvalidConfig(format!(
303303+ "Unknown metrics adapter: {}",
304304+ config.metrics_adapter
305305+ )))
306306+ }
307307+ }
308308+}
309309+310310+#[cfg(test)]
311311+mod tests {
312312+ use super::*;
313313+314314+ #[tokio::test]
315315+ async fn test_noop_metrics() {
316316+ let metrics = NoOpMetricsPublisher::new();
317317+318318+ // These should all be no-ops and not panic
319319+ metrics.incr("test.counter").await;
320320+ metrics.count("test.counter", 5).await;
321321+ metrics.incr_with_tags("test.counter", &[("env", "test")]).await;
322322+ metrics.count_with_tags("test.counter", 10, &[("env", "test"), ("service", "quickdid")]).await;
323323+ metrics.gauge("test.gauge", 100).await;
324324+ metrics.gauge_with_tags("test.gauge", 200, &[("host", "localhost")]).await;
325325+ metrics.time("test.timing", 42).await;
326326+ metrics.time_with_tags("test.timing", 84, &[("endpoint", "/resolve")]).await;
327327+ }
328328+329329+ #[tokio::test]
330330+ async fn test_shared_metrics() {
331331+ let metrics: SharedMetricsPublisher = Arc::new(NoOpMetricsPublisher::new());
332332+333333+ // Verify it can be used as a shared reference
334334+ metrics.incr("shared.counter").await;
335335+ metrics.gauge("shared.gauge", 50).await;
336336+337337+ // Verify it can be cloned
338338+ let metrics2 = Arc::clone(&metrics);
339339+ metrics2.count("cloned.counter", 3).await;
340340+ }
341341+342342+ #[test]
343343+ fn test_create_noop_publisher() {
344344+ use std::env;
345345+346346+ // Clean up any existing environment variables first
347347+ unsafe {
348348+ env::remove_var("METRICS_ADAPTER");
349349+ env::remove_var("METRICS_STATSD_HOST");
350350+ env::remove_var("METRICS_PREFIX");
351351+ env::remove_var("METRICS_TAGS");
352352+ }
353353+354354+ // Set up environment for noop adapter
355355+ unsafe {
356356+ env::set_var("HTTP_EXTERNAL", "test.example.com");
357357+ env::set_var("SERVICE_KEY", "did:key:test");
358358+ env::set_var("METRICS_ADAPTER", "noop");
359359+ }
360360+361361+ let config = Config::from_env().unwrap();
362362+ let metrics = create_metrics_publisher(&config).unwrap();
363363+364364+ // Should create successfully - actual type checking happens at compile time
365365+ assert!(Arc::strong_count(&metrics) == 1);
366366+367367+ // Clean up
368368+ unsafe {
369369+ env::remove_var("METRICS_ADAPTER");
370370+ }
371371+ }
372372+373373+ #[test]
374374+ fn test_create_statsd_publisher() {
375375+ use std::env;
376376+377377+ // Clean up any existing environment variables first
378378+ unsafe {
379379+ env::remove_var("METRICS_ADAPTER");
380380+ env::remove_var("METRICS_STATSD_HOST");
381381+ env::remove_var("METRICS_PREFIX");
382382+ env::remove_var("METRICS_TAGS");
383383+ }
384384+385385+ // Set up environment for statsd adapter
386386+ unsafe {
387387+ env::set_var("HTTP_EXTERNAL", "test.example.com");
388388+ env::set_var("SERVICE_KEY", "did:key:test");
389389+ env::set_var("METRICS_ADAPTER", "statsd");
390390+ env::set_var("METRICS_STATSD_HOST", "localhost:8125");
391391+ env::set_var("METRICS_PREFIX", "test");
392392+ env::set_var("METRICS_TAGS", "env:test,service:quickdid");
393393+ }
394394+395395+ let config = Config::from_env().unwrap();
396396+ let metrics = create_metrics_publisher(&config).unwrap();
397397+398398+ // Should create successfully
399399+ assert!(Arc::strong_count(&metrics) == 1);
400400+401401+ // Clean up
402402+ unsafe {
403403+ env::remove_var("METRICS_ADAPTER");
404404+ env::remove_var("METRICS_STATSD_HOST");
405405+ env::remove_var("METRICS_PREFIX");
406406+ env::remove_var("METRICS_TAGS");
407407+ }
408408+ }
409409+410410+ #[test]
411411+ fn test_missing_statsd_host() {
412412+ use std::env;
413413+414414+ // Clean up any existing environment variables first
415415+ unsafe {
416416+ env::remove_var("METRICS_ADAPTER");
417417+ env::remove_var("METRICS_STATSD_HOST");
418418+ env::remove_var("METRICS_PREFIX");
419419+ env::remove_var("METRICS_TAGS");
420420+ }
421421+422422+ // Set up environment for statsd adapter without host
423423+ unsafe {
424424+ env::set_var("HTTP_EXTERNAL", "test.example.com");
425425+ env::set_var("SERVICE_KEY", "did:key:test");
426426+ env::set_var("METRICS_ADAPTER", "statsd");
427427+ env::remove_var("METRICS_STATSD_HOST");
428428+ }
429429+430430+ let config = Config::from_env().unwrap();
431431+ let result = create_metrics_publisher(&config);
432432+433433+ // Should fail with invalid config error
434434+ assert!(result.is_err());
435435+ if let Err(e) = result {
436436+ assert!(matches!(e, MetricsError::InvalidConfig(_)));
437437+ }
438438+439439+ // Clean up
440440+ unsafe {
441441+ env::remove_var("METRICS_ADAPTER");
442442+ }
443443+ }
444444+445445+ #[test]
446446+ fn test_invalid_adapter() {
447447+ use std::env;
448448+449449+ // Clean up any existing environment variables first
450450+ unsafe {
451451+ env::remove_var("METRICS_ADAPTER");
452452+ env::remove_var("METRICS_STATSD_HOST");
453453+ env::remove_var("METRICS_PREFIX");
454454+ env::remove_var("METRICS_TAGS");
455455+ }
456456+457457+ // Set up environment with invalid adapter
458458+ unsafe {
459459+ env::set_var("HTTP_EXTERNAL", "test.example.com");
460460+ env::set_var("SERVICE_KEY", "did:key:test");
461461+ env::set_var("METRICS_ADAPTER", "invalid");
462462+ env::remove_var("METRICS_STATSD_HOST"); // Clean up from other tests
463463+ }
464464+465465+ let config = Config::from_env().unwrap();
466466+467467+ // Config validation should catch this
468468+ let validation_result = config.validate();
469469+ assert!(validation_result.is_err());
470470+471471+ // Clean up
472472+ unsafe {
473473+ env::remove_var("METRICS_ADAPTER");
474474+ }
475475+ }
476476+}