From 80de5ba437d71c4e78a32ed4ff657b70fef392b8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 26 Dec 2022 21:49:07 +0100 Subject: [PATCH] feat: add initial prometheus exporter --- collector.v | 24 +++++++++ collector_test.v | 116 +++++++++++++++++++++--------------------- exporter.v | 1 + exporter/exporter.v | 10 ---- exporter/prometheus.v | 4 -- metrics.v | 12 ++++- null.v | 8 +++ prometheus.v | 96 ++++++++++++++++++++++++++++++++++ prometheus_test.v | 54 ++++++++++++++++++++ 9 files changed, 252 insertions(+), 73 deletions(-) create mode 100644 exporter.v delete mode 100644 exporter/exporter.v delete mode 100644 exporter/prometheus.v create mode 100644 prometheus.v create mode 100644 prometheus_test.v diff --git a/collector.v b/collector.v index ca2ea2a..c116751 100644 --- a/collector.v +++ b/collector.v @@ -145,3 +145,27 @@ pub fn (c &DefaultCollector) gauge_get(metric Metric) ?f64 { entry.data[0] } } + +pub fn (c &DefaultCollector) histograms() []Metric { + mut metrics := []Metric{} + + rlock c.histograms { + for _, entry in c.histograms { + metrics << entry.metric + } + } + + return metrics +} + +pub fn (c &DefaultCollector) gauges() []Metric { + mut metrics := []Metric{} + + rlock c.gauges { + for _, entry in c.gauges { + metrics << entry.metric + } + } + + return metrics +} diff --git a/collector_test.v b/collector_test.v index fd79b4b..b00f462 100644 --- a/collector_test.v +++ b/collector_test.v @@ -18,13 +18,13 @@ fn test_counter_increment() { m.counter_increment(name: 'test') assert m.counter_get(name: 'test')? == u64(2) - // Test with labels - metric := Metric{ - name: 'test2' - labels: [['hi', 'label']!, ['hi2', 'label2']!] - } + // Test with labels + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } - m.counter_register(15, metric) + m.counter_register(15, metric) m.counter_increment(metric) assert m.counter_get(metric)? == u64(16) } @@ -32,81 +32,81 @@ fn test_counter_increment() { fn test_histogram() { mut m := new_default_collector() - m.histogram_register(name: 'test') - m.histogram_record(5.0, name: 'test') - assert m.histogram_get(name: 'test')? == [5.0] + m.histogram_register(name: 'test') + m.histogram_record(5.0, name: 'test') + assert m.histogram_get(name: 'test')? == [5.0] - m.histogram_record(7.0, name: 'test') - assert m.histogram_get(name: 'test')? == [5.0, 7.0] + m.histogram_record(7.0, name: 'test') + assert m.histogram_get(name: 'test')? == [5.0, 7.0] - // Test with labels - metric := Metric{ - name: 'test2' - labels: [['hi', 'label']!, ['hi2', 'label2']!] - } + // Test with labels + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } - m.histogram_register(metric) - m.histogram_record(5.0, metric) - assert m.histogram_get(metric)? == [5.0] + m.histogram_register(metric) + m.histogram_record(5.0, metric) + assert m.histogram_get(metric)? == [5.0] - m.histogram_record(7.0, metric) - assert m.histogram_get(metric)? == [5.0, 7.0] + m.histogram_record(7.0, metric) + assert m.histogram_get(metric)? == [5.0, 7.0] } fn test_gauge_add() { mut m := new_default_collector() - m.gauge_register(0.0, name: 'test') - m.gauge_add(5.0, name: 'test') - assert m.gauge_get(name: 'test')? == 5.0 + m.gauge_register(0.0, name: 'test') + m.gauge_add(5.0, name: 'test') + assert m.gauge_get(name: 'test')? == 5.0 - // Test with labels - metric := Metric{ - name: 'test2' - labels: [['hi', 'label']!, ['hi2', 'label2']!] - } + // Test with labels + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } - m.gauge_register(3.0, metric) - m.gauge_add(5.0, metric) - assert m.gauge_get(metric)? == 8.0 + m.gauge_register(3.0, metric) + m.gauge_add(5.0, metric) + assert m.gauge_get(metric)? == 8.0 } fn test_gauge_sub() { mut m := new_default_collector() - m.gauge_register(0.0, name: 'test') - m.gauge_sub(5.0, name: 'test') - assert m.gauge_get(name: 'test')? == -5.0 + m.gauge_register(0.0, name: 'test') + m.gauge_sub(5.0, name: 'test') + assert m.gauge_get(name: 'test')? == -5.0 - // Test with labels - metric := Metric{ - name: 'test2' - labels: [['hi', 'label']!, ['hi2', 'label2']!] - } + // Test with labels + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } - m.gauge_register(3.0, metric) - m.gauge_sub(5.0, metric) - assert m.gauge_get(metric)? == -2.0 + m.gauge_register(3.0, metric) + m.gauge_sub(5.0, metric) + assert m.gauge_get(metric)? == -2.0 } fn test_gauge_set() { mut m := new_default_collector() - m.gauge_register(0.0, name: 'test') - m.gauge_set(3.0, name: 'test') - assert m.gauge_get(name: 'test')? == 3.0 - m.gauge_set(5.0, name: 'test') - assert m.gauge_get(name: 'test')? == 5.0 + m.gauge_register(0.0, name: 'test') + m.gauge_set(3.0, name: 'test') + assert m.gauge_get(name: 'test')? == 3.0 + m.gauge_set(5.0, name: 'test') + assert m.gauge_get(name: 'test')? == 5.0 - // Test with labels - metric := Metric{ - name: 'test2' - labels: [['hi', 'label']!, ['hi2', 'label2']!] - } + // Test with labels + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } - m.gauge_register(0.0, metric) - m.gauge_set(3.0, metric) - assert m.gauge_get(metric)? == 3.0 - m.gauge_set(5.0, metric) - assert m.gauge_get(metric)? == 5.0 + m.gauge_register(0.0, metric) + m.gauge_set(3.0, metric) + assert m.gauge_get(metric)? == 3.0 + m.gauge_set(5.0, metric) + assert m.gauge_get(metric)? == 5.0 } diff --git a/exporter.v b/exporter.v new file mode 100644 index 0000000..971986c --- /dev/null +++ b/exporter.v @@ -0,0 +1 @@ +module metrics diff --git a/exporter/exporter.v b/exporter/exporter.v deleted file mode 100644 index c0f4f3f..0000000 --- a/exporter/exporter.v +++ /dev/null @@ -1,10 +0,0 @@ -module exporter - -import io -import metrics { MetricsCollector } - -pub interface MetricsExporter { - load(collector MetricsCollector) - export_to_writer(writer io.Writer) ! - export_to_string() string ! -} diff --git a/exporter/prometheus.v b/exporter/prometheus.v deleted file mode 100644 index 77af204..0000000 --- a/exporter/prometheus.v +++ /dev/null @@ -1,4 +0,0 @@ -module exporter - -pub struct PrometheusExporter { -} diff --git a/metrics.v b/metrics.v index 9ba93d8..b48f99b 100644 --- a/metrics.v +++ b/metrics.v @@ -1,5 +1,7 @@ module metrics +import io + [params] pub struct Metric { name string [required] @@ -8,7 +10,7 @@ pub struct Metric { [inline] fn join_two_array(arr [2]string) string { - return arr[0] + '=' + arr[1] + return '${arr[0]}="${arr[1]}"' } pub fn (m &Metric) str() string { @@ -25,12 +27,20 @@ pub interface MetricsCollector { counters() []Metric histogram_record(value f64, metric Metric) histogram_get(metric Metric) ?[]f64 + histograms() []Metric gauge_add(value f64, metric Metric) gauge_sub(value f64, metric Metric) gauge_set(value f64, metric Metric) gauge_get(metric Metric) ?f64 + gauges() []Metric mut: counter_register(value u64, metric Metric) histogram_register(metric Metric) gauge_register(value f64, metric Metric) } + +pub interface MetricsExporter { + load(collector MetricsCollector) + export_to_writer(writer io.Writer) ! + export_to_string() !string +} diff --git a/null.v b/null.v index 24fd490..01fc03d 100644 --- a/null.v +++ b/null.v @@ -38,3 +38,11 @@ pub fn (c &NullCollector) gauge_set(value f64, metric Metric) {} pub fn (c &NullCollector) gauge_get(metric Metric) ?f64 { return none } + +pub fn (c &NullCollector) histograms() []Metric { + return [] +} + +pub fn (c &NullCollector) gauges() []Metric { + return [] +} diff --git a/prometheus.v b/prometheus.v new file mode 100644 index 0000000..cc50f9a --- /dev/null +++ b/prometheus.v @@ -0,0 +1,96 @@ +module metrics + +import strings +import io +import arrays + +pub struct PrometheusExporter { + buckets []f64 +mut: + collector MetricsCollector +} + +pub fn new_prometheus_exporter(buckets []f64) PrometheusExporter { + return PrometheusExporter{ + buckets: buckets + } +} + +pub fn (mut e PrometheusExporter) load(collector MetricsCollector) { + e.collector = collector +} + +pub fn (mut e PrometheusExporter) export_to_string() !string { + mut builder := strings.new_builder(64) + + e.export_to_writer(mut builder)! + + return builder.str() +} + +pub fn (mut e PrometheusExporter) export_to_writer(mut writer io.Writer) ! { + for counter in e.collector.counters() { + val := e.collector.counter_get(counter) or { return error("This can't happen.") } + line := '$counter $val\n' + + writer.write(line.bytes())! + } + + for gauge in e.collector.gauges() { + val := e.collector.gauge_get(gauge) or { return error("This can't happen.") } + line := '$gauge $val\n' + + writer.write(line.bytes())! + } + + for hist in e.collector.histograms() { + data := e.collector.histogram_get(hist) or { return error("This can't happen.") } + + sum := arrays.sum(data) or { 0.0 } + total_count := data.len + + mut bucket_counts := []u64{len: e.buckets.len} + + mut i := bucket_counts.len - 1 + + // For each data point, increment all buckets that the value is + // contained in. Because the buckets are sorted, we can stop once we + // encounter one that it doesn't fit in + for val in data { + for i >= 0 && val <= e.buckets[i] { + bucket_counts[i]++ + + i -= 1 + } + + i = bucket_counts.len - 1 + } + + mut m := Metric{ + ...hist + name: '${hist.name}_count' + } + writer.write('$m $total_count\n'.bytes())! + + m = Metric{ + ...hist + name: '${hist.name}_sum' + } + writer.write('$m $sum\n'.bytes())! + + mut le_labels := [][2]string{} + le_labels.prepend(hist.labels) + le_labels << ['le', '']! + + for j, bucket in e.buckets { + le_labels[le_labels.len - 1][1] = bucket.str() + + m = Metric{ + name: '${hist.name}_bucket' + labels: le_labels + } + + writer.write('$m ${bucket_counts[j]}\n'.bytes())! + } + } +} diff --git a/prometheus_test.v b/prometheus_test.v new file mode 100644 index 0000000..ce77731 --- /dev/null +++ b/prometheus_test.v @@ -0,0 +1,54 @@ +module metrics + +fn test_only_counters() { + mut m := new_default_collector() + m.counter_register(0, name: 'test') + m.counter_increment(name: 'test') + + mut e := new_prometheus_exporter([]) + e.load(m) + + assert e.export_to_string()! == 'test 1\n' + + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } + m.counter_register(0, metric) + m.counter_increment(metric) + m.counter_increment(metric) + + assert e.export_to_string()! == 'test 1\ntest2{hi="label",hi2="label2"} 2\n' +} + +fn test_only_gauges() { + mut m := new_default_collector() + m.gauge_register(0.0, name: 'test') + m.gauge_set(3.25, name: 'test') + + mut e := new_prometheus_exporter([]) + e.load(m) + + assert e.export_to_string()! == 'test 3.25\n' + + metric := Metric{ + name: 'test2' + labels: [['hi', 'label']!, ['hi2', 'label2']!] + } + m.gauge_register(0.0, metric) + m.gauge_add(2.5, metric) + + assert e.export_to_string()! == 'test 3.25\ntest2{hi="label",hi2="label2"} 2.5\n' +} + +fn test_single_histogram() { + mut m := new_default_collector() + + m.histogram_register(name: 'test') + m.histogram_record(5.0, name: 'test') + + mut e := new_prometheus_exporter([0.5, 5.0]) + e.load(m) + + assert e.export_to_string()! == 'test_count 1\ntest_sum 5.0\ntest_bucket{le="0.5"} 0\ntest_bucket{le="5.0"} 1\n' +}