Skip to content

Commit 801d7ca

Browse files
author
Elias Ram
committed
ref(metrics): Add normalization and update set metrics hashing
1 parent 8c701e8 commit 801d7ca

File tree

10 files changed

+1376
-116
lines changed

10 files changed

+1376
-116
lines changed

Cargo.lock

Lines changed: 975 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sentry-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ UNSTABLE_cadence = ["dep:cadence", "UNSTABLE_metrics"]
3131

3232
[dependencies]
3333
cadence = { version = "0.29.0", optional = true }
34+
crc32fast = "1.4.0"
35+
itertools = "0.10.5"
3436
log = { version = "0.4.8", optional = true, features = ["std"] }
3537
once_cell = "1"
3638
rand = { version = "0.8.1", optional = true }
39+
regex = "1.7.3"
3740
sentry-types = { version = "0.32.3", path = "../sentry-types" }
3841
serde = { version = "1.0.104", features = ["derive"] }
3942
serde_json = { version = "1.0.46" }
43+
unicode-segmentation = "1.11.0"
4044
uuid = { version = "1.0.0", features = ["v4", "serde"], optional = true }
4145

4246
[dev-dependencies]

sentry-core/src/cadence.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,10 @@ mod tests {
154154

155155
println!("{metrics}");
156156

157-
assert!(metrics.contains("sentry.test.count.with.tags:1|c|#foo:bar|T"));
158-
assert!(metrics.contains("sentry.test.some.count:11|c|T"));
159-
assert!(metrics.contains("sentry.test.some.distr:1:2:3|d|T"));
157+
assert!(metrics
158+
.contains("sentry.test.count.with.tags@none:1|c|#environment:production,foo:bar|T"));
159+
assert!(metrics.contains("sentry.test.some.count@none:11|c|#environment:production|T"));
160+
assert!(metrics.contains("sentry.test.some.distr@none:1:2:3|d|#environment:production|T"));
160161
assert_eq!(items.next(), None);
161162
}
162163
}

sentry-core/src/metrics.rs renamed to sentry-core/src/metrics/mod.rs

Lines changed: 106 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@
4444
//!
4545
//! [our docs]: https://develop.sentry.dev/delightful-developer-metrics/
4646
47+
mod normalization;
48+
4749
use std::borrow::Cow;
48-
use std::collections::hash_map::{DefaultHasher, Entry};
50+
use std::collections::hash_map::Entry;
4951
use std::collections::{BTreeMap, BTreeSet, HashMap};
50-
use std::fmt::{self, Write};
52+
use std::fmt::{self, Display};
5153
use std::sync::{Arc, Mutex};
5254
use std::thread::{self, JoinHandle};
5355
use std::time::{Duration, SystemTime, UNIX_EPOCH};
5456

57+
use normalization::normalized_name::NormalizedName;
58+
use normalization::normalized_tags::NormalizedTags;
59+
use normalization::normalized_unit::NormalizedUnit;
5560
use sentry_types::protocol::latest::{Envelope, EnvelopeItem};
5661

5762
use crate::client::TransportArc;
@@ -168,15 +173,23 @@ impl MetricValue {
168173
}
169174
}
170175

176+
impl Display for MetricValue {
177+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178+
match self {
179+
Self::Counter(v) => write!(f, "{}", v),
180+
Self::Distribution(v) => write!(f, "{}", v),
181+
Self::Gauge(v) => write!(f, "{}", v),
182+
Self::Set(v) => write!(f, "{}", v),
183+
}
184+
}
185+
}
186+
171187
/// Hashes the given set value.
172188
///
173189
/// Sets only guarantee 32-bit accuracy, but arbitrary strings are allowed on the protocol. Upon
174190
/// parsing, they are hashed and only used as hashes subsequently.
175191
fn hash_set_value(string: &str) -> u32 {
176-
use std::hash::Hasher;
177-
let mut hasher = DefaultHasher::default();
178-
hasher.write(string.as_bytes());
179-
hasher.finish() as u32
192+
crc32fast::hash(string.as_bytes())
180193
}
181194

182195
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -510,6 +523,24 @@ impl Metric {
510523
client.add_metric(self);
511524
}
512525
}
526+
527+
/// Convert the metric into an [`Envelope`] containing a single [`EnvelopeItem::Statsd`].
528+
pub fn to_envelope(self) -> Envelope {
529+
let timestamp = SystemTime::now()
530+
.duration_since(UNIX_EPOCH)
531+
.unwrap_or_default()
532+
.as_secs();
533+
let data = format!(
534+
"{}@{}:{}|{}|#{}|T{}",
535+
NormalizedName::from(self.name.as_ref()),
536+
NormalizedUnit::from(self.unit),
537+
self.value,
538+
self.value.ty(),
539+
NormalizedTags::from(self.tags),
540+
timestamp
541+
);
542+
Envelope::from_item(EnvelopeItem::Statsd(data.into_bytes()))
543+
}
513544
}
514545

515546
/// A builder for metrics.
@@ -550,6 +581,26 @@ impl MetricBuilder {
550581
self
551582
}
552583

584+
/// Adds multiple tags to the metric.
585+
///
586+
/// Tags allow you to add dimensions to metrics. They are key-value pairs that can be filtered
587+
/// or grouped by in Sentry.
588+
///
589+
/// When sent to Sentry via [`MetricBuilder::send`] or when added to a
590+
/// [`Client`](crate::Client), the client may add default tags to the metrics, such as the
591+
/// `release` or the `environment` from the Scope.
592+
pub fn with_tags<T, K, V>(mut self, tags: T) -> Self
593+
where
594+
T: IntoIterator<Item = (K, V)>,
595+
K: Into<MetricStr>,
596+
V: Into<MetricStr>,
597+
{
598+
tags.into_iter().for_each(|(k, v)| {
599+
self.metric.tags.insert(k.into(), v.into());
600+
});
601+
self
602+
}
603+
553604
/// Sets the timestamp for the metric.
554605
///
555606
/// By default, the timestamp is set to the current time when the metric is built or sent.
@@ -723,9 +774,13 @@ fn get_default_tags(options: &ClientOptions) -> TagMap {
723774
if let Some(ref release) = options.release {
724775
tags.insert("release".into(), release.clone());
725776
}
726-
if let Some(ref environment) = options.environment {
727-
tags.insert("environment".into(), environment.clone());
728-
}
777+
tags.insert(
778+
"environment".into(),
779+
options
780+
.environment
781+
.clone()
782+
.unwrap_or(Cow::Borrowed("production")),
783+
);
729784
tags
730785
}
731786

@@ -778,11 +833,8 @@ impl Worker {
778833

779834
for (timestamp, buckets) in buckets {
780835
for (key, value) in buckets {
781-
write!(&mut out, "{}", SafeKey(key.name.as_ref()))?;
782-
if key.unit != MetricUnit::None {
783-
write!(&mut out, "@{}", key.unit)?;
784-
}
785-
836+
write!(&mut out, "{}", NormalizedName::from(key.name.as_ref()))?;
837+
write!(&mut out, "@{}", NormalizedUnit::from(key.unit))?;
786838
match value {
787839
BucketValue::Counter(c) => {
788840
write!(&mut out, ":{}", c)?;
@@ -807,16 +859,9 @@ impl Worker {
807859
}
808860

809861
write!(&mut out, "|{}", key.ty.as_str())?;
810-
811-
for (i, (k, v)) in key.tags.iter().chain(&self.default_tags).enumerate() {
812-
match i {
813-
0 => write!(&mut out, "|#")?,
814-
_ => write!(&mut out, ",")?,
815-
}
816-
817-
write!(&mut out, "{}:{}", SafeKey(k.as_ref()), SafeVal(v.as_ref()))?;
818-
}
819-
862+
let normalized_tags =
863+
NormalizedTags::from(key.tags).with_default_tags(&self.default_tags);
864+
write!(&mut out, "|#{}", normalized_tags)?;
820865
writeln!(&mut out, "|T{}", timestamp)?;
821866
}
822867
}
@@ -922,51 +967,6 @@ impl Drop for MetricAggregator {
922967
}
923968
}
924969

925-
fn safe_fmt<F>(f: &mut fmt::Formatter<'_>, string: &str, mut check: F) -> fmt::Result
926-
where
927-
F: FnMut(char) -> bool,
928-
{
929-
let mut valid = true;
930-
931-
for c in string.chars() {
932-
if check(c) {
933-
valid = true;
934-
f.write_char(c)?;
935-
} else if valid {
936-
valid = false;
937-
f.write_char('_')?;
938-
}
939-
}
940-
941-
Ok(())
942-
}
943-
944-
// Helper that serializes a string into a safe format for metric names or tag keys.
945-
struct SafeKey<'s>(&'s str);
946-
947-
impl<'s> fmt::Display for SafeKey<'s> {
948-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
949-
safe_fmt(f, self.0, |c| {
950-
c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/')
951-
})
952-
}
953-
}
954-
955-
// Helper that serializes a string into a safe format for tag values.
956-
struct SafeVal<'s>(&'s str);
957-
958-
impl<'s> fmt::Display for SafeVal<'s> {
959-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960-
safe_fmt(f, self.0, |c| {
961-
c.is_alphanumeric()
962-
|| matches!(
963-
c,
964-
'_' | ':' | '/' | '@' | '.' | '{' | '}' | '[' | ']' | '$' | '-'
965-
)
966-
})
967-
}
968-
}
969-
970970
#[cfg(test)]
971971
mod tests {
972972
use crate::test::{with_captured_envelopes, with_captured_envelopes_options};
@@ -1007,7 +1007,10 @@ mod tests {
10071007
});
10081008

10091009
let metrics = get_single_metrics(&envelopes);
1010-
assert_eq!(metrics, format!("my.metric:1|c|#and:more,foo:bar|T{ts}"));
1010+
assert_eq!(
1011+
metrics,
1012+
format!("my.metric@none:1|c|#and:more,environment:production,foo:bar|T{ts}")
1013+
);
10111014
}
10121015

10131016
#[test]
@@ -1022,7 +1025,10 @@ mod tests {
10221025
});
10231026

10241027
let metrics = get_single_metrics(&envelopes);
1025-
assert_eq!(metrics, format!("my.metric@custom:1|c|T{ts}"));
1028+
assert_eq!(
1029+
metrics,
1030+
format!("my.metric@custom:1|c|#environment:production|T{ts}")
1031+
);
10261032
}
10271033

10281034
#[test]
@@ -1034,7 +1040,10 @@ mod tests {
10341040
});
10351041

10361042
let metrics = get_single_metrics(&envelopes);
1037-
assert_eq!(metrics, format!("my_metric:1|c|T{ts}"));
1043+
assert_eq!(
1044+
metrics,
1045+
format!("my___metric@none:1|c|#environment:production|T{ts}")
1046+
);
10381047
}
10391048

10401049
#[test]
@@ -1051,29 +1060,17 @@ mod tests {
10511060
let metrics = get_single_metrics(&envelopes);
10521061
assert_eq!(
10531062
metrics,
1054-
format!("my.metric:1|c|#foo-bar_blub:_$föö{{}}|T{ts}")
1063+
format!("my.metric@none:1|c|#environment:production,foo-barblub:%$föö{{}}|T{ts}")
10551064
);
10561065
}
10571066

1058-
#[test]
1059-
fn test_own_namespace() {
1060-
let (time, ts) = current_time();
1061-
1062-
let envelopes = with_captured_envelopes(|| {
1063-
Metric::count("ns/my.metric").with_time(time).send();
1064-
});
1065-
1066-
let metrics = get_single_metrics(&envelopes);
1067-
assert_eq!(metrics, format!("ns/my.metric:1|c|T{ts}"));
1068-
}
1069-
10701067
#[test]
10711068
fn test_default_tags() {
10721069
let (time, ts) = current_time();
10731070

10741071
let options = ClientOptions {
10751072
release: Some("[email protected]".into()),
1076-
environment: Some("production".into()),
1073+
environment: Some("development".into()),
10771074
..Default::default()
10781075
};
10791076

@@ -1090,7 +1087,7 @@ mod tests {
10901087
let metrics = get_single_metrics(&envelopes);
10911088
assert_eq!(
10921089
metrics,
1093-
format!("requests:1|c|#foo:bar,environment:production,release:[email protected]|T{ts}")
1090+
format!("requests@none:1|c|#environment:development,foo:bar,release:[email protected]|T{ts}")
10941091
);
10951092
}
10961093

@@ -1104,7 +1101,10 @@ mod tests {
11041101
});
11051102

11061103
let metrics = get_single_metrics(&envelopes);
1107-
assert_eq!(metrics, format!("my.metric:3|c|T{ts}"));
1104+
assert_eq!(
1105+
metrics,
1106+
format!("my.metric@none:3|c|#environment:production|T{ts}")
1107+
);
11081108
}
11091109

11101110
#[test]
@@ -1121,7 +1121,10 @@ mod tests {
11211121
});
11221122

11231123
let metrics = get_single_metrics(&envelopes);
1124-
assert_eq!(metrics, format!("my.metric@second:0.2:0.1|d|T{ts}"));
1124+
assert_eq!(
1125+
metrics,
1126+
format!("my.metric@second:0.2:0.1|d|#environment:production|T{ts}")
1127+
);
11251128
}
11261129

11271130
#[test]
@@ -1138,7 +1141,10 @@ mod tests {
11381141
});
11391142

11401143
let metrics = get_single_metrics(&envelopes);
1141-
assert_eq!(metrics, format!("my.metric:2:1|d|T{ts}"));
1144+
assert_eq!(
1145+
metrics,
1146+
format!("my.metric@none:2:1|d|#environment:production|T{ts}")
1147+
);
11421148
}
11431149

11441150
#[test]
@@ -1153,7 +1159,10 @@ mod tests {
11531159
});
11541160

11551161
let metrics = get_single_metrics(&envelopes);
1156-
assert_eq!(metrics, format!("my.metric:3410894750:3817476724|s|T{ts}"));
1162+
assert_eq!(
1163+
metrics,
1164+
format!("my.metric@none:907060870:980881731|s|#environment:production|T{ts}")
1165+
);
11571166
}
11581167

11591168
#[test]
@@ -1167,7 +1176,10 @@ mod tests {
11671176
});
11681177

11691178
let metrics = get_single_metrics(&envelopes);
1170-
assert_eq!(metrics, format!("my.metric:1.5:1:2:4.5:3|g|T{ts}"));
1179+
assert_eq!(
1180+
metrics,
1181+
format!("my.metric@none:1.5:1:2:4.5:3|g|#environment:production|T{ts}")
1182+
);
11711183
}
11721184

11731185
#[test]
@@ -1182,8 +1194,8 @@ mod tests {
11821194
let metrics = get_single_metrics(&envelopes);
11831195
println!("{metrics}");
11841196

1185-
assert!(metrics.contains(&format!("my.metric:1|c|T{ts}")));
1186-
assert!(metrics.contains(&format!("my.dist:2|d|T{ts}")));
1197+
assert!(metrics.contains(&format!("my.metric@none:1|c|#environment:production|T{ts}")));
1198+
assert!(metrics.contains(&format!("my.dist@none:2|d|#environment:production|T{ts}")));
11871199
}
11881200

11891201
#[test]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod normalized_name;
2+
pub mod normalized_tags;
3+
pub mod normalized_unit;

0 commit comments

Comments
 (0)