Skip to content

Commit

Permalink
Add experimental metrics implementation (#618)
Browse files Browse the repository at this point in the history
Co-authored-by: Jan Michael Auer <[email protected]>
  • Loading branch information
Swatinem and jan-auer authored Dec 12, 2023
1 parent dfdb7b6 commit a2b60da
Show file tree
Hide file tree
Showing 12 changed files with 1,717 additions and 17 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

**Features**:

- Add experimental implementations for Sentry metrics and a cadence sink. These
require to use the `UNSTABLE_metrics` and `UNSTABLE_cadence` feature flags.
Note that these APIs are still under development and subject to change.

## 0.32.0

**Features**:
Expand Down
46 changes: 33 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions sentry-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ client = ["rand"]
# and macros actually expand features (and extern crate) where they are used!
debug-logs = ["dep:log"]
test = ["client"]
UNSTABLE_metrics = ["sentry-types/UNSTABLE_metrics"]
UNSTABLE_cadence = ["dep:cadence", "UNSTABLE_metrics"]

[dependencies]
cadence = { version = "0.29.0", optional = true }
log = { version = "0.4.8", optional = true, features = ["std"] }
once_cell = "1"
rand = { version = "0.8.1", optional = true }
Expand Down
162 changes: 162 additions & 0 deletions sentry-core/src/cadence.rs
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);
}
}
Loading

0 comments on commit a2b60da

Please sign in to comment.