{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
-- |
-- Description : Instrument wai with eventuo11y
-- Copyright : Copyright 2022 Shea Levy.
-- License : Apache-2.0
-- Maintainer : shea@shealevy.com
module Observe.Event.Wai
( -- * Application
application,
-- ** Instrumentation
ServeRequest (..),
renderServeRequest,
RequestField (..),
renderRequestField,
-- * setOnException
onExceptionCallback,
-- ** Instrumentation
OnException (..),
renderOnException,
OnExceptionField (..),
renderOnExceptionField,
-- * Miscellaneous instrumentation
renderRequest,
)
where
import Control.Exception
import Data.Aeson
import Data.CaseInsensitive
import Data.Text.Encoding
import Network.HTTP.Types.Status
import Network.HTTP.Types.Version
import Network.Socket
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Internal
import Observe.Event
import Observe.Event.Render.JSON
-- | Run an 'Application' with generic 'Request'/'Response' instrumentation.
application ::
EventBackend IO r ServeRequest ->
-- | The application, called with a reference to the parent event.
(r -> Application) ->
Application
application backend app req respond = withEvent backend ServeRequest \ev -> do
addField ev $ ReqField req
app (reference ev) req \res -> do
addField ev $ ResField res
respond res
-- | Event selector for 'application'.
data ServeRequest f where
ServeRequest :: ServeRequest RequestField
-- | Render a 'ServeRequest', and any 'Event's selected by it, to JSON
renderServeRequest :: RenderSelectorJSON ServeRequest
renderServeRequest ServeRequest = ("serve-request", renderRequestField)
-- | A field for v'ServeRequest' 'Event's.
data RequestField
= ReqField Request
| ResField Response
-- | Render a 'RequestField' to JSON
renderRequestField :: RenderFieldJSON RequestField
renderRequestField (ReqField req) =
( "request",
renderRequest req
)
renderRequestField (ResField res) = ("response-status" .= (statusCode $ responseStatus res))
-- | A 'Network.Wai.Handler.Warp.setOnException' callback which creates an 'Event' rendering
-- 'Exception's.
--
-- Ideally this would have a way to get a parent 'Event' from 'application'. Would be nice to
-- use 'vault', but there doesn't seem to be a way to get at the 'Request' that Warp will pass
-- here.
onExceptionCallback :: EventBackend IO r OnException -> Maybe Request -> SomeException -> IO ()
onExceptionCallback backend req e =
if defaultShouldDisplayException e
then withEvent backend OnException \ev -> addField ev $ OnExceptionField req e
else pure ()
-- | Selector for 'Observe.Event.Wai.onException'
data OnException f where
OnException :: OnException OnExceptionField
-- | Render an 'OnException', and its selected-for 'Event's, as JSON, with a provided base structured exception type.
renderOnException :: (Exception stex) => RenderExJSON stex -> RenderSelectorJSON OnException
renderOnException renderEx OnException = ("on-exception", renderOnExceptionField renderEx)
-- | A field for a v'OnException' 'Event'.
data OnExceptionField = OnExceptionField (Maybe Request) SomeException
-- | Render an 'OnExceptionField' as JSON, with a provided base structured exception type.
renderOnExceptionField :: (Exception stex) => RenderExJSON stex -> RenderFieldJSON OnExceptionField
renderOnExceptionField renderEx (OnExceptionField mreq e) =
( "uncaught-exception",
Object
( maybe mempty (("request" .=) . renderRequest) mreq
<> maybe ("unstructured-exception" .= show e) (("structured-exception" .=) . renderEx) (fromException e)
)
)
-- | Render a 'Request' to JSON.
renderRequest :: Request -> Value
renderRequest (Request {..}) =
Object
( "remote-addr" .= Object case remoteHost of
SockAddrInet port addr ->
( "port" .= toInteger port
<> "addr" .= hostAddressToTuple addr
)
SockAddrInet6 port flow addr scope ->
( "port" .= toInteger port
<> "flow" .= flow
<> "addr" .= hostAddress6ToTuple addr
<> "scope" .= scope
)
SockAddrUnix path -> "path" .= path
<> "method" .= decodeUtf8 requestMethod
<> "http-version" .= Object ("major" .= httpMajor httpVersion <> "minor" .= httpMinor httpVersion)
<> "path" .= pathInfo
<> "query"
.= fmap
( \(k, mv) ->
Object
( "param" .= decodeUtf8 k <> case mv of
Just v -> "value" .= decodeUtf8 v
Nothing -> mempty
)
)
queryString
<> ( case requestBodyLength of
ChunkedBody -> mempty
KnownLength l -> "length" .= l
)
<> ( if null requestHeaders
then mempty
else "headers" .= fmap (\(nm, val) -> Object ("name" .= decodeUtf8 (original nm) <> (if nm == "Authorization" then mempty else "val" .= decodeUtf8 val))) requestHeaders
)
)