Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sfauvel/1990/refactor prometheus metrics using struct #1998

Merged
merged 23 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6bc994a
Example using a dedicated struct
sfauvel Oct 9, 2024
b5acdcd
Create a wrapper to prometheus Metrics
sfauvel Oct 10, 2024
43f3113
Extract common part from the service file
sfauvel Oct 10, 2024
b1e0768
Remove metric constants
sfauvel Oct 10, 2024
941db95
Create a `internal/mithril-metric` crate
sfauvel Oct 11, 2024
d38d52d
Move the export function to the `mothril-metric`
sfauvel Oct 11, 2024
1f9da7b
Add a macro to generate the metric service
sfauvel Oct 11, 2024
1c0127e
Rename the `MetricsServiceTrait` to `MetricsServiceExporter`
sfauvel Oct 11, 2024
fdd5adc
Fix clippy warning on documentation and cargo.toml sort
sfauvel Oct 14, 2024
404bf86
Rename `MithrilMetric` to `MetricCollector` and `record`to `increment…
sfauvel Oct 14, 2024
7280f43
Split `commons`file to `helper`and `metric` files
sfauvel Oct 14, 2024
1e515d9
Remove feature='full'
sfauvel Oct 14, 2024
141fb1e
Use a generic instead of `Epoch` for `MetricGauge`
sfauvel Oct 14, 2024
6ce5a1e
Use a generic instead of `Epoch` for `MetricGauge`
sfauvel Oct 14, 2024
53bcd54
Move code of the function `metrics_tools::export_metrics` into the `e…
sfauvel Oct 14, 2024
51cad72
The name of the metric must be provided to the macro
sfauvel Oct 14, 2024
fa1e5fa
Upgrade versions and fix fmt check warning
sfauvel Oct 15, 2024
97c31a5
Harmonize log messages
sfauvel Oct 15, 2024
441dd74
Add missing documentation
sfauvel Oct 16, 2024
cedeb26
Add `Send + Sync` to MetricServiceExporter trait and remove `Copy` fr…
sfauvel Oct 16, 2024
006e9ef
Fix code in documentation
sfauvel Oct 16, 2024
78b1131
Remove obsolete functions from signer MetricsService
sfauvel Oct 16, 2024
98038e1
chore: upgrade crate versions
sfauvel Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"internal/mithril-build-script",
"internal/mithril-doc",
"internal/mithril-doc-derive",
"internal/mithril-metric",
"internal/mithril-persistence",
"mithril-aggregator",
"mithril-client",
Expand Down
30 changes: 30 additions & 0 deletions internal/mithril-metric/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "mithril-metric"
version = "0.1.0"
description = "Common tools to expose metrics."
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
repository = { workspace = true }

[lib]
crate-type = ["lib", "cdylib", "staticlib"]

[dependencies]
anyhow = "1.0.89"
axum = "0.7.7"
mithril-common = { path = "../../mithril-common" }
paste = "1.0.15"
prometheus = "0.13.4"
reqwest = { version = "0.12.8", features = ["json", "stream"] }
slog = { version = "2.7.0", features = [
"max_level_trace",
"release_max_level_debug",
] }
tokio = { version = "1.40.0" }

[dev-dependencies]
prometheus-parse = "0.2.5"
slog-async = "2.8.0"
slog-term = "2.9.1"
19 changes: 19 additions & 0 deletions internal/mithril-metric/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.PHONY: all build test check doc

CARGO = cargo

all: test build

build:
${CARGO} build --release

test:
${CARGO} test

check:
${CARGO} check --release --all-features --all-targets
${CARGO} clippy --release --all-features --all-targets
${CARGO} fmt --check

doc:
${CARGO} doc --no-deps --open
5 changes: 5 additions & 0 deletions internal/mithril-metric/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Mithril-metric

**This is a work in progress** 🛠

This crate contains material to expose metrics.
244 changes: 244 additions & 0 deletions internal/mithril-metric/src/helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//! Helper to create a metric service.

/// Create a MetricService.
///
/// To build the service you need to provide the structure name and a list of metrics.
/// Each metrics is defined by an attribute name, a type, a metric name and a help message.
///
/// The attribute name will be used to create a getter method for the metric.
///
/// Crate that use this macro should have `paste` as dependency.
///
/// # Example of 'build_metrics_service' and metrics usage
///
/// ```
/// use slog::Logger;
/// use mithril_common::{entities::Epoch, StdResult};
/// use mithril_metric::build_metrics_service;
/// use mithril_metric::{MetricCollector, MetricCounter, MetricGauge, MetricsServiceExporter};
///
/// build_metrics_service!(
/// MetricsService,
/// counter_example: MetricCounter(
/// "custom_counter_example_name",
/// "Example of a counter metric"
/// ),
/// gauge_example: MetricGauge(
/// "custom_gauge_example_name",
/// "Example of a gauge metric"
/// )
/// );
///
/// let service = MetricsService::new(Logger::root(slog::Discard, slog::o!())).unwrap();
/// service.get_counter_example().increment();
/// service.get_gauge_example().record(Epoch(12));
/// ```
#[macro_export]
macro_rules! build_metrics_service {
($service:ident, $($metric_attribute:ident:$metric_type:ident($name:literal, $help:literal)),*) => {
paste::item! {
/// Metrics service which is responsible for recording and exposing metrics.
pub struct $service {
registry: prometheus::Registry,
$(
$metric_attribute: $metric_type,
)*
}

impl $service {
/// Create a new MetricsService instance.
pub fn new(logger: slog::Logger) -> mithril_common::StdResult<Self> {

let registry = prometheus::Registry::new();

$(
let $metric_attribute = $metric_type::new(
logger.clone(),
$name,
$help,
)?;
registry.register($metric_attribute.collector())?;
)*

Ok(Self {
registry,
$(
$metric_attribute,
)*
})
}
$(
/// Get the `$metric_attribute` counter.
pub fn [<get_ $metric_attribute>](&self) -> &$metric_type {
&self.$metric_attribute
}
)*
}

impl MetricsServiceExporter for $service {
fn export_metrics(&self) -> mithril_common::StdResult<String> {
Ok(prometheus::TextEncoder::new().encode_to_string(&self.registry.gather())?)
}
}

}
};
}

#[cfg(test)]
pub mod test_tools {
use std::{io, sync::Arc};

use slog::{Drain, Logger};
use slog_async::Async;
use slog_term::{CompactFormat, PlainDecorator};
pub struct TestLogger;

impl TestLogger {
fn from_writer<W: io::Write + Send + 'static>(writer: W) -> Logger {
let decorator = PlainDecorator::new(writer);
let drain = CompactFormat::new(decorator).build().fuse();
let drain = Async::new(drain).build().fuse();
Logger::root(Arc::new(drain), slog::o!())
}

pub fn stdout() -> Logger {
Self::from_writer(slog_term::TestStdoutWriter)
}
}
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use crate::{MetricCollector, MetricCounter, MetricGauge, MetricsServiceExporter};

use super::*;
use mithril_common::{entities::Epoch, StdResult};
use prometheus::{Registry, TextEncoder};
use prometheus_parse::Value;
use slog::Logger;
use test_tools::TestLogger;

fn parse_metrics(raw_metrics: &str) -> StdResult<BTreeMap<String, Value>> {
Ok(
prometheus_parse::Scrape::parse(raw_metrics.lines().map(|s| Ok(s.to_owned())))?
.samples
.into_iter()
.map(|s| (s.metric, s.value))
.collect::<BTreeMap<_, _>>(),
)
}

pub struct MetricsServiceExample {
registry: Registry,
counter_example: MetricCounter,
gauge_example: MetricGauge,
}

impl MetricsServiceExample {
pub fn new(logger: Logger) -> StdResult<Self> {
let registry = Registry::new();

let counter_example = MetricCounter::new(
logger.clone(),
"counter_example",
"Example of a counter metric",
)?;
registry.register(counter_example.collector())?;

let gauge_example =
MetricGauge::new(logger.clone(), "gauge_example", "Example of a gauge metric")?;
registry.register(gauge_example.collector())?;

Ok(Self {
registry,
counter_example,
gauge_example,
})
}

/// Get the `counter_example` counter.
pub fn get_counter_example(&self) -> &MetricCounter {
&self.counter_example
}

/// Get the `gauge_example` counter.
pub fn get_gauge_example(&self) -> &MetricGauge {
&self.gauge_example
}
}

impl MetricsServiceExporter for MetricsServiceExample {
fn export_metrics(&self) -> StdResult<String> {
Ok(TextEncoder::new().encode_to_string(&self.registry.gather())?)
}
}

#[test]
fn test_service_creation() {
let service = MetricsServiceExample::new(TestLogger::stdout()).unwrap();
service.get_counter_example().increment();
service.get_counter_example().increment();
service.get_gauge_example().record(Epoch(12));

assert_eq!(2, service.get_counter_example().get());
assert_eq!(Epoch(12), Epoch(service.get_gauge_example().get() as u64));
}

build_metrics_service!(
MetricsServiceExampleBuildWithMacro,
counter_example: MetricCounter(
"custom_counter_example_name",
"Example of a counter metric"
),
gauge_example: MetricGauge(
"custom_gauge_example_name",
"Example of a gauge metric"
)
);

#[test]
fn test_service_creation_using_build_metrics_service_macro() {
let service = MetricsServiceExampleBuildWithMacro::new(TestLogger::stdout()).unwrap();
service.get_counter_example().increment();
service.get_counter_example().increment();
service.get_gauge_example().record(Epoch(12));

assert_eq!(2, service.get_counter_example().get());
assert_eq!(Epoch(12), Epoch(service.get_gauge_example().get() as u64));
}

#[test]
fn test_build_metrics_service_named_metrics_with_attribute_name() {
let service = MetricsServiceExampleBuildWithMacro::new(TestLogger::stdout()).unwrap();
assert_eq!(
"custom_counter_example_name",
service.get_counter_example().name()
);
assert_eq!(
"custom_gauge_example_name",
service.get_gauge_example().name()
);
}

#[test]
fn test_build_metrics_service_provide_a_functional_export_metrics_function() {
let service = MetricsServiceExampleBuildWithMacro::new(TestLogger::stdout()).unwrap();

service.counter_example.increment();
service.gauge_example.record(Epoch(12));

let exported_metrics = service.export_metrics().unwrap();

let parsed_metrics = parse_metrics(&exported_metrics).unwrap();

let parsed_metrics_expected = BTreeMap::from([
(service.counter_example.name(), Value::Counter(1.0)),
(service.gauge_example.name(), Value::Gauge(12.0)),
]);

assert_eq!(parsed_metrics_expected, parsed_metrics);
}
}
15 changes: 15 additions & 0 deletions internal/mithril-metric/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#![warn(missing_docs)]

//! metrics module.
sfauvel marked this conversation as resolved.
Show resolved Hide resolved
//! This module contains the tools to create a metrics service and a metrics server.

pub mod helper;
pub mod metric;
mod server;

pub use metric::*;
pub use server::MetricsServer;
pub use server::MetricsServiceExporter;

#[cfg(test)]
pub use helper::test_tools::TestLogger;
Loading
Loading