Skip to content

Commit

Permalink
feat(metrics): Further improvements to docs and cadence
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-auer committed Dec 12, 2023
1 parent 12bf258 commit d90d14c
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 65 deletions.
114 changes: 92 additions & 22 deletions sentry-core/src/cadence.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,122 @@
//! [`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;
use cadence::{MetricSink, NopMetricSink};

use crate::metrics::Metric;
use crate::{Client, Hub};

/// A [`cadence`] compatible [`MetricSink`].
/// 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.
///
/// This will ingest all the emitted metrics to Sentry as well as forward them
/// to the inner [`MetricSink`].
/// 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> {
client: Arc<Client>,
pub struct SentryMetricSink<S = NopMetricSink> {
client: Option<Arc<Client>>,
sink: S,
}

impl<S> SentryMetricSink<S> {
impl<S> SentryMetricSink<S>
where
S: MetricSink,
{
/// Creates a new [`SentryMetricSink`], wrapping the given [`MetricSink`].
pub fn try_new(sink: S) -> Result<Self, S> {
match Hub::current().client() {
Some(client) => Ok(Self { client, sink }),
None => Err(sink),
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<S> MetricSink for SentryMetricSink<S>
where
S: MetricSink,
{
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) {
self.client.add_metric(metric);
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<()> {
if self.client.flush(None) {
self.sink.flush()
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,
"Flushing Client failed",
"failed to flush metrics to Sentry",
))
} else {
sink_result
}
}
}
Expand All @@ -61,9 +133,7 @@ mod tests {
#[test]
fn test_basic_metrics() {
let envelopes = with_captured_envelopes(|| {
let sink = SentryMetricSink::try_new(cadence::NopMetricSink).unwrap();

let client = cadence::StatsdClient::from_sink("sentry.test", sink);
let client = cadence::StatsdClient::from_sink("sentry.test", SentryMetricSink::new());
client.count("some.count", 1).unwrap();
client.count("some.count", 10).unwrap();
client
Expand Down
4 changes: 1 addition & 3 deletions sentry-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub use crate::scope::{Scope, ScopeGuard};
pub use crate::transport::{Transport, TransportFactory};

#[cfg(all(feature = "client", feature = "UNSTABLE_cadence"))]
mod cadence;
pub mod cadence;
// client feature
#[cfg(feature = "client")]
mod client;
Expand All @@ -149,8 +149,6 @@ pub mod metrics;
mod session;
#[cfg(all(feature = "client", feature = "UNSTABLE_metrics"))]
mod units;
#[cfg(all(feature = "client", feature = "UNSTABLE_cadence"))]
pub use crate::cadence::SentryMetricSink;
#[cfg(feature = "client")]
pub use crate::client::Client;

Expand Down
163 changes: 126 additions & 37 deletions sentry-core/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@
//! Utilities to track metrics in Sentry.
//!
//! Metrics allow you to track the custom values related to the behavior and performance of your
//! application and send them to Sentry. See [`Metric`] for more information on how to build and
//! capture metrics.
//! Metrics are numerical values that can track anything about your environment over time, from
//! latency to error rates to user signups.
//!
//! Metrics at Sentry come in different flavors, in order to help you track your data in the most
//! efficient and cost-effective way. The types of metrics we currently support are:
//!
//! - **Counters** track a value that can only be incremented.
//! - **Distributions** track a list of values over time in on which you can perform aggregations
//! like max, min, avg.
//! - **Gauges** track a value that can go up and down.
//! - **Sets** track a set of values on which you can perform aggregations such as count_unique.
//!
//! For more information on metrics in Sentry, see [our docs].
//!
//! # Usage
//!
//! To collect a metric, use the [`Metric`] struct to capture all relevant properties of your
//! metric. Then, use [`send`](Metric::send) to send the metric to Sentry:
//!
//! ```
//! use std::time::Duration;
//! use sentry::metrics::Metric;
//!
//! Metric::count("requests")
//! .with_tag("method", "GET")
//! .send();
//!
//! Metric::timing("request.duration", Duration::from_millis(17))
//! .with_tag("status_code", "200")
//! // unit is added automatically by timing
//! .send();
//!
//! Metric::set("site.visitors", "user1")
//! .with_unit("user")
//! .send();
//! ```
//!
//! # Usage with Cadence
//!
//! [`cadence`] is a popular Statsd client for Rust and can be used to send metrics to Sentry. To
//! use Sentry directly with `cadence`, see the [`sentry-cadence`](crate::cadence) documentation.
//!
//! [our docs]: https://develop.sentry.dev/delightful-developer-metrics/
use std::borrow::Cow;
use std::collections::hash_map::{DefaultHasher, Entry};
Expand Down Expand Up @@ -372,7 +412,8 @@ impl AggregatorInner {
/// # Units
///
/// To make the most out of metrics in Sentry, consider assigning a unit during construction. This
/// can be achieved using the [`with_unit`](MetricBuilder::with_unit) builder method.
/// can be achieved using the [`with_unit`](MetricBuilder::with_unit) builder method. See the
/// documentation for more examples on units.
///
/// ```
/// use sentry::metrics::{Metric, InformationUnit};
Expand Down Expand Up @@ -400,7 +441,7 @@ impl AggregatorInner {
///
/// Metrics can also be sent to a custom client. This is useful if you want to send metrics to a
/// different Sentry project or with different configuration. To do so, finish building the metric
/// and then add it to the client:
/// and then call [`add_metric`](crate::Client::add_metric) to the client:
///
/// ```
/// use sentry::Hub;
Expand Down Expand Up @@ -722,6 +763,7 @@ impl MetricAggregator {

if guard.weight() > MAX_WEIGHT {
if let Some(ref handle) = self.handle {
guard.force_flush = true;
handle.thread().unpark();
}
}
Expand Down Expand Up @@ -883,46 +925,18 @@ mod tests {
}

#[test]
fn test_counter() {
fn test_tags() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::count("my.metric")
.with_tag("foo", "bar")
.with_time(time)
.send();

Metric::incr("my.metric", 2.0)
.with_tag("foo", "bar")
.with_time(time)
.send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric:3|c|#foo:bar|T{ts}"));
}

#[test]
fn test_timing() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::timing("my.metric", Duration::from_millis(200))
.with_tag("foo", "bar")
.with_time(time)
.send();

Metric::timing("my.metric", Duration::from_millis(100))
.with_tag("foo", "bar")
.with_time(time)
.send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(
metrics,
format!("my.metric@second:0.2:0.1|d|#foo:bar|T{ts}")
);
assert_eq!(metrics, format!("my.metric:1|c|#foo:bar|T{ts}"));
}

#[test]
Expand All @@ -931,14 +945,13 @@ mod tests {

let envelopes = with_captured_envelopes(|| {
Metric::count("my.metric")
.with_tag("foo", "bar")
.with_time(time)
.with_unit("custom")
.send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric@custom:1|c|#foo:bar|T{ts}"));
assert_eq!(metrics, format!("my.metric@custom:1|c|T{ts}"));
}

#[test]
Expand Down Expand Up @@ -967,4 +980,80 @@ mod tests {
format!("requests:1|c|#foo:bar,environment:production,release:[email protected]|T{ts}")
);
}

#[test]
fn test_counter() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::count("my.metric").with_time(time).send();
Metric::incr("my.metric", 2.0).with_time(time).send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric:3|c|T{ts}"));
}

#[test]
fn test_timing() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::timing("my.metric", Duration::from_millis(200))
.with_time(time)
.send();
Metric::timing("my.metric", Duration::from_millis(100))
.with_time(time)
.send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric@second:0.2:0.1|d|T{ts}"));
}

#[test]
fn test_distribution() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::distribution("my.metric", 2.0)
.with_time(time)
.send();
Metric::distribution("my.metric", 1.0)
.with_time(time)
.send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric:2:1|d|T{ts}"));
}

#[test]
fn test_set() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::set("my.metric", "hello").with_time(time).send();
// Duplicate that should not be reflected twice
Metric::set("my.metric", "hello").with_time(time).send();
Metric::set("my.metric", "world").with_time(time).send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric:3410894750:3817476724|s|T{ts}"));
}

#[test]
fn test_gauge() {
let (time, ts) = current_time();

let envelopes = with_captured_envelopes(|| {
Metric::gauge("my.metric", 2.0).with_time(time).send();
Metric::gauge("my.metric", 1.0).with_time(time).send();
Metric::gauge("my.metric", 1.5).with_time(time).send();
});

let metrics = get_single_metrics(&envelopes);
assert_eq!(metrics, format!("my.metric:1.5:1:2:4.5:3|g|T{ts}"));
}
}
Loading

0 comments on commit d90d14c

Please sign in to comment.