11{-# LANGUAGE BangPatterns #-}
22{-# LANGUAGE OverloadedStrings #-}
3+ {-# LANGUAGE RecordWildCards #-}
34
45module Web.Template.Log
56 ( bcdlog
67 , bcdlog400
8+ , debugLog
9+ , debugLogHandler
10+
11+ , logMiddlewareCustom
12+ , AccessLogRecord (.. )
13+ , formatTimeIso
714
815 , userIdVaultKey
916 , tokenVaultKey
1017 , pTokenVaultKey
1118 ) where
1219
20+ import Control.Monad (forM_ , when )
1321import Crypto.JWT (ClaimsSet )
1422import Data.Aeson (fromEncoding , pairs , (.=) )
1523import Data.ByteString.Builder (hPutBuilder , toLazyByteString )
1624import Data.IORef (IORef , newIORef , readIORef )
1725import Data.Text as T (Text , pack , unwords )
1826import Data.Text.Encoding (decodeUtf8 )
1927import qualified Data.Text.Encoding.Error as TE
28+ import qualified Data.Text.IO as TIO
29+ import qualified Data.Text.Lazy as TL
2030import qualified Data.Text.Lazy.Encoding as TLE (decodeUtf8With )
31+ import qualified Data.Text.Lazy.IO as TLIO
2132import Data.Time (ZonedTime , defaultTimeLocale , formatTime ,
2233 nominalDiffTimeToSeconds , utcToLocalZonedTime )
2334import Data.Time.Clock.POSIX (POSIXTime , getPOSIXTime , posixSecondsToUTCTime )
2435import Data.Vault.Lazy (Key , insert , newKey )
36+ import GHC.Generics (Generic )
2537import Network.HTTP.Types.Status (Status (.. ))
2638import Network.Wai (Middleware , rawPathInfo , requestMethod , responseStatus ,
2739 vault )
@@ -42,14 +54,33 @@ pTokenVaultKey :: Key (IORef (Maybe ClaimsSet))
4254pTokenVaultKey = unsafePerformIO newKey
4355{-# NOINLINE pTokenVaultKey #-}
4456
57+ data AccessLogRecord
58+ = AccessLogRecord
59+ { alStart :: ! POSIXTime
60+ , alFinishApp :: ! POSIXTime
61+ , alFinishNetwork :: ! POSIXTime
62+ , alMsg :: ! Text
63+ , alStatus :: ! Int
64+ , alURL :: ! Text
65+ , alUserId :: ! (Maybe Text )
66+ , alResponseBody :: Maybe TL. Text
67+ }
68+ deriving (Show , Generic )
69+
4570bcdlog :: Middleware
4671bcdlog = logMiddleware False
4772
4873bcdlog400 :: Middleware
4974bcdlog400 = logMiddleware True
5075
76+ debugLog :: Middleware
77+ debugLog = logMiddlewareCustom True $ Just debugLogHandler
78+
5179logMiddleware :: Bool -> Middleware
52- logMiddleware log400 app request respond = do
80+ logMiddleware log400 = logMiddlewareCustom log400 Nothing
81+
82+ logMiddlewareCustom :: Bool -> Maybe (AccessLogRecord -> IO () ) -> Middleware
83+ logMiddlewareCustom log400 mLogAction app request respond = do
5384 let
5485 url = decodeUtf8 $ rawPathInfo request
5586 method = decodeUtf8 $ requestMethod request
@@ -85,29 +116,63 @@ logMiddleware log400 app request respond = do
85116 -- but those may be big.
86117 ResponseBuilder _ _ b -> Just b
87118 _ -> Nothing
88- logLine = pairs
89- ( " datetime" .= toIso startZoned
90- <> " timestamp" .= floor @ _ @ Int (toMs start)
91- <> " duration" .= toMs (finishApp - start)
92- <> " send_duration" .= toMs (finishNetwork - finishApp)
93- <> " level" .= INFO
94- <> " app" .= (" scotty" :: Text )
95- <> " msg" .= msg'
96- <> " status" .= statusC
97- <> " url" .= url
98- <> maybe mempty (" userId" .= ) userId
99- <> if log400 && statusC >= 400
100- then maybe mempty (\ b -> " response" .= TLE. decodeUtf8With TE. lenientDecode (toLazyByteString b)) responseBody
101- else mempty
102- )
103-
104- hPutBuilder stdout (fromEncoding logLine <> " \n " )
105- hFlush stdout
119+ responseBodyText = TLE. decodeUtf8With TE. lenientDecode . toLazyByteString <$> responseBody
120+
121+ case mLogAction of
122+ Nothing -> do
123+ let
124+ logLine = pairs
125+ ( " datetime" .= formatTimeIso startZoned
126+ <> " timestamp" .= floor @ _ @ Int (toMs start)
127+ <> " duration" .= toMs (finishApp - start)
128+ <> " send_duration" .= toMs (finishNetwork - finishApp)
129+ <> " level" .= INFO
130+ <> " app" .= (" scotty" :: Text )
131+ <> " msg" .= msg'
132+ <> " status" .= statusC
133+ <> " url" .= url
134+ <> maybe mempty (" userId" .= ) userId
135+ <> if log400 && statusC >= 400
136+ then maybe mempty (" response" .= ) responseBodyText
137+ else mempty
138+ )
139+
140+ hPutBuilder stdout (fromEncoding logLine <> " \n " )
141+ hFlush stdout
142+ Just logAction -> do
143+ logAction AccessLogRecord
144+ { alStart = start
145+ , alFinishApp = finishApp
146+ , alFinishNetwork = finishNetwork
147+ , alMsg = msg'
148+ , alStatus = statusC
149+ , alURL = url
150+ , alUserId = userId
151+ , alResponseBody = responseBodyText
152+ }
106153
107154 return rcv
108- where
109- toIso :: ZonedTime -> Text
110- toIso = pack . formatTime defaultTimeLocale " %FT%T%z"
111155
112- toMs :: POSIXTime -> Double
113- toMs = realToFrac . (1000 * ) . nominalDiffTimeToSeconds
156+ formatTimeIso :: ZonedTime -> Text
157+ formatTimeIso = pack . formatTime defaultTimeLocale " %FT%T%z"
158+
159+ toMs :: POSIXTime -> Double
160+ toMs = realToFrac . (1000 * ) . nominalDiffTimeToSeconds
161+
162+ debugLogHandler :: AccessLogRecord -> IO ()
163+ debugLogHandler AccessLogRecord {.. } = do
164+ let
165+ duration = toMs (alFinishApp - alStart)
166+ sendDuration = toMs (alFinishNetwork - alFinishApp)
167+
168+ startZoned <- utcToLocalZonedTime $ posixSecondsToUTCTime alStart
169+ TIO. putStrLn $
170+ formatTimeIso startZoned
171+ <> " INFO "
172+ <> alMsg
173+ <> " " <> pack (show duration)
174+ <> " " <> pack (show sendDuration)
175+ when (alStatus >= 400 ) $
176+ forM_ alResponseBody TLIO. putStrLn
177+
178+ hFlush stdout
0 commit comments