Skip to content

Add experimental metrics implementation #618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9f33090
Add experimental metrics implementation
Swatinem Oct 17, 2023
c83b9aa
finish up impl
Swatinem Oct 18, 2023
dd22afb
implement sets the naive way
Swatinem Oct 18, 2023
2fc55ae
do not emit an empty envelope item on shutdown
Swatinem Oct 18, 2023
97c9a0c
review
Swatinem Oct 20, 2023
b6bdece
add rate limit category
Swatinem Oct 23, 2023
331a457
fix typo
Swatinem Oct 23, 2023
a61de48
fix build
Swatinem Oct 23, 2023
06c483a
try to fix flush race on windows
Swatinem Oct 23, 2023
c692048
fix doc builds
Swatinem Oct 23, 2023
b568ebc
remove forced shutdown from metrics aggregator
Swatinem Oct 23, 2023
255c578
Merge branch 'master' into swatinem/metrics
jan-auer Dec 11, 2023
dc5ea74
ref(metrics): Minor review comments
jan-auer Dec 11, 2023
f2c6410
ref: Separate cadence from metrics feature
jan-auer Dec 11, 2023
45a4ac1
ref(metrics): Minor code-level changes
jan-auer Dec 11, 2023
d24c9b2
ref(metrics): Refactor to match Python SDK
jan-auer Dec 11, 2023
b515db3
fix: First bugfixes
jan-auer Dec 11, 2023
1267668
fix: Flakey submission
jan-auer Dec 11, 2023
c164983
feat(metrics): Add a convenience API to track metrics directly
jan-auer Dec 11, 2023
ba4afe0
fix: Move statsd parsing to metrics module
jan-auer Dec 11, 2023
bcc665b
ref(metrics): Reorganize and add docs
jan-auer Dec 11, 2023
32e6baa
test: Add a first unit test
jan-auer Dec 11, 2023
0ef19a3
feat(metrics): Inject default tags
jan-auer Dec 11, 2023
12bf258
ref(metrics): Simplify unit handling
jan-auer Dec 12, 2023
d90d14c
feat(metrics): Further improvements to docs and cadence
jan-auer Dec 12, 2023
524a5a9
fix(metrics): Sanitation behavior
jan-auer Dec 12, 2023
c506251
fix: Docs
jan-auer Dec 12, 2023
c3f7247
ref(metrics): Refactor worker into separate struct
jan-auer Dec 12, 2023
edbbb95
ref(metrics): Add more Debug impls
jan-auer Dec 12, 2023
55b7f28
ref: Rename metric item to "statsd"
jan-auer Dec 12, 2023
ab14afa
ref(metrics): Flush synchronously
jan-auer Dec 12, 2023
0c4064c
ref: Address review feedback
jan-auer Dec 12, 2023
139cdf3
ref(metrics): Reduce duplication of bucket key
jan-auer Dec 12, 2023
69080ab
meta: Changelog
jan-auer Dec 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(metrics): Further improvements to docs and cadence
  • Loading branch information
jan-auer committed Dec 12, 2023
commit d90d14c84d7115cdc3fd0dd3d59a1c4f480b87d3
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:myapp@1.0.0|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