diff --git a/packages/libs/error-stack/Cargo.toml b/packages/libs/error-stack/Cargo.toml index dd7696292f3..e5a7c970aa6 100644 --- a/packages/libs/error-stack/Cargo.toml +++ b/packages/libs/error-stack/Cargo.toml @@ -14,12 +14,14 @@ categories = ["rust-patterns", "no-std"] [dependencies] tracing-error = { version = "0.2.0", optional = true, default_features = false } -once_cell = { version = "1.10.0", optional = true, default_features = false } +once_cell = { version = "1.10.0", optional = true, default_features = false, features = ['std'] } pin-project = { version = "1.0.10", optional = true, default_features = false } futures-core = { version = "0.3.21", optional = true, default-features = false } smallvec = { version = "1.9.0", optional = true, default_features = false, features = ['union'] } anyhow = { version = "1.0.58", default-features = false, optional = true } eyre = { version = "0.6.8", default-features = false, optional = true } +serde = { version = "1.0.139", default-features = false, optional = true, features = ['alloc'] } +erased-serde = { version = "0.3.21", default-features = false, optional = true, features = ['alloc'] } [dev-dependencies] serde = { version = "1.0.137", features = ["derive"] } @@ -34,12 +36,14 @@ once_cell = "1.13.0" rustc_version = "0.2.3" [features] -default = ["std", "small"] -std = ["anyhow?/std", "once_cell?/std"] +default = ["std", "small", "serde"] +std = ["anyhow?/std"] hooks = ["dep:once_cell", "std"] spantrace = ["dep:tracing-error"] futures = ["dep:pin-project", "dep:futures-core"] small = ["dep:smallvec"] +serde = ["dep:serde", "dep:erased-serde"] +experimental = [] [package.metadata.docs.rs] all-features = true diff --git a/packages/libs/error-stack/src/frame/frame_impl.rs b/packages/libs/error-stack/src/frame/frame_impl.rs index 96ae54f4e8c..e1caac89de2 100644 --- a/packages/libs/error-stack/src/frame/frame_impl.rs +++ b/packages/libs/error-stack/src/frame/frame_impl.rs @@ -214,3 +214,42 @@ impl Frame { } } } + +#[cfg(feature = "serde")] +#[repr(C)] +struct SerializableAttachmentFrame { + attachment: A, + location: &'static Location<'static>, + sources: Box<[Frame]>, +} + +#[cfg(feature = "serde")] +// SAFETY: `type_id` returns `A` and `A` is the first field in `#[repr(C)]` +unsafe impl FrameImpl + for SerializableAttachmentFrame +{ + fn kind(&self) -> FrameKind<'_> { + FrameKind::Attachment(AttachmentKind::Serializable(&self.attachment)) + } + + fn type_id(&self) -> TypeId { + TypeId::of::() + } + + fn location(&self) -> &'static Location<'static> { + self.location + } + + fn sources(&self) -> &[Frame] { + &self.sources + } + + fn sources_mut(&mut self) -> &mut [Frame] { + &mut self.sources + } + + #[cfg(nightly)] + fn provide<'a>(&'a self, demand: &mut Demand<'a>) { + demand.provide_ref(&self.attachment); + } +} diff --git a/packages/libs/error-stack/src/frame/kind.rs b/packages/libs/error-stack/src/frame/kind.rs index 8073921b17a..aa2cc15b595 100644 --- a/packages/libs/error-stack/src/frame/kind.rs +++ b/packages/libs/error-stack/src/frame/kind.rs @@ -29,6 +29,11 @@ pub enum AttachmentKind<'f> { /// /// [`attach_printable()`]: crate::Report::attach_printable Printable(&'f dyn Printable), + /// A serializable attachment created through [`attach_serializable()`]. + /// + /// [`attach_serializable()`]: crate::Report::attach_serializable + #[cfg(feature = "serde")] + Serializable(&'f dyn erased_serde::Serialize), } // TODO: Replace `Printable` by trait bounds when trait objects for multiple traits are supported. diff --git a/packages/libs/error-stack/src/frame/mod.rs b/packages/libs/error-stack/src/frame/mod.rs index 591c9bd50cd..811625fb5ee 100644 --- a/packages/libs/error-stack/src/frame/mod.rs +++ b/packages/libs/error-stack/src/frame/mod.rs @@ -118,6 +118,21 @@ impl Frame { unsafe { &mut *(self.frame.as_mut() as *mut dyn FrameImpl).cast::() } }) } + + /// Return a tuple of `(frame, parents)`, where parents are the frames where a "split" occurred, + /// ~> multiple sources exist + pub(crate) fn collect(&self) -> (Vec<&Frame>, &[Frame]) { + let mut stack = vec![self]; + + let mut ptr = self.sources(); + + while let [parent] = ptr { + stack.push(parent); + ptr = parent.sources(); + } + + (stack, ptr) + } } #[cfg(nightly)] diff --git a/packages/libs/error-stack/src/hook.rs b/packages/libs/error-stack/src/hook.rs index cd58b6431f0..b2b41ccab78 100644 --- a/packages/libs/error-stack/src/hook.rs +++ b/packages/libs/error-stack/src/hook.rs @@ -2,12 +2,16 @@ use std::{error::Error, fmt}; use once_cell::sync::OnceCell; +#[cfg(feature = "serde")] +use crate::ser::ErasedHooks; use crate::{Report, Result}; type FormatterHook = Box, &mut fmt::Formatter<'_>) -> fmt::Result + Send + Sync>; static DEBUG_HOOK: OnceCell = OnceCell::new(); static DISPLAY_HOOK: OnceCell = OnceCell::new(); +#[cfg(feature = "serde")] +static SERIALIZE_HOOK: OnceCell = OnceCell::new(); /// A hook can only be set once. /// @@ -127,6 +131,11 @@ impl Report<()> { { DISPLAY_HOOK.get() } + + #[cfg(all(feature = "serde", feature = "hooks"))] + pub(crate) fn serialize_hook() -> Option<&'static ErasedHooks> { + SERIALIZE_HOOK.get() + } } impl Report { diff --git a/packages/libs/error-stack/src/lib.rs b/packages/libs/error-stack/src/lib.rs index 59c8981ed3f..47e2f5ea099 100644 --- a/packages/libs/error-stack/src/lib.rs +++ b/packages/libs/error-stack/src/lib.rs @@ -443,6 +443,8 @@ mod context; mod ext; #[cfg(feature = "hooks")] mod hook; +#[cfg(feature = "serde")] +mod ser; #[doc(inline)] pub use self::ext::*; diff --git a/packages/libs/error-stack/src/report.rs b/packages/libs/error-stack/src/report.rs index 49aaf7399f7..9636f009924 100644 --- a/packages/libs/error-stack/src/report.rs +++ b/packages/libs/error-stack/src/report.rs @@ -233,8 +233,8 @@ impl Report { let provider = temporary_provider(&context); #[cfg(all(nightly, feature = "std"))] - let backtrace = if core::any::request_ref::(&provider) - .filter(|backtrace| backtrace.status() == BacktraceStatus::Captured) + let backtrace = if core::any::request_ref(&provider) + .filter(|backtrace: &&Backtrace| backtrace.status() == BacktraceStatus::Captured) .is_some() { None @@ -243,8 +243,8 @@ impl Report { }; #[cfg(all(nightly, feature = "spantrace"))] - let span_trace = if core::any::request_ref::(&provider) - .filter(|span_trace| span_trace.status() == SpanTraceStatus::CAPTURED) + let span_trace = if core::any::request_ref(&provider) + .filter(|span_trace: &&SpanTrace| span_trace.status() == SpanTraceStatus::CAPTURED) .is_some() { None @@ -393,6 +393,8 @@ impl Report { )) } + // TODO: attach_serializable + /// Adds additional (printable) information to the [`Frame`] stack. /// /// This behaves like [`attach()`] but the display implementation will be called when @@ -569,6 +571,16 @@ impl Report { pub fn downcast_mut(&mut self) -> Option<&mut T> { self.frames_mut().find_map(Frame::downcast_mut::) } + + /// Return a tuple of `(frame, parents)`, where parents are the frames where a "split" occurred, + /// ~> multiple sources exist + pub(crate) fn collect(&self) -> (Vec<&Frame>, &[Frame]) { + match self.current_frames() { + [] => (vec![], &[]), + [frame] => frame.collect(), + frames => (frames.iter().collect(), &[]), + } + } } impl Report { diff --git a/packages/libs/error-stack/src/ser/hook.rs b/packages/libs/error-stack/src/ser/hook.rs new file mode 100644 index 00000000000..8bc95c2764b --- /dev/null +++ b/packages/libs/error-stack/src/ser/hook.rs @@ -0,0 +1,142 @@ +use std::marker::PhantomData; + +use erased_serde::{Error, Serialize, Serializer}; +use serde::Serialize as _; + +use crate::{frame::Frame, ser::hook}; + +macro_rules! all_the_tuples { + ($name:ident) => { + $name!(T1); + $name!(T1, T2); + $name!(T1, T2, T3); + $name!(T1, T2, T3, T4); + $name!(T1, T2, T3, T4, T5); + $name!(T1, T2, T3, T4, T5, T6); + $name!(T1, T2, T3, T4, T5, T6, T7); + $name!(T1, T2, T3, T4, T5, T6, T7, T8); + $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9); + $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); + $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); + $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); + $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); + $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); + $name!( + T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15 + ); + $name!( + T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16 + ); + }; +} + +type UInt0 = (); +type UInt1 = ((), UInt0); +type UInt2 = ((), UInt1); + +trait UInt {} + +impl UInt for () {} +impl UInt for ((), T) {} + +pub trait Hook { + fn call(&self, frame: &T) -> Option>; +} + +impl Hook for F +where + F: Fn(&T) -> U, + T: Send + Sync + 'static, + U: serde::Serialize, +{ + fn call(&self, frame: &T) -> Option> { + Some(Box::new((self)(frame))) + } +} + +struct Phantom { + _marker: PhantomData, +} + +macro_rules! impl_hook_tuple { + () => {}; + + ( $($ty:ident),* $(,)? ) => { + #[allow(non_snake_case)] + #[automatically_derived] + impl<$($ty,)*> Hook for Phantom<($($ty,)*)> + where + $($ty: serde::Serialize + Send + Sync + 'static),* + { + fn call(&self, frame: &Frame) -> Option> { + $( + if let Some($ty) = frame.downcast_ref::<$ty>() { + return Some(Box::new($ty)) + } + )* + + None + } + } + } +} + +all_the_tuples!(impl_hook_tuple); + +struct Stack { + left: L, + right: R, + _marker: PhantomData, +} + +impl Hook for Stack +where + L: Hook, + T: Sync + Send + 'static, + R: Hook, +{ + fn call(&self, frame: &Frame) -> Option> { + frame + .downcast_ref() + .and_then(|value| self.left.call(value)) + .or_else(|| self.right.call(frame)) + } +} + +struct Combine { + left: L, + right: R, +} + +impl Hook for Combine +where + L: Hook, + R: Hook, +{ + fn call(&self, frame: &Frame) -> Option> { + self.left.call(frame).or_else(|| self.right.call(frame)) + } +} + +impl Hook for Box> { + fn call(&self, frame: &Frame) -> Option> { + let hook: &dyn Hook = self; + hook.call(frame) + } +} + +impl Hook for () { + fn call(&self, _: &Frame) -> Option> { + None + } +} + +pub struct Hooks>(T); + +impl> Hooks { + pub(crate) fn call(&self, frame: &Frame) -> Option> { + self.0.call(frame) + } +} + +pub(crate) type ErasedHooks = Hooks>>; diff --git a/packages/libs/error-stack/src/ser/mod.rs b/packages/libs/error-stack/src/ser/mod.rs new file mode 100644 index 00000000000..2e3962135e5 --- /dev/null +++ b/packages/libs/error-stack/src/ser/mod.rs @@ -0,0 +1,179 @@ +//! Serialization logic for report +//! +//! This implements the following serialization logic (rendered in json) +//! +//! ```json +//! { +//! "frames": [ +//! { +//! "type": "attachment", +//! "letter": "A" +//! }, +//! { +//! "type": "attachment", +//! "letter": "B" +//! }, +//! { +//! "type": "context", +//! "letter": "C" +//! } +//! ], +//! "sources": [ +//! { +//! "frames": [ +//! { +//! "type": "attachment", +//! "letter": "E" +//! }, +//! { +//! "type": "attachment", +//! "letter": "G" +//! }, +//! { +//! "type": "context", +//! "letter": "H" +//! } +//! ], +//! "sources": [] +//! }, +//! { +//! "frames": [ +//! { +//! "type": "context", +//! "letter": "F" +//! } +//! ], +//! "sources": [] +//! } +//! ], +//! } +//! ``` + +pub(crate) use hook::ErasedHooks; +use serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, Serializer, +}; + +#[cfg(all(nightly, feature = "experimental"))] +use crate::ser::nightly::SerializeDiagnostic; +use crate::{AttachmentKind, Frame, FrameKind, Report}; + +mod hook; +mod nightly; + +impl Serialize for Frame { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self.kind() { + FrameKind::Context(context) => { + let mut s = serializer.serialize_map(Some(2))?; + + s.serialize_entry("type", "context")?; + + #[cfg(all(nightly, feature = "experimental"))] + if let Some(diagnostic) = self.request_ref::() { + s.serialize_entry("value", diagnostic)?; + } else { + s.serialize_entry("value", &context.to_string())?; + } + + #[cfg(not(all(nightly, feature = "experimental")))] + { s.serialize_entry("value", &context.to_string()) }?; + + s.end() + } + FrameKind::Attachment(AttachmentKind::Opaque(attachment)) => { + let mut s = serializer.serialize_map(Some(2))?; + + s.serialize_entry("type", "attachment")?; + + let mut fallback = true; + #[cfg(all(nightly, feature = "experimental"))] + if let Some(diagnostic) = self.request_ref::() { + s.serialize_entry("value", diagnostic)?; + fallback = false; + } + + #[cfg(feature = "hooks")] + if let Some(hooks) = Report::serialize_hook() { + if let Some(value) = hooks.call(self) { + s.serialize_entry("value", &value)?; + fallback = false; + } + } + + if fallback { + // explicit `None` if the value isn't provided. + s.serialize_entry("value", &Option::<()>::None)?; + } + + s.end() + } + FrameKind::Attachment(AttachmentKind::Printable(attachment)) => { + let mut s = serializer.serialize_map(Some(2))?; + + s.serialize_entry("type", "attachment")?; + s.serialize_entry("value", &attachment.to_string())?; + + s.end() + } + FrameKind::Attachment(AttachmentKind::Serializable(attachment)) => { + let mut s = serializer.serialize_map(Some(2))?; + + s.serialize_entry("type", "attachment")?; + s.serialize_entry("value", attachment)?; + + s.end() + } + } + } +} + +struct Root<'a> { + stack: Vec<&'a Frame>, + split: &'a [Frame], +} + +impl<'a> From<&'a Frame> for Root<'a> { + fn from(frame: &'a Frame) -> Self { + let (stack, split) = frame.collect(); + + Self { stack, split } + } +} + +impl<'a, C> From<&'a Report> for Root<'a> { + fn from(frame: &'a Report) -> Self { + let (stack, split) = frame.collect(); + + Self { stack, split } + } +} + +impl Serialize for Root<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let Self { stack, split } = self; + + let mut s = serializer.serialize_map(Some(2))?; + + s.serialize_entry("frames", &stack)?; + s.serialize_entry("sources", &split.iter().map(Root::from).collect::>())?; + + s.end() + } +} + +impl Serialize for Report { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Root::from(self).serialize(serializer) + } +} diff --git a/packages/libs/error-stack/src/ser/nightly.rs b/packages/libs/error-stack/src/ser/nightly.rs new file mode 100644 index 00000000000..663d05c6425 --- /dev/null +++ b/packages/libs/error-stack/src/ser/nightly.rs @@ -0,0 +1,14 @@ +use serde::{Serialize, Serializer}; + +pub struct SerializeDiagnostic { + inner: Box, +} + +impl Serialize for SerializeDiagnostic { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.inner.serialize(serializer) + } +}