Skip to content
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

log the error to stderr when an error is thrown #115

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions src/Aws/Lambda/Runtime.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# LANGUAGE RankNTypes #-}

module Aws.Lambda.Runtime
( runLambda,
Expand All @@ -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
Expand All @@ -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 =
Expand All @@ -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 =>
Expand Down Expand Up @@ -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
31 changes: 28 additions & 3 deletions src/Aws/Lambda/Runtime/Configuration.hs
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions src/Aws/Lambda/Runtime/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
module Aws.Lambda.Runtime.Error
( EnvironmentVariableNotSet (..),
Parsing (..),
HandlerNotFound (..),
Invocation (..),
ErrorType (..),
toReadableType
)
where

Expand Down Expand Up @@ -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"
39 changes: 29 additions & 10 deletions src/Aws/Lambda/Runtime/Publish.hs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}

-- | Publishing of results/errors back to the
-- AWS Lambda runtime API
module Aws.Lambda.Runtime.Publish
( result,
invocationError,
parsingError,
handlerNotFoundError,
runtimeInitError,
)
where
Expand All @@ -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 ()
Expand All @@ -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
Expand Down
12 changes: 5 additions & 7 deletions src/Aws/Lambda/Setup.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 =>
Expand All @@ -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 ->
Expand Down Expand Up @@ -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
Expand Down