1use 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#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(default, rename_all = "camelCase")]
21pub struct Metrics {
22 #[serde(skip_serializing_if = "Vec::is_empty")]
24 pub cardinality_limits: Vec<CardinalityLimit>,
25}
26
27impl Metrics {
28 pub fn is_empty(&self) -> bool {
30 self.cardinality_limits.is_empty()
31 }
32}
33
34#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
36#[serde(default)]
37pub struct TagBlock {
38 #[serde(skip_serializing_if = "Patterns::is_empty")]
40 pub name: TypedPatterns,
41 #[serde(skip_serializing_if = "Patterns::is_empty")]
43 pub tags: TypedPatterns,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct TaggingRule {
50 pub condition: RuleCondition,
54 pub target_metrics: BTreeSet<String>,
56 pub target_tag: String,
58 pub tag_value: String,
60}
61
62const SESSION_EXTRACT_VERSION: u16 = 3;
64const EXTRACT_ABNORMAL_MECHANISM_VERSION: u16 = 2;
65
66#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
68#[serde(default, rename_all = "camelCase")]
69pub struct SessionMetricsConfig {
70 version: u16,
78}
79
80impl SessionMetricsConfig {
81 pub fn is_enabled(&self) -> bool {
83 self.version > 0 && self.version <= SESSION_EXTRACT_VERSION
84 }
85
86 pub fn is_disabled(&self) -> bool {
88 !self.is_enabled()
89 }
90
91 pub fn should_extract_abnormal_mechanism(&self) -> bool {
93 self.version >= EXTRACT_ABNORMAL_MECHANISM_VERSION
94 }
95}
96
97#[derive(Default, Debug, Clone, Serialize, Deserialize)]
99#[serde(default, rename_all = "camelCase")]
100pub struct CustomMeasurementConfig {
101 limit: usize,
103}
104
105const TRANSACTION_EXTRACT_MAX_SUPPORTED_VERSION: u16 = 6;
117
118const TRANSACTION_EXTRACT_MIN_SUPPORTED_VERSION: u16 = 3;
120
121#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
123#[serde(rename_all = "camelCase")]
124pub enum AcceptTransactionNames {
125 Strict,
127
128 #[serde(other)]
130 ClientBased,
131}
132
133impl Default for AcceptTransactionNames {
134 fn default() -> Self {
135 Self::ClientBased
136 }
137}
138
139#[derive(Default, Debug, Clone, Serialize, Deserialize)]
141#[serde(default, rename_all = "camelCase")]
142pub struct TransactionMetricsConfig {
143 pub version: u16,
145 pub extract_custom_tags: BTreeSet<String>,
147 pub custom_measurements: CustomMeasurementConfig,
149 #[serde(rename = "acceptTransactionNames")]
152 pub deprecated1: AcceptTransactionNames,
153}
154
155impl TransactionMetricsConfig {
156 pub fn new() -> Self {
158 Self {
159 version: TRANSACTION_EXTRACT_MAX_SUPPORTED_VERSION,
160 ..Self::default()
161 }
162 }
163
164 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#[derive(Debug, Clone, Copy)]
173pub struct CombinedMetricExtractionConfig<'a> {
174 global: &'a MetricExtractionGroups,
175 project: &'a MetricExtractionConfig,
176}
177
178impl<'a> CombinedMetricExtractionConfig<'a> {
179 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 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 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 fn from(value: &'a MetricExtractionConfig) -> Self {
225 Self::new(MetricExtractionGroups::EMPTY, value)
226 }
227}
228
229#[derive(Clone, Default, Debug, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct MetricExtractionGroups {
235 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
237 pub groups: BTreeMap<GroupKey, MetricExtractionGroup>,
238}
239
240impl MetricExtractionGroups {
241 pub const EMPTY: &'static Self = &Self {
243 groups: BTreeMap::new(),
244 };
245
246 pub fn is_empty(&self) -> bool {
248 self.groups.is_empty()
249 }
250}
251
252#[derive(Clone, Debug, Serialize, Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct MetricExtractionGroup {
256 pub is_enabled: bool,
260
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
263 pub metrics: Vec<MetricSpec>,
264
265 #[serde(default, skip_serializing_if = "Vec::is_empty")]
270 pub tags: Vec<TagMapping>,
271}
272
273#[derive(Clone, Default, Debug, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct MetricExtractionConfig {
277 pub version: u16,
279
280 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
285 pub global_groups: BTreeMap<GroupKey, MetricExtractionGroupOverride>,
286
287 #[serde(default, skip_serializing_if = "Vec::is_empty")]
289 pub metrics: Vec<MetricSpec>,
290
291 #[serde(default, skip_serializing_if = "Vec::is_empty")]
296 pub tags: Vec<TagMapping>,
297
298 #[serde(default)]
307 pub _conditional_tags_extended: bool,
308
309 #[serde(default)]
317 pub _span_metrics_extended: bool,
318}
319
320impl MetricExtractionConfig {
321 pub const MAX_SUPPORTED_VERSION: u16 = 4;
325
326 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 pub fn is_supported(&self) -> bool {
342 self.version <= Self::MAX_SUPPORTED_VERSION
343 }
344
345 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#[derive(Clone, Default, Debug, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct MetricExtractionGroupOverride {
359 pub is_enabled: bool,
361}
362
363#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
365pub enum GroupKey {
366 SpanMetricsCommon,
368 SpanMetricsAddons,
370 SpanMetricsTx,
372 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#[derive(Clone, Debug, Serialize, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct MetricSpec {
410 pub category: DataCategory,
412
413 pub mri: String,
415
416 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub field: Option<String>,
436
437 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub condition: Option<RuleCondition>,
443
444 #[serde(default, skip_serializing_if = "Vec::is_empty")]
450 pub tags: Vec<TagSpec>,
451}
452
453#[derive(Clone, Debug, Serialize, Deserialize)]
455#[serde(rename_all = "camelCase")]
456pub struct TagMapping {
457 #[serde(default)]
461 pub metrics: Vec<LazyGlob>,
462
463 #[serde(default)]
469 pub tags: Vec<TagSpec>,
470}
471
472impl TagMapping {
473 pub fn matches(&self, mri: &str) -> bool {
475 self.metrics
477 .iter()
478 .any(|glob| glob.compiled().is_match(mri))
479 }
480}
481
482#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
488#[serde(rename_all = "camelCase")]
489pub struct TagSpec {
490 pub key: String,
492
493 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub field: Option<String>,
500
501 #[serde(default, skip_serializing_if = "Option::is_none")]
505 pub value: Option<String>,
506
507 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub condition: Option<RuleCondition>,
513}
514
515impl TagSpec {
516 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
528pub struct Tag {
530 key: String,
531}
532
533impl Tag {
534 pub fn with_key(key: impl Into<String>) -> Self {
536 Self { key: key.into() }
537 }
538
539 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 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
560pub struct TagWithSource {
564 key: String,
565 field: Option<String>,
566 value: Option<String>,
567}
568
569impl TagWithSource {
570 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 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#[derive(Clone, Debug, PartialEq)]
595pub enum TagSource<'a> {
596 Literal(&'a str),
598 Field(&'a str),
600 Unknown,
602}
603
604pub fn convert_conditional_tagging(project_config: &mut ProjectConfig) {
607 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 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}