relay_event_schema/protocol/
thread.rs

1use std::fmt;
2
3use relay_protocol::{
4    Annotated, Empty, Error, ErrorKind, FromValue, IntoValue, Object, SkipSerialization, Value,
5};
6use serde::{Deserialize, Serialize, Serializer};
7
8use crate::processor::ProcessValue;
9use crate::protocol::{RawStacktrace, Stacktrace};
10
11/// Represents a thread id.
12#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
13#[serde(untagged)]
14pub enum ThreadId {
15    /// Integer representation of the thread id.
16    Int(u64),
17    /// String representation of the thread id.
18    String(String),
19}
20
21impl FromValue for ThreadId {
22    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
23        match value {
24            Annotated(Some(Value::String(value)), meta) => {
25                Annotated(Some(ThreadId::String(value)), meta)
26            }
27            Annotated(Some(Value::U64(value)), meta) => Annotated(Some(ThreadId::Int(value)), meta),
28            Annotated(Some(Value::I64(value)), meta) => {
29                Annotated(Some(ThreadId::Int(value as u64)), meta)
30            }
31            Annotated(None, meta) => Annotated(None, meta),
32            Annotated(Some(value), mut meta) => {
33                meta.add_error(Error::expected("a thread id"));
34                meta.set_original_value(Some(value));
35                Annotated(None, meta)
36            }
37        }
38    }
39}
40
41impl IntoValue for ThreadId {
42    fn into_value(self) -> Value {
43        match self {
44            ThreadId::String(value) => Value::String(value),
45            ThreadId::Int(value) => Value::U64(value),
46        }
47    }
48
49    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
50    where
51        Self: Sized,
52        S: Serializer,
53    {
54        match *self {
55            ThreadId::String(ref value) => Serialize::serialize(value, s),
56            ThreadId::Int(value) => Serialize::serialize(&value, s),
57        }
58    }
59}
60
61impl ProcessValue for ThreadId {}
62
63impl Empty for ThreadId {
64    #[inline]
65    fn is_empty(&self) -> bool {
66        match self {
67            ThreadId::Int(_) => false,
68            ThreadId::String(string) => string.is_empty(),
69        }
70    }
71}
72
73impl fmt::Display for ThreadId {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            ThreadId::Int(id) => write!(f, "{}", id),
77            ThreadId::String(id) => write!(f, "{}", id),
78        }
79    }
80}
81
82/// Possible lock types responsible for a thread's blocked state
83#[derive(Debug, Copy, Clone, Eq, PartialEq, ProcessValue, Empty)]
84pub enum LockReasonType {
85    /// Thread is Runnable but holding a lock object (generic case).
86    Locked = 1,
87    /// Thread TimedWaiting in Object.wait() with a timeout.
88    Waiting = 2,
89    /// Thread TimedWaiting in Thread.sleep().
90    Sleeping = 4,
91    /// Thread Blocked on a monitor/shared lock.
92    Blocked = 8,
93    // This enum does not have a `fallback_variant` because we consider it unlikely to be extended. If it is,
94    // The error added to `Meta` will tell us to update this enum.
95}
96
97impl LockReasonType {
98    fn from_android_lock_reason_type(value: u64) -> Option<LockReasonType> {
99        Some(match value {
100            1 => LockReasonType::Locked,
101            2 => LockReasonType::Waiting,
102            4 => LockReasonType::Sleeping,
103            8 => LockReasonType::Blocked,
104            _ => return None,
105        })
106    }
107}
108
109impl FromValue for LockReasonType {
110    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
111        match value {
112            Annotated(Some(Value::U64(val)), mut meta) => {
113                match LockReasonType::from_android_lock_reason_type(val) {
114                    Some(value) => Annotated(Some(value), meta),
115                    None => {
116                        meta.add_error(ErrorKind::InvalidData);
117                        meta.set_original_value(Some(val));
118                        Annotated(None, meta)
119                    }
120                }
121            }
122            Annotated(Some(Value::I64(val)), mut meta) => {
123                match LockReasonType::from_android_lock_reason_type(val as u64) {
124                    Some(value) => Annotated(Some(value), meta),
125                    None => {
126                        meta.add_error(ErrorKind::InvalidData);
127                        meta.set_original_value(Some(val));
128                        Annotated(None, meta)
129                    }
130                }
131            }
132            Annotated(None, meta) => Annotated(None, meta),
133            Annotated(Some(value), mut meta) => {
134                meta.add_error(Error::expected("lock reason type"));
135                meta.set_original_value(Some(value));
136                Annotated(None, meta)
137            }
138        }
139    }
140}
141
142impl IntoValue for LockReasonType {
143    fn into_value(self) -> Value {
144        Value::U64(self as u64)
145    }
146
147    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
148    where
149        Self: Sized,
150        S: Serializer,
151    {
152        Serialize::serialize(&(*self as u64), s)
153    }
154}
155
156/// Represents an instance of a held lock (java monitor object) in a thread.
157#[derive(Clone, Debug, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
158pub struct LockReason {
159    /// Type of lock on the thread with available options being blocked, waiting, sleeping and locked.
160    #[metastructure(field = "type", required = true)]
161    pub ty: Annotated<LockReasonType>,
162
163    /// Address of the java monitor object.
164    #[metastructure(skip_serialization = "empty")]
165    pub address: Annotated<String>,
166
167    /// Package name of the java monitor object.
168    #[metastructure(skip_serialization = "empty")]
169    pub package_name: Annotated<String>,
170
171    /// Class name of the java monitor object.
172    #[metastructure(skip_serialization = "empty")]
173    pub class_name: Annotated<String>,
174
175    /// Thread ID that's holding the lock.
176    #[metastructure(skip_serialization = "empty")]
177    pub thread_id: Annotated<ThreadId>,
178
179    /// Additional arbitrary fields for forwards compatibility.
180    #[metastructure(additional_properties)]
181    pub other: Object<Value>,
182}
183
184/// A process thread of an event.
185///
186/// The Threads Interface specifies threads that were running at the time an event happened. These threads can also contain stack traces.
187///
188/// An event may contain one or more threads in an attribute named `threads`.
189///
190/// The following example illustrates the threads part of the event payload and omits other attributes for simplicity.
191///
192/// ```json
193/// {
194///   "threads": {
195///     "values": [
196///       {
197///         "id": "0",
198///         "name": "main",
199///         "crashed": true,
200///         "stacktrace": {}
201///       }
202///     ]
203///   }
204/// }
205/// ```
206#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
207#[metastructure(process_func = "process_thread", value_type = "Thread")]
208pub struct Thread {
209    /// The ID of the thread. Typically a number or numeric string.
210    ///
211    /// Needs to be unique among the threads. An exception can set the `thread_id` attribute to cross-reference this thread.
212    #[metastructure(max_chars = 256, max_chars_allowance = 20)]
213    pub id: Annotated<ThreadId>,
214
215    /// Display name of this thread.
216    #[metastructure(max_chars = 1024, max_chars_allowance = 100)]
217    pub name: Annotated<String>,
218
219    /// Stack trace containing frames of this exception.
220    ///
221    /// The thread that crashed with an exception should not have a stack trace, but instead, the `thread_id` attribute should be set on the exception and Sentry will connect the two.
222    #[metastructure(skip_serialization = "empty")]
223    pub stacktrace: Annotated<Stacktrace>,
224
225    /// Optional unprocessed stack trace.
226    #[metastructure(skip_serialization = "empty", omit_from_schema)]
227    pub raw_stacktrace: Annotated<RawStacktrace>,
228
229    /// A flag indicating whether the thread crashed. Defaults to `false`.
230    pub crashed: Annotated<bool>,
231
232    /// A flag indicating whether the thread was in the foreground. Defaults to `false`.
233    pub current: Annotated<bool>,
234
235    /// A flag indicating whether the thread was responsible for rendering the user interface.
236    pub main: Annotated<bool>,
237
238    /// Thread state at the time of the crash.
239    #[metastructure(skip_serialization = "empty")]
240    pub state: Annotated<String>,
241
242    /// Represents a collection of locks (java monitor objects) held by a thread.
243    ///
244    /// A map of lock object addresses and their respective lock reason/details.
245    pub held_locks: Annotated<Object<LockReason>>,
246
247    /// Additional arbitrary fields for forwards compatibility.
248    #[metastructure(additional_properties)]
249    pub other: Object<Value>,
250}
251
252#[cfg(test)]
253mod tests {
254    use relay_protocol::Map;
255    use similar_asserts::assert_eq;
256
257    use super::*;
258
259    #[test]
260    fn test_thread_id() {
261        assert_eq!(
262            ThreadId::String("testing".into()),
263            Annotated::<ThreadId>::from_json("\"testing\"")
264                .unwrap()
265                .0
266                .unwrap()
267        );
268        assert_eq!(
269            ThreadId::String("42".into()),
270            Annotated::<ThreadId>::from_json("\"42\"")
271                .unwrap()
272                .0
273                .unwrap()
274        );
275        assert_eq!(
276            ThreadId::Int(42),
277            Annotated::<ThreadId>::from_json("42").unwrap().0.unwrap()
278        );
279    }
280
281    #[test]
282    fn test_thread_roundtrip() {
283        // stack traces are tested separately
284        let json = r#"{
285  "id": 42,
286  "name": "myname",
287  "crashed": true,
288  "current": true,
289  "main": true,
290  "state": "RUNNABLE",
291  "other": "value"
292}"#;
293        let thread = Annotated::new(Thread {
294            id: Annotated::new(ThreadId::Int(42)),
295            name: Annotated::new("myname".to_string()),
296            stacktrace: Annotated::empty(),
297            raw_stacktrace: Annotated::empty(),
298            crashed: Annotated::new(true),
299            current: Annotated::new(true),
300            main: Annotated::new(true),
301            state: Annotated::new("RUNNABLE".to_string()),
302            held_locks: Annotated::empty(),
303            other: {
304                let mut map = Map::new();
305                map.insert(
306                    "other".to_string(),
307                    Annotated::new(Value::String("value".to_string())),
308                );
309                map
310            },
311        });
312
313        assert_eq!(thread, Annotated::from_json(json).unwrap());
314        assert_eq!(json, thread.to_json_pretty().unwrap());
315    }
316
317    #[test]
318    fn test_thread_default_values() {
319        let json = "{}";
320        let thread = Annotated::new(Thread::default());
321
322        assert_eq!(thread, Annotated::from_json(json).unwrap());
323        assert_eq!(json, thread.to_json_pretty().unwrap());
324    }
325
326    #[test]
327    fn test_thread_lock_reason_roundtrip() {
328        // stack traces are tested separately
329        let input = r#"{
330  "id": 42,
331  "name": "myname",
332  "crashed": true,
333  "current": true,
334  "main": true,
335  "state": "BLOCKED",
336  "held_locks": {
337    "0x07d7437b": {
338      "type": 2,
339      "package_name": "io.sentry.samples",
340      "class_name": "MainActivity",
341      "thread_id": 7
342    },
343    "0x0d3a2f0a": {
344      "type": 1,
345      "package_name": "android.database.sqlite",
346      "class_name": "SQLiteConnection",
347      "thread_id": 2
348    }
349  },
350  "other": "value"
351}"#;
352        let thread = Annotated::new(Thread {
353            id: Annotated::new(ThreadId::Int(42)),
354            name: Annotated::new("myname".to_string()),
355            stacktrace: Annotated::empty(),
356            raw_stacktrace: Annotated::empty(),
357            crashed: Annotated::new(true),
358            current: Annotated::new(true),
359            main: Annotated::new(true),
360            state: Annotated::new("BLOCKED".to_string()),
361            held_locks: {
362                let mut locks = Object::new();
363                locks.insert(
364                    "0x07d7437b".to_string(),
365                    Annotated::new(LockReason {
366                        ty: Annotated::new(LockReasonType::Waiting),
367                        address: Annotated::empty(),
368                        package_name: Annotated::new("io.sentry.samples".to_string()),
369                        class_name: Annotated::new("MainActivity".to_string()),
370                        thread_id: Annotated::new(ThreadId::Int(7)),
371                        other: Default::default(),
372                    }),
373                );
374                locks.insert(
375                    "0x0d3a2f0a".to_string(),
376                    Annotated::new(LockReason {
377                        ty: Annotated::new(LockReasonType::Locked),
378                        address: Annotated::empty(),
379                        package_name: Annotated::new("android.database.sqlite".to_string()),
380                        class_name: Annotated::new("SQLiteConnection".to_string()),
381                        thread_id: Annotated::new(ThreadId::Int(2)),
382                        other: Default::default(),
383                    }),
384                );
385                Annotated::new(locks)
386            },
387            other: {
388                let mut map = Map::new();
389                map.insert(
390                    "other".to_string(),
391                    Annotated::new(Value::String("value".to_string())),
392                );
393                map
394            },
395        });
396
397        assert_eq!(thread, Annotated::from_json(input).unwrap());
398
399        assert_eq!(input, thread.to_json_pretty().unwrap());
400    }
401}