use std::collections::HashSet;
use std::fs;
use std::path::Path;
use opentelemetry::metrics::{Meter, MeterProvider as _};
use opentelemetry::KeyValue;
use opentelemetry::{InstrumentationScope, Key};
use opentelemetry_prometheus::{ExporterBuilder, ResourceSelector};
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_sdk::Resource;
use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, TELEMETRY_SDK_VERSION};
use prometheus::{Encoder, TextEncoder};
const BOUNDARIES: &[f64] = &[
0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 1000.0,
];
const BYTES_BOUNDARIES: &[f64] = &[
0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0,
10000.0,
];
#[ignore = "https://github.com/open-telemetry/opentelemetry-rust/pull/2224"]
#[test]
fn prometheus_exporter_integration() {
struct TestCase {
name: &'static str,
empty_resource: bool,
custom_resource_attrs: Vec<KeyValue>,
#[allow(clippy::type_complexity)]
record_metrics: Box<dyn Fn(Meter)>,
builder: ExporterBuilder,
expected_file: &'static str,
}
impl Default for TestCase {
fn default() -> Self {
TestCase {
name: "",
empty_resource: false,
custom_resource_attrs: Vec::new(),
record_metrics: Box::new(|_| {}),
builder: ExporterBuilder::default(),
expected_file: "",
}
}
}
let test_cases = vec![
TestCase {
name: "counter",
expected_file: "counter.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A", "B"),
KeyValue::new("C", "D"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
let counter = meter
.f64_counter("foo")
.with_description("a simple counter")
.with_unit("ms")
.build();
counter.add(5.0, &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
let attrs2 = vec![
KeyValue::new("A", "D"),
KeyValue::new("C", "B"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
counter.add(5.0, &attrs2);
}),
..Default::default()
},
TestCase {
name: "counter with suffixes disabled",
expected_file: "counter_disabled_suffix.txt",
builder: ExporterBuilder::default().without_counter_suffixes(),
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A", "B"),
KeyValue::new("C", "D"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
let counter = meter
.f64_counter("foo")
.with_description("a simple counter without a total suffix")
.with_unit("ms")
.build();
counter.add(5.0, &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
let attrs2 = vec![
KeyValue::new("A", "D"),
KeyValue::new("C", "B"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
counter.add(5.0, &attrs2);
}),
..Default::default()
},
TestCase {
name: "gauge",
expected_file: "gauge.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let gauge = meter
.f64_up_down_counter("bar")
.with_description("a fun little gauge")
.with_unit("1")
.build();
gauge.add(1.0, &attrs);
gauge.add(-0.25, &attrs);
}),
..Default::default()
},
TestCase {
name: "histogram",
expected_file: "histogram.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let histogram = meter
.f64_histogram("histogram_baz")
.with_description("a very nice histogram")
.with_unit("By")
.with_boundaries(BOUNDARIES.to_vec())
.build();
histogram.record(23.0, &attrs);
histogram.record(7.0, &attrs);
histogram.record(101.0, &attrs);
histogram.record(105.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "sanitized attributes to labels",
expected_file: "sanitized_labels.txt",
builder: ExporterBuilder::default().without_units(),
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A.B", "X"),
KeyValue::new("A.B", "Q"),
KeyValue::new("C.D", "Y"),
KeyValue::new("C/D", "Z"),
];
let counter = meter
.f64_counter("foo")
.with_description("a sanitary counter")
.with_unit("By")
.build();
counter.add(5.0, &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "invalid instruments are renamed",
expected_file: "sanitized_names.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let mut gauge = meter
.f64_up_down_counter("bar")
.with_description("a fun little gauge")
.build();
gauge.add(100., &attrs);
gauge.add(-25.0, &attrs);
gauge = meter
.f64_up_down_counter("invalid.gauge.name")
.with_description("a gauge with an invalid name")
.build();
gauge.add(100.0, &attrs);
let counter = meter
.f64_counter("0invalid.counter.name")
.with_description("a counter with an invalid name")
.build();
counter.add(100.0, &attrs);
let histogram = meter
.f64_histogram("invalid.hist.name")
.with_description("a histogram with an invalid name")
.with_boundaries(BOUNDARIES.to_vec())
.build();
histogram.record(23.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "empty resource",
empty_resource: true,
expected_file: "empty_resource.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A", "B"),
KeyValue::new("C", "D"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
let counter = meter
.f64_counter("foo")
.with_description("a simple counter")
.build();
counter.add(5.0, &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "custom resource",
custom_resource_attrs: vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")],
expected_file: "custom_resource.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A", "B"),
KeyValue::new("C", "D"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
let counter = meter
.f64_counter("foo")
.with_description("a simple counter")
.build();
counter.add(5., &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "without target_info",
builder: ExporterBuilder::default().without_target_info(),
expected_file: "without_target_info.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A", "B"),
KeyValue::new("C", "D"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
let counter = meter
.f64_counter("foo")
.with_description("a simple counter")
.build();
counter.add(5.0, &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "without scope_info",
builder: ExporterBuilder::default().without_scope_info(),
expected_file: "without_scope_info.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let gauge = meter
.i64_up_down_counter("bar")
.with_description("a fun little gauge")
.with_unit("1")
.build();
gauge.add(2, &attrs);
gauge.add(-1, &attrs);
}),
..Default::default()
},
TestCase {
name: "without scope_info and target_info",
builder: ExporterBuilder::default()
.without_scope_info()
.without_target_info(),
expected_file: "without_scope_and_target_info.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let counter = meter
.u64_counter("bar")
.with_description("a fun little counter")
.with_unit("By")
.build();
counter.add(2, &attrs);
counter.add(1, &attrs);
}),
..Default::default()
},
TestCase {
name: "with namespace",
builder: ExporterBuilder::default().with_namespace("test"),
expected_file: "with_namespace.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![
KeyValue::new("A", "B"),
KeyValue::new("C", "D"),
KeyValue::new("E", true),
KeyValue::new("F", 42),
];
let counter = meter
.f64_counter("foo")
.with_description("a simple counter")
.build();
counter.add(5.0, &attrs);
counter.add(10.3, &attrs);
counter.add(9.0, &attrs);
}),
..Default::default()
},
TestCase {
name: "with resource in every metrics",
builder: ExporterBuilder::default().with_resource_selector(ResourceSelector::All),
expected_file: "resource_in_every_metrics.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let gauge = meter
.i64_up_down_counter("bar")
.with_description("a fun little gauge")
.with_unit("1")
.build();
gauge.add(2, &attrs);
gauge.add(-1, &attrs);
}),
..Default::default()
},
TestCase {
name: "with select resource in every metrics",
builder: ExporterBuilder::default()
.with_resource_selector(HashSet::from([Key::new("service.name")])),
expected_file: "select_resource_in_every_metrics.txt",
record_metrics: Box::new(|meter| {
let attrs = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")];
let gauge = meter
.i64_up_down_counter("bar")
.with_description("a fun little gauge")
.with_unit("1")
.build();
gauge.add(2, &attrs);
gauge.add(-1, &attrs);
}),
..Default::default()
},
];
for tc in test_cases {
let registry = prometheus::Registry::new();
let exporter = tc.builder.with_registry(registry.clone()).build().unwrap();
let res = if tc.empty_resource {
Resource::builder_empty().build()
} else {
Resource::builder()
.with_attributes(
vec![
KeyValue::new(SERVICE_NAME, "prometheus_test"),
KeyValue::new(TELEMETRY_SDK_VERSION, "latest"),
]
.into_iter()
.chain(tc.custom_resource_attrs),
)
.build()
};
let provider = SdkMeterProvider::builder()
.with_resource(res)
.with_reader(exporter)
.build();
let scope = InstrumentationScope::builder("testmeter")
.with_version("v0.1.0")
.with_schema_url("https://opentelemetry.io/schemas/1.0.0")
.with_attributes(vec![KeyValue::new("k", "v")])
.build();
let meter = provider.meter_with_scope(scope);
(tc.record_metrics)(meter);
let content = fs::read_to_string(Path::new("./tests/data").join(tc.expected_file))
.expect(tc.expected_file);
gather_and_compare(registry, content, tc.name);
}
}
fn gather_and_compare(registry: prometheus::Registry, expected: String, name: &'static str) {
let mut output = Vec::new();
let encoder = TextEncoder::new();
let metric_families = registry.gather();
encoder.encode(&metric_families, &mut output).unwrap();
let expected = get_platform_specific_string(expected);
let output_string = get_platform_specific_string(String::from_utf8(output).unwrap());
assert_eq!(output_string, expected, "{name}");
}
fn get_platform_specific_string(input: String) -> String {
if cfg!(windows) && !input.ends_with("\r\n") && input.ends_with('\n') {
return input.replace('\n', "\r\n");
}
input
}
#[test]
fn multiple_scopes() {
let registry = prometheus::Registry::new();
let exporter = ExporterBuilder::default()
.with_registry(registry.clone())
.build()
.unwrap();
let resource = Resource::builder()
.with_attributes([
KeyValue::new(SERVICE_NAME, "prometheus_test"),
KeyValue::new(TELEMETRY_SDK_VERSION, "latest"),
])
.build();
let provider = SdkMeterProvider::builder()
.with_reader(exporter)
.with_resource(resource)
.build();
let scope_foo = InstrumentationScope::builder("meterfoo")
.with_version("v0.1.0")
.with_schema_url("https://opentelemetry.io/schemas/1.0.0")
.with_attributes(vec![KeyValue::new("k", "v")])
.build();
let foo_counter = provider
.meter_with_scope(scope_foo)
.u64_counter("foo")
.with_unit("ms")
.with_description("meter foo counter")
.build();
foo_counter.add(100, &[KeyValue::new("type", "foo")]);
let scope_bar = InstrumentationScope::builder("meterbar")
.with_version("v0.1.0")
.with_schema_url("https://opentelemetry.io/schemas/1.0.0")
.with_attributes(vec![KeyValue::new("k", "v")])
.build();
let bar_counter = provider
.meter_with_scope(scope_bar)
.u64_counter("bar")
.with_unit("ms")
.with_description("meter bar counter")
.build();
bar_counter.add(200, &[KeyValue::new("type", "bar")]);
let content = fs::read_to_string("./tests/data/multi_scopes.txt").unwrap();
gather_and_compare(registry, content, "multi_scope");
}
#[test]
fn duplicate_metrics() {
struct TestCase {
name: &'static str,
custom_resource_attrs: Vec<KeyValue>,
#[allow(clippy::type_complexity)]
record_metrics: Box<dyn Fn(Meter, Meter)>,
builder: ExporterBuilder,
expected_files: Vec<&'static str>,
}
impl Default for TestCase {
fn default() -> Self {
TestCase {
name: "",
custom_resource_attrs: Vec::new(),
record_metrics: Box::new(|_, _| {}),
builder: ExporterBuilder::default(),
expected_files: Vec::new(),
}
}
}
let test_cases = vec![
TestCase {
name: "no_conflict_two_counters",
record_metrics: Box::new(|meter_a, meter_b| {
let foo_a = meter_a
.u64_counter("foo")
.with_unit("By")
.with_description("meter counter foo")
.build();
foo_a.add(100, &[KeyValue::new("A", "B")]);
let foo_b = meter_b
.u64_counter("foo")
.with_unit("By")
.with_description("meter counter foo")
.build();
foo_b.add(100, &[KeyValue::new("A", "B")]);
}),
expected_files: vec!["no_conflict_two_counters.txt"],
..Default::default()
},
TestCase {
name: "no_conflict_two_updowncounters",
record_metrics: Box::new(|meter_a, meter_b| {
let foo_a = meter_a
.i64_up_down_counter("foo")
.with_unit("By")
.with_description("meter gauge foo")
.build();
foo_a.add(100, &[KeyValue::new("A", "B")]);
let foo_b = meter_b
.i64_up_down_counter("foo")
.with_unit("By")
.with_description("meter gauge foo")
.build();
foo_b.add(100, &[KeyValue::new("A", "B")]);
}),
expected_files: vec!["no_conflict_two_updowncounters.txt"],
..Default::default()
},
TestCase {
name: "no_conflict_two_histograms",
record_metrics: Box::new(|meter_a, meter_b| {
let foo_a = meter_a
.u64_histogram("foo")
.with_unit("By")
.with_description("meter histogram foo")
.with_boundaries(BYTES_BOUNDARIES.to_vec())
.build();
foo_a.record(100, &[KeyValue::new("A", "B")]);
let foo_b = meter_b
.u64_histogram("foo")
.with_unit("By")
.with_description("meter histogram foo")
.with_boundaries(BYTES_BOUNDARIES.to_vec())
.build();
foo_b.record(100, &[KeyValue::new("A", "B")]);
}),
expected_files: vec!["no_conflict_two_histograms.txt"],
..Default::default()
},
TestCase {
name: "conflict_help_two_counters",
record_metrics: Box::new(|meter_a, meter_b| {
let bar_a = meter_a
.u64_counter("bar")
.with_unit("By")
.with_description("meter a bar")
.build();
bar_a.add(100, &[KeyValue::new("type", "bar")]);
let bar_b = meter_b
.u64_counter("bar")
.with_unit("By")
.with_description("meter b bar")
.build();
bar_b.add(100, &[KeyValue::new("type", "bar")]);
}),
expected_files: vec![
"conflict_help_two_counters_1.txt",
"conflict_help_two_counters_2.txt",
],
..Default::default()
},
TestCase {
name: "conflict_help_two_updowncounters",
record_metrics: Box::new(|meter_a, meter_b| {
let bar_a = meter_a
.i64_up_down_counter("bar")
.with_unit("By")
.with_description("meter a bar")
.build();
bar_a.add(100, &[KeyValue::new("type", "bar")]);
let bar_b = meter_b
.i64_up_down_counter("bar")
.with_unit("By")
.with_description("meter b bar")
.build();
bar_b.add(100, &[KeyValue::new("type", "bar")]);
}),
expected_files: vec![
"conflict_help_two_updowncounters_1.txt",
"conflict_help_two_updowncounters_2.txt",
],
..Default::default()
},
TestCase {
name: "conflict_help_two_histograms",
record_metrics: Box::new(|meter_a, meter_b| {
let bar_a = meter_a
.u64_histogram("bar")
.with_unit("By")
.with_description("meter a bar")
.with_boundaries(BYTES_BOUNDARIES.to_vec())
.build();
bar_a.record(100, &[KeyValue::new("A", "B")]);
let bar_b = meter_b
.u64_histogram("bar")
.with_unit("By")
.with_description("meter b bar")
.with_boundaries(BYTES_BOUNDARIES.to_vec())
.build();
bar_b.record(100, &[KeyValue::new("A", "B")]);
}),
expected_files: vec![
"conflict_help_two_histograms_1.txt",
"conflict_help_two_histograms_2.txt",
],
..Default::default()
},
TestCase {
name: "conflict_unit_two_counters",
record_metrics: Box::new(|meter_a, meter_b| {
let baz_a = meter_a
.u64_counter("bar")
.with_unit("By")
.with_description("meter bar")
.build();
baz_a.add(100, &[KeyValue::new("type", "bar")]);
let baz_b = meter_b
.u64_counter("bar")
.with_unit("ms")
.with_description("meter bar")
.build();
baz_b.add(100, &[KeyValue::new("type", "bar")]);
}),
builder: ExporterBuilder::default().without_units(),
expected_files: vec!["conflict_unit_two_counters.txt"],
..Default::default()
},
TestCase {
name: "conflict_unit_two_updowncounters",
record_metrics: Box::new(|meter_a, meter_b| {
let bar_a = meter_a
.i64_up_down_counter("bar")
.with_unit("By")
.with_description("meter gauge bar")
.build();
bar_a.add(100, &[KeyValue::new("type", "bar")]);
let bar_b = meter_b
.i64_up_down_counter("bar")
.with_unit("ms")
.with_description("meter gauge bar")
.build();
bar_b.add(100, &[KeyValue::new("type", "bar")]);
}),
builder: ExporterBuilder::default().without_units(),
expected_files: vec!["conflict_unit_two_updowncounters.txt"],
..Default::default()
},
TestCase {
name: "conflict_unit_two_histograms",
record_metrics: Box::new(|meter_a, meter_b| {
let bar_a = meter_a
.u64_histogram("bar")
.with_unit("By")
.with_description("meter histogram bar")
.with_boundaries(BYTES_BOUNDARIES.to_vec())
.build();
bar_a.record(100, &[KeyValue::new("A", "B")]);
let bar_b = meter_b
.u64_histogram("bar")
.with_unit("ms")
.with_description("meter histogram bar")
.with_boundaries(BYTES_BOUNDARIES.to_vec())
.build();
bar_b.record(100, &[KeyValue::new("A", "B")]);
}),
builder: ExporterBuilder::default().without_units(),
expected_files: vec!["conflict_unit_two_histograms.txt"],
..Default::default()
},
TestCase {
name: "conflict_type_counter_and_updowncounter",
record_metrics: Box::new(|meter_a, _meter_b| {
let counter = meter_a
.u64_counter("foo")
.with_unit("By")
.with_description("meter foo")
.build();
counter.add(100, &[KeyValue::new("type", "foo")]);
let gauge = meter_a
.i64_up_down_counter("foo_total")
.with_unit("By")
.with_description("meter foo")
.build();
gauge.add(200, &[KeyValue::new("type", "foo")]);
}),
builder: ExporterBuilder::default().without_units(),
expected_files: vec![
"conflict_type_counter_and_updowncounter_1.txt",
"conflict_type_counter_and_updowncounter_2.txt",
],
..Default::default()
},
TestCase {
name: "conflict_type_histogram_and_updowncounter",
record_metrics: Box::new(|meter_a, _meter_b| {
let foo_a = meter_a
.i64_up_down_counter("foo")
.with_unit("By")
.with_description("meter gauge foo")
.build();
foo_a.add(100, &[KeyValue::new("A", "B")]);
let foo_histogram_a = meter_a
.u64_histogram("foo")
.with_unit("By")
.with_description("meter histogram foo")
.with_boundaries(BOUNDARIES.to_vec())
.build();
foo_histogram_a.record(100, &[KeyValue::new("A", "B")]);
}),
expected_files: vec![
"conflict_type_histogram_and_updowncounter_1.txt",
"conflict_type_histogram_and_updowncounter_2.txt",
],
..Default::default()
},
];
for tc in test_cases {
let registry = prometheus::Registry::new();
let exporter = tc.builder.with_registry(registry.clone()).build().unwrap();
let resource = Resource::builder()
.with_attributes(
vec![
KeyValue::new(SERVICE_NAME, "prometheus_test"),
KeyValue::new(TELEMETRY_SDK_VERSION, "latest"),
]
.into_iter()
.chain(tc.custom_resource_attrs),
)
.build();
let provider = SdkMeterProvider::builder()
.with_resource(resource)
.with_reader(exporter)
.build();
let scope_ma = InstrumentationScope::builder("ma")
.with_version("v0.1.0")
.with_schema_url("https://opentelemetry.io/schemas/1.0.0")
.with_attributes(vec![KeyValue::new("k", "v")])
.build();
let scope_mb = InstrumentationScope::builder("mb")
.with_version("v0.1.0")
.with_schema_url("https://opentelemetry.io/schemas/1.0.0")
.with_attributes(vec![KeyValue::new("k", "v")])
.build();
let meter_a = provider.meter_with_scope(scope_ma);
let meter_b = provider.meter_with_scope(scope_mb);
(tc.record_metrics)(meter_a, meter_b);
let possible_matches = tc
.expected_files
.into_iter()
.map(|f| fs::read_to_string(Path::new("./tests/data").join(f)).expect(f))
.map(get_platform_specific_string)
.collect();
gather_and_compare_multi(registry, possible_matches, tc.name);
}
}
fn gather_and_compare_multi(
registry: prometheus::Registry,
expected: Vec<String>,
name: &'static str,
) {
let mut output = Vec::new();
let encoder = TextEncoder::new();
let metric_families = registry.gather();
encoder.encode(&metric_families, &mut output).unwrap();
let output_string = get_platform_specific_string(String::from_utf8(output).unwrap());
assert!(
expected.contains(&output_string),
"mismatched output in {name}"
)
}