-
-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add experimental metrics implementation (#618)
Co-authored-by: Jan Michael Auer <[email protected]>
- Loading branch information
Showing
12 changed files
with
1,717 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
//! [`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 `UNSTABLE_cadence` 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:1|c|#foo:bar|T")); | ||
assert!(metrics.contains("sentry.test.some.count:11|c|T")); | ||
assert!(metrics.contains("sentry.test.some.distr:1:2:3|d|T")); | ||
assert_eq!(items.next(), None); | ||
} | ||
} |
Oops, something went wrong.