async-graphql: How to make Logger extension to utilize Debug implementation for my errors instead of Display?

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 perspective Display represents what consumers/users should see and Debug 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?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 32 (3 by maintainers)

Commits related to this issue

Most upvoted comments

I can’t design a more convenient API without generic specialization, so I can only keep ResolverError. 😒

Haha, I’m also looking forward to the specialization feature 😁! I will try to take some time to look into what you did tonight, I have also some ideas to write for the v3!

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 😃

Since Error no longer implements Display (I just found out), there will be no more type conflicts, so I deleted ResolverError. @Miaxos @gyzerok

https://github.com/async-graphql/async-graphql/blob/d33c9ce88bd7bc2f69332f9b415ffd139c84189d/tests/error_ext.rs#L80

It looks pretty good now!


It is still a breaking change, so I support it in v3.0.

This problem is not as easy to solve as it seems, and I need to think about it. 😁

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 async-graphql and make a useful contribution 🤷

I added Error::new_with_source method in v3, so the ResolverError type is no longer needed.

I can’t design a more convenient API without generic specialization, so I can only keep ResolverError. 😒

I just think ResolverError is not a good name. 🙂

It seems also that query function which helps to create cursor connections is forced to return async_graphql::Result. So I am not sure how to use it with my custom error types.

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!

BTW: Maybe you can try poem.

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 actix-web there so I am using it as well. However I’ve decided to diverge a bit and use GraphQL instead of writing REST. While working with your library simple simple and fun, I believe that enough of additional learning complexity for me now.

Perhaps once I feel confident with all the things I have now on my place I can give a try to poem. Thank you for suggestion!

I’m done, now the only trouble is that you need to manually convert your error type to Failure, like the following:

let s = String::from_utf8(bytes).map_err(Failure)?;

Get concrete error from ServerError:

server_err.concrete_error::<FromUtf8Error>()

Here is the test:

https://github.com/async-graphql/async-graphql/blob/d62aca805269b40f2093042d7791188d24fe3074/tests/error_ext.rs#L80

Because Rust does not support generic specialization, this Failure type is required.

Your proposal

The downside I see in your proposal is boilerplate. Let’s imagine that each my resolver has it’s own specific error type enum Failure {}. Then for each such type I need to implement ResolverError trait.

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 proposal

My 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 Logger implementation if only I could get raw error.

What if instead of converting errors from async_graphql::Error to async_graphql::ServerError before passing response to schema extension we would do it only after? This way developers would be able to write some custom logic for operating on errors before they get converted and sent to the client.

To do this I would change async_graphql::Error like so:

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 source to do whatever I like.