Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion foundations-metrics-registry/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#[allow(missing_docs, unreachable_pub, clippy::all)]
mod model;

pub use model::{BucketSpan, Counter, Gauge, Histogram, Metric, MetricFamily, MetricType};
pub use model::{
BucketSpan, Counter, Gauge, Histogram, LabelPair, Metric, MetricFamily, MetricType,
};

#[cfg(test)]
mod tests {
Expand Down
3 changes: 3 additions & 0 deletions foundations-metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ license.workspace = true
[dependencies]
foundations-metrics-registry = { workspace = true }
prometheus-client = "0.25.0"
serde = { workspace = true, features = ["derive"] }
ryu = "1.0.23"
parking_lot = { workspace = true }

[lints]
workspace = true
61 changes: 61 additions & 0 deletions foundations-metrics/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Reporting of non-fatal diagnostics produced while collecting metrics.

use std::error::Error;
use std::fmt;
use std::io::{self, Write};
use std::sync::OnceLock;

type CollectErrorHook = Box<dyn for<'a> Fn(fmt::Arguments<'a>) + Send + Sync>;

static COLLECT_ERROR_HOOK: OnceLock<CollectErrorHook> = OnceLock::new();

/// Error returned by [`set_collect_error_hook`] when a hook is already installed.
#[derive(Debug)]
pub struct CollectErrorHookAlreadySet(());

impl fmt::Display for CollectErrorHookAlreadySet {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a metric collection error hook is already installed")
}
}

impl Error for CollectErrorHookAlreadySet {}

/// Routes non-fatal metric-collection diagnostics through a custom hook.
///
/// Collecting metrics never fails: label sets or metric groups that cannot be
/// encoded are skipped and a non-fatal diagnostic is emitted instead. By default
/// these diagnostics are written to standard error.
///
/// `foundations-metrics` is the low-level layer of the metrics stack and
/// deliberately depends on no logging framework, keeping logging decoupled from
/// metrics and avoiding a dependency cycle with higher-level crates. Instead of
/// calling into a logger directly, it exposes this seam: install a hook to route
/// collection diagnostics into your logging pipeline (typically at warning
/// level). The hook applies process-wide and can only be set once.
///
/// ```no_run
/// foundations_metrics::set_collect_error_hook(|args| {
/// // Forward to your logging framework, e.g. `log`, `tracing`, or `slog`.
/// eprintln!("{args}");
/// })
/// .expect("collect error hook installed once during start-up");
/// ```
pub fn set_collect_error_hook(
hook: impl for<'a> Fn(fmt::Arguments<'a>) + Send + Sync + 'static,
) -> Result<(), CollectErrorHookAlreadySet> {
COLLECT_ERROR_HOOK
.set(Box::new(hook))
.map_err(|_| CollectErrorHookAlreadySet(()))
}

/// Emits a non-fatal collection diagnostic through the installed hook, falling
/// back to standard error when no hook is set.
pub(crate) fn report_collect_error(args: fmt::Arguments<'_>) {
match COLLECT_ERROR_HOOK.get() {
Some(hook) => hook(args),
None => {
let _ = writeln!(io::stderr().lock(), "{args}");
}
}
}
35 changes: 35 additions & 0 deletions foundations-metrics/src/labels/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::error::Error;
use std::fmt;

// Adapted from prometools' `serde::error::Error`
// (https://github.com/nox/prometools, licensed MIT OR Apache-2.0).
/// An error produced while serializing a metric label set.
#[derive(Debug)]
pub struct LabelError {
message: String,
}

impl LabelError {
pub(crate) fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}

impl fmt::Display for LabelError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}

impl Error for LabelError {}

impl serde::ser::Error for LabelError {
fn custom<T>(message: T) -> Self
where
T: fmt::Display,
{
Self::new(message.to_string())
}
}
22 changes: 22 additions & 0 deletions foundations-metrics/src/labels/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Serialization of metric label sets into the protobuf data model.

mod error;
mod serializer;

pub use error::LabelError;

use foundations_metrics_registry::proto::LabelPair;
use serde::Serialize;

// Adapted from prometools' `serde` label serializer
// (https://github.com/nox/prometools, licensed MIT OR Apache-2.0).
/// Serializes a label set into protobuf label pairs.
///
/// Label values are stored as raw strings. OpenMetrics escaping is deliberately
/// deferred until text encoding.
pub fn to_label_pairs<S>(labels: &S) -> Result<Vec<LabelPair>, LabelError>
where
S: Serialize + ?Sized,
{
labels.serialize(serializer::LabelSetSerializer)
}
Loading