1
{-# LANGUAGE OverloadedStrings #-}
2
module Network.Wai.Middleware.RequestLogger
12
import System.IO (stdout, hFlush)
13
import qualified Data.ByteString as BS
14
import Data.ByteString.Char8 (pack, unpack)
15
import Control.Monad.IO.Class (liftIO)
16
import Network.Wai (Request(..), Middleware, responseStatus)
17
import System.Log.FastLogger
18
import Network.HTTP.Types as H
20
import Network.Wai.Parse (sinkRequestBody, lbsBackEnd, fileName, Param, File, getRequestBodyType)
21
import qualified Data.ByteString.Lazy as LBS
23
import qualified Data.Conduit as C
24
import qualified Data.Conduit.List as CL
26
import System.Console.ANSI
28
import System.IO.Unsafe
30
logHandle :: (BS.ByteString -> IO ()) -> Middleware
31
logHandle = logCallback
32
{-# DEPRECATED logHandle "Please use logCallback instead." #-}
33
logHandleDev :: (BS.ByteString -> IO ()) -> Middleware
34
logHandleDev = logCallbackDev
35
{-# DEPRECATED logHandleDev "Please use logCallbackDev instead." #-}
37
-- | Production request logger middleware.
38
-- Implemented on top of "logCallback", but prints to 'stdout'
39
logStdout :: Middleware
40
logStdout = logHandle $ \bs -> hPutLogStr stdout [LB bs]
42
-- | Development request logger middleware.
43
-- Implemented on top of "logCallbackDev", but prints to 'stdout'
45
-- Flushes 'stdout' on each request, which would be inefficient in production use.
46
-- Use "logStdout" in production.
47
logStdoutDev :: Middleware
48
logStdoutDev = logHandleDev $ \bs -> hPutLogStr stdout [LB bs] >> hFlush stdout
50
-- | Prints a message using the given callback function for each request.
51
-- Designed for fast production use at the expense of convenience.
52
-- In particular, no POST parameter information is currently given
54
-- This is lower-level - use "logStdout" unless you need this greater control
55
logCallback :: (BS.ByteString -> IO ()) -- ^ A function that logs the ByteString log message.
57
logCallback cb app req = do
58
liftIO $ cb $ BS.concat
65
, maybe "" toBS $ lookup "Accept" $ requestHeaders req
70
toBS :: H.Ascii -> BS.ByteString
73
-- no black or white which are expected to be existing terminal colors.
74
colors :: IORef [Color]
75
colors = unsafePerformIO $ newIORef $ [
84
rotateColors :: [Color] -> ([Color], Color)
85
rotateColors [] = error "Impossible! There must be colors!"
86
rotateColors (c:cs) = (cs ++ [c], c)
88
-- | Prints a message using the given callback function for each request.
89
-- This is not for serious production use- it is inefficient.
90
-- It immediately consumes a POST body and fills it back in and is otherwise inefficient
92
-- Note that it logs the request immediately when it is received.
93
-- This meanst that you can accurately see the interleaving of requests.
94
-- And if the app crashes you have still logged the request.
95
-- However, if you are simulating 10 simultaneous users you may find this confusing.
96
-- The request and response are connected by color on Unix and also by the request path.
98
-- This is lower-level - use "logStdoutDev" unless you need greater control.
103
-- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
105
-- Status: 200 OK. search
107
-- GET static/css/normalize.css
108
-- Accept: text/css,*/*;q=0.1
109
-- GET [("LXwioiBG","")]
111
-- Status: 304 Not Modified. static/css/normalize.css
112
logCallbackDev :: (BS.ByteString -> IO ()) -- ^ A function that logs the ByteString log message.
114
logCallbackDev cb app req = do
115
let mlen = lookup "content-length" (requestHeaders req) >>= readInt
118
-- log the request body if it is small
119
Just len | len <= 2048 -> do
120
body <- requestBody req C.$$ CL.consume
121
-- logging the body here consumes it, so fill it back up
122
-- obviously not efficient, but this is the development logger
123
let req' = req { requestBody = CL.sourceList body }
125
_ -> return (req, [])
127
postParams <- if any (requestMethod req ==) ["GET", "HEAD"]
129
else do postParams <- liftIO $ allPostParams body
130
return $ collectPostParams postParams
132
let getParams = map emptyGetParam $ queryString req
134
color <- liftIO $ atomicModifyIORef colors rotateColors
136
-- log the request immediately.
137
liftIO $ cb $ BS.concat $ ansiColor color (requestMethod req) ++
142
, maybe "" id $ lookup "Accept" $ requestHeaders req
143
, paramsToBS "GET " getParams
144
, paramsToBS "POST " postParams
150
-- log the status of the response
151
-- this is color coordinated with the request logging
152
-- also includes the request path to connect it to the request
153
liftIO $ cb $ BS.concat $ ansiColor color "Status: " ++ [
158
, rawPathInfo req -- if you need help matching the 2 logging statements
163
ansiColor color bs = [
164
pack $ setSGRCode [SetColor Foreground Vivid color]
166
, pack $ setSGRCode [Reset]
168
sCode = pack . show . statusCode . responseStatus
169
msg = statusMessage . responseStatus
171
paramsToBS prefix params =
172
if null params then ""
173
else BS.concat ["\n", prefix, pack (show params)]
176
case getRequestBodyType req of
177
Nothing -> return ([], [])
178
Just rbt -> C.runResourceT $ CL.sourceList body C.$$ sinkRequestBody lbsBackEnd rbt
180
emptyGetParam :: (BS.ByteString, Maybe BS.ByteString) -> (BS.ByteString, BS.ByteString)
181
emptyGetParam (k, Just v) = (k,v)
182
emptyGetParam (k, Nothing) = (k,"")
184
collectPostParams :: ([Param], [File LBS.ByteString]) -> [Param]
185
collectPostParams (postParams, files) = postParams ++
186
(map (\(k,v) -> (k, BS.append "FILE: " (fileName v))) files)
189
case reads $ unpack bs of
190
(i, _):_ -> Just (i :: Int)