Repository navigation
feat(encoding): Add HTTP content negotiation for choosing the exposition format - #381
Conversation
|
Hey @krisztianfekete I opened this pr in draft since I wanted to discuss a few things:
impl Format {
pub fn encode<W: io::Write>(self, writer: &mut W, registry: &Registry) -> io::Result<()> {
match self {
Format::OpenMetricsText => {
let mut buffer = String::new();
text::encode(&mut buffer, registry).map_err(io::Error::other)?;
writer.write_all(buffer.as_bytes())
}
Format::PrometheusText => {
let mut buffer = String::new();
prometheus_text::encode(&mut buffer, registry).map_err(io::Error::other)?;
writer.write_all(buffer.as_bytes())
}
// `encode_to_vec` writes each `MetricFamily` with a length prefix.
#[cfg(feature = "prometheus_protobuf")]
Format::PrometheusProtobuf => {
let buffer = prometheus_protobuf::encode_to_vec(registry).map_err(io::Error::other)?;
writer.write_all(&buffer)
}
#[cfg(feature = "openmetrics_protobuf")]
Format::OpenMetricsProtobuf => {
use prost::Message;
let metric_set = openmetrics_protobuf::encode(registry).map_err(io::Error::other)?;
writer.write_all(&metric_set.encode_to_vec())
}
}
}
}This way the caller would not have to think about handling the returned format differently. Also this is just some example code and maybe there is a way to get rid of intermediate buffers. I did not update any example code yet since I wanted to discuss these points first. Also 2 could be done in a separate pr if you want it and to not make this pr too big. |
64159a2 to
917975d
Compare
|
Personally, I think we should make the negotiation mechanism more configurable that what is currently implemented, and make the returned type not be a format, but a media type (so we can later support escaping schemes). I was thinking something like the following: #[derive(Clone, Debug)]
pub struct Negotiator {
formats: Vec<Format>,
escaping_schemes: Vec<EscapingScheme>,
default_escaping: EscapingScheme,
fallback: MediaType,
}
impl Negotiator {
pub fn new() -> Self { Self::default() }
pub fn with_formats(self, formats: impl IntoIterator<Item = Format>) -> Self;
pub fn with_escaping_schemes(self, schemes: impl IntoIterator<Item = EscapingScheme>) -> Self;
pub fn with_default_escaping(self, scheme: EscapingScheme) -> Self;
pub fn with_fallback(self, media_type: MediaType) -> Self;
pub fn negotiate(&self, accept: Option<&str>) -> MediaType;
}
pub enum Format {
OpenMetricsText,
PrometheusText0_0_4,
PrometheusText1_0_0,
#[cfg(feature = "prometheus_protobuf")]
#[cfg_attr(docsrs, doc(cfg(feature = "prometheus_protobuf")))]
PrometheusProtobuf,
}
pub struct MediaType {
format: Format,
escaping: EscapingScheme,
}
impl MediaType {
pub const fn new(format: Format, escaping: EscapingScheme) -> Self;
pub const fn format(&self) -> Format { self.format }
pub const fn escaping(&self) -> EscapingScheme { self.escaping }
pub fn encode<W: io::Write>(&self, registry: &Registry, writer: &mut W) -> Result<(), EncodeError>;
}Also, +1 to adding an encoding helper which performs the encoding dispatching for us -- I think many users would find this useful. |
|
Yes I agree that it should probably be a bit more configurable and you have a good point about escaping scheme. I also just found https://prometheus.io/docs/instrumenting/content_negotiation/ so need to make sure we follow that. |
I think we can defer adding the escaping scheme support to content negotiation for now, but it's good to keep in mind. I have a WIP PR for adding this and can integrate the changes from this PR once it's merged. |
|
Thanks both for the discussion on the proposal! Just to keep this one focused:
Small things to align with Go's
Once that's in and the examples are updated, feel free to mark it ready for review! |
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
|
Thanks for the feedback!
Not sure how this would work in a nice way since we just return the Format enum. If we add escpaing support later we would have to update that enum with a separate type that includes escaping info to not break existing users e.g. Current: pub enum Format {
PrometheusText0_0_4,
OpenMetricsText1_0_0,
PrometheusProtobuf,
}Updated: pub enum Format {
PrometheusText0_0_4,
OpenMetricsText1_0_0,
OpenMetricsText1_0_0Escaped(EscapingScheme),
PrometheusProtobuf,
}So I like the idea that @herin049 had about returning a structure, from his example |
Fair point! Since escaping ends up in the |
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
917975d to
b4fcb0a
Compare
krisztianfekete
left a comment
There was a problem hiding this comment.
Thanks, this is close! Added a few small comments inline, + signature fix is needed, see CI failure.
| CONTENT_TYPE, | ||
| "application/openmetrics-text; version=1.0.0; charset=utf-8", | ||
| ) | ||
| .header(CONTENT_TYPE, media_type.content_type()) |
There was a problem hiding this comment.
Please also set Vary: Accept here and in the other examples, like promhttp does.
| let q = params | ||
| .split(';') | ||
| .filter_map(split_param) | ||
| .rfind(|(k, _)| *k == "q") | ||
| .map_or(1.0, |(_, v)| v.parse().unwrap_or(0.0)); | ||
|
|
||
| Some(MediaRange { | ||
| type_, | ||
| subtype, | ||
| q, | ||
| params, | ||
| }) | ||
| } |
There was a problem hiding this comment.
q=0 means "not acceptable", so let's skip those ranges, and treat q values outside 0.0..=1.0 (e.g. NaN) as invalid.
There was a problem hiding this comment.
Looks good! Could you add a few test cases for q=0, q=NaN, and an uppercase media type, so this doesn't regress?
There was a problem hiding this comment.
Added a couple of more test for it
| } | ||
|
|
||
| #[test] | ||
| fn negotiates_formats() { |
There was a problem hiding this comment.
Can you please add test cases for a browser Accept header and Prometheus's default scrape Accept header as well?
| if (self.type_ != "*" && self.type_ != type_) | ||
| || (self.subtype != "*" && self.subtype != subtype) |
There was a problem hiding this comment.
Nit: media types are case-insensitive, so eq_ignore_ascii_case would be safer here.
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
| pub struct MediaType { | ||
| format: Format, | ||
| } |
There was a problem hiding this comment.
Nit: let's derive Hash too, to match Format.
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
|
Thanks, addressed all comment 👍 Will follow up with the encode helper after this one is merged! |
| /// ); | ||
| /// assert!(!body.is_empty()); | ||
| /// ``` | ||
| pub fn negotiate(accept: Option<&str>, offered: impl AsRef<[Format]>) -> MediaType { |
There was a problem hiding this comment.
Let's still change this to offered: &[Format]. With impl AsRef, calling it as negotiate(accept, &offered) like the doc example does triggers clippy::needless_borrows_for_generic_args in users' code, the same lint that failed CI here.
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Signed-off-by: Kalle <23356117+kalleep@users.noreply.github.com>
Adds
encoding::negotiateso a scrape endpoint can answer in the format the scraper asks for.negotiate returns a MediaType, which has the Format to encode in and the Content-Type to respond with. MediaType has private fields, so escaping can be added to it later without breaking anything.
Format is #[non_exhaustive] and has PrometheusText0_0_4 and OpenMetricsText1_0_0, plus PrometheusProtobuf with the prometheus_protobuf feature.
I tried to match the Go implementation as much as possible:
Fixes: #377
Notes
Added missing CONTENT_TYPE constants to text and prometheus_protobuf