saluki_context/
context.rs

1use std::{fmt, hash, sync::Arc};
2
3use metrics::Gauge;
4use stringtheory::MetaString;
5
6use crate::{
7    hash::{hash_context, ContextKey},
8    tags::{SharedTagSet, TagSet},
9};
10
11const BASE_CONTEXT_SIZE: usize = std::mem::size_of::<Context>() + std::mem::size_of::<ContextInner>();
12
13/// A metric context.
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15pub struct Context {
16    inner: Arc<ContextInner>,
17}
18
19impl Context {
20    /// Creates a new `Context` from the given static name.
21    pub fn from_static_name(name: &'static str) -> Self {
22        let tags = SharedTagSet::default();
23        let origin_tags = SharedTagSet::default();
24
25        let (key, _) = hash_context(name, &tags, &origin_tags);
26        Self {
27            inner: Arc::new(ContextInner {
28                name: MetaString::from_static(name),
29                tags,
30                origin_tags,
31                key,
32                active_count: Gauge::noop(),
33            }),
34        }
35    }
36
37    /// Creates a new `Context` from the given static name and given static tags.
38    pub fn from_static_parts(name: &'static str, tags: &[&'static str]) -> Self {
39        let mut tag_set = TagSet::with_capacity(tags.len());
40        for tag in tags {
41            tag_set.insert_tag(MetaString::from_static(tag));
42        }
43
44        let origin_tags = SharedTagSet::default();
45
46        let (key, _) = hash_context(name, tags, &origin_tags);
47        Self {
48            inner: Arc::new(ContextInner {
49                name: MetaString::from_static(name),
50                tags: tag_set.into_shared(),
51                origin_tags,
52                key,
53                active_count: Gauge::noop(),
54            }),
55        }
56    }
57
58    /// Creates a new `Context` from the given name and given tags.
59    pub fn from_parts<S: Into<MetaString>>(name: S, tags: SharedTagSet) -> Self {
60        let name = name.into();
61        let origin_tags = SharedTagSet::default();
62        let (key, _) = hash_context(&name, &tags, &origin_tags);
63        Self {
64            inner: Arc::new(ContextInner {
65                name,
66                tags,
67                origin_tags,
68                key,
69                active_count: Gauge::noop(),
70            }),
71        }
72    }
73
74    /// Clones this context, and uses the given name for the cloned context.
75    pub fn with_name<S: Into<MetaString>>(&self, name: S) -> Self {
76        // Regenerate the context key to account for the new name.
77        let name = name.into();
78        let tags = self.inner.tags.clone();
79        let origin_tags = self.inner.origin_tags.clone();
80        let (key, _) = hash_context(&name, &tags, &origin_tags);
81
82        Self {
83            inner: Arc::new(ContextInner {
84                name,
85                tags,
86                origin_tags,
87                key,
88                active_count: Gauge::noop(),
89            }),
90        }
91    }
92
93    pub(crate) fn from_inner(inner: ContextInner) -> Self {
94        Self { inner: Arc::new(inner) }
95    }
96
97    #[cfg(test)]
98    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
99        Arc::ptr_eq(&self.inner, &other.inner)
100    }
101
102    /// Returns the name of this context.
103    pub fn name(&self) -> &MetaString {
104        &self.inner.name
105    }
106
107    /// Returns the instrumented tags of this context.
108    pub fn tags(&self) -> &SharedTagSet {
109        &self.inner.tags
110    }
111
112    /// Returns the origin tags of this context.
113    pub fn origin_tags(&self) -> &SharedTagSet {
114        &self.inner.origin_tags
115    }
116
117    /// Returns the size of this context in bytes.
118    ///
119    /// A context's size is the sum of the sizes of its fields and the size of the `Context` struct itself, and
120    /// includes:
121    /// - the context name
122    /// - the context tags (both instrumented and origin)
123    ///
124    /// Since origin tags can potentially be expensive to calculate, this method will cache the size of the origin tags
125    /// when this method is first called.
126    ///
127    /// Additionally, the value returned by this method does not compensate for externalities such as origin tags
128    /// potentially being shared by multiple contexts, or whether or not tags are are inlined, interned, or heap
129    /// allocated. This means that the value returned is essentially the worst-case usage, and should be used as a rough
130    /// estimate.
131    pub fn size_of(&self) -> usize {
132        let name_size = self.inner.name.len();
133        let tags_size = self.inner.tags.size_of();
134        let origin_tags_size = self.inner.origin_tags.size_of();
135
136        BASE_CONTEXT_SIZE + name_size + tags_size + origin_tags_size
137    }
138}
139
140impl From<&'static str> for Context {
141    fn from(name: &'static str) -> Self {
142        Self::from_static_name(name)
143    }
144}
145
146impl<'a> From<(&'static str, &'a [&'static str])> for Context {
147    fn from((name, tags): (&'static str, &'a [&'static str])) -> Self {
148        Self::from_static_parts(name, tags)
149    }
150}
151
152impl fmt::Display for Context {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "{}", self.inner.name)?;
155        if !self.inner.tags.is_empty() {
156            write!(f, "{{")?;
157
158            let mut needs_separator = false;
159            for tag in &self.inner.tags {
160                if needs_separator {
161                    write!(f, ", ")?;
162                } else {
163                    needs_separator = true;
164                }
165
166                write!(f, "{}", tag)?;
167            }
168
169            write!(f, "}}")?;
170        }
171
172        Ok(())
173    }
174}
175
176pub(super) struct ContextInner {
177    key: ContextKey,
178    name: MetaString,
179    tags: SharedTagSet,
180    origin_tags: SharedTagSet,
181    active_count: Gauge,
182}
183
184impl ContextInner {
185    pub fn from_parts(
186        key: ContextKey, name: MetaString, tags: SharedTagSet, origin_tags: SharedTagSet, active_count: Gauge,
187    ) -> Self {
188        Self {
189            key,
190            name,
191            tags,
192            origin_tags,
193            active_count,
194        }
195    }
196}
197
198impl Clone for ContextInner {
199    fn clone(&self) -> Self {
200        Self {
201            key: self.key,
202            name: self.name.clone(),
203            tags: self.tags.clone(),
204            origin_tags: self.origin_tags.clone(),
205
206            // We're specifically detaching this context from the statistics of the resolver from which `self`
207            // originated, as we only want to track the statistics of the contexts created _directly_ through the
208            // resolver.
209            active_count: Gauge::noop(),
210        }
211    }
212}
213
214impl Drop for ContextInner {
215    fn drop(&mut self) {
216        self.active_count.decrement(1);
217    }
218}
219
220impl PartialEq<ContextInner> for ContextInner {
221    fn eq(&self, other: &ContextInner) -> bool {
222        // TODO: Note about why we consider the hash good enough for equality.
223        self.key == other.key
224    }
225}
226
227impl Eq for ContextInner {}
228
229impl hash::Hash for ContextInner {
230    fn hash<H: hash::Hasher>(&self, state: &mut H) {
231        self.key.hash(state);
232    }
233}
234
235impl fmt::Debug for ContextInner {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        f.debug_struct("ContextInner")
238            .field("name", &self.name)
239            .field("tags", &self.tags)
240            .field("key", &self.key)
241            .finish()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::tags::Tag;
249
250    const SIZE_OF_CONTEXT_NAME: &str = "size_of_test_metric";
251    const SIZE_OF_CONTEXT_CHANGED_NAME: &str = "size_of_test_metric_changed";
252    const SIZE_OF_CONTEXT_TAGS: &[&str] = &["size_of_test_tag1", "size_of_test_tag2"];
253    const SIZE_OF_CONTEXT_ORIGIN_TAGS: &[&str] = &["size_of_test_origin_tag1", "size_of_test_origin_tag2"];
254
255    fn tag_set(tags: &[&str]) -> SharedTagSet {
256        tags.iter().map(|s| Tag::from(*s)).collect::<TagSet>().into_shared()
257    }
258
259    #[test]
260    fn size_of_context_from_static_name() {
261        let context = Context::from_static_name(SIZE_OF_CONTEXT_NAME);
262        assert_eq!(context.size_of(), BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len());
263    }
264
265    #[test]
266    fn size_of_context_from_static_parts() {
267        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
268
269        let context = Context::from_static_parts(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS);
270        assert_eq!(
271            context.size_of(),
272            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of()
273        );
274    }
275
276    #[test]
277    fn size_of_context_from_parts() {
278        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
279
280        let context = Context::from_parts(SIZE_OF_CONTEXT_NAME, tags.clone());
281        assert_eq!(
282            context.size_of(),
283            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of()
284        );
285    }
286
287    #[test]
288    fn size_of_context_with_name() {
289        // Check the check after `with_name` when there's both tags and no tags.
290        let context = Context::from_static_name(SIZE_OF_CONTEXT_NAME).with_name(SIZE_OF_CONTEXT_CHANGED_NAME);
291        assert_eq!(
292            context.size_of(),
293            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_CHANGED_NAME.len()
294        );
295
296        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
297
298        let context = Context::from_static_parts(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS)
299            .with_name(SIZE_OF_CONTEXT_CHANGED_NAME);
300        assert_eq!(
301            context.size_of(),
302            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_CHANGED_NAME.len() + tags.size_of()
303        );
304    }
305
306    #[test]
307    fn size_of_context_origin_tags() {
308        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
309        let origin_tags = tag_set(SIZE_OF_CONTEXT_ORIGIN_TAGS);
310
311        let (key, _) = hash_context(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS, SIZE_OF_CONTEXT_ORIGIN_TAGS);
312
313        let context = Context::from_inner(ContextInner {
314            key,
315            name: MetaString::from_static(SIZE_OF_CONTEXT_NAME),
316            tags: tags.clone(),
317            origin_tags: origin_tags.clone(),
318            active_count: Gauge::noop(),
319        });
320
321        // Make sure the size of the context is correct with origin tags.
322        assert_eq!(
323            context.size_of(),
324            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of() + origin_tags.size_of()
325        );
326    }
327}