Skip to content

Commit

Permalink
Add logExceptionsMiddleware
Browse files Browse the repository at this point in the history
We currently log all handler exceptions through the `errorHandler`
member of the `Yesod` type-class. This is not ideal because it only has
access to an `InternalError Text` value, in which any exception has
already been turned into its string representation. This means we get an
ugly message like `"AnnotatedException {...}"` and aren't able to
actually construct a structured log.

We should stop doing that, and recover that logging by adding a Yesod
middleware that catches, logs, and re-throws all exceptions instead.

Then it can be handled as an actual (annotated) exception, and produce a
nice, structured log message (i.e. with `error.stack`). That's what this
middleware does. It's also what necessitated adding `withException`.
  • Loading branch information
pbrisbin committed Jan 31, 2024
1 parent 1e86fc8 commit 3144114
Showing 1 changed file with 14 additions and 0 deletions.
14 changes: 14 additions & 0 deletions library/Freckle/App/Yesod.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
module Freckle.App.Yesod
( respondQueryCanceled
, respondQueryCanceledHeaders
, logExceptionsMiddleware
) where

import Freckle.App.Prelude
Expand All @@ -11,12 +12,15 @@ import Database.PostgreSQL.Simple (SqlError (..))
import Freckle.App.Exception
( AnnotatedException (..)
, annotatedExceptionMessageFrom
, fromException
, withException
)
import Freckle.App.Stats (HasStatsClient)
import qualified Freckle.App.Stats as Stats
import Network.HTTP.Types (ResponseHeaders, status503)
import qualified Network.Wai as W
import Yesod.Core.Handler (HandlerFor, sendWaiResponse)
import Yesod.Core.Types (HandlerContents)

-- | Catch 'SqlError' when queries are canceled due to timeout and respond 503
--
Expand All @@ -37,6 +41,16 @@ respondQueryCanceledHeaders headers handler =
Stats.increment "query_canceled"
sendWaiResponse $ W.responseLBS status503 headers "Query canceled"

logExceptionsMiddleware :: (MonadUnliftIO m, MonadLogger m) => m a -> m a
logExceptionsMiddleware f =
f `withException` \ex ->
unless (isHandlerContents ex) $
logErrorNS "yesod" $
annotatedExceptionMessageFrom (const "Handler exception") ex

isHandlerContents :: AnnotatedException SomeException -> Bool
isHandlerContents = isJust . fromException @HandlerContents . exception

queryCanceled
:: AnnotatedException SqlError -> Maybe (AnnotatedException SqlError)
queryCanceled ex = ex <$ guard (sqlState (exception ex) == "57014")

0 comments on commit 3144114

Please sign in to comment.