Skip to content
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

Add experimental metrics implementation #618

Merged
merged 34 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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);
}
loewenheim marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
Loading