Skip to main content

relay_event_schema/protocol/trace_metric/
mod.rs

1use relay_protocol::{
2    Annotated, Empty, FromValue, Getter, IntoValue, Object, SkipSerialization, Value,
3};
4use std::collections::BTreeMap;
5use std::fmt::{self, Display};
6
7use relay_base_schema::metrics::MetricUnit;
8use serde::{Deserialize, Serialize, Serializer};
9
10use crate::processor::ProcessValue;
11use crate::protocol::{Attributes, SpanId, Timestamp, TraceId};
12
13pub mod container;
14
15#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
16#[metastructure(process_func = "process_trace_metric", value_type = "TraceMetric")]
17pub struct TraceMetric {
18    /// Timestamp when the metric was created.
19    #[metastructure(required = true)]
20    pub timestamp: Annotated<Timestamp>,
21
22    /// The ID of the trace the metric belongs to.
23    #[metastructure(required = true, trim = false)]
24    pub trace_id: Annotated<TraceId>,
25
26    /// The Span this metric belongs to.
27    #[metastructure(required = false, trim = false)]
28    pub span_id: Annotated<SpanId>,
29
30    /// The metric name.
31    #[metastructure(required = true, trim = false)]
32    pub name: Annotated<String>,
33
34    /// The metric type.
35    #[metastructure(required = true, field = "type")]
36    pub ty: Annotated<MetricType>,
37
38    /// The metric unit.
39    #[metastructure(required = false)]
40    pub unit: Annotated<MetricUnit>,
41
42    /// The metric value.
43    ///
44    /// Should be constrained to a number.
45    #[metastructure(pii = "maybe", required = true, trim = false)]
46    pub value: Annotated<Value>,
47
48    /// Arbitrary attributes on a metric.
49    #[metastructure(pii = "maybe", trim = false)]
50    pub attributes: Annotated<Attributes>,
51
52    /// Additional arbitrary fields for forwards compatibility.
53    #[metastructure(additional_properties, retain = false)]
54    pub other: Object<Value>,
55}
56
57/// Relay specific metadata embedded into the trace metric item.
58///
59/// This metadata is purely an internal protocol extension used by Relay,
60/// no one except Relay should be sending this data, nor should anyone except Relay rely on it.
61#[derive(Clone, Debug, Default, Serialize, Deserialize)]
62pub struct TraceMetricHeader {
63    /// Original (calculated) size of the trace metric item when it was first received by a Relay.
64    ///
65    /// If this value exists, Relay uses it as quantity for all outcomes emitted to the
66    /// trace metric byte data category.
67    pub byte_size: Option<u64>,
68
69    /// Forward compatibility for additional headers.
70    #[serde(flatten)]
71    pub other: BTreeMap<String, Value>,
72}
73
74impl Getter for TraceMetric {
75    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
76        Some(match path.strip_prefix("trace_metric.")? {
77            "name" => self.name.as_str()?.into(),
78            path => {
79                let key = path.strip_prefix("attributes.")?;
80                let key = key.strip_suffix(".value")?;
81                self.attributes.value()?.get_value(key)?.into()
82            }
83        })
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
88pub enum MetricType {
89    /// A gauge metric represents a single numerical value that can arbitrarily go up and down.
90    Gauge,
91    /// A distribution metric represents a collection of values that can be aggregated.
92    Distribution,
93    /// A counter metric represents a single numerical value.
94    Counter,
95    /// Unknown type, for forward compatibility.
96    Unknown(String),
97}
98
99impl MetricType {
100    fn as_str(&self) -> &str {
101        match self {
102            MetricType::Gauge => "gauge",
103            MetricType::Distribution => "distribution",
104            MetricType::Counter => "counter",
105            MetricType::Unknown(s) => s.as_str(),
106        }
107    }
108}
109
110impl Display for MetricType {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{}", self.as_str())
113    }
114}
115
116impl From<String> for MetricType {
117    fn from(value: String) -> Self {
118        match value.as_str() {
119            "gauge" => MetricType::Gauge,
120            "distribution" => MetricType::Distribution,
121            "counter" => MetricType::Counter,
122            _ => MetricType::Unknown(value),
123        }
124    }
125}
126
127impl FromValue for MetricType {
128    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
129        match String::from_value(value) {
130            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
131            Annotated(None, meta) => Annotated(None, meta),
132        }
133    }
134}
135
136impl IntoValue for MetricType {
137    fn into_value(self) -> Value {
138        Value::String(self.to_string())
139    }
140
141    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
142    where
143        Self: Sized,
144        S: Serializer,
145    {
146        Serialize::serialize(self.as_str(), s)
147    }
148}
149
150impl ProcessValue for MetricType {}
151
152impl Empty for MetricType {
153    #[inline]
154    fn is_empty(&self) -> bool {
155        false
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use relay_protocol::SerializableAnnotated;
163
164    #[test]
165    fn test_trace_metric_serialization() {
166        let json = r#"{
167            "timestamp": 1544719860.0,
168            "trace_id": "5b8efff798038103d269b633813fc60c",
169            "span_id": "eee19b7ec3c1b174",
170            "name": "http.request.duration",
171            "type": "distribution",
172            "value": 123.45,
173            "attributes": {
174                "http.method": {
175                    "value": "GET",
176                    "type": "string"
177                },
178                "http.status_code": {
179                    "value": "200",
180                    "type": "integer"
181                }
182            }
183        }"#;
184
185        let data = Annotated::<TraceMetric>::from_json(json).unwrap();
186
187        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
188        {
189          "timestamp": 1544719860.0,
190          "trace_id": "5b8efff798038103d269b633813fc60c",
191          "span_id": "eee19b7ec3c1b174",
192          "name": "http.request.duration",
193          "type": "distribution",
194          "value": 123.45,
195          "attributes": {
196            "http.method": {
197              "type": "string",
198              "value": "GET"
199            },
200            "http.status_code": {
201              "type": "integer",
202              "value": "200"
203            }
204          }
205        }
206        "###);
207    }
208
209    #[test]
210    fn test_trace_metric_with_custom_unit_preserved() {
211        let json = r#"{
212            "timestamp": 1544719860.0,
213            "trace_id": "5b8efff798038103d269b633813fc60c",
214            "name": "custom.metric",
215            "type": "counter",
216            "value": 42,
217            "unit": "customunit"
218        }"#;
219
220        let data = Annotated::<TraceMetric>::from_json(json).unwrap();
221        let trace_metric = data.value().unwrap();
222
223        assert_eq!(
224            trace_metric.unit.value(),
225            Some(&MetricUnit::Custom("customunit".parse().unwrap()))
226        );
227    }
228
229    #[test]
230    fn test_trace_metric_with_nil_uuid() {
231        let json = r#"{
232            "timestamp": 1544719860.0,
233            "trace_id": "00000000000000000000000000000000",
234            "name": "custom.metric",
235            "type": "counter",
236            "value": 42
237        }"#;
238
239        let data = Annotated::<TraceMetric>::from_json(json).unwrap();
240        let trace_metric = data.value().unwrap();
241
242        assert!(!trace_metric.trace_id.value().unwrap().is_nil());
243
244        insta::assert_debug_snapshot!(trace_metric.trace_id.meta(), @r#"
245        Meta {
246            remarks: [
247                Remark {
248                    ty: Substituted,
249                    rule_id: "nil_trace_id",
250                    range: None,
251                },
252            ],
253            errors: [],
254            original_length: None,
255            original_value: None,
256        }
257        "#);
258    }
259}