From 3e963f23fcabe26313c22fe4b8ea538a75b3853c Mon Sep 17 00:00:00 2001 From: Steve Mao Date: Tue, 15 Mar 2022 02:12:28 +1100 Subject: [PATCH 1/3] HandlerNotFound should be published as Runtime.HandlerNotFound --- src/Aws/Lambda/Runtime.hs | 10 ++++++---- src/Aws/Lambda/Runtime/Error.hs | 11 +++++++++++ src/Aws/Lambda/Runtime/Publish.hs | 9 +++++++++ src/Aws/Lambda/Setup.hs | 5 ++--- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Aws/Lambda/Runtime.hs b/src/Aws/Lambda/Runtime.hs index 2ce9114..c0758d5 100644 --- a/src/Aws/Lambda/Runtime.hs +++ b/src/Aws/Lambda/Runtime.hs @@ -43,12 +43,14 @@ runLambda initializeCustomContext callback = do -- Purposefully shadowing to prevent using the initial "empty" context context <- Context.setEventData context event - ( ( ( invokeAndRun callback manager lambdaApi event context - `Checked.catch` \err -> Publish.parsingError err lambdaApi context manager + ( ( ( ( invokeAndRun callback manager lambdaApi event context + `Checked.catch` \err -> Publish.parsingError err lambdaApi context manager + ) + `Checked.catch` \err -> Publish.invocationError err lambdaApi context manager ) - `Checked.catch` \err -> Publish.invocationError err lambdaApi context manager + `Checked.catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager ) - `Checked.catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager + `Unchecked.catch` \(err :: Error.HandlerNotFound) -> Publish.handlerNotFoundError err lambdaApi context manager ) `Unchecked.catch` \err -> Publish.invocationError err lambdaApi context manager diff --git a/src/Aws/Lambda/Runtime/Error.hs b/src/Aws/Lambda/Runtime/Error.hs index 1ea1cde..4ce30ed 100644 --- a/src/Aws/Lambda/Runtime/Error.hs +++ b/src/Aws/Lambda/Runtime/Error.hs @@ -2,6 +2,7 @@ module Aws.Lambda.Runtime.Error ( EnvironmentVariableNotSet (..), Parsing (..), + HandlerNotFound (..), Invocation (..), ) where @@ -36,6 +37,16 @@ instance ToJSON Parsing where "errorMessage" .= ("Could not parse '" <> valueName <> "': " <> errorMessage) ] +newtype HandlerNotFound = HandlerNotFound Text + deriving (Show, Exception) + +instance ToJSON HandlerNotFound where + toJSON (HandlerNotFound handler) = + object + [ "errorType" .= ("Runtime.HandlerNotFound" :: Text), + "errorMessage" .= ("Could not find handler '" <> handler <> "'.") + ] + newtype Invocation = Invocation LBS.ByteString deriving (Show, Exception) diff --git a/src/Aws/Lambda/Runtime/Publish.hs b/src/Aws/Lambda/Runtime/Publish.hs index 5032231..ccab487 100644 --- a/src/Aws/Lambda/Runtime/Publish.hs +++ b/src/Aws/Lambda/Runtime/Publish.hs @@ -6,6 +6,7 @@ module Aws.Lambda.Runtime.Publish ( result, invocationError, parsingError, + handlerNotFoundError, runtimeInitError, ) where @@ -56,6 +57,14 @@ parsingError err lambdaApi context = (Endpoints.invocationError lambdaApi $ awsRequestId context) context +-- | Publishes a HandlerNotFound error back to AWS Lambda +handlerNotFoundError :: Error.HandlerNotFound -> Text -> Context context -> Http.Manager -> IO () +handlerNotFoundError err lambdaApi context = + publish + (encode err) + (Endpoints.invocationError lambdaApi $ awsRequestId context) + context + -- | Publishes a runtime initialization error back to AWS Lambda runtimeInitError :: ToJSON err => err -> Text -> Context context -> Http.Manager -> IO () runtimeInitError err lambdaApi = diff --git a/src/Aws/Lambda/Setup.hs b/src/Aws/Lambda/Setup.hs index 31555cd..c744774 100644 --- a/src/Aws/Lambda/Setup.hs +++ b/src/Aws/Lambda/Setup.hs @@ -64,6 +64,7 @@ import qualified Data.Text as Text import Data.Typeable (Typeable) import GHC.IO.Handle.FD (stderr) import GHC.IO.Handle.Text (hPutStr) +import qualified Aws.Lambda.Runtime.Error as Error type Handlers handlerType m context request response error = HM.HashMap HandlerName (Handler handlerType m context request response error) @@ -129,9 +130,7 @@ run dispatcherOptions mToIO handlers (LambdaOptions eventObject functionHandler case HM.lookup functionHandler asIOCallbacks of Just handlerToCall -> handlerToCall Nothing -> - throwM $ - userError $ - "Could not find handler '" <> (Text.unpack . unHandlerName $ functionHandler) <> "'." + throwM $ Error.HandlerNotFound (unHandlerName functionHandler) addStandaloneLambdaHandler :: HandlerName -> From fa98c631eb00be721c65d1697c5cd984b817b493 Mon Sep 17 00:00:00 2001 From: Steve Mao Date: Tue, 15 Mar 2022 21:51:31 +1100 Subject: [PATCH 2/3] log the error to stderr when an error is thrown --- src/Aws/Lambda/Runtime.hs | 29 ++++++++++----------- src/Aws/Lambda/Runtime/Configuration.hs | 31 +++++++++++++++++++--- src/Aws/Lambda/Runtime/Error.hs | 8 ++++++ src/Aws/Lambda/Runtime/Publish.hs | 34 ++++++++++++++++--------- src/Aws/Lambda/Setup.hs | 6 ++--- 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/Aws/Lambda/Runtime.hs b/src/Aws/Lambda/Runtime.hs index c0758d5..06e2ee5 100644 --- a/src/Aws/Lambda/Runtime.hs +++ b/src/Aws/Lambda/Runtime.hs @@ -2,6 +2,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} +{-# LANGUAGE RankNTypes #-} module Aws.Lambda.Runtime ( runLambda, @@ -27,11 +28,12 @@ import Data.IORef (newIORef) import Data.Text (Text, unpack) import qualified Network.HTTP.Client as Http import System.IO (hFlush, stderr, stdout) +import Aws.Lambda.Runtime.Configuration ( flushOutput, ErrorLogger ) -- | Runs the user @haskell_lambda@ executable and posts back the -- results. This is called from the layer's @main@ function. -runLambda :: forall context handlerType. IO context -> Runtime.RunCallback handlerType context -> IO () -runLambda initializeCustomContext callback = do +runLambda :: forall context handlerType. ErrorLogger -> IO context -> Runtime.RunCallback handlerType context -> IO () +runLambda logger initializeCustomContext callback = do manager <- Http.newManager httpManagerSettings customContext <- initializeCustomContext customContextRef <- newIORef customContext @@ -43,16 +45,16 @@ runLambda initializeCustomContext callback = do -- Purposefully shadowing to prevent using the initial "empty" context context <- Context.setEventData context event - ( ( ( ( invokeAndRun callback manager lambdaApi event context - `Checked.catch` \err -> Publish.parsingError err lambdaApi context manager + ( ( ( ( invokeAndRun logger callback manager lambdaApi event context + `Checked.catch` \err -> Publish.parsingError logger err lambdaApi context manager ) - `Checked.catch` \err -> Publish.invocationError err lambdaApi context manager + `Checked.catch` \err -> Publish.invocationError logger err lambdaApi context manager ) - `Checked.catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager + `Checked.catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError logger err lambdaApi context manager ) - `Unchecked.catch` \(err :: Error.HandlerNotFound) -> Publish.handlerNotFoundError err lambdaApi context manager + `Unchecked.catch` \(err :: Error.HandlerNotFound) -> Publish.handlerNotFoundError logger err lambdaApi context manager ) - `Unchecked.catch` \err -> Publish.invocationError err lambdaApi context manager + `Unchecked.catch` \err -> Publish.invocationError logger err lambdaApi context manager httpManagerSettings :: Http.ManagerSettings httpManagerSettings = @@ -64,17 +66,18 @@ httpManagerSettings = invokeAndRun :: Throws Error.Invocation => Throws Error.EnvironmentVariableNotSet => + ErrorLogger -> Runtime.RunCallback handlerType context -> Http.Manager -> Text -> ApiInfo.Event -> Context.Context context -> IO () -invokeAndRun callback manager lambdaApi event context = do +invokeAndRun logger callback manager lambdaApi event context = do result <- invokeWithCallback callback event context Publish.result result lambdaApi context manager - `catch` \err -> Publish.invocationError err lambdaApi context manager + `catch` \err -> Publish.invocationError logger err lambdaApi context manager invokeWithCallback :: Throws Error.Invocation => @@ -115,9 +118,3 @@ variableNotSet (Error.EnvironmentVariableNotSet env) = errorParsing :: Error.Parsing -> IO a errorParsing Error.Parsing {..} = error ("Failed parsing " <> unpack errorMessage <> ", got" <> unpack actualValue) - --- | Flush standard output ('stdout') and standard error output ('stderr') handlers -flushOutput :: IO () -flushOutput = do - hFlush stdout - hFlush stderr \ No newline at end of file diff --git a/src/Aws/Lambda/Runtime/Configuration.hs b/src/Aws/Lambda/Runtime/Configuration.hs index fb438d0..1b0ceba 100644 --- a/src/Aws/Lambda/Runtime/Configuration.hs +++ b/src/Aws/Lambda/Runtime/Configuration.hs @@ -1,16 +1,41 @@ +{-# LANGUAGE RankNTypes #-} module Aws.Lambda.Runtime.Configuration ( DispatcherOptions (..), defaultDispatcherOptions, + ErrorLogger, + flushOutput ) where import Aws.Lambda.Runtime.APIGateway.Types (ApiGatewayDispatcherOptions (..)) +import Data.Text (Text) +import Data.Text.IO (hPutStrLn) +import Aws.Lambda.Runtime.Context +import Aws.Lambda.Runtime.Error +import System.IO (stderr, hFlush, stdout) + +type ErrorLogger = forall context. Context context -> ErrorType -> Text -> IO () + +defaultErrorLogger :: ErrorLogger +defaultErrorLogger Context {awsRequestId=requestId} errorType message = do + hPutStrLn stderr $ requestId <> "\t" + <> "ERROR" <> "\t" + <> toReadableType errorType <> "\t" + <> message + flushOutput -- | Options that the dispatcher generator expects -newtype DispatcherOptions = DispatcherOptions - { apiGatewayDispatcherOptions :: ApiGatewayDispatcherOptions +data DispatcherOptions = DispatcherOptions + { apiGatewayDispatcherOptions :: ApiGatewayDispatcherOptions, + errorLogger :: ErrorLogger } defaultDispatcherOptions :: DispatcherOptions defaultDispatcherOptions = - DispatcherOptions (ApiGatewayDispatcherOptions True) + DispatcherOptions (ApiGatewayDispatcherOptions True) defaultErrorLogger + +-- | Flush standard output ('stdout') and standard error output ('stderr') handlers +flushOutput :: IO () +flushOutput = do + hFlush stdout + hFlush stderr diff --git a/src/Aws/Lambda/Runtime/Error.hs b/src/Aws/Lambda/Runtime/Error.hs index 4ce30ed..6645c3a 100644 --- a/src/Aws/Lambda/Runtime/Error.hs +++ b/src/Aws/Lambda/Runtime/Error.hs @@ -4,6 +4,8 @@ module Aws.Lambda.Runtime.Error Parsing (..), HandlerNotFound (..), Invocation (..), + ErrorType (..), + toReadableType ) where @@ -50,3 +52,9 @@ instance ToJSON HandlerNotFound where newtype Invocation = Invocation LBS.ByteString deriving (Show, Exception) + +data ErrorType = InvocationError | InitializationError + +toReadableType :: ErrorType -> Text +toReadableType InvocationError = "Invocation Error" +toReadableType InitializationError = "Initialization Error" diff --git a/src/Aws/Lambda/Runtime/Publish.hs b/src/Aws/Lambda/Runtime/Publish.hs index ccab487..181ac9e 100644 --- a/src/Aws/Lambda/Runtime/Publish.hs +++ b/src/Aws/Lambda/Runtime/Publish.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GADTs #-} +{-# LANGUAGE RankNTypes #-} -- | Publishing of results/errors back to the -- AWS Lambda runtime API @@ -22,6 +23,10 @@ import qualified Data.ByteString.Lazy as LBS import Data.Text (Text, unpack) import qualified Data.Text.Encoding as T import qualified Network.HTTP.Client as Http +import Aws.Lambda.Runtime.Configuration +import Aws.Lambda.Runtime.Error +import Data.Text.Encoding (decodeUtf8) +import Data.ByteString.Lazy (toStrict) -- | Publishes the result back to AWS Lambda result :: LambdaResult handlerType -> Text -> Context context -> Http.Manager -> IO () @@ -45,33 +50,38 @@ result lambdaResult lambdaApi context manager = do void $ Http.httpNoBody request manager -- | Publishes an invocation error back to AWS Lambda -invocationError :: Error.Invocation -> Text -> Context context -> Http.Manager -> IO () -invocationError (Error.Invocation err) lambdaApi context = - publish err (Endpoints.invocationError lambdaApi $ awsRequestId context) context +invocationError :: ErrorLogger -> Error.Invocation -> Text -> Context context -> Http.Manager -> IO () +invocationError logger (Error.Invocation err) lambdaApi context = + publish logger InvocationError err (Endpoints.invocationError lambdaApi $ awsRequestId context) context -- | Publishes a parsing error back to AWS Lambda -parsingError :: Error.Parsing -> Text -> Context context -> Http.Manager -> IO () -parsingError err lambdaApi context = +parsingError :: ErrorLogger -> Error.Parsing -> Text -> Context context -> Http.Manager -> IO () +parsingError logger err lambdaApi context = publish + logger + InvocationError (encode err) (Endpoints.invocationError lambdaApi $ awsRequestId context) context -- | Publishes a HandlerNotFound error back to AWS Lambda -handlerNotFoundError :: Error.HandlerNotFound -> Text -> Context context -> Http.Manager -> IO () -handlerNotFoundError err lambdaApi context = +handlerNotFoundError :: ErrorLogger -> Error.HandlerNotFound -> Text -> Context context -> Http.Manager -> IO () +handlerNotFoundError logger err lambdaApi context = publish + logger + InvocationError (encode err) (Endpoints.invocationError lambdaApi $ awsRequestId context) context -- | Publishes a runtime initialization error back to AWS Lambda -runtimeInitError :: ToJSON err => err -> Text -> Context context -> Http.Manager -> IO () -runtimeInitError err lambdaApi = - publish (encode err) (Endpoints.runtimeInitError lambdaApi) +runtimeInitError :: ToJSON err => ErrorLogger -> err -> Text -> Context context -> Http.Manager -> IO () +runtimeInitError logger err lambdaApi = + publish logger Error.InitializationError (encode err) (Endpoints.runtimeInitError lambdaApi) -publish :: LBS.ByteString -> Endpoints.Endpoint -> Context context -> Http.Manager -> IO () -publish err (Endpoints.Endpoint endpoint) _context manager = do +publish :: ErrorLogger -> Error.ErrorType -> LBS.ByteString -> Endpoints.Endpoint -> Context context -> Http.Manager -> IO () +publish logger errorType err (Endpoints.Endpoint endpoint) context manager = do + logger context errorType $ decodeUtf8 $ toStrict err rawRequest <- Http.parseRequest . unpack $ endpoint let requestBody = Http.RequestBodyLBS err diff --git a/src/Aws/Lambda/Setup.hs b/src/Aws/Lambda/Setup.hs index c744774..2f77241 100644 --- a/src/Aws/Lambda/Setup.hs +++ b/src/Aws/Lambda/Setup.hs @@ -43,7 +43,7 @@ import Aws.Lambda.Runtime.Common RawEventObject, ) import Aws.Lambda.Runtime.Configuration - ( DispatcherOptions (apiGatewayDispatcherOptions), + ( DispatcherOptions (apiGatewayDispatcherOptions, DispatcherOptions, errorLogger), ) import Aws.Lambda.Runtime.Context (Context) import Aws.Lambda.Runtime.StandaloneLambda.Types @@ -114,9 +114,9 @@ runLambdaHaskellRuntime :: (forall a. m a -> IO a) -> HandlersM handlerType m context request response error () -> IO () -runLambdaHaskellRuntime options initializeContext mToIO initHandlers = do +runLambdaHaskellRuntime options@DispatcherOptions{errorLogger=logger} initializeContext mToIO initHandlers = do handlers <- fmap snd . flip runStateT HM.empty . runHandlersM $ initHandlers - runLambda initializeContext (run options mToIO handlers) + runLambda logger initializeContext (run options mToIO handlers) run :: RuntimeContext handlerType m context request response error => From c89986a887a9a5f49187776a6daac9163f19406c Mon Sep 17 00:00:00 2001 From: Steve Mao Date: Wed, 16 Mar 2022 14:30:08 +1100 Subject: [PATCH 3/3] remove duplicated log --- src/Aws/Lambda/Setup.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Aws/Lambda/Setup.hs b/src/Aws/Lambda/Setup.hs index 2f77241..a4c4cea 100644 --- a/src/Aws/Lambda/Setup.hs +++ b/src/Aws/Lambda/Setup.hs @@ -192,7 +192,6 @@ handlerToCallback dispatcherOptions rawEventObject context handlerToCall = Left err -> albErr 400 . toALBResponseBody . Text.pack . show $ err handleError (exception :: SomeException) = do - liftIO $ hPutStr stderr . show $ exception case handlerToCall of StandaloneLambdaHandler _ -> return . Left . StandaloneLambdaError . toStandaloneLambdaResponse . Text.pack . show $ exception