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