relay_dynamic_config/
metrics.rs

1//! Dynamic configuration for metrics extraction from sessions and transactions.
2
3use core::fmt;
4use std::collections::{BTreeMap, BTreeSet};
5use std::convert::Infallible;
6use std::str::FromStr;
7
8use relay_base_schema::data_category::DataCategory;
9use relay_cardinality::CardinalityLimit;
10use relay_common::glob2::LazyGlob;
11use relay_common::impl_str_serde;
12use relay_pattern::{Patterns, TypedPatterns};
13use relay_protocol::RuleCondition;
14use serde::{Deserialize, Serialize};
15
16use crate::project::ProjectConfig;
17
18/// Configuration for metrics filtering.
19#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(default, rename_all = "camelCase")]
21pub struct Metrics {
22    /// List of cardinality limits to enforce for this project.
23    #[serde(skip_serializing_if = "Vec::is_empty")]
24    pub cardinality_limits: Vec<CardinalityLimit>,
25}
26
27impl Metrics {
28    /// Returns `true` if there are no changes to the metrics config.
29    pub fn is_empty(&self) -> bool {
30        self.cardinality_limits.is_empty()
31    }
32}
33
34/// Configuration for removing tags matching the `tag` pattern on metrics whose name matches the `name` pattern.
35#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
36#[serde(default)]
37pub struct TagBlock {
38    /// Name of metric of which we want to remove certain tags.
39    #[serde(skip_serializing_if = "Patterns::is_empty")]
40    pub name: TypedPatterns,
41    /// Pattern to match keys of tags that we want to remove.
42    #[serde(skip_serializing_if = "Patterns::is_empty")]
43    pub tags: TypedPatterns,
44}
45
46/// Rule defining when a target tag should be set on a metric.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct TaggingRule {
50    // note: could add relay_sampling::RuleType here, but right now we only support transaction
51    // events
52    /// Condition that defines when to set the tag.
53    pub condition: RuleCondition,
54    /// Metrics on which the tag is set.
55    pub target_metrics: BTreeSet<String>,
56    /// Name of the tag that is set.
57    pub target_tag: String,
58    /// Value of the tag that is set.
59    pub tag_value: String,
60}
61
62/// Current version of metrics extraction.
63const SESSION_EXTRACT_VERSION: u16 = 3;
64const EXTRACT_ABNORMAL_MECHANISM_VERSION: u16 = 2;
65
66/// Configuration for metric extraction from sessions.
67#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
68#[serde(default, rename_all = "camelCase")]
69pub struct SessionMetricsConfig {
70    /// The revision of the extraction algorithm.
71    ///
72    /// Provided the revision is lower than or equal to the revision supported by this Relay,
73    /// metrics are extracted. If the revision is higher than what this Relay supports, it does not
74    /// extract metrics from sessions, and instead forwards them to the upstream.
75    ///
76    /// Version `0` (default) disables extraction.
77    version: u16,
78}
79
80impl SessionMetricsConfig {
81    /// Returns `true` if session metrics is enabled and compatible.
82    pub fn is_enabled(&self) -> bool {
83        self.version > 0 && self.version <= SESSION_EXTRACT_VERSION
84    }
85
86    /// Returns `true` if Relay should not extract metrics from sessions.
87    pub fn is_disabled(&self) -> bool {
88        !self.is_enabled()
89    }
90
91    /// Whether or not the abnormal mechanism should be extracted as a tag.
92    pub fn should_extract_abnormal_mechanism(&self) -> bool {
93        self.version >= EXTRACT_ABNORMAL_MECHANISM_VERSION
94    }
95}
96
97/// Configuration for extracting custom measurements from transaction payloads.
98#[derive(Default, Debug, Clone, Serialize, Deserialize)]
99#[serde(default, rename_all = "camelCase")]
100pub struct CustomMeasurementConfig {
101    /// The maximum number of custom measurements to extract. Defaults to zero.
102    limit: usize,
103}
104
105/// Maximum supported version of metrics extraction from transactions.
106///
107/// The version is an integer scalar, incremented by one on each new version:
108///  - 1: Initial version.
109///  - 2: Moves `acceptTransactionNames` to global config.
110///  - 3:
111///      - Emit a `usage` metric and use it for rate limiting.
112///      - Delay metrics extraction for indexed transactions.
113///  - 4: Adds support for `RuleConfigs` with string comparisons.
114///  - 5: No change, bumped together with [`MetricExtractionConfig::MAX_SUPPORTED_VERSION`].
115///  - 6: Bugfix to make transaction metrics extraction apply globally defined tag mappings.
116const TRANSACTION_EXTRACT_MAX_SUPPORTED_VERSION: u16 = 6;
117
118/// Minimum supported version of metrics extraction from transaction.
119const TRANSACTION_EXTRACT_MIN_SUPPORTED_VERSION: u16 = 3;
120
121/// Deprecated. Defines whether URL transactions should be considered low cardinality.
122#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
123#[serde(rename_all = "camelCase")]
124#[derive(Default)]
125pub enum AcceptTransactionNames {
126    /// Only accept transaction names with a low-cardinality source.
127    Strict,
128
129    /// For some SDKs, accept all transaction names, while for others, apply strict rules.
130    #[serde(other)]
131    #[default]
132    ClientBased,
133}
134
135/// Configuration for extracting metrics from transaction payloads.
136#[derive(Default, Debug, Clone, Serialize, Deserialize)]
137#[serde(default, rename_all = "camelCase")]
138pub struct TransactionMetricsConfig {
139    /// The required version to extract transaction metrics.
140    pub version: u16,
141    /// Custom event tags that are transferred from the transaction to metrics.
142    pub extract_custom_tags: BTreeSet<String>,
143    /// Deprecated in favor of top-level config field. Still here to be forwarded to external relays.
144    pub custom_measurements: CustomMeasurementConfig,
145    /// Deprecated. Defines whether URL transactions should be considered low cardinality.
146    /// Keep this around for external Relays.
147    #[serde(rename = "acceptTransactionNames")]
148    pub deprecated1: AcceptTransactionNames,
149}
150
151impl TransactionMetricsConfig {
152    /// Creates an enabled configuration with empty defaults.
153    pub fn new() -> Self {
154        Self {
155            version: TRANSACTION_EXTRACT_MAX_SUPPORTED_VERSION,
156            ..Self::default()
157        }
158    }
159
160    /// Returns `true` if metrics extraction is enabled and compatible with this Relay.
161    pub fn is_enabled(&self) -> bool {
162        self.version >= TRANSACTION_EXTRACT_MIN_SUPPORTED_VERSION
163            && self.version <= TRANSACTION_EXTRACT_MAX_SUPPORTED_VERSION
164    }
165}
166
167/// Combined view of global and project-specific metrics extraction configs.
168#[derive(Debug, Clone, Copy)]
169pub struct CombinedMetricExtractionConfig<'a> {
170    global: &'a MetricExtractionGroups,
171    project: &'a MetricExtractionConfig,
172}
173
174impl<'a> CombinedMetricExtractionConfig<'a> {
175    /// Creates a new combined view from two references.
176    pub fn new(global: &'a MetricExtractionGroups, project: &'a MetricExtractionConfig) -> Self {
177        for key in project.global_groups.keys() {
178            if !global.groups.contains_key(key) {
179                relay_log::error!(
180                    "Metrics group configured for project missing in global config: {key:?}"
181                )
182            }
183        }
184
185        Self { global, project }
186    }
187
188    /// Returns an iterator of metric specs.
189    pub fn metrics(&self) -> impl Iterator<Item = &MetricSpec> {
190        let project = self.project.metrics.iter();
191        let enabled_global = self
192            .enabled_groups()
193            .flat_map(|template| template.metrics.iter());
194
195        project.chain(enabled_global)
196    }
197
198    /// Returns an iterator of tag mappings.
199    pub fn tags(&self) -> impl Iterator<Item = &TagMapping> {
200        let project = self.project.tags.iter();
201        let enabled_global = self
202            .enabled_groups()
203            .flat_map(|template| template.tags.iter());
204
205        project.chain(enabled_global)
206    }
207
208    fn enabled_groups(&self) -> impl Iterator<Item = &MetricExtractionGroup> {
209        self.global.groups.iter().filter_map(|(key, template)| {
210            let is_enabled_by_override = self.project.global_groups.get(key).map(|c| c.is_enabled);
211            let is_enabled = is_enabled_by_override.unwrap_or(template.is_enabled);
212
213            is_enabled.then_some(template)
214        })
215    }
216}
217
218impl<'a> From<&'a MetricExtractionConfig> for CombinedMetricExtractionConfig<'a> {
219    /// Creates a combined config with an empty global component. Used in tests.
220    fn from(value: &'a MetricExtractionConfig) -> Self {
221        Self::new(MetricExtractionGroups::EMPTY, value)
222    }
223}
224
225/// Global groups for metric extraction.
226///
227/// Templates can be enabled or disabled by project configs.
228#[derive(Clone, Default, Debug, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct MetricExtractionGroups {
231    /// Mapping from group name to metrics specs & tags.
232    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
233    pub groups: BTreeMap<GroupKey, MetricExtractionGroup>,
234}
235
236impl MetricExtractionGroups {
237    /// Empty config, used in tests and as a fallback.
238    pub const EMPTY: &'static Self = &Self {
239        groups: BTreeMap::new(),
240    };
241
242    /// Returns `true` if the contained groups are empty.
243    pub fn is_empty(&self) -> bool {
244        self.groups.is_empty()
245    }
246}
247
248/// Group of metrics & tags that can be enabled or disabled as a group.
249#[derive(Clone, Debug, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct MetricExtractionGroup {
252    /// Whether the set is enabled by default.
253    ///
254    /// Project configs can overwrite this flag to opt-in or out of a set.
255    pub is_enabled: bool,
256
257    /// A list of metric specifications to extract.
258    #[serde(default, skip_serializing_if = "Vec::is_empty")]
259    pub metrics: Vec<MetricSpec>,
260
261    /// A list of tags to add to previously extracted metrics.
262    ///
263    /// These tags add further tags to a range of metrics. If some metrics already have a matching
264    /// tag extracted, the existing tag is left unchanged.
265    #[serde(default, skip_serializing_if = "Vec::is_empty")]
266    pub tags: Vec<TagMapping>,
267}
268
269/// Configuration for generic extraction of metrics from all data categories.
270#[derive(Clone, Default, Debug, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct MetricExtractionConfig {
273    /// Versioning of metrics extraction. Relay skips extraction if the version is not supported.
274    pub version: u16,
275
276    /// Configuration of global metric groups.
277    ///
278    /// The groups themselves are configured in [`crate::GlobalConfig`],
279    /// but can be enabled or disabled here.
280    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
281    pub global_groups: BTreeMap<GroupKey, MetricExtractionGroupOverride>,
282
283    /// A list of metric specifications to extract.
284    #[serde(default, skip_serializing_if = "Vec::is_empty")]
285    pub metrics: Vec<MetricSpec>,
286
287    /// A list of tags to add to previously extracted metrics.
288    ///
289    /// These tags add further tags to a range of metrics. If some metrics already have a matching
290    /// tag extracted, the existing tag is left unchanged.
291    #[serde(default, skip_serializing_if = "Vec::is_empty")]
292    pub tags: Vec<TagMapping>,
293
294    /// This config has been extended with fields from `conditional_tagging`.
295    ///
296    /// At the moment, Relay will parse `conditional_tagging` rules and insert them into the `tags`
297    /// mapping in this struct. If the flag is `true`, this has already happened and should not be
298    /// repeated.
299    ///
300    /// This is a temporary flag that will be removed once the transaction metric extraction version
301    /// is bumped to `2`.
302    #[serde(default)]
303    pub _conditional_tags_extended: bool,
304
305    /// This config has been extended with default span metrics.
306    ///
307    /// Relay checks for the span extraction flag and adds built-in metrics and tags to this struct.
308    /// If the flag is `true`, this has already happened and should not be repeated.
309    ///
310    /// This is a temporary flag that will be removed once the transaction metric extraction version
311    /// is bumped to `2`.
312    #[serde(default)]
313    pub _span_metrics_extended: bool,
314}
315
316impl MetricExtractionConfig {
317    /// The latest version for this config struct.
318    ///
319    /// This is the maximum version supported by this Relay instance.
320    pub const MAX_SUPPORTED_VERSION: u16 = 4;
321
322    /// Returns an empty `MetricExtractionConfig` with the latest version.
323    ///
324    /// As opposed to `default()`, this will be enabled once populated with specs.
325    pub fn empty() -> Self {
326        Self {
327            version: Self::MAX_SUPPORTED_VERSION,
328            global_groups: BTreeMap::new(),
329            metrics: Default::default(),
330            tags: Default::default(),
331            _conditional_tags_extended: false,
332            _span_metrics_extended: false,
333        }
334    }
335
336    /// Returns `true` if the version of this metric extraction config is supported.
337    pub fn is_supported(&self) -> bool {
338        self.version <= Self::MAX_SUPPORTED_VERSION
339    }
340
341    /// Returns `true` if metric extraction is configured and compatible with this Relay.
342    pub fn is_enabled(&self) -> bool {
343        self.version > 0
344            && self.is_supported()
345            && !(self.metrics.is_empty() && self.tags.is_empty() && self.global_groups.is_empty())
346    }
347}
348
349/// Configures global metrics extraction groups.
350///
351/// Project configs can enable or disable globally defined groups.
352#[derive(Clone, Default, Debug, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct MetricExtractionGroupOverride {
355    /// `true` if a template should be enabled.
356    pub is_enabled: bool,
357}
358
359/// Enumeration of keys in [`MetricExtractionGroups`]. In JSON, this is simply a string.
360#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
361pub enum GroupKey {
362    /// Metric extracted for all plans.
363    SpanMetricsCommon,
364    /// "addon" metrics.
365    SpanMetricsAddons,
366    /// Metrics extracted from spans in the transaction namespace.
367    SpanMetricsTx,
368    /// Any other group defined by the upstream.
369    Other(String),
370}
371
372impl fmt::Display for GroupKey {
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        write!(
375            f,
376            "{}",
377            match self {
378                GroupKey::SpanMetricsCommon => "span_metrics_common",
379                GroupKey::SpanMetricsAddons => "span_metrics_addons",
380                GroupKey::SpanMetricsTx => "span_metrics_tx",
381                GroupKey::Other(s) => &s,
382            }
383        )
384    }
385}
386
387impl FromStr for GroupKey {
388    type Err = Infallible;
389
390    fn from_str(s: &str) -> Result<Self, Self::Err> {
391        Ok(match s {
392            "span_metrics_common" => GroupKey::SpanMetricsCommon,
393            "span_metrics_addons" => GroupKey::SpanMetricsAddons,
394            "span_metrics_tx" => GroupKey::SpanMetricsTx,
395            s => GroupKey::Other(s.to_owned()),
396        })
397    }
398}
399
400impl_str_serde!(GroupKey, "a metrics extraction group key");
401
402/// Specification for a metric to extract from some data.
403#[derive(Clone, Debug, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct MetricSpec {
406    /// Category of data to extract this metric for.
407    pub category: DataCategory,
408
409    /// The Metric Resource Identifier (MRI) of the metric to extract.
410    pub mri: String,
411
412    /// A path to the field to extract the metric from.
413    ///
414    /// This value contains a fully qualified expression pointing at the data field in the payload
415    /// to extract the metric from. It follows the `Getter` syntax that is also used for dynamic
416    /// sampling.
417    ///
418    /// How the value is treated depends on the metric type:
419    ///
420    /// - **Counter** metrics are a special case, since the default product counters do not count
421    ///   any specific field but rather the occurrence of the event. As such, there is no value
422    ///   expression, and the field is set to `None`. Semantics of specifying remain undefined at
423    ///   this point.
424    /// - **Distribution** metrics require a numeric value. If the value at the specified path is
425    ///   not numeric, metric extraction will be skipped.
426    /// - **Set** metrics require a string value, which is then emitted into the set as unique
427    ///   value. Insertion of numbers and other types is undefined.
428    ///
429    /// If the field does not exist, extraction is skipped.
430    #[serde(default, skip_serializing_if = "Option::is_none")]
431    pub field: Option<String>,
432
433    /// An optional condition to meet before extraction.
434    ///
435    /// See [`RuleCondition`] for all available options to specify and combine conditions. If no
436    /// condition is specified, the metric is extracted unconditionally.
437    #[serde(default, skip_serializing_if = "Option::is_none")]
438    pub condition: Option<RuleCondition>,
439
440    /// A list of tags to add to the metric.
441    ///
442    /// Tags can be conditional, see [`TagSpec`] for configuration options. For this reason, it is
443    /// possible to list tag keys multiple times, each with different conditions. The first matching
444    /// condition will be applied.
445    #[serde(default, skip_serializing_if = "Vec::is_empty")]
446    pub tags: Vec<TagSpec>,
447}
448
449/// Mapping between extracted metrics and additional tags to extract.
450#[derive(Clone, Debug, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct TagMapping {
453    /// A list of Metric Resource Identifiers (MRI) to apply tags to.
454    ///
455    /// Entries in this list can contain wildcards to match metrics with dynamic MRIs.
456    #[serde(default)]
457    pub metrics: Vec<LazyGlob>,
458
459    /// A list of tags to add to the metric.
460    ///
461    /// Tags can be conditional, see [`TagSpec`] for configuration options. For this reason, it is
462    /// possible to list tag keys multiple times, each with different conditions. The first matching
463    /// condition will be applied.
464    #[serde(default)]
465    pub tags: Vec<TagSpec>,
466}
467
468impl TagMapping {
469    /// Returns `true` if this mapping matches the provided MRI.
470    pub fn matches(&self, mri: &str) -> bool {
471        // TODO: Use a globset, instead.
472        self.metrics
473            .iter()
474            .any(|glob| glob.compiled().is_match(mri))
475    }
476}
477
478/// Configuration for a tag to add to a metric.
479///
480/// Tags values can be static if defined through `value` or dynamically queried from the payload if
481/// defined through `field`. These two options are mutually exclusive, behavior is undefined if both
482/// are specified.
483#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct TagSpec {
486    /// The key of the tag to extract.
487    pub key: String,
488
489    /// Path to a field containing the tag's value.
490    ///
491    /// It follows the `Getter` syntax to read data from the payload.
492    ///
493    /// Mutually exclusive with `value`.
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    pub field: Option<String>,
496
497    /// Literal value of the tag.
498    ///
499    /// Mutually exclusive with `field`.
500    #[serde(default, skip_serializing_if = "Option::is_none")]
501    pub value: Option<String>,
502
503    /// An optional condition to meet before extraction.
504    ///
505    /// See [`RuleCondition`] for all available options to specify and combine conditions. If no
506    /// condition is specified, the tag is added unconditionally, provided it is not already there.
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub condition: Option<RuleCondition>,
509}
510
511impl TagSpec {
512    /// Returns the source of tag values, either literal or a field.
513    pub fn source(&self) -> TagSource<'_> {
514        if let Some(ref field) = self.field {
515            TagSource::Field(field)
516        } else if let Some(ref value) = self.value {
517            TagSource::Literal(value)
518        } else {
519            TagSource::Unknown
520        }
521    }
522}
523
524/// Builder for [`TagSpec`].
525pub struct Tag {
526    key: String,
527}
528
529impl Tag {
530    /// Prepares a tag with a given tag name.
531    pub fn with_key(key: impl Into<String>) -> Self {
532        Self { key: key.into() }
533    }
534
535    /// Defines the field from which the tag value gets its data.
536    pub fn from_field(self, field_name: impl Into<String>) -> TagWithSource {
537        let Self { key } = self;
538        TagWithSource {
539            key,
540            field: Some(field_name.into()),
541            value: None,
542        }
543    }
544
545    /// Defines what value to set for a tag.
546    pub fn with_value(self, value: impl Into<String>) -> TagWithSource {
547        let Self { key } = self;
548        TagWithSource {
549            key,
550            field: None,
551            value: Some(value.into()),
552        }
553    }
554}
555
556/// Intermediate result of the tag spec builder.
557///
558/// Can be transformed into [`TagSpec`].
559pub struct TagWithSource {
560    key: String,
561    field: Option<String>,
562    value: Option<String>,
563}
564
565impl TagWithSource {
566    /// Defines a tag that is extracted unconditionally.
567    pub fn always(self) -> TagSpec {
568        let Self { key, field, value } = self;
569        TagSpec {
570            key,
571            field,
572            value,
573            condition: None,
574        }
575    }
576
577    /// Defines a tag that is extracted under the given condition.
578    pub fn when(self, condition: RuleCondition) -> TagSpec {
579        let Self { key, field, value } = self;
580        TagSpec {
581            key,
582            field,
583            value,
584            condition: Some(condition),
585        }
586    }
587}
588
589/// Specifies how to obtain the value of a tag in [`TagSpec`].
590#[derive(Clone, Debug, PartialEq)]
591pub enum TagSource<'a> {
592    /// A literal value.
593    Literal(&'a str),
594    /// Path to a field to evaluate.
595    Field(&'a str),
596    /// An unsupported or unknown source.
597    Unknown,
598}
599
600/// Converts the given tagging rules from `conditional_tagging` to the newer metric extraction
601/// config.
602pub fn convert_conditional_tagging(project_config: &mut ProjectConfig) {
603    // NOTE: This clones the rules so that they remain in the project state for old Relays that
604    // do not support generic metrics extraction. Once the migration is complete, this can be
605    // removed with a version bump of the transaction metrics config.
606    let rules = &project_config.metric_conditional_tagging;
607    if rules.is_empty() {
608        return;
609    }
610
611    let config = project_config
612        .metric_extraction
613        .get_or_insert_with(MetricExtractionConfig::empty);
614
615    if !config.is_supported() || config._conditional_tags_extended {
616        return;
617    }
618
619    config.tags.extend(TaggingRuleConverter {
620        rules: rules.iter().cloned().peekable(),
621        tags: Vec::new(),
622    });
623
624    config._conditional_tags_extended = true;
625    if config.version == 0 {
626        config.version = MetricExtractionConfig::MAX_SUPPORTED_VERSION;
627    }
628}
629
630struct TaggingRuleConverter<I: Iterator<Item = TaggingRule>> {
631    rules: std::iter::Peekable<I>,
632    tags: Vec<TagSpec>,
633}
634
635impl<I> Iterator for TaggingRuleConverter<I>
636where
637    I: Iterator<Item = TaggingRule>,
638{
639    type Item = TagMapping;
640
641    fn next(&mut self) -> Option<Self::Item> {
642        loop {
643            let old = self.rules.next()?;
644
645            self.tags.push(TagSpec {
646                key: old.target_tag,
647                field: None,
648                value: Some(old.tag_value),
649                condition: Some(old.condition),
650            });
651
652            // Optimization: Collect tags for consecutive tagging rules for the same set of metrics.
653            // Then, emit a single entry with all tag specs at once.
654            if self.rules.peek().map(|r| &r.target_metrics) == Some(&old.target_metrics) {
655                continue;
656            }
657
658            return Some(TagMapping {
659                metrics: old.target_metrics.into_iter().map(LazyGlob::new).collect(),
660                tags: std::mem::take(&mut self.tags),
661            });
662        }
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use similar_asserts::assert_eq;
670
671    #[test]
672    fn test_empty_metrics_deserialize() {
673        let m: Metrics = serde_json::from_str("{}").unwrap();
674        assert!(m.is_empty());
675        assert_eq!(m, Metrics::default());
676    }
677
678    #[test]
679    fn parse_tag_spec_value() {
680        let json = r#"{"key":"foo","value":"bar"}"#;
681        let spec: TagSpec = serde_json::from_str(json).unwrap();
682        assert_eq!(spec.source(), TagSource::Literal("bar"));
683    }
684
685    #[test]
686    fn parse_tag_spec_field() {
687        let json = r#"{"key":"foo","field":"bar"}"#;
688        let spec: TagSpec = serde_json::from_str(json).unwrap();
689        assert_eq!(spec.source(), TagSource::Field("bar"));
690    }
691
692    #[test]
693    fn parse_tag_spec_unsupported() {
694        let json = r#"{"key":"foo","somethingNew":"bar"}"#;
695        let spec: TagSpec = serde_json::from_str(json).unwrap();
696        assert_eq!(spec.source(), TagSource::Unknown);
697    }
698
699    #[test]
700    fn parse_tag_mapping() {
701        let json = r#"{"metrics": ["d:spans/*"], "tags": [{"key":"foo","field":"bar"}]}"#;
702        let mapping: TagMapping = serde_json::from_str(json).unwrap();
703        assert!(mapping.metrics[0].compiled().is_match("d:spans/foo"));
704    }
705
706    fn groups() -> MetricExtractionGroups {
707        serde_json::from_value::<MetricExtractionGroups>(serde_json::json!({
708            "groups": {
709                "group1": {
710                    "isEnabled": false,
711                    "metrics": [{
712                        "category": "transaction",
713                        "mri": "c:metric1/counter@none",
714                    }],
715                    "tags": [
716                        {
717                            "metrics": ["c:metric1/counter@none"],
718                            "tags": [{
719                                "key": "tag1",
720                                "value": "value1"
721                            }]
722                        }
723                    ]
724                },
725                "group2": {
726                    "isEnabled": true,
727                    "metrics": [{
728                        "category": "transaction",
729                        "mri": "c:metric2/counter@none",
730                    }],
731                    "tags": [
732                        {
733                            "metrics": ["c:metric2/counter@none"],
734                            "tags": [{
735                                "key": "tag2",
736                                "value": "value2"
737                            }]
738                        }
739                    ]
740                }
741            }
742        }))
743        .unwrap()
744    }
745
746    #[test]
747    fn metric_extraction_global_defaults() {
748        let global = groups();
749        let project: MetricExtractionConfig = serde_json::from_value(serde_json::json!({
750            "version": 1,
751            "global_templates": {}
752        }))
753        .unwrap();
754        let combined = CombinedMetricExtractionConfig::new(&global, &project);
755
756        assert_eq!(
757            combined
758                .metrics()
759                .map(|m| m.mri.as_str())
760                .collect::<Vec<_>>(),
761            vec!["c:metric2/counter@none"]
762        );
763        assert_eq!(
764            combined
765                .tags()
766                .map(|t| t.tags[0].key.as_str())
767                .collect::<Vec<_>>(),
768            vec!["tag2"]
769        );
770    }
771
772    #[test]
773    fn metric_extraction_override() {
774        let global = groups();
775        let project: MetricExtractionConfig = serde_json::from_value(serde_json::json!({
776            "version": 1,
777            "globalGroups": {
778                "group1": {"isEnabled": true},
779                "group2": {"isEnabled": false}
780            }
781        }))
782        .unwrap();
783        let combined = CombinedMetricExtractionConfig::new(&global, &project);
784
785        assert_eq!(
786            combined
787                .metrics()
788                .map(|m| m.mri.as_str())
789                .collect::<Vec<_>>(),
790            vec!["c:metric1/counter@none"]
791        );
792        assert_eq!(
793            combined
794                .tags()
795                .map(|t| t.tags[0].key.as_str())
796                .collect::<Vec<_>>(),
797            vec!["tag1"]
798        );
799    }
800}