Skip to content

Commit f37ffd9

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

File tree

7 files changed

+346
-0
lines changed

7 files changed

+346
-0
lines changed

quickwit/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/quickwit-integration-tests/Cargo.toml

+1
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

+2
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};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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 std::collections::HashMap;
16+
17+
use regex::Regex;
18+
19+
#[derive(Debug, PartialEq, Clone)]
20+
pub struct PrometheusMetric {
21+
pub name: String,
22+
pub labels: HashMap<String, String>,
23+
pub metric_value: f64,
24+
}
25+
26+
/// Parse Prometheus metrics serialized with prometheus::TextEncoder
27+
///
28+
/// Unfortunately, the prometheus crate does not provide a way to parse metrics
29+
pub fn parse_prometheus_metrics(input: &str) -> Vec<PrometheusMetric> {
30+
let mut metrics = Vec::new();
31+
let re = Regex::new(r"(?P<name>[^{]+)(?:\{(?P<labels>[^\}]*)\})? (?P<value>.+)").unwrap();
32+
33+
for line in input.lines() {
34+
if line.starts_with('#') {
35+
continue;
36+
}
37+
38+
if let Some(caps) = re.captures(line) {
39+
let name = caps.name("name").unwrap().as_str().to_string();
40+
let metric_value: f64 = caps
41+
.name("value")
42+
.unwrap()
43+
.as_str()
44+
.parse()
45+
.expect("Failed to parse value");
46+
47+
let labels = caps.name("labels").map_or(HashMap::new(), |m| {
48+
m.as_str()
49+
.split(',')
50+
.map(|label| {
51+
let mut parts = label.splitn(2, '=');
52+
let key = parts.next().unwrap().to_string();
53+
let value = parts.next().unwrap().trim_matches('"').to_string();
54+
(key, value)
55+
})
56+
.collect()
57+
});
58+
59+
metrics.push(PrometheusMetric {
60+
name,
61+
labels,
62+
metric_value,
63+
});
64+
}
65+
}
66+
67+
metrics
68+
}
69+
70+
/// Filter metrics by name and a subset of the available labels
71+
///
72+
/// Specify an empty Vec of labels to return all metrics with the specified name
73+
pub fn filter_metrics(
74+
metrics: &[PrometheusMetric],
75+
name: &str,
76+
labels: Vec<(&'static str, &'static str)>,
77+
) -> Vec<PrometheusMetric> {
78+
metrics
79+
.iter()
80+
.filter(|metric| metric.name == name)
81+
.filter(|metric| {
82+
labels
83+
.iter()
84+
.all(|(key, value)| metric.labels.get(*key).map(String::as_str) == Some(*value))
85+
})
86+
.cloned()
87+
.collect()
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
94+
const TEST_INPUT: &str = r#"
95+
quickwit_search_leaf_search_single_split_warmup_num_bytes_sum 0
96+
# HELP quickwit_storage_object_storage_request_duration_seconds Duration of object storage requests in seconds.
97+
# TYPE quickwit_storage_object_storage_request_duration_seconds histogram
98+
quickwit_storage_object_storage_request_duration_seconds_bucket{action="delete_objects",le="30"} 0
99+
quickwit_storage_object_storage_request_duration_seconds_bucket{action="delete_objects",le="+Inf"} 0
100+
quickwit_storage_object_storage_request_duration_seconds_sum{action="delete_objects"} 0
101+
quickwit_search_root_search_request_duration_seconds_sum{kind="server",status="success"} 0.004093958
102+
quickwit_storage_object_storage_requests_total{action="delete_object"} 0
103+
quickwit_storage_object_storage_requests_total{action="delete_objects"} 0
104+
"#;
105+
106+
#[test]
107+
fn test_parse_prometheus_metrics() {
108+
let metrics = parse_prometheus_metrics(TEST_INPUT);
109+
assert_eq!(metrics.len(), 7);
110+
assert_eq!(
111+
metrics[0],
112+
PrometheusMetric {
113+
name: "quickwit_search_leaf_search_single_split_warmup_num_bytes_sum".to_string(),
114+
labels: HashMap::new(),
115+
metric_value: 0.0,
116+
}
117+
);
118+
assert_eq!(
119+
metrics[1],
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(), "30".to_string())
125+
]
126+
.iter()
127+
.cloned()
128+
.collect(),
129+
metric_value: 0.0,
130+
}
131+
);
132+
assert_eq!(
133+
metrics[2],
134+
PrometheusMetric {
135+
name: "quickwit_storage_object_storage_request_duration_seconds_bucket".to_string(),
136+
labels: [
137+
("action".to_string(), "delete_objects".to_string()),
138+
("le".to_string(), "+Inf".to_string())
139+
]
140+
.iter()
141+
.cloned()
142+
.collect(),
143+
metric_value: 0.0,
144+
}
145+
);
146+
assert_eq!(
147+
metrics[3],
148+
PrometheusMetric {
149+
name: "quickwit_storage_object_storage_request_duration_seconds_sum".to_string(),
150+
labels: [("action".to_string(), "delete_objects".to_string())]
151+
.iter()
152+
.cloned()
153+
.collect(),
154+
metric_value: 0.0,
155+
}
156+
);
157+
assert_eq!(
158+
metrics[4],
159+
PrometheusMetric {
160+
name: "quickwit_search_root_search_request_duration_seconds_sum".to_string(),
161+
labels: [
162+
("kind".to_string(), "server".to_string()),
163+
("status".to_string(), "success".to_string())
164+
]
165+
.iter()
166+
.cloned()
167+
.collect(),
168+
metric_value: 0.004093958,
169+
}
170+
);
171+
assert_eq!(
172+
metrics[5],
173+
PrometheusMetric {
174+
name: "quickwit_storage_object_storage_requests_total".to_string(),
175+
labels: [("action".to_string(), "delete_object".to_string())]
176+
.iter()
177+
.cloned()
178+
.collect(),
179+
metric_value: 0.0,
180+
}
181+
);
182+
assert_eq!(
183+
metrics[6],
184+
PrometheusMetric {
185+
name: "quickwit_storage_object_storage_requests_total".to_string(),
186+
labels: [("action".to_string(), "delete_objects".to_string())]
187+
.iter()
188+
.cloned()
189+
.collect(),
190+
metric_value: 0.0,
191+
}
192+
);
193+
}
194+
195+
#[test]
196+
fn test_filter_prometheus_metrics() {
197+
let metrics = parse_prometheus_metrics(TEST_INPUT);
198+
{
199+
let filtered_metric = filter_metrics(
200+
&metrics,
201+
"quickwit_storage_object_storage_request_duration_seconds_bucket",
202+
vec![],
203+
);
204+
assert_eq!(filtered_metric.len(), 2);
205+
}
206+
{
207+
let filtered_metric = filter_metrics(
208+
&metrics,
209+
"quickwit_search_root_search_request_duration_seconds_sum",
210+
vec![("status", "success")],
211+
);
212+
assert_eq!(filtered_metric.len(), 1);
213+
}
214+
{
215+
let filtered_metric =
216+
filter_metrics(&metrics, "quickwit_doest_not_exist_metric", vec![]);
217+
assert_eq!(filtered_metric.len(), 0);
218+
}
219+
{
220+
let filtered_metric = filter_metrics(
221+
&metrics,
222+
"quickwit_storage_object_storage_requests_total",
223+
vec![("does_not_exist_label", "value")],
224+
);
225+
assert_eq!(filtered_metric.len(), 0);
226+
}
227+
{
228+
let filtered_metric = filter_metrics(
229+
&metrics,
230+
"quickwit_storage_object_storage_requests_total",
231+
vec![("action", "does_not_exist_value")],
232+
);
233+
assert_eq!(filtered_metric.len(), 0);
234+
}
235+
}
236+
}

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

+1
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;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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());
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+
// The assertions validate some very specific metrics. Feel free to add more as needed.
76+
{
77+
let quickwit_http_requests_total_get_metrics = filter_metrics(
78+
&metrics,
79+
"quickwit_http_requests_total",
80+
vec![("method", "GET")],
81+
);
82+
assert_eq!(quickwit_http_requests_total_get_metrics.len(), 1);
83+
// we don't know exactly how many GET requests to expect as they are used to
84+
// poll the node state
85+
assert!(quickwit_http_requests_total_get_metrics[0].metric_value > 0.0);
86+
}
87+
{
88+
let quickwit_http_requests_total_post_metrics = filter_metrics(
89+
&metrics,
90+
"quickwit_http_requests_total",
91+
vec![("method", "POST")],
92+
);
93+
assert_eq!(quickwit_http_requests_total_post_metrics.len(), 1);
94+
// 2 POST requests: create index + search
95+
assert_eq!(
96+
quickwit_http_requests_total_post_metrics[0].metric_value,
97+
2.0
98+
);
99+
}
100+
sandbox.shutdown().await.unwrap();
101+
}

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

+4
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)