Skip to content

Commit d138c3e

Browse files
feat: Added logic to automatically infer list of metrics and labels for each metric from Prometheus (#261)
* Added logic to automatically infer list of metrics and labels for each metric from Prometheus * Modified asap-quickstart and asap-tools/experiments pipeline to account for cmd line changes to asap-planner-rs * added debug statements, added option to fallback to metrics if auto-infer fails, updated tests * fixed bug in experiment scripts * Addewd retry logic to asap-planner for getting metrics/labels from Prometheus * Added some delay to Prometheus and modified remote write config to have a limit * Changed CI to NOT use auto-discovery of metrics/labels * refactored changed to from_file_with_schema * refactor
1 parent 6dac082 commit d138c3e

33 files changed

Lines changed: 556 additions & 162 deletions

‎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.

‎asap-planner-rs/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ clap.workspace = true
2727
indexmap.workspace = true
2828
chrono.workspace = true
2929
promql-parser = "0.5.0"
30+
reqwest = { version = "0.11", features = ["blocking", "json"] }
3031

3132
[dev-dependencies]
3233
tempfile = "3.20"

‎asap-planner-rs/docker-compose.yml.j2‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ services:
99
"--input_config", "/app/input/config.yaml",
1010
"--output_dir", "/app/output",
1111
"--prometheus_scrape_interval", "{{ prometheus_scrape_interval }}",
12-
"--streaming_engine", "{{ streaming_engine }}"{% if punting %},
12+
"--streaming_engine", "{{ streaming_engine }}",
13+
"--prometheus-url", "{{ prometheus_url }}"{% if punting %},
1314
"--enable-punting"{% endif %}
1415
]
1516
restart: no

‎asap-planner-rs/src/config/input.rs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
1+
use promql_utilities::data_model::KeyByLabelNames;
12
use serde::Deserialize;
3+
use sketch_db_common::PromQLSchema;
24

35
#[derive(Debug, Clone, Deserialize)]
6+
#[serde(deny_unknown_fields)]
47
pub struct ControllerConfig {
58
pub query_groups: Vec<QueryGroup>,
6-
pub metrics: Vec<MetricDefinition>,
79
pub sketch_parameters: Option<SketchParameterOverrides>,
810
pub aggregate_cleanup: Option<AggregateCleanupConfig>,
11+
/// Optional hint: per-metric label sets used as a fallback when Prometheus
12+
/// returns no series for a metric. Prometheus-inferred labels take priority.
13+
#[serde(default)]
14+
pub metrics: Option<Vec<MetricDefinition>>,
15+
}
16+
17+
impl ControllerConfig {
18+
/// Build a `PromQLSchema` from the `metrics` hints in this config.
19+
/// Returns an empty schema if no hints are present.
20+
pub fn schema_from_hints(&self) -> PromQLSchema {
21+
let mut schema = PromQLSchema::new();
22+
if let Some(metrics) = &self.metrics {
23+
for m in metrics {
24+
schema =
25+
schema.add_metric(m.metric.clone(), KeyByLabelNames::new(m.labels.clone()));
26+
}
27+
}
28+
schema
29+
}
930
}
1031

1132
#[derive(Debug, Clone, Deserialize)]

‎asap-planner-rs/src/error.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ pub enum ControllerError {
1818
SqlParse(String),
1919
#[error("Unknown table: {0}")]
2020
UnknownTable(String),
21+
#[error("Prometheus client error: {0}")]
22+
PrometheusClient(String),
2123
}

‎asap-planner-rs/src/lib.rs‎

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@ pub mod config;
22
pub mod error;
33
pub mod output;
44
pub mod planner;
5+
pub mod prometheus_client;
56
pub mod query_log;
67

8+
use promql_utilities::data_model::KeyByLabelNames;
79
use serde_yaml::Value as YamlValue;
810
use sketch_db_common::enums::QueryLanguage;
911
use sketch_db_common::inference_config::InferenceConfig;
1012
use sketch_db_common::streaming_config::StreamingConfig;
1113
use std::path::Path;
14+
use tracing::debug;
1215

1316
pub use config::input::ControllerConfig;
1417
pub use config::input::SQLControllerConfig;
1518
pub use error::ControllerError;
1619
pub use output::generator::{GeneratorOutput, PuntedQuery};
1720
pub use output::sql_generator::SQLRuntimeOptions;
21+
pub use prometheus_client::build_schema_from_prometheus;
22+
pub use sketch_db_common::PromQLSchema;
1823

1924
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2025
pub enum StreamingEngine {
@@ -34,6 +39,7 @@ pub struct RuntimeOptions {
3439

3540
pub struct Controller {
3641
config: ControllerConfig,
42+
schema: PromQLSchema,
3743
options: RuntimeOptions,
3844
}
3945

@@ -302,49 +308,135 @@ impl SQLController {
302308
}
303309

304310
impl Controller {
305-
pub fn new(config: ControllerConfig, options: RuntimeOptions) -> Self {
306-
Self { config, options }
311+
pub fn new(config: ControllerConfig, schema: PromQLSchema, options: RuntimeOptions) -> Self {
312+
Self {
313+
config,
314+
schema,
315+
options,
316+
}
307317
}
308318

309-
pub fn from_file(path: &Path, opts: RuntimeOptions) -> Result<Self, ControllerError> {
319+
/// Build a `Controller` from a config file, fetching metric labels from Prometheus.
320+
///
321+
/// `prometheus_url` is queried via `GET /api/v1/series?match[]=<metric>` for each metric
322+
/// name found in the config's PromQL queries.
323+
pub fn from_file(
324+
path: &Path,
325+
opts: RuntimeOptions,
326+
prometheus_url: &str,
327+
) -> Result<Self, ControllerError> {
310328
let yaml_str = std::fs::read_to_string(path)?;
311-
Self::from_yaml(&yaml_str, opts)
329+
let config: ControllerConfig = serde_yaml::from_str(&yaml_str)?;
330+
let all_queries: Vec<String> = config
331+
.query_groups
332+
.iter()
333+
.flat_map(|qg| qg.queries.clone())
334+
.collect();
335+
let mut schema =
336+
prometheus_client::build_schema_from_prometheus(prometheus_url, &all_queries)?;
337+
// For any metric that Prometheus had no series for, fall back to the
338+
// `metrics` hint in the config file (if present).
339+
if let Some(metric_hints) = &config.metrics {
340+
for hint in metric_hints {
341+
if !schema.config.contains_key(&hint.metric) {
342+
debug!(
343+
"Prometheus had no series for '{}'; falling back to config-file hint with labels {:?}",
344+
hint.metric, hint.labels
345+
);
346+
schema = schema.add_metric(
347+
hint.metric.clone(),
348+
KeyByLabelNames::new(hint.labels.clone()),
349+
);
350+
}
351+
}
352+
}
353+
Ok(Self {
354+
config,
355+
schema,
356+
options: opts,
357+
})
358+
}
359+
360+
/// Build a `Controller` from a config file with a caller-supplied `PromQLSchema`.
361+
///
362+
/// Use this when the schema is available without querying Prometheus (e.g. in tests
363+
/// or when the schema is constructed in-process by the caller).
364+
pub fn from_file_with_schema(
365+
path: &Path,
366+
schema: PromQLSchema,
367+
opts: RuntimeOptions,
368+
) -> Result<Self, ControllerError> {
369+
let yaml_str = std::fs::read_to_string(path)?;
370+
let config: ControllerConfig = serde_yaml::from_str(&yaml_str)?;
371+
Ok(Self {
372+
config,
373+
schema,
374+
options: opts,
375+
})
312376
}
313377

314-
pub fn from_yaml(yaml: &str, opts: RuntimeOptions) -> Result<Self, ControllerError> {
378+
/// Build a `Controller` from a YAML string with a caller-supplied `PromQLSchema`.
379+
pub fn from_yaml_with_schema(
380+
yaml: &str,
381+
schema: PromQLSchema,
382+
opts: RuntimeOptions,
383+
) -> Result<Self, ControllerError> {
315384
let config: ControllerConfig = serde_yaml::from_str(yaml)?;
316385
Ok(Self {
317386
config,
387+
schema,
318388
options: opts,
319389
})
320390
}
321391

322-
/// Build a `Controller` from a Prometheus query log file and a metrics config YAML.
392+
/// Build a `Controller` from a Prometheus query log file, fetching metric labels from
393+
/// Prometheus.
323394
///
324395
/// - `log_path`: newline-delimited JSON query log (Prometheus `--query.log-file` output)
325-
/// - `metrics_path`: YAML file with a `metrics:` section listing metric names and labels
396+
/// - `prometheus_url`: base URL queried for label discovery
326397
pub fn from_query_log(
327398
log_path: &Path,
328-
metrics_path: &Path,
329399
opts: RuntimeOptions,
400+
prometheus_url: &str,
330401
) -> Result<Self, ControllerError> {
331402
let entries = query_log::parse_log_file(log_path)?;
332403
let (instants, ranges) =
333404
query_log::infer_queries(&entries, opts.prometheus_scrape_interval);
405+
let config = query_log::to_controller_config(instants, ranges);
406+
let all_queries: Vec<String> = config
407+
.query_groups
408+
.iter()
409+
.flat_map(|qg| qg.queries.clone())
410+
.collect();
411+
let schema = prometheus_client::build_schema_from_prometheus(prometheus_url, &all_queries)?;
412+
Ok(Self {
413+
config,
414+
schema,
415+
options: opts,
416+
})
417+
}
334418

335-
let metrics_yaml = std::fs::read_to_string(metrics_path)?;
336-
let metrics_config: query_log::MetricsConfig = serde_yaml::from_str(&metrics_yaml)?;
337-
338-
let config = query_log::to_controller_config(instants, ranges, metrics_config.metrics);
339-
419+
/// Build a `Controller` from a Prometheus query log file with a caller-supplied `PromQLSchema`.
420+
///
421+
/// Use this when the schema is available without querying Prometheus (e.g. in tests).
422+
pub fn from_query_log_with_schema(
423+
log_path: &Path,
424+
schema: PromQLSchema,
425+
opts: RuntimeOptions,
426+
) -> Result<Self, ControllerError> {
427+
let entries = query_log::parse_log_file(log_path)?;
428+
let (instants, ranges) =
429+
query_log::infer_queries(&entries, opts.prometheus_scrape_interval);
430+
let config = query_log::to_controller_config(instants, ranges);
340431
Ok(Self {
341432
config,
433+
schema,
342434
options: opts,
343435
})
344436
}
345437

346438
pub fn generate(&self) -> Result<PlannerOutput, ControllerError> {
347-
let output = output::generator::generate_plan(&self.config, &self.options)?;
439+
let output = output::generator::generate_plan(&self.config, &self.schema, &self.options)?;
348440
Ok(PlannerOutput {
349441
punted_queries: output.punted_queries,
350442
streaming_yaml: output.streaming_yaml,

‎asap-planner-rs/src/main.rs‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ struct Args {
1414
#[arg(long = "query-log", conflicts_with = "input_config")]
1515
query_log: Option<PathBuf>,
1616

17-
/// Path to a metrics config YAML (required when using --query-log).
18-
#[arg(long = "metrics-config", requires = "query_log")]
19-
metrics_config: Option<PathBuf>,
20-
2117
#[arg(long = "output_dir")]
2218
output_dir: PathBuf,
2319

2420
#[arg(long = "prometheus_scrape_interval", required = false)]
2521
prometheus_scrape_interval: Option<u64>,
2622

23+
/// Base URL of the Prometheus instance used to auto-infer metric label sets.
24+
/// Optional: when provided, the planner queries Prometheus for label discovery.
25+
/// When absent, labels are taken from the `metrics` hint in the config file.
26+
/// Example: http://localhost:9090
27+
#[arg(long = "prometheus-url", required = false)]
28+
prometheus_url: Option<String>,
29+
2730
#[arg(long = "streaming_engine", value_enum)]
2831
streaming_engine: EngineArg,
2932

@@ -82,13 +85,24 @@ fn main() -> anyhow::Result<()> {
8285
range_duration: args.range_duration,
8386
step: args.step,
8487
};
85-
let controller = match (args.input_config, args.query_log) {
86-
(Some(config_path), None) => Controller::from_file(&config_path, opts)?,
87-
(None, Some(log_path)) => {
88-
let metrics_path = args
89-
.metrics_config
90-
.expect("--metrics-config is required when using --query-log");
91-
Controller::from_query_log(&log_path, &metrics_path, opts)?
88+
let controller = match (args.input_config, args.query_log, args.prometheus_url) {
89+
(Some(config_path), None, Some(url)) => {
90+
Controller::from_file(&config_path, opts, &url)?
91+
}
92+
(Some(config_path), None, None) => {
93+
let yaml_str = std::fs::read_to_string(&config_path)?;
94+
let config: asap_planner::ControllerConfig = serde_yaml::from_str(&yaml_str)?;
95+
let schema = config.schema_from_hints();
96+
Controller::from_file_with_schema(&config_path, schema, opts)?
97+
}
98+
(None, Some(log_path), Some(url)) => {
99+
Controller::from_query_log(&log_path, opts, &url)?
100+
}
101+
(None, Some(_log_path), None) => {
102+
anyhow::bail!(
103+
"--prometheus-url is required when using --query-log \
104+
(query logs have no metrics hint to fall back on)"
105+
)
92106
}
93107
_ => anyhow::bail!(
94108
"exactly one of --input_config or --query-log must be provided for PromQL mode"

‎asap-planner-rs/src/output/generator.rs‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::HashMap;
55

66
use promql_utilities::data_model::KeyByLabelNames;
77
use sketch_db_common::enums::CleanupPolicy;
8+
use sketch_db_common::PromQLSchema;
89

910
use crate::config::input::ControllerConfig;
1011
use crate::error::ControllerError;
@@ -17,14 +18,10 @@ type LeafEntries = Vec<(String, Vec<(String, Option<u64>)>)>;
1718
/// Run the full planning pipeline and produce YAML outputs
1819
pub fn generate_plan(
1920
controller_config: &ControllerConfig,
21+
schema: &PromQLSchema,
2022
opts: &RuntimeOptions,
2123
) -> Result<GeneratorOutput, ControllerError> {
22-
// Build metric schema
23-
let mut metric_schema = sketch_db_common::PromQLSchema::new();
24-
for md in &controller_config.metrics {
25-
metric_schema =
26-
metric_schema.add_metric(md.metric.clone(), KeyByLabelNames::new(md.labels.clone()));
27-
}
24+
let metric_schema = schema.clone();
2825

2926
// Determine cleanup policy
3027
let cleanup_policy_str = controller_config

0 commit comments

Comments
 (0)