Skip to content

Commit

Permalink
feat(receiver-mock): add support for regexes in log queries
Browse files Browse the repository at this point in the history
  • Loading branch information
aboguszewski-sumo committed Nov 22, 2022
1 parent ad7c6c0 commit c1ac3f5
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 30 deletions.
34 changes: 30 additions & 4 deletions src/rust/receiver-mock/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/rust/receiver-mock/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ itertools = "0.10.5"
log = "0.4.17"
simple_logger = "4.0.0"
hex = "0.4.3"
fancy-regex = "0.10.0"
11 changes: 10 additions & 1 deletion src/rust/receiver-mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,14 +267,23 @@ The following endpoints provide information about received logs:
Returns the number of logs received between `from_ts` and `to_ts`. The values are epoch timestamps in milliseconds, and the range represented by them is inclusive at the start and exclusive at the end. Both values are optional.

It's also possible to filter by log metadata. Any query parameter without a fixed meaning (such as `from_ts`) will be treated
as a key-value pair of metadata, and only logs containing that pair will be counted. Similarly to the metrics samples endpoint, an empty value is treated as a wildcard.
as a key-value pair of metadata. Logs are the only data that can be queried using regexes.
To achieve backward compatibility, there are two exceptions to the regexes:
- an empty value is treated as a wildcard
- only exact matches are matched, for example regex `foo` will match only string `foo` and nothing else
In case of any doubt regarding the regexes, please refer to the documentation of [fancy-regex] crate.
Sample response:
```json
{
"count": 7
}
```
[fancy-regex]: https://docs.rs/fancy-regex/0.10.0/fancy_regex/index.html
## Dump message
Receiver mock comes with special `/dump` endpoint, which is going to print message on stdout independently on the header value.
Expand Down
115 changes: 97 additions & 18 deletions src/rust/receiver-mock/src/logs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::time;
use anyhow::Result;
use fancy_regex::Regex;
use log::warn;
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
Expand Down Expand Up @@ -98,37 +100,45 @@ impl LogRepository {

// Count logs with timestamps in the provided range, with the provided metadata. Empty values
// in the metadata map mean we just check if the key is there.
pub fn get_message_count(&self, from_ts: u64, to_ts: u64, metadata_query: HashMap<&str, &str>) -> usize {
pub fn get_message_count(&self, from_ts: u64, to_ts: u64, metadata_query: HashMap<&str, &str>) -> Result<usize> {
let mut count = 0;
let entries = self.messages_by_ts.range(from_ts..to_ts);
for (_, messages) in entries {
for message in messages {
if Self::metadata_matches(&metadata_query, &message.metadata) {
if Self::metadata_matches(&metadata_query, &message.metadata)? {
count += 1
}
}
}
return count;
return Ok(count);
}

// Check if log metadata matches a query in the form of a map of string to string.
// There's a match if the metadata contains the same keys and values as the query.
// The query value of an empty string has special meaning, it matches anything.
fn metadata_matches(query: &HashMap<&str, &str>, target: &Metadata) -> bool {
fn metadata_matches(query: &HashMap<&str, &str>, target: &Metadata) -> Result<bool> {
for (key, value) in query.iter() {
let target_value = match target.get(*key) {
// get the value from the target
Some(v) => v,
None => return false, // key not present, no match
None => return Ok(false), // key not present, no match
};
if value.len() > 0 {
// TODO: regex support is available, so we can remove support for "", but it will break the API
// always match if query value is ""
if value != target_value {
return false; // different values, no match

// TODO: add caching the regexes
// To keep backward compatibility, by default match only when whole string is the match.
let re = Regex::new(&format!("^{}$", value))?;

let match_res = re.is_match(target_value)?;

if !match_res {
return Ok(false);
}
}
}
return true;
return Ok(true);
}
}

Expand Down Expand Up @@ -207,9 +217,9 @@ mod tests {
.collect();
let repository = LogRepository::from_raw_logs(raw_logs).unwrap();

assert_eq!(repository.get_message_count(1, 6, HashMap::new()), 2);
assert_eq!(repository.get_message_count(0, 10, HashMap::new()), 3);
assert_eq!(repository.get_message_count(2, 3, HashMap::new()), 0);
assert_eq!(repository.get_message_count(1, 6, HashMap::new()).unwrap(), 2);
assert_eq!(repository.get_message_count(0, 10, HashMap::new()).unwrap(), 3);
assert_eq!(repository.get_message_count(2, 3, HashMap::new()).unwrap(), 0);
}

#[test]
Expand All @@ -233,23 +243,92 @@ mod tests {
let repository = LogRepository::from_raw_logs(raw_logs).unwrap();

assert_eq!(
repository.get_message_count(0, 100, HashMap::from_iter(vec![("key", "value")].into_iter())),
repository
.get_message_count(0, 100, HashMap::from_iter(vec![("key", "value")].into_iter()))
.unwrap(),
1
);
assert_eq!(
repository.get_message_count(0, 100, HashMap::from_iter(vec![("key", "")].into_iter())),
repository
.get_message_count(0, 100, HashMap::from_iter(vec![("key", "")].into_iter()))
.unwrap(),
2
);
assert_eq!(
repository.get_message_count(
0,
100,
HashMap::from_iter(vec![("key", "valueprime"), ("key2", "value2")].into_iter())
),
repository
.get_message_count(
0,
100,
HashMap::from_iter(vec![("key", "valueprime"), ("key2", "value2")].into_iter())
)
.unwrap(),
1
);
}

#[test]
fn test_repo_metadata_query_regex() {
let metadata = [
Metadata::from_iter(vec![("key".to_string(), "value".to_string())].into_iter()),
Metadata::from_iter(vec![("key".to_string(), "valueSUFFIX".to_string())].into_iter()),
Metadata::from_iter(vec![("key".to_string(), "PREFIXvalue".to_string())].into_iter()),
Metadata::from_iter(vec![("key".to_string(), "PREFIXvalueSUFFIX".to_string())].into_iter()),
Metadata::from_iter(vec![("key".to_string(), "undefined".to_string())].into_iter()),
Metadata::from_iter(vec![("key".to_string(), "not undefined".to_string())].into_iter()),
];
let body = "{\"log\": \"Log message\", \"timestamp\": 1}";
let raw_logs = metadata
.iter()
.map(|mt| (body.to_string(), mt.to_owned()))
.collect();
let repository = LogRepository::from_raw_logs(raw_logs).unwrap();

// Check backward compatibility (match only exact matches)
assert_eq!(
repository
.get_message_count(0, 100, HashMap::from_iter(vec![("key", "value")].into_iter()))
.unwrap(),
1
);

// Check backward compatibility (empty matches all)
assert_eq!(
repository
.get_message_count(0, 100, HashMap::from_iter(vec![("key", "")].into_iter()))
.unwrap(),
6
);

assert_eq!(
repository
.get_message_count(0, 100, HashMap::from_iter(vec![("key", "value.*")].into_iter()))
.unwrap(),
2
);

assert_eq!(
repository
.get_message_count(
0,
100,
HashMap::from_iter(vec![("key", ".*value.*")].into_iter())
)
.unwrap(),
4
);

assert_eq!(
repository
.get_message_count(
0,
100,
HashMap::from_iter(vec![("key", "(?!undefined$).*")].into_iter())
)
.unwrap(),
5
);
}

#[test]
fn test_get_timestamp_from_body() {
assert_eq!(
Expand Down
18 changes: 11 additions & 7 deletions src/rust/receiver-mock/src/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,17 @@ pub async fn handler_logs_count(
.filter(|(key, _)| !fixed_params.contains(key.as_str()))
.map(|(key, value)| (key.as_str(), value.as_str()))
.collect();
let count = app_state
.log_messages
.read()
.unwrap()
.get_message_count(params.from_ts, params.to_ts, metadata_params);

HttpResponse::Ok().json(LogsCountResponse { count })
let count_res =
app_state
.log_messages
.read()
.unwrap()
.get_message_count(params.from_ts, params.to_ts, metadata_params);

match count_res {
Ok(count) => HttpResponse::Ok().json(LogsCountResponse { count }),
Err(e) => HttpResponse::BadRequest().json(e.to_string()),
}
}

pub async fn handler_dump(body: web::Bytes) -> impl Responder {
Expand Down

0 comments on commit c1ac3f5

Please sign in to comment.