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")]
124#[derive(Default)]
125pub enum AcceptTransactionNames {
126 Strict,
128
129 #[serde(other)]
131 #[default]
132 ClientBased,
133}
134
135#[derive(Default, Debug, Clone, Serialize, Deserialize)]
137#[serde(default, rename_all = "camelCase")]
138pub struct TransactionMetricsConfig {
139 pub version: u16,
141 pub extract_custom_tags: BTreeSet<String>,
143 pub custom_measurements: CustomMeasurementConfig,
145 #[serde(rename = "acceptTransactionNames")]
148 pub deprecated1: AcceptTransactionNames,
149}
150
151impl TransactionMetricsConfig {
152 pub fn new() -> Self {
154 Self {
155 version: TRANSACTION_EXTRACT_MAX_SUPPORTED_VERSION,
156 ..Self::default()
157 }
158 }
159
160 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#[derive(Debug, Clone, Copy)]
169pub struct CombinedMetricExtractionConfig<'a> {
170 global: &'a MetricExtractionGroups,
171 project: &'a MetricExtractionConfig,
172}
173
174impl<'a> CombinedMetricExtractionConfig<'a> {
175 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 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 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 fn from(value: &'a MetricExtractionConfig) -> Self {
221 Self::new(MetricExtractionGroups::EMPTY, value)
222 }
223}
224
225#[derive(Clone, Default, Debug, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct MetricExtractionGroups {
231 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
233 pub groups: BTreeMap<GroupKey, MetricExtractionGroup>,
234}
235
236impl MetricExtractionGroups {
237 pub const EMPTY: &'static Self = &Self {
239 groups: BTreeMap::new(),
240 };
241
242 pub fn is_empty(&self) -> bool {
244 self.groups.is_empty()
245 }
246}
247
248#[derive(Clone, Debug, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct MetricExtractionGroup {
252 pub is_enabled: bool,
256
257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
259 pub metrics: Vec<MetricSpec>,
260
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
266 pub tags: Vec<TagMapping>,
267}
268
269#[derive(Clone, Default, Debug, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct MetricExtractionConfig {
273 pub version: u16,
275
276 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
281 pub global_groups: BTreeMap<GroupKey, MetricExtractionGroupOverride>,
282
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 pub metrics: Vec<MetricSpec>,
286
287 #[serde(default, skip_serializing_if = "Vec::is_empty")]
292 pub tags: Vec<TagMapping>,
293
294 #[serde(default)]
303 pub _conditional_tags_extended: bool,
304
305 #[serde(default)]
313 pub _span_metrics_extended: bool,
314}
315
316impl MetricExtractionConfig {
317 pub const MAX_SUPPORTED_VERSION: u16 = 4;
321
322 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 pub fn is_supported(&self) -> bool {
338 self.version <= Self::MAX_SUPPORTED_VERSION
339 }
340
341 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#[derive(Clone, Default, Debug, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct MetricExtractionGroupOverride {
355 pub is_enabled: bool,
357}
358
359#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
361pub enum GroupKey {
362 SpanMetricsCommon,
364 SpanMetricsAddons,
366 SpanMetricsTx,
368 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#[derive(Clone, Debug, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct MetricSpec {
406 pub category: DataCategory,
408
409 pub mri: String,
411
412 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub field: Option<String>,
432
433 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub condition: Option<RuleCondition>,
439
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
446 pub tags: Vec<TagSpec>,
447}
448
449#[derive(Clone, Debug, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct TagMapping {
453 #[serde(default)]
457 pub metrics: Vec<LazyGlob>,
458
459 #[serde(default)]
465 pub tags: Vec<TagSpec>,
466}
467
468impl TagMapping {
469 pub fn matches(&self, mri: &str) -> bool {
471 self.metrics
473 .iter()
474 .any(|glob| glob.compiled().is_match(mri))
475 }
476}
477
478#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct TagSpec {
486 pub key: String,
488
489 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub field: Option<String>,
496
497 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub value: Option<String>,
502
503 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub condition: Option<RuleCondition>,
509}
510
511impl TagSpec {
512 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
524pub struct Tag {
526 key: String,
527}
528
529impl Tag {
530 pub fn with_key(key: impl Into<String>) -> Self {
532 Self { key: key.into() }
533 }
534
535 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 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
556pub struct TagWithSource {
560 key: String,
561 field: Option<String>,
562 value: Option<String>,
563}
564
565impl TagWithSource {
566 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 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#[derive(Clone, Debug, PartialEq)]
591pub enum TagSource<'a> {
592 Literal(&'a str),
594 Field(&'a str),
596 Unknown,
598}
599
600pub fn convert_conditional_tagging(project_config: &mut ProjectConfig) {
603 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 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}