@@ -13,12 +13,14 @@ module Network.Wai.Middleware.RequestLogger
13 , destination
13 , destination
14 , OutputFormat (..)
14 , OutputFormat (..)
15 , OutputFormatter
15 , OutputFormatter
16+ , OutputFormatterWithDetails
16 , Destination (..)
17 , Destination (..)
17 , Callback
18 , Callback
18 , IPAddrSource (..)
19 , IPAddrSource (..)
21import System.IO (Handle, stdout)
22import System.IO (Handle, stdout)
23+import qualified Blaze.ByteString.Builder as B
22import qualified Data.ByteString as BS
24import qualified Data.ByteString as BS
23import Data.ByteString.Char8 (pack, unpack)
25import Data.ByteString.Char8 (pack, unpack)
24import Control.Monad.IO.Class (liftIO)
26import Control.Monad.IO.Class (liftIO)
@@ -27,7 +29,7 @@ import System.Log.FastLogger
27import Network.HTTP.Types as H
29import Network.HTTP.Types as H
28import Data.Maybe (fromMaybe)
30import Data.Maybe (fromMaybe)
29import Data.Monoid (mconcat, (<>))
31import Data.Monoid (mconcat, (<>))
30−import Data.Time (getCurrentTime, diffUTCTime)
32+import Data.Time (getCurrentTime, diffUTCTime, NominalDiffTime)
31import Network.Wai.Parse (sinkRequestBody, lbsBackEnd, fileName, Param, File
33import Network.Wai.Parse (sinkRequestBody, lbsBackEnd, fileName, Param, File
32 , getRequestBodyType)
34 , getRequestBodyType)
33import qualified Data.ByteString.Lazy as LBS
35import qualified Data.ByteString.Lazy as LBS
@@ -35,7 +37,7 @@ import qualified Data.ByteString.Char8 as S8
35import System.Console.ANSI
37import System.Console.ANSI
36import Data.IORef.Lifted
38import Data.IORef.Lifted
37import System.IO.Unsafe
39import System.IO.Unsafe
38−import Network.Wai.Internal (Response (ResponseRaw))
40+import Network.Wai.Internal (Response (..))
39import Data.Default.Class (Default (def))
41import Data.Default.Class (Default (def))
40import Network.Wai.Logger
42import Network.Wai.Logger
41import Network.Wai.Middleware.RequestLogger.Internal
43import Network.Wai.Middleware.RequestLogger.Internal
@@ -44,8 +46,11 @@ import Data.Text.Encoding (decodeUtf8')
44data OutputFormat = Apache IPAddrSource
46data OutputFormat = Apache IPAddrSource
45 | Detailed Bool -- ^ use colors?
47 | Detailed Bool -- ^ use colors?
46 | CustomOutputFormat OutputFormatter
48 | CustomOutputFormat OutputFormatter
49+ | CustomOutputFormatWithDetails OutputFormatterWithDetails
48type OutputFormatter = ZonedDate -> Request -> Status -> Maybe Integer -> LogStr
51type OutputFormatter = ZonedDate -> Request -> Status -> Maybe Integer -> LogStr
52+type OutputFormatterWithDetails =
53+ ZonedDate -> Request -> Status -> Maybe Integer -> NominalDiffTime -> [S8.ByteString] -> B.Builder -> LogStr
50data Destination = Handle Handle
55data Destination = Handle Handle
51 | Logger LoggerSet
56 | Logger LoggerSet
@@ -93,8 +98,11 @@ mkRequestLogger RequestLoggerSettings{..} = do
93 (\str -> callback str >> flusher)
98 (\str -> callback str >> flusher)
95 CustomOutputFormat formatter -> do
100 CustomOutputFormat formatter -> do
101+ getDate <- getDateGetter flusher
102+ return $ customMiddleware callback getDate formatter
103+ CustomOutputFormatWithDetails formatter -> do
96 getdate <- getDateGetter flusher
104 getdate <- getDateGetter flusher
97− return $ customMiddleware callback getdate formatter
105+ return $ customMiddlewareWithDetails callback getdate formatter
99apacheMiddleware :: ApacheLoggerActions -> Middleware
107apacheMiddleware :: ApacheLoggerActions -> Middleware
100apacheMiddleware ala app req sendResponse = app req $ \res -> do
108apacheMiddleware ala app req sendResponse = app req $ \res -> do
@@ -113,6 +121,22 @@ customMiddleware cb getdate formatter app req sendResponse = app req $ \res -> d
113 liftIO $ cb $ formatter date req (responseStatus res) Nothing
121 liftIO $ cb $ formatter date req (responseStatus res) Nothing
114 sendResponse res
122 sendResponse res
124+customMiddlewareWithDetails :: Callback -> IO ZonedDate -> OutputFormatterWithDetails -> Middleware
125+customMiddlewareWithDetails cb getdate formatter app req sendResponse = do
126+ (req', reqBody) <- getRequestBody req
127+ t0 <- getCurrentTime
128+ app req' $ \res -> do
129+ t1 <- getCurrentTime
130+ date <- liftIO getdate
131+ -- We use Nothing for the response size since we generally don't know it
132+ builderIO <- newIORef ""
133+ res' <- recordChunks builderIO res
134+ rspRcv <- sendResponse res'
136+ formatter date req' (responseStatus res') Nothing (t1 `diffUTCTime` t0) reqBody =<<
116-- | Production request logger middleware.
140-- | Production request logger middleware.
117{-# NOINLINE logStdout #-}
141{-# NOINLINE logStdout #-}
118logStdout :: Middleware
142logStdout :: Middleware
@@ -182,6 +206,40 @@ ansiStatusCode' c t = case S8.take 1 c of
182 "5" -> ansiColor' Magenta t
206 "5" -> ansiColor' Magenta t
183 _ -> ansiColor' Blue t
207 _ -> ansiColor' Blue t
209+recordChunks :: IORef B.Builder -> Response -> IO Response
210+recordChunks i (ResponseStream s h sb) =
211+ return . ResponseStream s h $ (\send flush -> sb (\b -> modifyIORef i (<> b) >> send b) flush)
212+recordChunks i (ResponseBuilder s h b) =
213+ modifyIORef i (<> b) >> (return $ ResponseBuilder s h b)
217+getRequestBody :: Request -> IO (Request, [S8.ByteString])
218+getRequestBody req = do
220+ bs <- requestBody req
222+ then return $ front []
223+ else loop $ front . (bs:)
225+ -- logging the body here consumes it, so fill it back up
226+ -- obviously not efficient, but this is the development logger
228+ -- Note: previously, we simply used CL.sourceList. However,
229+ -- that meant that you could read the request body in twice.
230+ -- While that in itself is not a problem, the issue is that,
231+ -- in production, you wouldn't be able to do this, and
232+ -- therefore some bugs wouldn't show up during testing. This
233+ -- implementation ensures that each chunk is only returned
235+ ichunks <- newIORef body
236+ let rbody = atomicModifyIORef ichunks $ \chunks ->
238+ [] -> ([], S8.empty)
240+ let req' = req { requestBody = rbody }
185detailedMiddleware' :: Callback
243detailedMiddleware' :: Callback
186 -> (Color -> BS.ByteString -> [BS.ByteString])
244 -> (Color -> BS.ByteString -> [BS.ByteString])
187 -> (BS.ByteString -> [BS.ByteString])
245 -> (BS.ByteString -> [BS.ByteString])
@@ -192,30 +250,7 @@ detailedMiddleware' cb ansiColor ansiMethod ansiStatusCode app req sendResponse
192 (req', body) <-
250 (req', body) <-
193 case mlen of
251 case mlen of
194 -- log the request body if it is small
252 -- log the request body if it is small
195− Just len | len <= 2048 -> do
253+ Just len | len <= 2048 -> getRequestBody req
197− bs <- requestBody req
199− then return $ front []
200− else loop $ front . (bs:)
202− -- logging the body here consumes it, so fill it back up
203− -- obviously not efficient, but this is the development logger
205− -- Note: previously, we simply used CL.sourceList. However,
206− -- that meant that you could read the request body in twice.
207− -- While that in itself is not a problem, the issue is that,
208− -- in production, you wouldn't be able to do this, and
209− -- therefore some bugs wouldn't show up during testing. This
210− -- implementation ensures that each chunk is only returned
212− ichunks <- newIORef body
213− let rbody = atomicModifyIORef ichunks $ \chunks ->
215− [] -> ([], S8.empty)
217− let req' = req { requestBody = rbody }
219 _ -> return (req, [])
254 _ -> return (req, [])
221 let reqbodylog _ = if null body then [""] else ansiColor White " Request Body: " <> body <> ["\n"]
256 let reqbodylog _ = if null body then [""] else ansiColor White " Request Body: " <> body <> ["\n"]