metrique_writer_core/entry/
mod.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This module contains the [Entry] trait, which represents an entry that can be written to
5//! an [EntryWriter] in order to emit metrics
6
7use std::{any::Any, borrow::Cow, sync::Arc, time::SystemTime};
8
9mod boxed;
10pub use boxed::BoxEntry;
11
12mod map;
13
14mod merged;
15pub use merged::{Merged, MergedRef};
16
17use crate::Value;
18
19/// The core trait to be implemented by application data structures holding metric values.
20///
21/// Implementations of [`Entry`] should generally be pure functions, emitting the same metric values
22/// independent of the environment. External dependencies, such as metrics that are relative to
23/// the current time, or metrics that refer to the value behind a mutex or atomic, should be resolved
24/// before creating an [`Entry`].
25///
26/// It's analogous to the [`std::fmt::Display`] trait that works with a [`std::fmt::Formatter`] to display a value as a
27/// string. In this case, the implementer of `Entry` contains the metric data and works with an [`EntryWriter`] to write
28/// the data as a metric entry in some format. The `EntryWriter` trait abstracts away the different formatting logic
29/// from the `Entry` implementer, just like `Formatter` does for `Display` implementers.
30///
31/// ## Creating Entries
32///
33/// The `metrique` crate provides abstractions for easily creating an [`Entry`] while having a step
34/// where dependencies of the metric on external value can be resolved.
35///
36/// If your [`Entry`] does not need to contain external dependencies and you are not using `metrique`,
37/// the easiest way of creating an [`Entry`] is by using `derive(Entry)`, for example:
38///
39/// ```
40/// # use std::time::{Duration, SystemTime};
41/// # use metrique_writer::Entry;
42/// # use metrique_writer::unit::{AsBytes, AsMicroseconds};
43///
44/// #[derive(Entry, Debug)]
45/// #[entry(rename_all = "PascalCase")]
46/// struct RequestMetrics {
47///     #[entry(timestamp)]
48///     request_start: SystemTime,
49///     // emitted as `0` or `1`
50///     success: bool,
51///     // will be skipped if `None`
52///     optional_value: Option<u32>,
53///     byte_size: AsBytes<u64>,
54///     // An `Entry` can nest inner `Entry` structs
55///     #[entry(flatten)]
56///     subprocess: SubprocessMetrics,
57/// }
58///
59/// #[derive(Entry, Default, Debug)]
60/// #[entry(rename_all = "PascalCase")]
61/// struct SubprocessMetrics {
62///     counter: u32,
63///     // this Duration will be emitted as milliseconds (the default)
64///     duration: Duration,
65///     // this Duration will be emitted as microseconds
66///     duration_microseconds: AsMicroseconds<Duration>,
67/// }
68/// ```
69///
70/// You can also implement the [`Entry`] trait manually (this does the same as the
71/// macro-using code above):
72///
73/// ```no_run
74/// # use metrique_writer_core::{Entry, EntryWriter};
75/// # use std::time::{Duration, SystemTime};
76/// # use metrique_writer::unit::{AsBytes, AsMicroseconds};
77///
78/// struct RequestMetrics {
79///     request_start: SystemTime,
80///     // emitted as `0` or `1`
81///     success: bool,
82///     // will be skipped if `None`
83///     optional_value: Option<u32>,
84///     byte_size: AsBytes<u64>,
85///     // Multiple entries can be merged or flattened into a single written entry by
86///     // invoking [`Entry::write()`] multiple times with the same `EntryWriter`.
87///     // This makes nesting separate structs easy.
88///     subprocess: SubprocessMetrics,
89/// }
90///
91/// struct SubprocessMetrics {
92///     counter: u32,
93///     // this Duration will be emitted as milliseconds (the default
94///     // of `impl Value for Duration` and `impl MetricValue for Duration`).
95///     duration: Duration,
96///     // this Duration will be emitted as microseconds
97///     duration_microseconds: AsMicroseconds<Duration>,
98/// }
99///
100/// impl Entry for RequestMetrics {
101///     fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
102///         writer.timestamp(self.request_start);
103///         writer.value("Success", &self.success);
104///         writer.value("OptionalValue", &self.optional_value);
105///         writer.value("ByteSize", &self.byte_size);
106///         self.subprocess.write(writer);
107///     }
108/// }
109///
110/// impl Entry for SubprocessMetrics {
111///     fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
112///         writer.value("Counter", &self.counter);
113///         writer.value("Duration", &self.duration);
114///         writer.value("DurationMicroseconds", &self.duration_microseconds);
115///     }
116/// }
117/// ```
118///
119#[diagnostic::on_unimplemented(
120    message = "`{Self}` is not a metric entry",
121    note = "Entry structs created by the `#[metrics]` macro implement `InflectableEntry` rather than `Entry`, and need to be rooted via `RootEntry`"
122)]
123pub trait Entry {
124    /// Write the metric values contained in this entry to the format-provided [`EntryWriter`]. The `writer` corresponds
125    /// to an atomic entry written to the metrics consumer, like CloudWatch.
126    fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>);
127
128    /// The key used to group "similar" entries when sampling. Defaults to the empty group.
129    ///
130    /// If the output format is unsampled or is using a naive sampling strategy, like a
131    /// [`FixedFractionSample`], this is unused.
132    ///
133    /// For adaptive sampling strategies, like [`CongressSample`], the sample group should reflect
134    /// representative buckets for the service. A sane starting point for request-reply services would include the API
135    /// name and resulting status code. This ensures that less frequent APIs and less frequent status codes aren't lost
136    /// in a low sample rate.
137    ///
138    /// The order of (key, value) pairs in the group doesn't matter, but each key must be unique. Implementations should
139    /// panic on duplicate keys in debug builds but only emit a [`tracing`] error otherwise.
140    ///
141    /// # Example
142    /// For a request-reply service, typically the API name and result should be used.
143    /// ```
144    /// # use metrique_writer::Entry;
145    /// # use std::collections::HashMap;
146    /// #[derive(Entry, Default, Debug)]
147    /// #[entry(rename_all = "PascalCase")]
148    /// struct RequestMetrics {
149    ///     #[entry(sample_group)]
150    ///     operation: &'static str,
151    ///     #[entry(sample_group)]
152    ///     result: &'static str,
153    ///     some_counter: u64,
154    ///     // ...
155    /// }
156    ///
157    /// let metrics = RequestMetrics {
158    ///     operation: "Foo",
159    ///     result: "ValidationError",
160    ///     ..Default::default()
161    /// };
162    /// let sample_group = metrics.sample_group().collect::<HashMap<_, _>>();
163    /// assert_eq!(&sample_group["Operation"], "Foo");
164    /// assert_eq!(&sample_group["Result"], "ValidationError");
165    /// ```
166    ///
167    /// [`FixedFractionSample`]: https://docs.rs/metrique-writer/0.1/metrique_writer/sample/struct.FixedFractionSample.html
168    /// [`CongressSample`]: https://docs.rs/metrique-writer/0.1/metrique_writer/sample/struct.CongressSample.html
169    fn sample_group(&self) -> impl Iterator<Item = SampleGroupElement> {
170        [].into_iter()
171    }
172
173    /// Create a new entry that writes all the contents of this entry and then all of the contents of `other`.
174    ///
175    /// Useful to merge in global constants or metrics collected by different subsystems.
176    fn merge<E>(self, other: E) -> Merged<Self, E>
177    where
178        Self: Sized,
179    {
180        Merged(self, other)
181    }
182
183    /// Like [`Entry::merge`], but does so by reference.
184    fn merge_by_ref<'a, E: 'a + Entry>(&'a self, other: &'a E) -> MergedRef<'a, Self, E> {
185        MergedRef(self, other)
186    }
187
188    /// Move the entry to the heap and rely on dynamic dispatch.
189    ///
190    /// Useful for creating heterogeneous collections of entries.
191    fn boxed(self) -> BoxEntry
192    where
193        Self: Sized + Send + 'static,
194    {
195        BoxEntry::new(self)
196    }
197}
198
199/// A `(key, value)` pair, part of a sample group
200pub type SampleGroupElement = (Cow<'static, str>, Cow<'static, str>);
201
202/// [`Entry`] that will write no fields.
203///
204/// Useful for specifying empty globals when attaching [`crate::global`] sinks.
205#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
206pub struct EmptyEntry;
207
208impl Entry for EmptyEntry {
209    fn write<'a>(&'a self, _writer: &mut impl EntryWriter<'a>) {}
210}
211
212/// Trait for format-specific Entry configuration, formats will downcast this to the specific config
213pub trait EntryConfig: Any + std::fmt::Debug {}
214
215/// Provided by a format for each atomic entry that will be written to the metric destination.
216///
217/// Note that the lifetime `'a` corresponds to the lifetime of all metric names (if [`Cow::Borrowed`]) for the format
218/// entry. In most cases, metric names are `'static` strings anyways, but this allows formats to store hash sets of all
219/// written names for validation without allocating copies.
220pub trait EntryWriter<'a> {
221    /// Set the timestamp associated with the metric entry. If this is never invoked, formats are free to use the
222    /// current system time.
223    ///
224    /// This must never panic, but if invoked twice may result in a validation panic on [`crate::EntrySink::append()`]
225    /// for test sinks or a `tracing` event on production queues.
226    fn timestamp(&mut self, timestamp: SystemTime);
227
228    /// Record a metric [`Value`] in the entry. Each format may have more specific requirements, but typically each
229    /// `name` must be unique within the entry.
230    ///
231    /// This must never panic, but if invalid names or values may result in a panic on [`crate::EntrySink::append()`]
232    /// for test sinks or a `tracing` event on production queues.
233    fn value(&mut self, name: impl Into<Cow<'a, str>>, value: &(impl Value + ?Sized));
234
235    /// Pass format-specific entry configuration. Formatters should ignore configuration they are unaware of.
236    fn config(&mut self, config: &'a dyn EntryConfig);
237}
238
239impl<'a, W: EntryWriter<'a>> EntryWriter<'a> for &mut W {
240    fn timestamp(&mut self, timestamp: SystemTime) {
241        (**self).timestamp(timestamp)
242    }
243
244    fn value(&mut self, name: impl Into<Cow<'a, str>>, value: &(impl Value + ?Sized)) {
245        (**self).value(name, value)
246    }
247
248    fn config(&mut self, config: &'a dyn EntryConfig) {
249        (**self).config(config)
250    }
251}
252
253impl<T: Entry + ?Sized> Entry for &T {
254    fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
255        (**self).write(writer)
256    }
257
258    fn sample_group(&self) -> impl Iterator<Item = SampleGroupElement> {
259        (**self).sample_group()
260    }
261}
262
263impl<T: Entry> Entry for Option<T> {
264    fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
265        if let Some(entry) = self.as_ref() {
266            entry.write(writer)
267        }
268    }
269
270    fn sample_group(&self) -> impl Iterator<Item = SampleGroupElement> {
271        if let Some(entry) = self.as_ref() {
272            itertools::Either::Left(entry.sample_group())
273        } else {
274            itertools::Either::Right([].into_iter())
275        }
276    }
277}
278
279impl<T: Entry + ?Sized> Entry for Box<T> {
280    fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
281        (**self).write(writer)
282    }
283
284    fn sample_group(&self) -> impl Iterator<Item = SampleGroupElement> {
285        (**self).sample_group()
286    }
287}
288
289impl<T: Entry + ?Sized> Entry for Arc<T> {
290    fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
291        (**self).write(writer)
292    }
293
294    fn sample_group(&self) -> impl Iterator<Item = SampleGroupElement> {
295        (**self).sample_group()
296    }
297}
298
299impl<T: Entry + ToOwned + ?Sized> Entry for Cow<'_, T> {
300    fn write<'a>(&'a self, writer: &mut impl EntryWriter<'a>) {
301        (**self).write(writer)
302    }
303
304    fn sample_group(&self) -> impl Iterator<Item = SampleGroupElement> {
305        (**self).sample_group()
306    }
307}