Skip to content

Commit a2b60da

Browse files
Swatinemjan-auer
andauthored
Add experimental metrics implementation (#618)
Co-authored-by: Jan Michael Auer <[email protected]>
1 parent dfdb7b6 commit a2b60da

File tree

12 files changed

+1717
-17
lines changed

12 files changed

+1717
-17
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
**Features**:
6+
7+
- Add experimental implementations for Sentry metrics and a cadence sink. These
8+
require to use the `UNSTABLE_metrics` and `UNSTABLE_cadence` feature flags.
9+
Note that these APIs are still under development and subject to change.
10+
311
## 0.32.0
412

513
**Features**:

Cargo.lock

Lines changed: 33 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sentry-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ client = ["rand"]
2626
# and macros actually expand features (and extern crate) where they are used!
2727
debug-logs = ["dep:log"]
2828
test = ["client"]
29+
UNSTABLE_metrics = ["sentry-types/UNSTABLE_metrics"]
30+
UNSTABLE_cadence = ["dep:cadence", "UNSTABLE_metrics"]
2931

3032
[dependencies]
33+
cadence = { version = "0.29.0", optional = true }
3134
log = { version = "0.4.8", optional = true, features = ["std"] }
3235
once_cell = "1"
3336
rand = { version = "0.8.1", optional = true }

sentry-core/src/cadence.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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

Comments
 (0)