diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index cfb41900f..8ab37e1be 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -31,7 +31,7 @@ pub use pb::Status; mod richer_error; pub use richer_error::{ - BadRequest, ErrorDetail, ErrorDetails, FieldViolation, RetryInfo, StatusExt, + BadRequest, DebugInfo, ErrorDetail, ErrorDetails, FieldViolation, RetryInfo, StatusExt, }; mod sealed { diff --git a/tonic-types/src/richer_error/error_details/mod.rs b/tonic-types/src/richer_error/error_details/mod.rs index 0067a0fd8..ab4390369 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -1,6 +1,6 @@ use std::time; -use super::std_messages::{BadRequest, FieldViolation, RetryInfo}; +use super::std_messages::{BadRequest, DebugInfo, FieldViolation, RetryInfo}; pub(crate) mod vec; @@ -14,6 +14,9 @@ pub struct ErrorDetails { /// This field stores [`RetryInfo`] data, if any. pub(crate) retry_info: Option, + /// This field stores [`DebugInfo`] data, if any. + pub(crate) debug_info: Option, + /// This field stores [`BadRequest`] data, if any. pub(crate) bad_request: Option, } @@ -31,6 +34,7 @@ impl ErrorDetails { pub fn new() -> Self { ErrorDetails { retry_info: None, + debug_info: None, bad_request: None, } } @@ -53,6 +57,25 @@ impl ErrorDetails { } } + /// Generates an [`ErrorDetails`] struct with [`DebugInfo`] details and + /// remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails}; + /// + /// let err_stack = vec!["...".into(), "...".into()]; + /// + /// let err_details = ErrorDetails::with_debug_info(err_stack, "error details"); + /// ``` + pub fn with_debug_info(stack_entries: Vec, detail: impl Into) -> Self { + ErrorDetails { + debug_info: Some(DebugInfo::new(stack_entries, detail)), + ..ErrorDetails::new() + } + } + /// Generates an [`ErrorDetails`] struct with [`BadRequest`] details and /// remaining fields set to `None`. /// @@ -101,6 +124,11 @@ impl ErrorDetails { self.retry_info.clone() } + /// Get [`DebugInfo`] details, if any + pub fn debug_info(&self) -> Option { + self.debug_info.clone() + } + /// Get [`BadRequest`] details, if any pub fn bad_request(&self) -> Option { self.bad_request.clone() @@ -124,6 +152,29 @@ impl ErrorDetails { self } + /// Set [`DebugInfo`] details. Can be chained with other `.set_` and + /// `.add_` [`ErrorDetails`] methods. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::{ErrorDetails}; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// let err_stack = vec!["...".into(), "...".into()]; + /// + /// err_details.set_debug_info(err_stack, "error details"); + /// ``` + pub fn set_debug_info( + &mut self, + stack_entries: Vec, + detail: impl Into, + ) -> &mut Self { + self.debug_info = Some(DebugInfo::new(stack_entries, detail)); + self + } + /// Set [`BadRequest`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// diff --git a/tonic-types/src/richer_error/error_details/vec.rs b/tonic-types/src/richer_error/error_details/vec.rs index 350f2453c..50b0b9a0f 100644 --- a/tonic-types/src/richer_error/error_details/vec.rs +++ b/tonic-types/src/richer_error/error_details/vec.rs @@ -1,4 +1,4 @@ -use super::super::std_messages::{BadRequest, RetryInfo}; +use super::super::std_messages::{BadRequest, DebugInfo, RetryInfo}; /// Wraps the structs corresponding to the standard error messages, allowing /// the implementation and handling of vectors containing any of them. @@ -8,6 +8,9 @@ pub enum ErrorDetail { /// Wraps the [`RetryInfo`] struct. RetryInfo(RetryInfo), + /// Wraps the [`DebugInfo`] struct. + DebugInfo(DebugInfo), + /// Wraps the [`BadRequest`] struct. BadRequest(BadRequest), } @@ -18,6 +21,12 @@ impl From for ErrorDetail { } } +impl From for ErrorDetail { + fn from(err_detail: DebugInfo) -> Self { + ErrorDetail::DebugInfo(err_detail) + } +} + impl From for ErrorDetail { fn from(err_detail: BadRequest) -> Self { ErrorDetail::BadRequest(err_detail) diff --git a/tonic-types/src/richer_error/mod.rs b/tonic-types/src/richer_error/mod.rs index 50462e0b9..853bc15e6 100644 --- a/tonic-types/src/richer_error/mod.rs +++ b/tonic-types/src/richer_error/mod.rs @@ -8,7 +8,7 @@ mod std_messages; use super::pb; pub use error_details::{vec::ErrorDetail, ErrorDetails}; -pub use std_messages::{BadRequest, FieldViolation, RetryInfo}; +pub use std_messages::{BadRequest, DebugInfo, FieldViolation, RetryInfo}; trait IntoAny { fn into_any(self) -> Any; @@ -266,6 +266,28 @@ pub trait StatusExt: crate::sealed::Sealed { /// ``` fn get_details_retry_info(&self) -> Option; + /// Get first [`DebugInfo`] details found on `tonic::Status`, if any. If + /// some `prost::DecodeError` occurs, returns `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::{StatusExt}; + /// + /// fn handle_request_result(req_result: Result, Status>) { + /// match req_result { + /// Ok(_) => {}, + /// Err(status) => { + /// if let Some(debug_info) = status.get_details_debug_info() { + /// // Handle debug_info details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_debug_info(&self) -> Option; + /// Get first [`BadRequest`] details found on `tonic::Status`, if any. If /// some `prost::DecodeError` occurs, returns `None`. /// @@ -306,6 +328,10 @@ impl StatusExt for tonic::Status { conv_details.push(retry_info.into_any()); } + if let Some(debug_info) = details.debug_info { + conv_details.push(debug_info.into_any()); + } + if let Some(bad_request) = details.bad_request { conv_details.push(bad_request.into_any()); } @@ -334,6 +360,9 @@ impl StatusExt for tonic::Status { ErrorDetail::RetryInfo(retry_info) => { conv_details.push(retry_info.into_any()); } + ErrorDetail::DebugInfo(debug_info) => { + conv_details.push(debug_info.into_any()); + } ErrorDetail::BadRequest(bad_req) => { conv_details.push(bad_req.into_any()); } @@ -368,6 +397,9 @@ impl StatusExt for tonic::Status { RetryInfo::TYPE_URL => { details.retry_info = Some(RetryInfo::from_any(any)?); } + DebugInfo::TYPE_URL => { + details.debug_info = Some(DebugInfo::from_any(any)?); + } BadRequest::TYPE_URL => { details.bad_request = Some(BadRequest::from_any(any)?); } @@ -392,6 +424,9 @@ impl StatusExt for tonic::Status { RetryInfo::TYPE_URL => { details.push(RetryInfo::from_any(any)?.into()); } + DebugInfo::TYPE_URL => { + details.push(DebugInfo::from_any(any)?.into()); + } BadRequest::TYPE_URL => { details.push(BadRequest::from_any(any)?.into()); } @@ -422,6 +457,22 @@ impl StatusExt for tonic::Status { None } + fn get_details_debug_info(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + match any.type_url.as_str() { + DebugInfo::TYPE_URL => match DebugInfo::from_any(any) { + Ok(detail) => return Some(detail), + Err(_) => {} + }, + _ => {} + } + } + + None + } + fn get_details_bad_request(&self) -> Option { let status = pb::Status::decode(self.details()).ok()?; @@ -444,7 +495,7 @@ mod tests { use std::time::Duration; use tonic::{Code, Status}; - use super::{BadRequest, ErrorDetails, RetryInfo, StatusExt}; + use super::{BadRequest, DebugInfo, ErrorDetails, RetryInfo, StatusExt}; #[test] fn gen_status_with_details() { @@ -452,12 +503,21 @@ mod tests { err_details .set_retry_info(Some(Duration::from_secs(5))) + .set_debug_info( + vec!["trace3".into(), "trace2".into(), "trace1".into()], + "details", + ) .add_bad_request_violation("field", "description"); let fmt_details = format!("{:?}", err_details); let err_details_vec = vec![ RetryInfo::new(Some(Duration::from_secs(5))).into(), + DebugInfo::new( + vec!["trace3".into(), "trace2".into(), "trace1".into()], + "details", + ) + .into(), BadRequest::with_violation("field", "description").into(), ]; diff --git a/tonic-types/src/richer_error/std_messages/debug_info.rs b/tonic-types/src/richer_error/std_messages/debug_info.rs new file mode 100644 index 000000000..bde7aa805 --- /dev/null +++ b/tonic-types/src/richer_error/std_messages/debug_info.rs @@ -0,0 +1,112 @@ +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::{pb, FromAny, IntoAny}; + +/// Used to encode/decode the `DebugInfo` standard error message described in +/// [error_details.proto]. Describes additional debugging info. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct DebugInfo { + /// Stack trace entries indicating where the error occurred. + pub stack_entries: Vec, + + /// Additional debugging information provided by the server. + pub detail: String, +} + +impl DebugInfo { + /// Type URL of the `DebugInfo` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.DebugInfo"; + + /// Creates a new [`DebugInfo`] struct. + pub fn new(stack_entries: Vec, detail: impl Into) -> Self { + DebugInfo { + stack_entries, + detail: detail.into(), + } + } +} + +impl DebugInfo { + /// Returns `true` if [`DebugInfo`] fields are empty, and `false` if they + /// are not. + pub fn is_empty(&self) -> bool { + self.stack_entries.is_empty() && self.detail.is_empty() + } +} + +impl IntoAny for DebugInfo { + fn into_any(self) -> Any { + let detail_data = pb::DebugInfo { + stack_entries: self.stack_entries, + detail: self.detail, + }; + + Any { + type_url: DebugInfo::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for DebugInfo { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let debug_info = pb::DebugInfo::decode(buf)?; + + let debug_info = DebugInfo { + stack_entries: debug_info.stack_entries, + detail: debug_info.detail, + }; + + Ok(debug_info) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::{FromAny, IntoAny}; + use super::DebugInfo; + + #[test] + fn gen_debug_info() { + let debug_info = DebugInfo::new( + vec!["trace 3".into(), "trace 2".into(), "trace 1".into()], + "details about the error", + ); + + let formatted = format!("{:?}", debug_info); + + let expected_filled = "DebugInfo { stack_entries: [\"trace 3\", \"trace 2\", \"trace 1\"], detail: \"details about the error\" }"; + + assert!( + formatted.eq(expected_filled), + "filled DebugInfo differs from expected result" + ); + + let gen_any = debug_info.into_any(); + let formatted = format!("{:?}", gen_any); + + let expected = + "Any { type_url: \"type.googleapis.com/google.rpc.DebugInfo\", value: [10, 7, 116, 114, 97, 99, 101, 32, 51, 10, 7, 116, 114, 97, 99, 101, 32, 50, 10, 7, 116, 114, 97, 99, 101, 32, 49, 18, 23, 100, 101, 116, 97, 105, 108, 115, 32, 97, 98, 111, 117, 116, 32, 116, 104, 101, 32, 101, 114, 114, 111, 114] }"; + + assert!( + formatted.eq(expected), + "Any from filled DebugInfo differs from expected result" + ); + + let br_details = match DebugInfo::from_any(gen_any) { + Err(error) => panic!("Error generating DebugInfo from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + assert!( + formatted.eq(expected_filled), + "DebugInfo from Any differs from expected result" + ); + } +} diff --git a/tonic-types/src/richer_error/std_messages/mod.rs b/tonic-types/src/richer_error/std_messages/mod.rs index ee8ff2138..345f42b70 100644 --- a/tonic-types/src/richer_error/std_messages/mod.rs +++ b/tonic-types/src/richer_error/std_messages/mod.rs @@ -2,6 +2,10 @@ mod retry_info; pub use retry_info::RetryInfo; +mod debug_info; + +pub use debug_info::DebugInfo; + mod bad_request; pub use bad_request::{BadRequest, FieldViolation};