use std::{
borrow::Cow,
ops::{Deref, DerefMut},
time::SystemTime,
};
use smallvec::SmallVec;
use crate::{
CowStr, Entry, EntryConfig, EntryWriter, MetricFlags, MetricValue, Observation, Unit,
ValidationError, Value, ValueWriter, value::VALUES_INLINE_CAPACITY,
};
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct WithDimensions<V, const N: usize> {
value: V,
dimensions: SmallVec<[(CowStr, CowStr); N]>,
}
impl<V, const N: usize> WithDimensions<V, N> {
pub fn map_value<U>(self, f: impl Fn(V) -> U) -> WithDimensions<U, N> {
WithDimensions {
value: f(self.value),
dimensions: self.dimensions,
}
}
}
pub type WithDimension<V> = WithDimensions<V, 1>;
pub type WithVecDimensions<V> = WithDimensions<V, 0>;
impl<V, const N: usize> Deref for WithDimensions<V, N> {
type Target = V;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<V, const N: usize> DerefMut for WithDimensions<V, N> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl<V, const N: usize> From<V> for WithDimensions<V, N> {
fn from(value: V) -> Self {
Self {
value,
dimensions: Default::default(),
}
}
}
impl<V> WithDimension<V> {
pub fn new(value: V, class: impl Into<CowStr>, instance: impl Into<CowStr>) -> Self {
Self::new_with_dimensions(value, [(class, instance)])
}
}
impl<V, const N: usize> WithDimensions<V, N> {
pub const fn new_const(value: V) -> Self {
Self {
value,
dimensions: SmallVec::new_const(),
}
}
pub fn new_with_dimensions<C, I>(value: V, dimensions: impl IntoIterator<Item = (C, I)>) -> Self
where
C: Into<CowStr>,
I: Into<CowStr>,
{
Self {
value,
dimensions: dimensions
.into_iter()
.map(|(c, i)| (c.into(), i.into()))
.collect(),
}
}
pub fn dimensions(&self) -> &[(CowStr, CowStr)] {
&self.dimensions
}
pub fn add_dimension(&mut self, key: impl Into<CowStr>, value: impl Into<CowStr>) -> &mut Self {
self.dimensions.push((key.into(), value.into()));
self
}
pub fn clear_dimensions(&mut self) {
self.dimensions.clear()
}
pub fn entry_writer_wrapper<'a, 'b, W: EntryWriter<'b>>(
&'a self,
writer: W,
) -> impl EntryWriter<'b> + use<'a, 'b, W, V, N> {
Wrapper {
value: writer,
dimensions: &self.dimensions,
}
}
}
#[derive(Debug)]
struct Wrapper<'a, V> {
value: V,
dimensions: &'a [(CowStr, CowStr)],
}
impl<'a, W: EntryWriter<'a>> EntryWriter<'a> for Wrapper<'_, W> {
fn timestamp(&mut self, timestamp: SystemTime) {
self.value.timestamp(timestamp);
}
fn value(&mut self, name: impl Into<Cow<'a, str>>, value: &(impl Value + ?Sized)) {
self.value.value(
name,
&Wrapper {
value,
dimensions: self.dimensions,
},
)
}
fn config(&mut self, config: &'a dyn EntryConfig) {
self.value.config(config);
}
}
impl<V: Value> Value for Wrapper<'_, V> {
const SHAPE: crate::descriptor::FieldShape<'static> = V::SHAPE;
const UNIT: crate::Unit = V::UNIT;
fn write(&self, writer: impl ValueWriter) {
self.value.write(Wrapper {
value: writer,
dimensions: self.dimensions,
})
}
}
impl<W: ValueWriter> ValueWriter for Wrapper<'_, W> {
fn string(self, value: &str) {
self.value.string(value);
}
fn metric<'a>(
self,
distribution: impl IntoIterator<Item = Observation>,
unit: Unit,
dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
flags: MetricFlags<'_>,
) {
#[allow(clippy::map_identity)]
self.value.metric(
distribution,
unit,
dimensions
.into_iter()
.map(|(k, v)| (k, v)) .chain(self.dimensions.iter().map(|(c, i)| (&**c, &**i))),
flags,
)
}
fn error(self, error: ValidationError) {
self.value.error(error)
}
fn values<'a, V: Value + 'a>(self, values: impl IntoIterator<Item = &'a V>) {
let dimensions = self.dimensions;
let wrapped: SmallVec<[Wrapper<'_, &'a V>; VALUES_INLINE_CAPACITY]> = values
.into_iter()
.map(|value| Wrapper { value, dimensions })
.collect();
self.value.values(wrapped.iter())
}
}
impl<V: Value, const N: usize> Value for WithDimensions<V, N> {
const SHAPE: crate::descriptor::FieldShape<'static> = V::SHAPE;
const UNIT: crate::Unit = V::UNIT;
fn write(&self, writer: impl ValueWriter) {
self.value.write(Wrapper {
value: writer,
dimensions: self.dimensions(),
})
}
}
impl<V: MetricValue, const N: usize> MetricValue for WithDimensions<V, N> {
type Unit = V::Unit;
}
impl<E: Entry, const N: usize> Entry for WithDimensions<E, N> {
fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
self.value.write(&mut self.entry_writer_wrapper(writer))
}
fn descriptors(&self) -> crate::Descriptors<'_> {
self.value.descriptors()
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use metrique_writer::{
Entry, EntryConfig, EntryWriter, MetricFlags, Observation, Unit, ValidationError, Value,
ValueWriter,
unit::{Millisecond, UnitTag as _},
value::MetricValue,
value::{WithDimension, WithDimensions},
};
#[test]
fn adds_dimensions() {
struct Writer;
impl ValueWriter for Writer {
fn string(self, value: &str) {
panic!("shouldn't have written {value}");
}
fn metric<'a>(
self,
distribution: impl IntoIterator<Item = Observation>,
unit: Unit,
dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
_flags: MetricFlags<'_>,
) {
let distribution = distribution.into_iter().collect::<Vec<_>>();
let dimensions = dimensions.into_iter().collect::<Vec<_>>();
assert_eq!(distribution, &[Observation::Floating(42.0)]);
assert_eq!(unit, Millisecond::UNIT);
assert_eq!(dimensions, &[("foo", "bar")]);
}
fn error(self, error: ValidationError) {
panic!("unexpected error {error}");
}
}
WithDimension::new(Duration::from_millis(42), "foo", "bar").write(Writer);
}
#[test]
fn runs_on_entries() {
#[derive(Entry)]
struct TestEntry {
#[entry(timestamp)]
ts: SystemTime,
#[entry(flatten)]
config: TestConfigEntry,
f1: Duration,
f2: Duration,
}
#[derive(Debug)]
struct TestConfig;
impl EntryConfig for TestConfig {}
struct TestConfigEntry;
impl Entry for TestConfigEntry {
fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
writer.config(&TestConfig);
}
}
let entry = WithDimensions::new(
TestEntry {
ts: SystemTime::UNIX_EPOCH,
config: TestConfigEntry,
f1: Duration::from_millis(42),
f2: Duration::from_millis(43),
},
"foo",
"bar",
);
let entry = metrique_writer::test_util::to_test_entry(&entry);
assert_eq!(entry.metrics["f1"], 42);
assert_eq!(
entry.metrics["f1"].dimensions,
vec![("foo".to_string(), "bar".to_string())]
);
assert_eq!(entry.metrics["f2"], 43);
assert_eq!(
entry.metrics["f2"].dimensions,
vec![("foo".to_string(), "bar".to_string())]
);
assert!(entry.timestamp.is_some());
}
#[test]
fn appends_after_existing_dimensions() {
struct Writer;
impl ValueWriter for Writer {
fn string(self, value: &str) {
panic!("shouldn't have written {value}");
}
fn metric<'a>(
self,
distribution: impl IntoIterator<Item = Observation>,
unit: Unit,
dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
_flags: MetricFlags<'_>,
) {
let distribution = distribution.into_iter().collect::<Vec<_>>();
let dimensions = dimensions.into_iter().collect::<Vec<_>>();
assert_eq!(distribution, &[Observation::Floating(42.0)]);
assert_eq!(unit, Millisecond::UNIT);
assert_eq!(dimensions, &[("foo", "bar"), ("a", "b"), ("c", "d")]);
}
fn error(self, error: ValidationError) {
panic!("unexpected error {error}");
}
}
let existing = Duration::from_millis(42).with_dimension("foo", "bar");
WithDimension::new_with_dimensions(existing, [("a", "b"), ("c", "d")]).write(Writer);
}
#[test]
fn test_const_with_dimensions() {
let empty_with_dimensions: WithDimensions<Duration, 1> =
WithDimensions::new_const(Duration::from_millis(19));
let from_with_dimensions = WithDimensions::from(Duration::from_millis(19));
assert_eq!(empty_with_dimensions, from_with_dimensions);
}
#[test]
fn forwards_values_with_dimensions() {
#[derive(Debug, PartialEq)]
enum Event {
String(String),
ValuesStart,
Metric {
value: u64,
dimensions: Vec<(String, String)>,
},
}
struct Recorder<'a>(&'a mut Vec<Event>);
impl ValueWriter for Recorder<'_> {
fn string(self, value: &str) {
self.0.push(Event::String(value.to_string()));
}
fn metric<'a>(
self,
distribution: impl IntoIterator<Item = Observation>,
_unit: Unit,
dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
_flags: MetricFlags<'_>,
) {
let Some(Observation::Unsigned(value)) = distribution.into_iter().next() else {
panic!("unexpected distribution");
};
self.0.push(Event::Metric {
value,
dimensions: dimensions
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
});
}
fn error(self, error: ValidationError) {
panic!("unexpected error {error}");
}
fn values<'a, V: Value + 'a>(self, values: impl IntoIterator<Item = &'a V>) {
self.0.push(Event::ValuesStart);
for value in values {
value.write(Recorder(self.0));
}
}
}
let mut events = Vec::new();
WithDimension::new(vec![1u64, 2u64], "foo", "bar").write(Recorder(&mut events));
let dimensions = vec![("foo".to_string(), "bar".to_string())];
assert_eq!(
events,
[
Event::ValuesStart,
Event::Metric {
value: 1,
dimensions: dimensions.clone()
},
Event::Metric {
value: 2,
dimensions
},
],
);
}
}