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);
    }
}