Skip to content

Commit 5df4068

Browse files
committed
Add prometheus parse and scrape
prometheus parse will parse prometheus or openmetrics format files provided as pipeline input and output parsed metrics data prometheus scrape will scrape a prometheus target and output parsed metrics data
1 parent 37af41d commit 5df4068

File tree

9 files changed

+308
-2
lines changed

9 files changed

+308
-2
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ categories = ["command-line-utilities"]
1818
chrono = "0.4.39"
1919
nom = "8"
2020
nom-language = "0.1"
21+
nom-openmetrics = { git = "https://github.com/drbrain/nom-openmetrics" }
2122
nu-plugin = "0.102.0"
2223
nu-protocol = { version = "0.102.0", features = [ "plugin" ] }
2324
prometheus-http-query = "0.8.3"

src/client.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ mod label_names_builder;
33
mod label_values;
44
mod label_values_builder;
55
mod metric_metadata;
6+
mod parse;
67
mod query_builder;
78
mod query_instant;
89
mod query_range;
10+
mod scrape;
911
mod selector_parser;
1012
mod series;
1113
mod targets;
@@ -16,9 +18,12 @@ pub use label_values::LabelValues;
1618
pub use label_values_builder::LabelValuesBuilder;
1719
pub use metric_metadata::MetricMetadata;
1820
use nu_protocol::{LabeledError, Span};
21+
pub use parse::Parse;
22+
pub use parse::ParseFormat;
1923
pub use query_builder::QueryBuilder;
2024
pub use query_instant::QueryInstant;
2125
pub use query_range::QueryRange;
26+
pub use scrape::Scrape;
2227
pub use selector_parser::SelectorParser;
2328
pub use series::Series;
2429
pub use targets::Targets;

src/client/parse.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use crate::Client;
2+
use nom_openmetrics::{
3+
parser::{exposition, prometheus},
4+
Family, MetricDescriptor, Sample,
5+
};
6+
use nu_protocol::{record, LabeledError, Record, Span, Value};
7+
8+
#[derive(Default)]
9+
pub enum ParseFormat {
10+
#[default]
11+
Prometheus,
12+
Openmetrics,
13+
}
14+
15+
pub struct Parse<'a> {
16+
input: &'a Value,
17+
format: ParseFormat,
18+
}
19+
20+
impl<'a> Parse<'a> {
21+
pub fn new(input: &'a Value) -> Self {
22+
Self {
23+
input,
24+
format: Default::default(),
25+
}
26+
}
27+
28+
pub fn run(self) -> Result<Value, LabeledError> {
29+
let Self { input, format } = self;
30+
31+
let (_, families) = match format {
32+
ParseFormat::Prometheus => prometheus(input.as_str()?).unwrap(),
33+
ParseFormat::Openmetrics => exposition(input.as_str()?).unwrap(),
34+
};
35+
36+
let families = families
37+
.iter()
38+
.map(|family| family_to_value(family))
39+
.collect();
40+
41+
Ok(Value::list(families, Span::unknown()))
42+
}
43+
44+
pub fn set_format(&mut self, format: ParseFormat) {
45+
self.format = format;
46+
}
47+
}
48+
49+
impl<'a> Client for Parse<'a> {}
50+
51+
fn family_to_value(family: &Family) -> Value {
52+
let descriptors = family
53+
.descriptors
54+
.iter()
55+
.map(|descriptor| descriptor_to_value(descriptor))
56+
.collect();
57+
58+
let samples = family
59+
.samples
60+
.iter()
61+
.map(|sample| sample_to_value(sample))
62+
.collect();
63+
64+
let record = record! {
65+
"descriptors" => Value::list(descriptors, Span::unknown()),
66+
"samples" => Value::list(samples, Span::unknown()),
67+
};
68+
69+
Value::record(record, Span::unknown())
70+
}
71+
72+
fn descriptor_to_value(descriptor: &MetricDescriptor) -> Value {
73+
let record = match descriptor {
74+
MetricDescriptor::Type { metric, r#type } => record! {
75+
"descriptor" => Value::string("type", Span::unknown()),
76+
"metric" => Value::string(*metric, Span::unknown()),
77+
"type" => Value::string(r#type.to_string(), Span::unknown())
78+
},
79+
MetricDescriptor::Help { metric, help } => record! {
80+
"descriptor" => Value::string("help", Span::unknown()),
81+
"metric" => Value::string(*metric, Span::unknown()),
82+
"help" => Value::string(help, Span::unknown())
83+
},
84+
MetricDescriptor::Unit { metric, unit } => record! {
85+
"descriptor" => Value::string("unit", Span::unknown()),
86+
"metric" => Value::string(*metric, Span::unknown()),
87+
"unit" => Value::string(*unit, Span::unknown())
88+
},
89+
};
90+
91+
Value::record(record, Span::unknown())
92+
}
93+
94+
fn sample_to_value(sample: &Sample) -> Value {
95+
let mut record = Record::new();
96+
97+
record.insert("name", Value::string(sample.name(), Span::unknown()));
98+
record.insert("value", Value::float(sample.number(), Span::unknown()));
99+
100+
Value::record(record, Span::unknown())
101+
}

src/client/scrape.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use nom_openmetrics::{Family, Sample};
2+
use nu_protocol::{LabeledError, Record, Span, Value};
3+
4+
use crate::client::Client;
5+
6+
pub struct Scrape {
7+
target: String,
8+
}
9+
10+
impl Scrape {
11+
pub fn new(target: String) -> Self {
12+
Self { target }
13+
}
14+
15+
pub fn run(self) -> Result<Value, LabeledError> {
16+
let Self { ref target, .. } = self;
17+
18+
self.runtime()?.block_on(async {
19+
let body = reqwest::get(target).await.unwrap().bytes().await.unwrap();
20+
21+
let body = String::from_utf8(body.to_vec()).unwrap();
22+
23+
let (_, families) = nom_openmetrics::parser::prometheus(&body).unwrap();
24+
25+
let families = families
26+
.iter()
27+
.map(|family| family_to_value(family))
28+
.collect();
29+
30+
Ok(Value::list(families, Span::unknown()))
31+
})
32+
}
33+
}
34+
35+
impl Client for Scrape {}
36+
37+
fn family_to_value(family: &Family) -> Value {
38+
let mut record = Record::new();
39+
40+
let samples = family
41+
.samples
42+
.iter()
43+
.map(|sample| sample_to_value(sample))
44+
.collect();
45+
46+
record.insert("samples", Value::list(samples, Span::unknown()));
47+
48+
Value::record(record, Span::unknown())
49+
}
50+
51+
fn sample_to_value(sample: &Sample) -> Value {
52+
let mut record = Record::new();
53+
54+
record.insert("name", Value::string(sample.name(), Span::unknown()));
55+
record.insert("value", Value::float(sample.number(), Span::unknown()));
56+
57+
Value::record(record, Span::unknown())
58+
}

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ mod query;
44
mod source;
55

66
use client::Client;
7-
use nu_plugin::{serve_plugin, JsonSerializer};
7+
use nu_plugin::{serve_plugin, MsgPackSerializer};
88
use prometheus::Prometheus;
99
use query::Query;
1010
use source::Source;
1111

1212
fn main() {
13-
serve_plugin(&Prometheus, JsonSerializer)
13+
serve_plugin(&Prometheus, MsgPackSerializer)
1414
}

src/prometheus.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod label_names_command;
22
mod label_values_command;
33
mod metric_metadata_command;
4+
mod parse_command;
45
mod prometheus_command;
56
mod query_command;
67
mod query_range_command;
8+
mod scrape_command;
79
mod series_command;
810
mod sources_command;
911
mod targets_command;
@@ -16,6 +18,8 @@ use crate::prometheus::{
1618
targets_command::TargetsCommand,
1719
};
1820
use nu_plugin::Plugin;
21+
use parse_command::ParseCommand;
22+
use scrape_command::ScrapeCommand;
1923

2024
#[derive(Clone)]
2125
pub struct Prometheus;
@@ -26,10 +30,12 @@ impl Plugin for Prometheus {
2630
Box::new(LabelNamesCommand),
2731
Box::new(LabelValuesCommand),
2832
Box::new(MetricMetadataCommand),
33+
Box::new(ParseCommand),
2934
Box::new(PrometheusCommand),
3035
Box::new(QueryCommand),
3136
Box::new(QueryRangeCommand),
3237
Box::new(SeriesCommand),
38+
Box::new(ScrapeCommand),
3339
Box::new(SourcesCommand),
3440
Box::new(TargetsCommand),
3541
]

src/prometheus/parse_command.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use crate::{
2+
client::{Parse, ParseFormat},
3+
Prometheus,
4+
};
5+
use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
6+
use nu_protocol::{LabeledError, Signature, Span, SyntaxShape, Type, Value};
7+
8+
#[derive(Clone, Default)]
9+
pub struct ParseCommand;
10+
11+
impl SimplePluginCommand for ParseCommand {
12+
type Plugin = Prometheus;
13+
14+
fn name(&self) -> &str {
15+
"prometheus parse"
16+
}
17+
18+
fn signature(&self) -> Signature {
19+
Signature::build(self.name())
20+
.description(self.description())
21+
.named(
22+
"format",
23+
SyntaxShape::String,
24+
"Metrics format, prometheus (default) or openmetrics",
25+
None,
26+
)
27+
.input_output_types(vec![(Type::String, Type::table())])
28+
}
29+
30+
fn description(&self) -> &str {
31+
"Parse prometheus or openmetrics output"
32+
}
33+
34+
fn run(
35+
&self,
36+
_plugin: &Self::Plugin,
37+
_engine: &EngineInterface,
38+
call: &EvaluatedCall,
39+
input: &Value,
40+
) -> Result<Value, LabeledError> {
41+
let format = call
42+
.get_flag_value("format")
43+
.unwrap_or(Value::string("prometheus", Span::unknown()));
44+
let format_span = format.span();
45+
let format = format.into_string()?;
46+
47+
let mut parser = Parse::new(input);
48+
49+
match format.as_str() {
50+
"prometheus" => parser.set_format(ParseFormat::Prometheus),
51+
"openmetrics" => parser.set_format(ParseFormat::Openmetrics),
52+
_ => {
53+
return Err(LabeledError::new("Invalid format")
54+
.with_label("must be prometheus or openmetrics", format_span));
55+
}
56+
}
57+
58+
parser.run()
59+
}
60+
}

0 commit comments

Comments
 (0)