-
-
Notifications
You must be signed in to change notification settings - Fork 440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to make Logger extension to utilize Debug implementation for my errors instead of Display? #671
Comments
I looked a bit inside So even if I am to implement my own custom logger, it seems like there is no way to use debug output of my errors for logging. Help me :) |
So far the only way to achieve the behavior I want is to extract resolvers into their own functions and to do logging manually. #[derive(thiserror::Error)]
pub enum Failure {
// errors
}
impl Debug for Failure {
// implementation
}
#[Object]
impl UserQuery {
pub async fn viewer(&self, ctx: &async_graphql::Context<'_>) -> Result<User, Failure> {
viewer(ctx).await.map_err(|e| {
tracing::error!("{:?}", e);
e
})
}
}
async fn viewer(ctx: &async_graphql::Context<'_>) -> Result<User, Failure> {
// some logic here
} However this means that I have to add |
Well, in fact when you use the async-graphql/derive/src/object.rs Line 198 in 878a1d4
I though a little about it and this is what could be done to allow more flexibility to error management (but without doing it in an extension): Right now, the
Which would expose the main behavior for an Error and allow you to implement it as you like, so you'll be able to create a I'll be able to look into it this week. EDIT: no so easy to implement, an issue little linked to this one #652. |
Hello @Miaxos and thank you for the detailed response! I think I might have a slightly different idea. Let me outline it a bit later today/tomorrow so we can compare them. I am pretty new to Rust, but maybe it's my opportunity to dig a bit deeper into |
Your proposalThe downside I see in your proposal is boilerplate. Let's imagine that each my resolver has it's own specific error type pub enum ViewerResolverFailure {}
impl ResoverError for ViewerResolverFailure {}
pub enum SomeOtherResolverFailure {}
impl ResoverError for SomeOtherResolverFailure {} Which in a way will be almost exactly (but with traits) what I am currently doing with functions #[Object]
impl UserQuery {
pub async fn viewer(&self, ctx: &async_graphql::Context<'_>) -> Result<User, ViewerResolverFailure> {
viewer(ctx).await.map_err(|e| {
tracing::error!("{:?}", e);
e
})
}
pub async fn some_other(&self, ctx: &async_graphql::Context<'_>) -> Result<User, SomeOtherResolverFailure> {
some_other(ctx).await.map_err(|e| {
tracing::error!("{:?}", e);
e
})
}
} And what I actually want is to get rid of this repetition + make sure that you can't forget to log errors correctly while writing new code. My proposalMy suggestion is to focus around saving the initial error so one can use it later in the pipeline. For example I would be able to easily achieve what I want by writing custom What if instead of converting errors from To do this I would change pub struct Error {
// not sure about the exact syntax here, not so proficient in Rust yet
pub source: std::error::Error,
#[serde(skip_serializing_if = "error_extensions_is_empty")]
pub extensions: Option<ErrorExtensionValues>,
}
impl Error {
/// Create an error from the given error message.
pub fn new(message: impl Into<String>) -> Self {
Self {
source: Err(message.into()),
extensions: None,
}
}
/// Convert the error to a server error.
#[must_use]
pub fn into_server_error(self, pos: Pos) -> ServerError {
ServerError {
message: self.source.to_string(),
locations: vec![pos],
path: Vec::new(),
extensions: self.extensions,
}
}
}
impl From<std::error::Error> for Error {
fn from(e: T) -> Self {
Self {
source: e,
extensions: None,
}
}
} And then later in the logger I could use |
@sunli829 do you have opinion on the matter? What I am proposing is looking somewhat similar to how actix does it. But I am a Rust newbie, so can't really grasp all the tricky detail here. And probably contributing this change goes beyond my skillset (unless you provide some guidance). |
This problem is not as easy to solve as it seems, and I need to think about it. 😁 |
I believe I have solved this problem, here are some tests. async-graphql/tests/error_ext.rs Line 81 in e898998
Hope to get your feedback.😁 @gyzerok |
@sunli829 do I understand correctly that your solution is to pass debug message alongside regular message? It will probably solve my problem, that's true. Otherwise I believe I don't have much experience in Rust to review. Personally I would try to go for a solution which delays converting errors to Now I started wondering if configuring schema with my own type of error would be a possible solution. Imagine something like the following. Bare with me, I do not know enough Rust to write actual code, so it's more like a pseudocode 😅 impl From<T: Display> for ServerError {
fn from(e: T) -> ServerError {
format!("{}", e)
}
}
pub trait MyCustomError: fmt::Debug + fmt::Display {}
// Not sure how to write it here.
// The idea is somehow force all the resolvers to resolve to MyCustomError.
pub type MySchema = Schema<Query<MyCustomError>, Mutation<MyCustomError>, Subscription<MyCustomError>>; And then make it so that the extension is also parametrized with the error type. So in the end I can get my own errors inside extensions. This way I can define multiple arbitrary behaviors on my own trait and then do whatever I want. With your solution I would be able only to get the debug string. Which does solve my problem while being not generic to support any other use cases. Like what if I want to log different errors from different resolvers with various rates (not 100%). Some errors are more important than others, and those are 100% logged. But some are very frequent and would quickly overwhelm log database. And it's not such an imaginary case. In my company we are working with a lot of traffic, so we have to deal with this :) With my suggestion you could do something like this: pub trait MyCustomError: fmt::Debug + fmt::Display {
pub fn log_rate(&self) -> u8 { 100 };
}
struct MyActualError {
AuthError,
ValidationError
}
impl MyCustomError for MyActualError {
pub fn log_rate(&self) -> u8 {
match self {
MyActualError::AuthError => 100, // very important
MyActualError::ValidationError => 10, // not that important
}
}
}
// now somewhere in my custom logger extension implementation
if random(0, 100) <= err.log_rate() { println("{:?}" err) } |
This is a breaking change, so I cannot use this solution.
You are right, I thought of a better way, wait for my news. 🙂 |
I'm done, now the only trouble is that you need to manually convert your error type to let s = String::from_utf8(bytes).map_err(Failure)?; Get concrete error from server_err.concrete_error::<FromUtf8Error>() Here is the test: async-graphql/tests/error_ext.rs Line 80 in d62aca8
Because Rust does not support generic specialization, this |
Is there any way to provide "default" behavior, so it won't be a breaking change? Like you can define default type parameters. You do it when you define your own
You second iteration does address broader use-cases and is job well done! I understand that I have great time using your library so far and believe others feel the same. So in my opinion it's worth making extra effort to maintain that great feeling in terms of APIs. |
I improved again, and this time I think you should be satisfied. 🙂 @gyzerok async-graphql/tests/error_ext.rs Line 183 in 490cfec
|
Yeah, if I understand everything correctly it looks great now! Thank you a lot for all the iterations and being so responsive 👏 May I suggest some subjective naming alterations?
Anyway I would be happy to try this out to see how it works in my app. Would you mind updating actix-web 4 branch after releasing this, so I can try it out? |
Thank you for always helping to improve this feature, naming is very difficult for me. 🙂 |
Released in |
Thank you, gonna try it now 😄 And I actually found a case why the order of fields in the response do matter. The version before that fix breaks my tests. So your update is right on time!
Currently I am at my early stages of learning Rust. And most of the stuff I am doing is based on Zero to Production book. Author is using Perhaps once I feel confident with all the things I have now on my place I can give a try to |
Ok, I've tried the code and it looks like I misunderstood how it was supposed to work. My apologies, Rust is still new to me. Here is an example code from my project: #[derive(Default)]
pub struct UserQuery;
#[Object]
impl UserQuery {
pub async fn viewer(
&self,
ctx: &async_graphql::Context<'_>,
) -> Result<UserObject, ResolverError> {
Ok(viewer(ctx).await?)
}
}
async fn viewer(ctx: &async_graphql::Context<'_>) -> Result<UserObject, Problem> {
// some code to get viewer from context
let user = UserRepo::get_user_by_id(pool, &viewer.id)
.await
.context("failed to get user from the repo")?
.ok_or_else(|| Problem::UnexpectedError(anyhow!("user not found")))?;
Ok(UserObject { user })
} The following problems appeared:
Nonetheless I think it is better already, since I don't have to repeat However at least for me as a Rust newbee it wasn't really clear why the things are not working. Also now it looks like we have regular error (which is part of libraries Given all this I am not sure what to suggest. Personally I would really love this feature to work great for everybody. So I would rather struggle without this feature myself then do disservice to the library. Maybe you have some ideas? |
It seems also that |
… callback to use any type that implements `Into<Error>`. #671
I changed the signature of the |
At the moment I think this |
I just think |
For referencee, I proposed 'ResolverError' first thinking of a previous implementation we did at work: Errors are implemented by implementing a trait which will tell:
The ResolverError was the error which returned None for the upper optinal parent. |
The current implementation is more complicated, I decided to refactor in the |
Since async-graphql/tests/error_ext.rs Line 80 in d33c9ce
It looks pretty good now! It is still a breaking change, so I support it in |
I can't design a more convenient API without generic specialization, so I can only keep |
I will definitely be interested to contribute to the package in some way. Maybe improving docs is a possible option. Currently I am working on a project and trying to collect my thoughts around the library APIs and the ways things are not working that well for me. Will try to wrap all the experiences up and share, once I am ready. Hopefully we can take things forward from that point :) |
Haha, I'm also looking forward to the specialization feature 😁! |
I added |
Hello @sunli829! I am a bit confused with |
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days. |
This issue was closed because it has been stalled for 5 days with no activity. |
Hello!
Recently I've started using
async-graphql
and it feels really great so far. Thank you for working on it!When I use logger extension in my logs I see strings which come from
Display
trait implementation for my errors. From my perspectiveDisplay
represents what consumers/users should see andDebug
represents what developers should see when they look at the error.There are cases where I want to hide some details from users/consumers and just output "unexpected error", but I would like to have full details in the logs for debugging purposes.
How can I achieve that?
The text was updated successfully, but these errors were encountered: