yesod-katip-0.1.0.0: src/Yesod/Katip/Orphans.hs
{-# OPTIONS_GHC -Wno-orphans #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-|
Module : Yesod.Katip.Orphans
Description : Orphan instances for logging HTTP structures in Katip contexts
Copyright : (c) Isaac van Bakel, 2020
License : BSD3
Maintainer : ivb@vanbakel.io
Stability : experimental
Portability : POSIX
If you configure your Yesod site to log HTTP structures to Yesod, you will need
instances of 'ToObject' and 'LogItem' for the structures you want to log.
By default, @KatipContextSite@ will add the WAI request to the context - which
is why its @Yesod@ instance requires those classes on the 'Request' type. This
module contains simple implementations for those classes on 'Request', to help
set up a quick and easy use of @KatipContextSite@.
-}
module Yesod.Katip.Orphans () where
import Data.Aeson
import Katip (LogItem (..), PayloadSelection (..), ToObject (..), Verbosity (..))
import Network.Wai
#if MIN_VERSION_wai_extra(3, 1, 4)
import Network.Wai.Middleware.RequestLogger.JSON (requestToJSON)
#else
-- Because the exposure of this particular API in Wai is relatively recent
-- (December 2020), this polyfills the implementation
import Data.CaseInsensitive (original)
import Data.IP (fromHostAddress, fromIPv4)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Network.HTTP.Types
import Network.Socket (SockAddr (..))
requestToJSON :: Request -> [String] -> Maybe () -> Value
requestToJSON req body _duration
= object
[ "method" .= decodeUtf8With lenientDecode (requestMethod req)
, "path" .= decodeUtf8With lenientDecode (rawPathInfo req)
, "queryString" .= map queryItemToJSON (queryString req)
, "size" .= requestBodyLengthToJSON (requestBodyLength req)
, "body" .= concat body
, "remoteHost" .= sockToJSON (remoteHost req)
, "httpVersion" .= httpVersionToJSON (httpVersion req)
, "headers" .= requestHeadersToJSON (requestHeaders req)
]
where
requestHeadersToJSON = toJSON . map hToJ where
-- Redact cookies
hToJ ("Cookie", _) = toJSON ("Cookie" :: T.Text, "-RDCT-" :: T.Text)
hToJ hd = headerToJSON hd
queryItemToJSON (name, mValue) = toJSON (decodeUtf8With lenientDecode name, fmap (decodeUtf8With lenientDecode) mValue)
requestBodyLengthToJSON ChunkedBody = String "Unknown"
requestBodyLengthToJSON (KnownLength l) = toJSON l
sockToJSON (SockAddrInet pn ha) =
object
[ "port" .= portToJSON pn
, "hostAddress" .= word32ToHostAddress ha
]
sockToJSON (SockAddrInet6 pn _ ha _) =
object
[ "port" .= portToJSON pn
, "hostAddress" .= ha
]
sockToJSON (SockAddrUnix sock) =
object [ "unix" .= sock ]
#if !MIN_VERSION_network(3, 0, 0)
sockToJSON (SockAddrCan i) =
object [ "can" .= i ]
#endif
headerToJSON (headerName, header) = toJSON (decodeUtf8With lenientDecode . original $ headerName, decodeUtf8With lenientDecode header)
word32ToHostAddress = T.intercalate "." . map (T.pack . show) . fromIPv4 . fromHostAddress
portToJSON = toJSON . toInteger
httpVersionToJSON (HttpVersion major minor) = String $ T.pack (show major) <> "." <> T.pack (show minor)
#endif
instance ToObject Request where
toObject req
= case requestToJSON req ["<omitted by default>"] Nothing of
Object obj -> obj
_ -> error "`requestToJSON` produced a JSON representation for `Request` that wasn't an object!"
instance LogItem Request where
payloadKeys verbosity _req = case verbosity of
V0 -> SomeKeys ["method", "path", "queryString", "remotehost"]
V1 -> SomeKeys ["size", "body"]
V2 -> SomeKeys ["headers", "httpVersion"]
V3 -> AllKeys