use std::io::Write;
use errors::{Result, Error};
use proto::MetricFamily;
use proto::{self, MetricType};
pub trait Encoder {
fn encode(&self, &[MetricFamily], &mut Write) -> Result<()>;
fn format_type(&self) -> &str;
}
pub type Format = &'static str;
pub const TEXT_FORMAT: Format = "text/plain; version=0.0.4";
#[derive(Debug, Default)]
pub struct TextEncoder;
impl TextEncoder {
pub fn new() -> TextEncoder {
TextEncoder
}
}
impl Encoder for TextEncoder {
fn encode(&self, metric_familys: &[MetricFamily], writer: &mut Write) -> Result<()> {
for mf in metric_familys {
if mf.get_metric().is_empty() {
return Err(Error::Msg("MetricFamily has no metrics".to_owned()));
}
let name = mf.get_name();
if name.is_empty() {
return Err(Error::Msg("MetricFamily has no name".to_owned()));
}
let help = mf.get_help();
if !help.is_empty() {
try!(write!(writer, "# HELP {} {}\n", name, escape_string(help, false)));
}
let metric_type = mf.get_field_type();
let lowercase_type = format!("{:?}", metric_type).to_lowercase();
try!(write!(writer, "# TYPE {} {}\n", name, lowercase_type));
for m in mf.get_metric() {
match metric_type {
MetricType::COUNTER => {
try!(write_sample(name, m, "", "", m.get_counter().get_value(), writer));
}
MetricType::GAUGE | MetricType::SUMMARY | MetricType::HISTOGRAM |
MetricType::UNTYPED => unimplemented!(),
}
}
}
Ok(())
}
fn format_type(&self) -> &str {
TEXT_FORMAT
}
}
fn write_sample(name: &str,
mc: &proto::Metric,
additional_label_name: &str,
additional_label_value: &str,
value: f64,
writer: &mut Write)
-> Result<()> {
try!(writer.write_all(name.as_bytes()));
try!(label_pairs_to_text(mc.get_label(),
additional_label_name,
additional_label_value,
writer));
try!(write!(writer, " {}", value));
let timestamp = mc.get_timestamp_ms();
if timestamp != 0 {
try!(write!(writer, " {}", timestamp));
}
try!(writer.write_all(b"\n"));
Ok(())
}
fn label_pairs_to_text(pairs: &[proto::LabelPair],
additional_label_name: &str,
additional_label_value: &str,
writer: &mut Write)
-> Result<()> {
if pairs.is_empty() && additional_label_name.is_empty() {
return Ok(());
}
let mut separator = "{";
for lp in pairs {
try!(write!(writer,
"{}{}=\"{}\"",
separator,
lp.get_name(),
escape_string(lp.get_value(), true)));
separator = ",";
}
if !additional_label_name.is_empty() {
try!(write!(writer,
"{}{}=\"{}\"",
separator,
additional_label_name,
escape_string(additional_label_value, true)));
}
try!(writer.write_all(b"}"));
Ok(())
}
pub fn escape_string(v: &str, include_double_quote: bool) -> String {
let mut escaped = String::with_capacity(v.len() * 2);
for c in v.chars() {
match c {
'\\' | '\n' => {
escaped.extend(c.escape_default());
}
'"' if include_double_quote => {
escaped.extend(c.escape_default());
}
_ => {
escaped.push(c);
}
}
}
escaped.shrink_to_fit();
escaped
}
#[cfg(test)]
mod tests {
use counter::Counter;
use metrics::{Opts, Collector};
use super::*;
#[test]
fn test_ecape_string() {
assert_eq!(r"\\", escape_string("\\", false));
assert_eq!(r"a\\", escape_string("a\\", false));
assert_eq!(r"\n", escape_string("\n", false));
assert_eq!(r"a\n", escape_string("a\n", false));
assert_eq!(r"\\n", escape_string("\\n", false));
assert_eq!(r##"\\n\""##, escape_string("\\n\"", true));
assert_eq!(r##"\\\n\""##, escape_string("\\\n\"", true));
assert_eq!(r##"\\\\n\""##, escape_string("\\\\n\"", true));
assert_eq!(r##"\"\\n\""##, escape_string("\"\\n\"", true));
}
#[test]
fn test_text_encoder() {
let opts = Opts::new("test", "test help").const_label("a", "1").const_label("b", "2");
let counter = Counter::with_opts(opts).unwrap();
counter.inc();
let mf = counter.collect();
let mut writer = Vec::<u8>::new();
let encoder = TextEncoder::new();
let txt = encoder.encode(&[mf], &mut writer);
assert!(txt.is_ok());
let ans = r##"# HELP test test help
# TYPE test counter
test{a="1",b="2"} 1
"##;
assert_eq!(ans.as_bytes(), writer.as_slice());
}
}