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