diff --git a/src/Aws/Lambda/Runtime.hs b/src/Aws/Lambda/Runtime.hs index 2ce9114..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,14 +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 logger err lambdaApi context manager ) - `Checked.catch` \err -> Publish.invocationError err lambdaApi context manager + `Checked.catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError logger err lambdaApi context manager ) - `Checked.catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError 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 = @@ -62,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 => @@ -113,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 1ea1cde..6645c3a 100644 --- a/src/Aws/Lambda/Runtime/Error.hs +++ b/src/Aws/Lambda/Runtime/Error.hs @@ -2,7 +2,10 @@ module Aws.Lambda.Runtime.Error ( EnvironmentVariableNotSet (..), Parsing (..), + HandlerNotFound (..), Invocation (..), + ErrorType (..), + toReadableType ) where @@ -36,6 +39,22 @@ 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) + +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 5032231..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 @@ -6,6 +7,7 @@ module Aws.Lambda.Runtime.Publish ( result, invocationError, parsingError, + handlerNotFoundError, runtimeInitError, ) where @@ -21,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 () @@ -44,25 +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 :: 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 31555cd..a4c4cea 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 @@ -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) @@ -113,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 => @@ -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 -> @@ -193,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