|
| 1 | +//! [`cadence`] integration for Sentry. |
| 2 | +//! |
| 3 | +//! [`cadence`] is a popular Statsd client for Rust. The [`SentryMetricSink`] provides a drop-in |
| 4 | +//! integration to send metrics captured via `cadence` to Sentry. For direct usage of Sentry |
| 5 | +//! metrics, see the [`metrics`](crate::metrics) module. |
| 6 | +//! |
| 7 | +//! # Usage |
| 8 | +//! |
| 9 | +//! To use the `cadence` integration, enable the `UNSTABLE_cadence` feature in your `Cargo.toml`. |
| 10 | +//! Then, create a [`SentryMetricSink`] and pass it to your `cadence` client: |
| 11 | +//! |
| 12 | +//! ``` |
| 13 | +//! use cadence::StatsdClient; |
| 14 | +//! use sentry::cadence::SentryMetricSink; |
| 15 | +//! |
| 16 | +//! let client = StatsdClient::from_sink("sentry.test", SentryMetricSink::new()); |
| 17 | +//! ``` |
| 18 | +//! |
| 19 | +//! # Side-by-side Usage |
| 20 | +//! |
| 21 | +//! If you want to send metrics to Sentry and another backend at the same time, you can use |
| 22 | +//! [`SentryMetricSink::wrap`] to wrap another [`MetricSink`]: |
| 23 | +//! |
| 24 | +//! ``` |
| 25 | +//! use cadence::{StatsdClient, NopMetricSink}; |
| 26 | +//! use sentry::cadence::SentryMetricSink; |
| 27 | +//! |
| 28 | +//! let sink = SentryMetricSink::wrap(NopMetricSink); |
| 29 | +//! let client = StatsdClient::from_sink("sentry.test", sink); |
| 30 | +//! ``` |
| 31 | +
|
| 32 | +use std::sync::Arc; |
| 33 | + |
| 34 | +use cadence::{MetricSink, NopMetricSink}; |
| 35 | + |
| 36 | +use crate::metrics::Metric; |
| 37 | +use crate::{Client, Hub}; |
| 38 | + |
| 39 | +/// A [`MetricSink`] that sends metrics to Sentry. |
| 40 | +/// |
| 41 | +/// This metric sends all metrics to Sentry. The Sentry client is internally buffered, so submission |
| 42 | +/// will be delayed. |
| 43 | +/// |
| 44 | +/// Optionally, this sink can also forward metrics to another [`MetricSink`]. This is useful if you |
| 45 | +/// want to send metrics to Sentry and another backend at the same time. Use |
| 46 | +/// [`SentryMetricSink::wrap`] to construct such a sink. |
| 47 | +#[derive(Debug)] |
| 48 | +pub struct SentryMetricSink<S = NopMetricSink> { |
| 49 | + client: Option<Arc<Client>>, |
| 50 | + sink: S, |
| 51 | +} |
| 52 | + |
| 53 | +impl<S> SentryMetricSink<S> |
| 54 | +where |
| 55 | + S: MetricSink, |
| 56 | +{ |
| 57 | + /// Creates a new [`SentryMetricSink`], wrapping the given [`MetricSink`]. |
| 58 | + pub fn wrap(sink: S) -> Self { |
| 59 | + Self { client: None, sink } |
| 60 | + } |
| 61 | + |
| 62 | + /// Creates a new [`SentryMetricSink`] sending data to the given [`Client`]. |
| 63 | + pub fn with_client(mut self, client: Arc<Client>) -> Self { |
| 64 | + self.client = Some(client); |
| 65 | + self |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +impl SentryMetricSink { |
| 70 | + /// Creates a new [`SentryMetricSink`]. |
| 71 | + /// |
| 72 | + /// It is not required that a client is available when this sink is created. The sink sends |
| 73 | + /// metrics to the client of the Sentry hub that is registered when the metrics are emitted. |
| 74 | + pub fn new() -> Self { |
| 75 | + Self { |
| 76 | + client: None, |
| 77 | + sink: NopMetricSink, |
| 78 | + } |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +impl Default for SentryMetricSink { |
| 83 | + fn default() -> Self { |
| 84 | + Self::new() |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +impl MetricSink for SentryMetricSink { |
| 89 | + fn emit(&self, string: &str) -> std::io::Result<usize> { |
| 90 | + if let Ok(metric) = Metric::parse_statsd(string) { |
| 91 | + if let Some(ref client) = self.client { |
| 92 | + client.add_metric(metric); |
| 93 | + } else if let Some(client) = Hub::current().client() { |
| 94 | + client.add_metric(metric); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + // NopMetricSink returns `0`, which is correct as Sentry is buffering the metrics. |
| 99 | + self.sink.emit(string) |
| 100 | + } |
| 101 | + |
| 102 | + fn flush(&self) -> std::io::Result<()> { |
| 103 | + let flushed = if let Some(ref client) = self.client { |
| 104 | + client.flush(None) |
| 105 | + } else if let Some(client) = Hub::current().client() { |
| 106 | + client.flush(None) |
| 107 | + } else { |
| 108 | + true |
| 109 | + }; |
| 110 | + |
| 111 | + let sink_result = self.sink.flush(); |
| 112 | + |
| 113 | + if !flushed { |
| 114 | + Err(std::io::Error::new( |
| 115 | + std::io::ErrorKind::Other, |
| 116 | + "failed to flush metrics to Sentry", |
| 117 | + )) |
| 118 | + } else { |
| 119 | + sink_result |
| 120 | + } |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +#[cfg(test)] |
| 125 | +mod tests { |
| 126 | + use cadence::{Counted, Distributed}; |
| 127 | + use sentry_types::protocol::latest::EnvelopeItem; |
| 128 | + |
| 129 | + use crate::test::with_captured_envelopes; |
| 130 | + |
| 131 | + use super::*; |
| 132 | + |
| 133 | + #[test] |
| 134 | + fn test_basic_metrics() { |
| 135 | + let envelopes = with_captured_envelopes(|| { |
| 136 | + let client = cadence::StatsdClient::from_sink("sentry.test", SentryMetricSink::new()); |
| 137 | + client.count("some.count", 1).unwrap(); |
| 138 | + client.count("some.count", 10).unwrap(); |
| 139 | + client |
| 140 | + .count_with_tags("count.with.tags", 1) |
| 141 | + .with_tag("foo", "bar") |
| 142 | + .send(); |
| 143 | + client.distribution("some.distr", 1).unwrap(); |
| 144 | + client.distribution("some.distr", 2).unwrap(); |
| 145 | + client.distribution("some.distr", 3).unwrap(); |
| 146 | + }); |
| 147 | + assert_eq!(envelopes.len(), 1); |
| 148 | + |
| 149 | + let mut items = envelopes[0].items(); |
| 150 | + let Some(EnvelopeItem::Statsd(metrics)) = items.next() else { |
| 151 | + panic!("expected metrics"); |
| 152 | + }; |
| 153 | + let metrics = std::str::from_utf8(metrics).unwrap(); |
| 154 | + |
| 155 | + println!("{metrics}"); |
| 156 | + |
| 157 | + assert!(metrics.contains("sentry.test.count.with.tags:1|c|#foo:bar|T")); |
| 158 | + assert!(metrics.contains("sentry.test.some.count:11|c|T")); |
| 159 | + assert!(metrics.contains("sentry.test.some.distr:1:2:3|d|T")); |
| 160 | + assert_eq!(items.next(), None); |
| 161 | + } |
| 162 | +} |
0 commit comments