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

fix(metrics): Remove/reject nul-bytes from metric strings [ingest-1204] #1235

Merged
merged 24 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0434a7d
fix(metrics): Remove/reject nul-bytes from metric strings [ingest-1204]
untitaker Apr 21, 2022
8e69309
Update relay-metrics/src/aggregation.rs
untitaker Apr 21, 2022
ddf33c8
fix wrapping
untitaker Apr 21, 2022
48455d0
remove release/environment character validation
untitaker Apr 21, 2022
4539d6d
add changelog
untitaker Apr 21, 2022
708928c
ignore errors in merge_all
untitaker Apr 22, 2022
5461474
make aggregatemetricserror an enum
untitaker Apr 25, 2022
b2d8f18
ref(metrics): Refactor aggregation error, recover from errors more gr…
untitaker Apr 25, 2022
3ef65b3
add changelog
untitaker Apr 25, 2022
df08cc2
Merge branch 'ref/aggregation-error' into fix/reject-null-values
untitaker Apr 25, 2022
dd7369f
address review feedback
untitaker Apr 25, 2022
75a463c
Merge remote-tracking branch 'origin/master' into fix/reject-null-values
untitaker Apr 25, 2022
5a74087
fix lint
untitaker Apr 25, 2022
3a6559a
restore debug stmts
untitaker Apr 25, 2022
88e3bf9
fix mri validation
untitaker Apr 25, 2022
8ab8097
fix mri validation
untitaker Apr 25, 2022
ef658b9
fix formatting
untitaker Apr 25, 2022
02f0b08
fix tests
untitaker Apr 25, 2022
2d695c8
Merge branch 'fix/mri-validation' into fix/reject-null-values
untitaker Apr 25, 2022
c526013
fix tests
untitaker Apr 25, 2022
3618181
fix docstring
untitaker Apr 25, 2022
ce8f57f
Merge branch 'fix/mri-validation' into fix/reject-null-values
untitaker Apr 25, 2022
df4f9f3
fix tests
untitaker Apr 25, 2022
1b4b7ed
fix docs
untitaker Apr 26, 2022
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Add platform, op, http.method and status tag to all extracted transaction metrics. ([#1227](https://github.com/getsentry/relay/pull/1227))
- Add units in built-in measurements. ([#1229](https://github.com/getsentry/relay/pull/1229))

**Internal**:

- Remove/reject nul-bytes from metric strings. ([#1235](https://github.com/getsentry/relay/pull/1235))

## 22.4.0

**Features**:
Expand Down
4 changes: 2 additions & 2 deletions relay-general/src/protocol/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ pub struct Event {
/// can be the git SHA for the given project, or a product identifier with a semantic version.
#[metastructure(
max_chars = "tag_value", // release ends in tag
deny_chars = "\r\n\x0c\t/\\",
// release allowed chars are validated in the sentry-release-parser crate!
Copy link
Member Author

@untitaker untitaker Apr 21, 2022

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

  • create ticket

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required = "false",
trim_whitespace = "true",
nonempty = "true",
Expand Down Expand Up @@ -374,7 +374,7 @@ pub struct Event {
/// ```
#[metastructure(
max_chars = "environment",
deny_chars = "\r\n\x0C/",
// environment allowed chars are validated in the sentry-release-parser crate!
nonempty = "true",
required = "false",
trim_whitespace = "true"
Expand Down
25 changes: 1 addition & 24 deletions relay-general/src/store/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fn verify_value_characters(
mod tests {
use super::SchemaProcessor;
use crate::processor::{process_value, ProcessingState};
use crate::types::{Annotated, Array, Error, Object, Value};
use crate::types::{Annotated, Array, Error, Object};

fn assert_nonempty_base<T>()
where
Expand Down Expand Up @@ -148,29 +148,6 @@ mod tests {
assert_nonempty_base::<Object<u64>>();
}

#[test]
fn test_release_invalid_newlines() {
use crate::protocol::Event;

let mut event = Annotated::new(Event {
release: Annotated::new("a\nb".to_string().into()),
..Default::default()
});

process_value(&mut event, &mut SchemaProcessor, ProcessingState::root()).unwrap();

assert_eq_dbg!(
event,
Annotated::new(Event {
release: Annotated::from_error(
Error::invalid("invalid character \'\\n\'"),
Some(Value::String("a\nb".into())),
),
..Default::default()
})
);
}

#[test]
fn test_invalid_email() {
use crate::protocol::User;
Expand Down
73 changes: 73 additions & 0 deletions relay-metrics/src/aggregation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The 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 is_valid_name. parse_tags doesn't have such logic yet.

More of a code style suggestion: BucketKey is a plain data object used for bucketing. I would suggest to put such validator functions
on types that specify the public protocol or directly in the aggregator if it's easily possible.

///
/// Returns `Err` if the metric should be dropped.
fn validate_strings(mut self) -> Result<Self, ()> {
if self.metric_name.contains('\0') {
Copy link
Member

Choose a reason for hiding this comment

The 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(());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not log here but rather return AggregateMetricsError that can be logged somewhere else.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 AggregateMetricsError but I can do it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether you return () or the error enum shouldn't affect the result size here since Self is larger anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. Maybe log this on debug! then, we certainly don't want that in a production instance.

Copy link
Member Author

Choose a reason for hiding this comment

The 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');
Copy link
Member

Choose a reason for hiding this comment

The 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`].
Expand Down Expand Up @@ -1055,6 +1080,8 @@ impl Aggregator {
let timestamp = key.timestamp;
let project_key = key.project_key;

let key = key.validate_strings().map_err(|()| AggregateMetricsError)?;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will fail the entire batch. we have two options:

  • map the error to return Ok(())
  • make the error type an enum with multiple variants, and have matching logic in merge_all

Copy link
Member Author

Choose a reason for hiding this comment

The 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 get_bucket_timestamps where the entire batch fails as well, for no discernible reason


match self.buckets.entry(key) {
Entry::Occupied(mut entry) => {
relay_statsd::metric!(
Expand Down Expand Up @@ -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();
}
}