saluki_core/observability/metrics/remapper.rs
1//! Remapper rules for translating source metrics into a different name and tag shape.
2//!
3//! A [`RemapperRule`] declares how to match a source metric (by name, optionally with a required
4//! tag set) and how the matched metric should be rewritten: a new name, a set of tags copied
5//! and/or renamed from the source, and an optional set of additional fixed tags. Rules also carry
6//! optional help text that the renderer emits in the Prometheus `# HELP` header.
7
8use saluki_context::{tags::TagSet, Context};
9use stringtheory::MetaString;
10
11/// A metric remapping rule.
12///
13/// Rules define the basic matching behavior (metric name, and optionally tags) as well as how to
14/// remap the new copy of the metric. This can include copying tags as-is from the source metric,
15/// copying specific tags over with a new name, and adding an additional fixed set of tags to the
16/// new metric.
17#[derive(Clone)]
18pub struct RemapperRule {
19 existing_name: &'static str,
20 existing_tags: &'static [&'static str],
21 new_name: &'static str,
22 remapped_tags: Vec<(&'static str, &'static str)>,
23 additional_tags: Vec<MetaString>,
24 continue_matching: bool,
25 help_text: Option<&'static str>,
26}
27
28impl RemapperRule {
29 /// Creates a new `RemapperRule` that matches a source metric by name only.
30 pub fn by_name(existing_name: &'static str, new_name: &'static str) -> Self {
31 Self {
32 existing_name,
33 existing_tags: &[],
34 new_name,
35 remapped_tags: Vec::new(),
36 additional_tags: Vec::new(),
37 continue_matching: false,
38 help_text: None,
39 }
40 }
41
42 /// Creates a new `RemapperRule` that matches a source metric by name and tags.
43 pub fn by_name_and_tags(
44 existing_name: &'static str, existing_tags: &'static [&'static str], new_name: &'static str,
45 ) -> Self {
46 Self {
47 existing_name,
48 existing_tags,
49 new_name,
50 remapped_tags: Vec::new(),
51 additional_tags: Vec::new(),
52 continue_matching: false,
53 help_text: None,
54 }
55 }
56
57 /// Adds a set of tags to remap from the source metric by changing their name.
58 ///
59 /// Remapped tags must be given in the form of `(source_tag, destination_tag)`. If a tag by the name `source_tag` is
60 /// found in the source metric, it's copied to the remapped metric with a name of `destination_tag`.
61 ///
62 /// This method is additive, so it can be called multiple times to add more remapped tags. Tag remapping is
63 /// order-dependent, so if a tag is configured to be remapped, or copied, multiple times, then the first match will
64 /// take precedence.
65 pub fn with_remapped_tags<I>(mut self, remapped_tags: I) -> Self
66 where
67 I: IntoIterator<Item = (&'static str, &'static str)>,
68 {
69 self.remapped_tags.extend(remapped_tags);
70 self
71 }
72
73 /// Adds a set of tags to remap from the source metric without changing their name.
74 ///
75 /// Remapped tags must be given in the form of `source_tag`. If a tag by the name `source_tag` is found in the
76 /// source metric, it's copied to the remapped metric with the same name.
77 ///
78 /// This method is additive, so it can be called multiple times to add more original tags. Tag remapping is
79 /// order-dependent, so if a tag is configured to be copied, or remapped, multiple times, then the first match will
80 /// take precedence.
81 pub fn with_original_tags<I>(mut self, original_tags: I) -> Self
82 where
83 I: IntoIterator<Item = &'static str>,
84 {
85 self.remapped_tags
86 .extend(original_tags.into_iter().map(|tag| (tag, tag)));
87 self
88 }
89
90 /// Adds a fixed set of tags to add to the remapped metric.
91 ///
92 /// Additional tags are given in the form of `tag`, which can be any valid tag value: bare or key/value.
93 ///
94 /// This method is additive, so it can be called multiple times to add more additional tags.
95 pub fn with_additional_tags<I>(mut self, additional_tags: I) -> Self
96 where
97 I: IntoIterator<Item = &'static str>,
98 {
99 self.additional_tags
100 .extend(additional_tags.into_iter().map(MetaString::from_static));
101 self
102 }
103
104 /// Allows later rules to also match the same source metric.
105 pub fn with_continued_matching(mut self) -> Self {
106 self.continue_matching = true;
107 self
108 }
109
110 /// Sets the Prometheus `# HELP` text that should be emitted alongside the remapped metric.
111 ///
112 /// Some downstream collectors (notably the Datadog Agent's OpenMetrics check) require a
113 /// specific help text on overlapped metric names. Setting this here keeps the help text next
114 /// to the rule that produces the name.
115 pub fn with_help_text(mut self, help_text: &'static str) -> Self {
116 self.help_text = Some(help_text);
117 self
118 }
119
120 /// Returns `true` if matching should continue after this rule matches.
121 pub const fn should_continue_matching(&self) -> bool {
122 self.continue_matching
123 }
124
125 /// Returns the remapped metric name produced by this rule.
126 pub const fn remapped_name(&self) -> &'static str {
127 self.new_name
128 }
129
130 /// Returns the help text associated with this rule, if any.
131 pub const fn help_text(&self) -> Option<&'static str> {
132 self.help_text
133 }
134
135 /// Attempts to match the given context against this rule.
136 ///
137 /// If the rule matches, returns a [`RemappedMetric`] containing the new metric name and tags.
138 pub fn try_match_no_context(&self, context: &Context) -> Option<RemappedMetric> {
139 if context.name() != self.existing_name {
140 return None;
141 }
142
143 let metric_tags = context.tags();
144 for existing_tag in self.existing_tags {
145 if !metric_tags.has_tag(existing_tag) {
146 return None;
147 }
148 }
149
150 let tags = self.build_remapped_tags(metric_tags);
151 Some(RemappedMetric {
152 name: self.new_name,
153 tags,
154 })
155 }
156
157 /// Builds the remapped tags for a matched metric.
158 fn build_remapped_tags(&self, metric_tags: &TagSet) -> Vec<MetaString> {
159 let mut new_tags = vec![];
160
161 for (original_tag_name, new_tag_name) in &self.remapped_tags {
162 if let Some(tag) = metric_tags.get_single_tag(original_tag_name) {
163 if original_tag_name == new_tag_name {
164 // Just clone the tag since the name isn't changing.
165 new_tags.push(tag.clone().into_inner());
166 } else {
167 // Build our new tag if this one has a value.
168 match tag.value() {
169 Some(value) => {
170 new_tags.push(MetaString::from(format!("{}:{}", new_tag_name, value)));
171 }
172 None => {
173 new_tags.push(MetaString::from(*new_tag_name));
174 }
175 }
176 }
177 }
178 }
179
180 for additional_tag in &self.additional_tags {
181 new_tags.push(additional_tag.clone());
182 }
183
184 new_tags
185 }
186}
187
188/// A metric that has been remapped by a [`RemapperRule`].
189pub struct RemappedMetric {
190 /// The remapped metric name.
191 pub name: &'static str,
192
193 /// The remapped tags in `key:value` (or bare) format.
194 pub tags: Vec<MetaString>,
195}