relay_event_schema/protocol/trace_metric/
mod.rs1use 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 #[metastructure(required = true)]
20 pub timestamp: Annotated<Timestamp>,
21
22 #[metastructure(required = true, trim = false)]
24 pub trace_id: Annotated<TraceId>,
25
26 #[metastructure(required = false, trim = false)]
28 pub span_id: Annotated<SpanId>,
29
30 #[metastructure(required = true, trim = false)]
32 pub name: Annotated<String>,
33
34 #[metastructure(required = true, field = "type")]
36 pub ty: Annotated<MetricType>,
37
38 #[metastructure(required = false)]
40 pub unit: Annotated<MetricUnit>,
41
42 #[metastructure(pii = "maybe", required = true, trim = false)]
46 pub value: Annotated<Value>,
47
48 #[metastructure(pii = "maybe", trim = false)]
50 pub attributes: Annotated<Attributes>,
51
52 #[metastructure(additional_properties, retain = false)]
54 pub other: Object<Value>,
55}
56
57#[derive(Clone, Debug, Default, Serialize, Deserialize)]
62pub struct TraceMetricHeader {
63 pub byte_size: Option<u64>,
68
69 #[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 Gauge,
91 Distribution,
93 Counter,
95 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}