-
Notifications
You must be signed in to change notification settings - Fork 91
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
fix(metrics): Remove/reject nul-bytes from metric strings [ingest-1204] #1235
Changes from 5 commits
0434a7d
8e69309
ddf33c8
48455d0
4539d6d
708928c
5461474
b2d8f18
3ef65b3
df08cc2
dd7369f
75a463c
5a74087
3a6559a
88e3bf9
8ab8097
ef658b9
02f0b08
2d695c8
c526013
3618181
ce8f57f
df4f9f3
1b4b7ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -745,6 +745,31 @@ impl BucketKey { | |
std::hash::Hash::hash(self, &mut hasher); | ||
hasher.finalize() as i64 | ||
} | ||
|
||
/// Remove invalid characters from tags and metric names. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metric names and metric tags have different character allow lists, let's have separate validator functions. Especially, there is already More of a code style suggestion: |
||
/// | ||
/// Returns `Err` if the metric should be dropped. | ||
fn validate_strings(mut self) -> Result<Self, ()> { | ||
if self.metric_name.contains('\0') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you instead reject all control characters here? |
||
relay_log::error!("invalid metric name {:?}", self.metric_name); | ||
return Err(()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not log here but rather return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't go for that because it would bloat the size of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whether you return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I mean if I were to store the metric name in the error. and yeah even then it's not struct size but the extra allocation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see. Maybe log this on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have removed it entirely now :/ |
||
} | ||
|
||
self.metric_name.retain(|c| c != '\0'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this redundant given that we're rejecting the name just before? |
||
self.tags.retain(|tag_key, _| { | ||
if !tag_key.contains('\0') { | ||
true | ||
} else { | ||
relay_log::error!("invalid metric tag key {:?}", tag_key); | ||
false | ||
} | ||
}); | ||
for (_, tag_value) in self.tags.iter_mut() { | ||
tag_value.retain(|c| c != '\0'); | ||
} | ||
|
||
Ok(self) | ||
} | ||
} | ||
|
||
/// Parameters used by the [`Aggregator`]. | ||
|
@@ -1055,6 +1080,8 @@ impl Aggregator { | |
let timestamp = key.timestamp; | ||
let project_key = key.project_key; | ||
|
||
let key = key.validate_strings().map_err(|()| AggregateMetricsError)?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will fail the entire batch. we have two options:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jjbayer @jan-auer @iker-barriocanal I believe we should change merge_all. there is a related case involving |
||
|
||
match self.buckets.entry(key) { | ||
Entry::Occupied(mut entry) => { | ||
relay_statsd::metric!( | ||
|
@@ -2078,4 +2105,50 @@ mod tests { | |
rounded_now + 10 | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_validate_strings() { | ||
let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap(); | ||
|
||
let bucket_key = BucketKey { | ||
project_key, | ||
timestamp: UnixTimestamp::now(), | ||
metric_name: "hergus.bergus".to_owned(), | ||
metric_type: MetricType::Counter, | ||
metric_unit: MetricUnit::None, | ||
tags: { | ||
let mut tags = BTreeMap::new(); | ||
// There are some SDKs which mess up content encodings, and interpret the raw bytes | ||
// of an UTF-16 string as UTF-8. Leading to ASCII | ||
// strings getting null-bytes interleaved. | ||
// | ||
// Somehow those values end up as release tag in sessions, while in error events we | ||
// haven't observed this malformed encoding. We believe it's slightly better to | ||
// strip out NUL-bytes instead of dropping the tag such that those values line up | ||
// again across sessions and events. Should that cause too high cardinality we'll | ||
// have to drop tags. | ||
untitaker marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// | ||
// Note that releases are validated separately against much stricter character set, | ||
// but the above idea should still apply to other tags. | ||
tags.insert( | ||
"is_it_garbage".to_owned(), | ||
"a\0b\0s\0o\0l\0u\0t\0e\0l\0y".to_owned(), | ||
); | ||
tags.insert("another\0garbage".to_owned(), "bye".to_owned()); | ||
tags | ||
}, | ||
}; | ||
|
||
let mut bucket_key = bucket_key.validate_strings().unwrap(); | ||
|
||
assert_eq!(bucket_key.tags.len(), 1); | ||
assert_eq!( | ||
bucket_key.tags.get("is_it_garbage"), | ||
Some(&"absolutely".to_owned()) | ||
); | ||
assert_eq!(bucket_key.tags.get("another\0garbage"), None); | ||
|
||
bucket_key.metric_name = "hergus\0bergus".to_owned(); | ||
bucket_key.validate_strings().unwrap_err(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change does mean that the release name is only validated in the store normalizer. I think we will need to follow up with moving this part of store normalization to run before metrics extraction as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it even make sense to remove the
deny_chars
at this point? Or should we do that as part of the follow up?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A separate PR that moves this in one go would've been slightly more semantic, but let's keep it this way in light of moving fast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://getsentry.atlassian.net/browse/INGEST-1285