relay_metrics/
protocol.rs1use std::hash::Hasher as _;
2
3use hash32::{FnvHasher, Hasher as _};
4use relay_protocol::FiniteF64;
5
6#[doc(inline)]
7pub use relay_base_schema::metrics::{
8 CustomUnit, DurationUnit, FractionUnit, InformationUnit, MetricName, MetricNamespace,
9 MetricResourceIdentifier, MetricType, MetricUnit, ParseMetricError, ParseMetricUnitError,
10};
11#[doc(inline)]
12pub use relay_common::time::UnixTimestamp;
13
14use crate::{Bucket, MetricTags};
15
16pub type CounterType = FiniteF64;
18
19pub type DistributionType = FiniteF64;
21
22pub type SetType = u32;
24
25pub type GaugeType = FiniteF64;
27
28#[derive(Debug, thiserror::Error)]
30pub enum NormalizationError {
31 #[error("unsupported metric namespace")]
33 UnsupportedNamespace,
34 #[error("invalid metric name: {0:?}")]
36 InvalidMetricName(MetricName),
37}
38
39pub fn normalize_bucket(bucket: &mut Bucket) -> Result<(), NormalizationError> {
48 normalize_metric_name(&mut bucket.name)?;
49 normalize_metric_tags(&mut bucket.tags);
50 Ok(())
51}
52
53fn normalize_metric_name(name: &mut MetricName) -> Result<(), NormalizationError> {
60 *name = match MetricResourceIdentifier::parse(name) {
61 Ok(mri) => {
62 if matches!(mri.namespace, MetricNamespace::Unsupported) {
63 return Err(NormalizationError::UnsupportedNamespace);
64 }
65
66 mri.to_string().into()
69 }
70 Err(_) => {
71 return Err(NormalizationError::InvalidMetricName(name.clone()));
72 }
73 };
74
75 Ok(())
76}
77
78fn normalize_metric_tags(tags: &mut MetricTags) {
82 tags.retain(|tag_key, tag_value| {
83 if !is_valid_tag_key(tag_key) {
84 relay_log::debug!("invalid metric tag key {tag_key:?}");
85 return false;
86 }
87
88 normalize_tag_value(tag_value);
89
90 true
91 });
92}
93
94pub(crate) fn is_valid_tag_key(tag_key: &str) -> bool {
98 for &byte in tag_key.as_bytes() {
100 if (byte as char).is_ascii_control() {
101 return false;
102 }
103 }
104 true
105}
106
107pub(crate) fn normalize_tag_value(tag_value: &mut String) {
112 tag_value.retain(|c| !c.is_control());
113}
114
115pub(crate) fn hash_set_value(string: &str) -> u32 {
119 let mut hasher = FnvHasher::default();
120 hasher.write(string.as_bytes());
121 hasher.finish32()
122}
123
124#[cfg(test)]
125mod tests {
126 use insta::assert_json_snapshot;
127
128 use crate::BucketValue;
129
130 use super::*;
131
132 #[test]
133 fn test_normalize_invalid_name() {
134 let mut bucket = Bucket {
135 timestamp: UnixTimestamp::from_secs(5000),
136 width: 0,
137 name: "c:transactions/\0hergus.bergus@none".into(),
138 value: BucketValue::Counter(0.into()),
139 tags: Default::default(),
140 metadata: Default::default(),
141 };
142
143 assert!(matches!(
144 normalize_bucket(&mut bucket),
145 Err(NormalizationError::InvalidMetricName(_))
146 ));
147 }
148
149 #[test]
150 fn test_normalize_invalid_namespace() {
151 let mut bucket = Bucket {
152 timestamp: UnixTimestamp::from_secs(5000),
153 width: 0,
154 name: "c:lol/hergus.bergus@none".into(),
155 value: BucketValue::Counter(0.into()),
156 tags: Default::default(),
157 metadata: Default::default(),
158 };
159
160 assert!(matches!(
161 normalize_bucket(&mut bucket),
162 Err(NormalizationError::UnsupportedNamespace)
163 ));
164 }
165
166 #[test]
167 fn test_normalize_name() {
168 let mut bucket = Bucket {
169 timestamp: UnixTimestamp::from_secs(5000),
170 width: 0,
171 name: "c:spans/hergus\0\0bergus".into(),
172 value: BucketValue::Counter(0.into()),
173 tags: Default::default(),
174 metadata: Default::default(),
175 };
176
177 normalize_bucket(&mut bucket).unwrap();
178
179 assert_eq!(&bucket.name, "c:spans/hergus_bergus@none");
180 }
181
182 #[test]
183 fn test_normalize_tag_key_chars() {
184 let mut bucket = Bucket {
185 timestamp: UnixTimestamp::from_secs(5000),
186 width: 0,
187 name: "c:spans/hergus.bergus".into(),
188 value: BucketValue::Counter(0.into()),
189 tags: {
190 let mut tags = MetricTags::new();
191 tags.insert(
204 "is_it_garbage".to_owned(),
205 "a\0b\0s\0o\0l\0u\0t\0e\0l\0y".to_owned(),
206 );
207 tags.insert("another\0garbage".to_owned(), "bye".to_owned());
208 tags
209 },
210 metadata: Default::default(),
211 };
212
213 normalize_bucket(&mut bucket).unwrap();
214
215 assert_json_snapshot!(bucket, @r###"
216 {
217 "timestamp": 5000,
218 "width": 0,
219 "name": "c:spans/hergus.bergus@none",
220 "type": "c",
221 "value": 0.0,
222 "tags": {
223 "is_it_garbage": "absolutely"
224 }
225 }
226 "###);
227 }
228}