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