Skip to content

Commit 2557394

Browse files
committed
Add Prometheus metrics test
1 parent ec36892 commit 2557394

File tree

7 files changed

+289
-0
lines changed

7 files changed

+289
-0
lines changed

quickwit/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/quickwit-integration-tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ aws-sdk-sqs = { workspace = true }
2424
futures-util = { workspace = true }
2525
hyper = { workspace = true }
2626
itertools = { workspace = true }
27+
regex = { workspace = true }
2728
reqwest = { workspace = true }
2829
serde_json = { workspace = true }
2930
tempfile = { workspace = true }

quickwit/quickwit-integration-tests/src/test_utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414

1515
mod cluster_sandbox;
16+
mod prometheus_parser;
1617
mod shutdown;
1718

1819
pub(crate) use cluster_sandbox::{ingest, ClusterSandbox, ClusterSandboxBuilder};
20+
pub(crate) use prometheus_parser::{filter_metrics, parse_prometheus_metrics};
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use std::collections::HashMap;
2+
3+
use regex::Regex;
4+
5+
#[derive(Debug, PartialEq, Clone)]
6+
pub struct PrometheusMetric {
7+
pub name: String,
8+
pub labels: HashMap<String, String>,
9+
pub metric_value: f64,
10+
}
11+
12+
/// Parse Prometheus metrics serialized with prometheus::TextEncoder
13+
///
14+
/// Unfortunately, the prometheus crate does not provide a way to parse metrics
15+
pub fn parse_prometheus_metrics(input: &str) -> Vec<PrometheusMetric> {
16+
let mut metrics = Vec::new();
17+
let re = Regex::new(r"(?P<name>[^{]+)(?:\{(?P<labels>[^\}]*)\})? (?P<value>.+)").unwrap();
18+
19+
for line in input.lines() {
20+
if line.starts_with('#') {
21+
continue;
22+
}
23+
24+
if let Some(caps) = re.captures(line) {
25+
let name = caps.name("name").unwrap().as_str().to_string();
26+
let metric_value: f64 = caps
27+
.name("value")
28+
.unwrap()
29+
.as_str()
30+
.parse()
31+
.expect("Failed to parse value");
32+
33+
let labels = caps.name("labels").map_or(HashMap::new(), |m| {
34+
m.as_str()
35+
.split(',')
36+
.map(|label| {
37+
let mut parts = label.splitn(2, '=');
38+
let key = parts.next().unwrap().to_string();
39+
let value = parts.next().unwrap().trim_matches('"').to_string();
40+
(key, value)
41+
})
42+
.collect()
43+
});
44+
45+
metrics.push(PrometheusMetric {
46+
name,
47+
labels,
48+
metric_value,
49+
});
50+
}
51+
}
52+
53+
metrics
54+
}
55+
56+
/// Filter metrics by name and a subset of the available labels
57+
///
58+
/// Specify an empty Vec of labels to return all metrics with the specified name
59+
pub fn filter_metrics(
60+
metrics: &[PrometheusMetric],
61+
name: &str,
62+
labels: Vec<(&'static str, &'static str)>,
63+
) -> Vec<PrometheusMetric> {
64+
metrics
65+
.into_iter()
66+
.filter(|metric| metric.name == name)
67+
.filter(|metric| {
68+
labels
69+
.iter()
70+
.all(|(key, value)| metric.labels.get(*key).unwrap() == value)
71+
})
72+
.cloned()
73+
.collect()
74+
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
use super::*;
79+
80+
#[test]
81+
fn test_parse_prometheus_metrics() {
82+
let input = r#"
83+
quickwit_search_leaf_search_single_split_warmup_num_bytes_sum 0
84+
# HELP quickwit_storage_object_storage_request_duration_seconds Duration of object storage requests in seconds.
85+
# TYPE quickwit_storage_object_storage_request_duration_seconds histogram
86+
quickwit_storage_object_storage_request_duration_seconds_bucket{action="delete_objects",le="30"} 0
87+
quickwit_storage_object_storage_request_duration_seconds_bucket{action="delete_objects",le="+Inf"} 0
88+
quickwit_storage_object_storage_request_duration_seconds_sum{action="delete_objects"} 0
89+
quickwit_search_root_search_request_duration_seconds_sum{kind="server",status="success"} 0.004093958
90+
quickwit_storage_object_storage_requests_total{action="delete_object"} 0
91+
quickwit_storage_object_storage_requests_total{action="delete_objects"} 0
92+
"#;
93+
94+
let metrics = parse_prometheus_metrics(input);
95+
assert_eq!(metrics.len(), 7);
96+
assert_eq!(
97+
metrics[0],
98+
PrometheusMetric {
99+
name: "quickwit_search_leaf_search_single_split_warmup_num_bytes_sum".to_string(),
100+
labels: HashMap::new(),
101+
metric_value: 0.0,
102+
}
103+
);
104+
assert_eq!(
105+
metrics[1],
106+
PrometheusMetric {
107+
name: "quickwit_storage_object_storage_request_duration_seconds_bucket".to_string(),
108+
labels: [
109+
("action".to_string(), "delete_objects".to_string()),
110+
("le".to_string(), "30".to_string())
111+
]
112+
.iter()
113+
.cloned()
114+
.collect(),
115+
metric_value: 0.0,
116+
}
117+
);
118+
assert_eq!(
119+
metrics[2],
120+
PrometheusMetric {
121+
name: "quickwit_storage_object_storage_request_duration_seconds_bucket".to_string(),
122+
labels: [
123+
("action".to_string(), "delete_objects".to_string()),
124+
("le".to_string(), "+Inf".to_string())
125+
]
126+
.iter()
127+
.cloned()
128+
.collect(),
129+
metric_value: 0.0,
130+
}
131+
);
132+
assert_eq!(
133+
metrics[3],
134+
PrometheusMetric {
135+
name: "quickwit_storage_object_storage_request_duration_seconds_sum".to_string(),
136+
labels: [("action".to_string(), "delete_objects".to_string())]
137+
.iter()
138+
.cloned()
139+
.collect(),
140+
metric_value: 0.0,
141+
}
142+
);
143+
assert_eq!(
144+
metrics[4],
145+
PrometheusMetric {
146+
name: "quickwit_search_root_search_request_duration_seconds_sum".to_string(),
147+
labels: [
148+
("kind".to_string(), "server".to_string()),
149+
("status".to_string(), "success".to_string())
150+
]
151+
.iter()
152+
.cloned()
153+
.collect(),
154+
metric_value: 0.004093958,
155+
}
156+
);
157+
assert_eq!(
158+
metrics[5],
159+
PrometheusMetric {
160+
name: "quickwit_storage_object_storage_requests_total".to_string(),
161+
labels: [("action".to_string(), "delete_object".to_string())]
162+
.iter()
163+
.cloned()
164+
.collect(),
165+
metric_value: 0.0,
166+
}
167+
);
168+
assert_eq!(
169+
metrics[6],
170+
PrometheusMetric {
171+
name: "quickwit_storage_object_storage_requests_total".to_string(),
172+
labels: [("action".to_string(), "delete_objects".to_string())]
173+
.iter()
174+
.cloned()
175+
.collect(),
176+
metric_value: 0.0,
177+
}
178+
);
179+
}
180+
}

quickwit/quickwit-integration-tests/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod ingest_v1_tests;
1717
mod ingest_v2_tests;
1818
mod no_cp_tests;
1919
mod otlp_tests;
20+
mod prometheus_tests;
2021
#[cfg(feature = "sqs-localstack-tests")]
2122
mod sqs_tests;
2223
mod tls_tests;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2021-Present Datadog, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use quickwit_config::service::QuickwitService;
16+
use quickwit_serve::SearchRequestQueryString;
17+
18+
use crate::test_utils::{filter_metrics, parse_prometheus_metrics, ClusterSandboxBuilder};
19+
20+
#[tokio::test]
21+
async fn test_metrics_standalone_server() {
22+
quickwit_common::setup_logging_for_tests();
23+
let sandbox = ClusterSandboxBuilder::build_and_start_standalone().await;
24+
let client = sandbox.rest_client(QuickwitService::Indexer);
25+
26+
client
27+
.indexes()
28+
.create(
29+
r#"
30+
version: 0.8
31+
index_id: my-new-index
32+
doc_mapping:
33+
field_mappings:
34+
- name: body
35+
type: text
36+
"#,
37+
quickwit_config::ConfigFormat::Yaml,
38+
false,
39+
)
40+
.await
41+
.unwrap();
42+
43+
assert_eq!(
44+
client
45+
.search(
46+
"my-new-index",
47+
SearchRequestQueryString {
48+
query: "body:test".to_string(),
49+
max_hits: 10,
50+
..Default::default()
51+
},
52+
)
53+
.await
54+
.unwrap()
55+
.num_hits,
56+
0
57+
);
58+
59+
let prometheus_url = format!("{}metrics", client.base_url().to_string());
60+
let response = reqwest::Client::new()
61+
.get(&prometheus_url)
62+
.send()
63+
.await
64+
.expect("Failed to send request");
65+
66+
assert!(
67+
response.status().is_success(),
68+
"Request failed with status {}",
69+
response.status(),
70+
);
71+
72+
let body = response.text().await.expect("Failed to read response body");
73+
// println!("Prometheus metrics:\n{}", body);
74+
let metrics = parse_prometheus_metrics(&body);
75+
{
76+
let quickwit_http_requests_total_get_metrics = filter_metrics(
77+
&metrics,
78+
"quickwit_http_requests_total",
79+
vec![("method", "GET")],
80+
);
81+
assert_eq!(quickwit_http_requests_total_get_metrics.len(), 1);
82+
// we don't know exactly how many GET requests to expect as they are used to
83+
// poll the node state
84+
assert!(quickwit_http_requests_total_get_metrics[0].metric_value > 0.0);
85+
}
86+
{
87+
let quickwit_http_requests_total_post_metrics = filter_metrics(
88+
&metrics,
89+
"quickwit_http_requests_total",
90+
vec![("method", "POST")],
91+
);
92+
assert_eq!(quickwit_http_requests_total_post_metrics.len(), 1);
93+
// 2 POST requests: create index + search
94+
assert_eq!(
95+
quickwit_http_requests_total_post_metrics[0].metric_value,
96+
2.0
97+
);
98+
}
99+
sandbox.shutdown().await.unwrap();
100+
}

quickwit/quickwit-rest-client/src/rest_client.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@ impl QuickwitClient {
342342

343343
Ok(cumulated_resp)
344344
}
345+
346+
pub fn base_url(&self) -> &Url {
347+
&self.transport.base_url
348+
}
345349
}
346350

347351
pub enum IngestEvent {

0 commit comments

Comments
 (0)