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}