Skip to content

Commit

Permalink
encoding/: Adopt serde style encoding
Browse files Browse the repository at this point in the history
Adopt encoding style similar to serde. `EncodeXXX` for a type that can be
encoded (see serde's `Serialize`) and `XXXEncoder` for a supported encoding i.e.
text and protobuf (see serde's `Serializer`).

- Compatible with Rust's additive features. Enabling the `protobuf` feature does
not change type signatures.
- `EncodeMetric` is trait object safe, and thus `Registry` can use dynamic dispatch.
- Implement a single encoding trait `EncodeMetric` per metric type instead of
one implementation per metric AND per encoding format.
- Leverage `std::fmt::Write` instead of `std::io::Write`. The OpenMetrics text
format has to be unicode compliant. The OpenMetrics Protobuf format requires
labels to be valid strings.

Signed-off-by: Max Inden <mail@max-inden.de>
  • Loading branch information
mxinden committed Nov 19, 2022
1 parent ef379f2 commit 9c5810d
Show file tree
Hide file tree
Showing 22 changed files with 1,435 additions and 1,108 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Expand Up @@ -11,7 +11,8 @@ homepage = "https://github.com/prometheus/client_rust"
documentation = "https://docs.rs/prometheus-client"

[features]
protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build", "dep:void", "prometheus-client-derive-encode/protobuf"]
default = []
protobuf = ["dep:prost", "dep:prost-types", "dep:prost-build"]

[workspace]
members = ["derive-encode"]
Expand All @@ -23,7 +24,6 @@ parking_lot = "0.12"
prometheus-client-derive-encode = { version = "0.3.0", path = "derive-encode" }
prost = { version = "0.9.0", optional = true }
prost-types = { version = "0.9.0", optional = true }
void = { version = "1.0", optional = true }

[dev-dependencies]
async-std = { version = "1", features = ["attributes"] }
Expand Down
2 changes: 1 addition & 1 deletion benches/encoding/proto.rs
Expand Up @@ -2,7 +2,7 @@
// https://github.com/tikv/rust-prometheus/blob/ab1ca7285d3463504381a5025ae1951e020d6796/benches/text_encoder.rs:write

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use prometheus_client::encoding::proto::{encode, EncodeMetric};
use prometheus_client::encoding::protobuf::{encode, EncodeMetric};
use prometheus_client::encoding::Encode;
use prometheus_client::metrics::counter::Counter;
use prometheus_client::metrics::family::Family;
Expand Down
8 changes: 4 additions & 4 deletions benches/encoding/text.rs
Expand Up @@ -35,7 +35,7 @@ pub fn text(c: &mut Criterion) {
}

impl prometheus_client::encoding::text::Encode for Status {
fn encode(&self, writer: &mut dyn Write) -> Result<(), std::io::Error> {
fn encode(&self, writer: &mut dyn Write) -> Result<(), std::fmt::Error> {
let status = match self {
Status::Two => b"200",
Status::Four => b"400",
Expand All @@ -47,14 +47,14 @@ pub fn text(c: &mut Criterion) {
}

#[cfg(feature = "protobuf")]
impl prometheus_client::encoding::proto::EncodeLabels for Status {
fn encode(&self, labels: &mut Vec<prometheus_client::encoding::proto::Label>) {
impl prometheus_client::encoding::protobuf::EncodeLabels for Status {
fn encode(&self, labels: &mut Vec<prometheus_client::encoding::protobuf::Label>) {
let value = match self {
Status::Two => "200".to_string(),
Status::Four => "400".to_string(),
Status::Five => "500".to_string(),
};
labels.push(prometheus_client::encoding::proto::Label {
labels.push(prometheus_client::encoding::protobuf::Label {
name: "status".to_string(),
value,
});
Expand Down
3 changes: 0 additions & 3 deletions derive-encode/Cargo.toml
Expand Up @@ -9,9 +9,6 @@ repository = "https://github.com/prometheus/client_rust"
homepage = "https://github.com/prometheus/client_rust"
documentation = "https://docs.rs/prometheus-client-derive-text-encode"

[features]
protobuf = []

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
Expand Down
152 changes: 54 additions & 98 deletions derive-encode/src/lib.rs
@@ -1,38 +1,43 @@
extern crate proc_macro;
#![deny(dead_code)]
#![deny(missing_docs)]
#![deny(unused)]
#![forbid(unsafe_code)]
#![warn(missing_debug_implementations)]

//! Derive crate for [`prometheus_client`].

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::DeriveInput;

#[proc_macro_derive(Encode)]
pub fn derive_encode(input: TokenStream) -> TokenStream {
/// Derive [`prometheus_client::encoding::EncodeLabelSet`].
#[proc_macro_derive(EncodeLabelSet)]
pub fn derive_encode_label_set(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;

let body = match ast.clone().data {
let body: TokenStream2 = match ast.clone().data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(syn::FieldsNamed { named, .. }) => named
.into_iter()
.enumerate()
.map(|(i, f)| {
.map(|f| {
let ident = f.ident.unwrap();
let ident_string = KEYWORD_IDENTIFIERS
.iter()
.find(|pair| ident == pair.1)
.map(|pair| pair.0.to_string())
.unwrap_or_else(|| ident.to_string());

let maybe_comma = if i == 0 {
TokenStream2::default()
} else {
quote! { writer.write_all(b",")?; }
};
quote! {
#maybe_comma
writer.write_all(concat!(#ident_string, "=\"").as_bytes())?;
prometheus_client::encoding::text::Encode::encode(&self.#ident, writer)?;
writer.write_all(b"\"")?;
let mut label_encoder = encoder.encode_label();
let mut label_key_encoder = label_encoder.encode_label_key()?;
EncodeLabelKey::encode(&#ident_string, &mut label_key_encoder)?;

let mut label_value_encoder = label_key_encoder.encode_label_value()?;
EncodeLabelValue::encode(&self.#ident, &mut label_value_encoder)?;

label_value_encoder.finish()?;
}
})
.collect(),
Expand All @@ -41,121 +46,72 @@ pub fn derive_encode(input: TokenStream) -> TokenStream {
}
syn::Fields::Unit => panic!("Can not derive Encode for struct with unit field."),
},
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let match_arms: TokenStream2 = variants
.into_iter()
.map(|v| {
let ident = v.ident;
quote! {
#name::#ident => writer.write_all(stringify!(#ident).as_bytes())?,
}
})
.collect();

quote! {
match self {
#match_arms
}
}
syn::Data::Enum(syn::DataEnum { .. }) => {
panic!("Can not derive Encode for enum.")
}
syn::Data::Union(_) => panic!("Can not derive Encode for union."),
};

let gen = quote! {
impl prometheus_client::encoding::text::Encode for #name {
fn encode(&self, writer: &mut dyn std::io::Write) -> std::result::Result<(), std::io::Error> {
impl prometheus_client::encoding::EncodeLabelSet for #name {
fn encode(&self, mut encoder: prometheus_client::encoding::LabelSetEncoder) -> std::result::Result<(), std::fmt::Error> {
use prometheus_client::encoding::EncodeLabel;
use prometheus_client::encoding::EncodeLabelKey;
use prometheus_client::encoding::EncodeLabelValue;

#body

Ok(())
}
}
};

#[cfg(feature = "protobuf")]
let gen = {
let protobuf = derive_protobuf_encode(ast);
quote! {
#gen

#protobuf
}
};

gen.into()
}

#[cfg(feature = "protobuf")]
fn derive_protobuf_encode(ast: DeriveInput) -> TokenStream2 {
/// Derive [`prometheus_client::encoding::EncodeLabelValue`].
#[proc_macro_derive(EncodeLabelValue)]
pub fn derive_encode_label_value(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;

match ast.data {
syn::Data::Struct(s) => match s.fields {
syn::Fields::Named(syn::FieldsNamed { named, .. }) => {
let push_labels: TokenStream2 = named
.into_iter()
.map(|f| {
let ident = f.ident.unwrap();
let ident_string = KEYWORD_IDENTIFIERS
.iter()
.find(|pair| ident == pair.1)
.map(|pair| pair.0.to_string())
.unwrap_or_else(|| ident.to_string());

quote! {
let mut label = {
let mut labels = vec![];
prometheus_client::encoding::proto::EncodeLabels::encode(&self.#ident, &mut labels);
debug_assert_eq!(1, labels.len(), "Labels encoded from {} should have only one label.", #ident_string);
labels.pop().expect("should have an element")
};
// Override the label name with the field name of this struct.
label.name = #ident_string.to_string();
labels.push(label);
}
})
.collect();

quote! {
impl prometheus_client::encoding::proto::EncodeLabels for #name {
fn encode(&self, labels: &mut Vec<prometheus_client::encoding::proto::Label>) {
#push_labels
}
}
}
}
syn::Fields::Unnamed(_) => {
panic!("Can not derive Encode for struct with unnamed fields.")
}
syn::Fields::Unit => panic!("Can not derive Encode for struct with unit field."),
},
let body = match ast.clone().data {
syn::Data::Struct(_) => {
panic!("Can not derive EncodeLabel for struct.")
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let match_arms: TokenStream2 = variants
.into_iter()
.map(|v| {
let ident = v.ident;
quote! {
#name::#ident => {
let mut label = prometheus_client::encoding::proto::Label::default();
label.name = stringify!(#name).to_string();
label.value = stringify!(#ident).to_string();
labels.push(label);
}
#name::#ident => encoder.write_str(stringify!(#ident))?,
}
})
.collect();

quote! {
impl prometheus_client::encoding::proto::EncodeLabels for #name {
fn encode(&self, labels: &mut Vec<prometheus_client::encoding::proto::Label>) {
match self {
#match_arms
};
}
match self {
#match_arms
}
}
}
syn::Data::Union(_) => panic!("Can not derive Encode for union."),
}
};

let gen = quote! {
impl prometheus_client::encoding::EncodeLabelValue for #name {
fn encode(&self, encoder: &mut prometheus_client::encoding::LabelValueEncoder) -> std::result::Result<(), std::fmt::Error> {
use std::fmt::Write;

#body

Ok(())
}
}
};

gen.into()
}

// Copied from https://github.com/djc/askama (MIT and APACHE licensed) and
Expand Down

0 comments on commit 9c5810d

Please sign in to comment.