Skip to content

Commit c308769

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

File tree

7 files changed

+332
-0
lines changed

7 files changed

+332
-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,222 @@
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+
.iter()
66+
.filter(|metric| metric.name == name)
67+
.filter(|metric| {
68+
labels
69+
.iter()
70+
.all(|(key, value)| metric.labels.get(*key).map(String::as_str) == Some(*value))
71+
})
72+
.cloned()
73+
.collect()
74+
}
75+
76+
#[cfg(test)]
77+
mod tests {
78+
use super::*;
79+
80+
const TEST_INPUT: &str = r#"
81+
quickwit_search_leaf_search_single_split_warmup_num_bytes_sum 0
82+
# HELP quickwit_storage_object_storage_request_duration_seconds Duration of object storage requests in seconds.
83+
# TYPE quickwit_storage_object_storage_request_duration_seconds histogram
84+
quickwit_storage_object_storage_request_duration_seconds_bucket{action="delete_objects",le="30"} 0
85+
quickwit_storage_object_storage_request_duration_seconds_bucket{action="delete_objects",le="+Inf"} 0
86+
quickwit_storage_object_storage_request_duration_seconds_sum{action="delete_objects"} 0
87+
quickwit_search_root_search_request_duration_seconds_sum{kind="server",status="success"} 0.004093958
88+
quickwit_storage_object_storage_requests_total{action="delete_object"} 0
89+
quickwit_storage_object_storage_requests_total{action="delete_objects"} 0
90+
"#;
91+
92+
#[test]
93+
fn test_parse_prometheus_metrics() {
94+
let metrics = parse_prometheus_metrics(TEST_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+
181+
#[test]
182+
fn test_filter_prometheus_metrics() {
183+
let metrics = parse_prometheus_metrics(TEST_INPUT);
184+
{
185+
let filtered_metric = filter_metrics(
186+
&metrics,
187+
"quickwit_storage_object_storage_request_duration_seconds_bucket",
188+
vec![],
189+
);
190+
assert_eq!(filtered_metric.len(), 2);
191+
}
192+
{
193+
let filtered_metric = filter_metrics(
194+
&metrics,
195+
"quickwit_search_root_search_request_duration_seconds_sum",
196+
vec![("status", "success")],
197+
);
198+
assert_eq!(filtered_metric.len(), 1);
199+
}
200+
{
201+
let filtered_metric =
202+
filter_metrics(&metrics, "quickwit_doest_not_exist_metric", vec![]);
203+
assert_eq!(filtered_metric.len(), 0);
204+
}
205+
{
206+
let filtered_metric = filter_metrics(
207+
&metrics,
208+
"quickwit_storage_object_storage_requests_total",
209+
vec![("does_not_exist_label", "value")],
210+
);
211+
assert_eq!(filtered_metric.len(), 0);
212+
}
213+
{
214+
let filtered_metric = filter_metrics(
215+
&metrics,
216+
"quickwit_storage_object_storage_requests_total",
217+
vec![("action", "does_not_exist_value")],
218+
);
219+
assert_eq!(filtered_metric.len(), 0);
220+
}
221+
}
222+
}

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)