relay_event_schema/protocol/
trace_metric.rs

1use relay_protocol::{
2    Annotated, Empty, FromValue, Getter, IntoValue, Object, SkipSerialization, Value,
3};
4use std::fmt::{self, Display};
5
6use relay_base_schema::metrics::MetricUnit;
7use serde::{Serialize, Serializer};
8
9use crate::processor::ProcessValue;
10use crate::protocol::{Attributes, SpanId, Timestamp, TraceId};
11
12#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
13#[metastructure(process_func = "process_trace_metric", value_type = "TraceMetric")]
14pub struct TraceMetric {
15    /// Timestamp when the metric was created.
16    #[metastructure(required = true)]
17    pub timestamp: Annotated<Timestamp>,
18
19    /// The ID of the trace the metric belongs to.
20    #[metastructure(required = true, trim = false)]
21    pub trace_id: Annotated<TraceId>,
22
23    /// The Span this metric belongs to.
24    #[metastructure(required = false, trim = false)]
25    pub span_id: Annotated<SpanId>,
26
27    /// The metric name.
28    #[metastructure(required = true, trim = false)]
29    pub name: Annotated<String>,
30
31    /// The metric type.
32    #[metastructure(required = true, field = "type")]
33    pub ty: Annotated<MetricType>,
34
35    /// The metric unit.
36    #[metastructure(required = false)]
37    pub unit: Annotated<MetricUnit>,
38
39    /// The metric value.
40    ///
41    /// Should be constrained to a number.
42    #[metastructure(pii = "maybe", required = true, trim = false)]
43    pub value: Annotated<Value>,
44
45    /// Arbitrary attributes on a metric.
46    #[metastructure(pii = "maybe", trim = false)]
47    pub attributes: Annotated<Attributes>,
48
49    /// Additional arbitrary fields for forwards compatibility.
50    #[metastructure(additional_properties, retain = false)]
51    pub other: Object<Value>,
52}
53
54impl Getter for TraceMetric {
55    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
56        Some(match path.strip_prefix("trace_metric.")? {
57            "name" => self.name.as_str()?.into(),
58            path => {
59                if let Some(key) = path.strip_prefix("attributes.") {
60                    let key = key.strip_suffix(".value")?;
61                    self.attributes.value()?.get_value(key)?.into()
62                } else {
63                    return None;
64                }
65            }
66        })
67    }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
71pub enum MetricType {
72    /// A gauge metric represents a single numerical value that can arbitrarily go up and down.
73    Gauge,
74    /// A distribution metric represents a collection of values that can be aggregated.
75    Distribution,
76    /// A counter metric represents a single numerical value.
77    Counter,
78    /// Unknown type, for forward compatibility.
79    Unknown(String),
80}
81
82impl MetricType {
83    fn as_str(&self) -> &str {
84        match self {
85            MetricType::Gauge => "gauge",
86            MetricType::Distribution => "distribution",
87            MetricType::Counter => "counter",
88            MetricType::Unknown(s) => s.as_str(),
89        }
90    }
91}
92
93impl Display for MetricType {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "{}", self.as_str())
96    }
97}
98
99impl From<String> for MetricType {
100    fn from(value: String) -> Self {
101        match value.as_str() {
102            "gauge" => MetricType::Gauge,
103            "distribution" => MetricType::Distribution,
104            "counter" => MetricType::Counter,
105            _ => MetricType::Unknown(value),
106        }
107    }
108}
109
110impl FromValue for MetricType {
111    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
112        match String::from_value(value) {
113            Annotated(Some(value), meta) => Annotated(Some(value.into()), meta),
114            Annotated(None, meta) => Annotated(None, meta),
115        }
116    }
117}
118
119impl IntoValue for MetricType {
120    fn into_value(self) -> Value {
121        Value::String(self.to_string())
122    }
123
124    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
125    where
126        Self: Sized,
127        S: Serializer,
128    {
129        Serialize::serialize(self.as_str(), s)
130    }
131}
132
133impl ProcessValue for MetricType {}
134
135impl Empty for MetricType {
136    #[inline]
137    fn is_empty(&self) -> bool {
138        false
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use relay_protocol::SerializableAnnotated;
146
147    #[test]
148    fn test_trace_metric_serialization() {
149        let json = r#"{
150            "timestamp": 1544719860.0,
151            "trace_id": "5b8efff798038103d269b633813fc60c",
152            "span_id": "eee19b7ec3c1b174",
153            "name": "http.request.duration",
154            "type": "distribution",
155            "value": 123.45,
156            "attributes": {
157                "http.method": {
158                    "value": "GET",
159                    "type": "string"
160                },
161                "http.status_code": {
162                    "value": "200",
163                    "type": "integer"
164                }
165            }
166        }"#;
167
168        let data = Annotated::<TraceMetric>::from_json(json).unwrap();
169
170        insta::assert_json_snapshot!(SerializableAnnotated(&data), @r###"
171        {
172          "timestamp": 1544719860.0,
173          "trace_id": "5b8efff798038103d269b633813fc60c",
174          "span_id": "eee19b7ec3c1b174",
175          "name": "http.request.duration",
176          "type": "distribution",
177          "value": 123.45,
178          "attributes": {
179            "http.method": {
180              "type": "string",
181              "value": "GET"
182            },
183            "http.status_code": {
184              "type": "integer",
185              "value": "200"
186            }
187          }
188        }
189        "###);
190    }
191}