|
| 1 | +{-# LANGUAGE BangPatterns #-} |
1 | 2 | {-# LANGUAGE OverloadedStrings #-} |
2 | 3 |
|
3 | 4 | module Web.Template.Log |
4 | 5 | ( bcdlog |
5 | 6 | , bcdlog400 |
6 | 7 | ) where |
7 | 8 |
|
8 | | -import Data.Aeson (pairs, (.=)) |
9 | | -import Data.Aeson.Encoding (encodingToLazyByteString) |
10 | | -import Data.ByteString.Builder (toLazyByteString) |
11 | | -import Data.Default (Default (..)) |
12 | | -import Data.Text as T (Text, pack, unpack, unwords) |
13 | | -import Data.Text.Encoding (decodeUtf8) |
14 | | -import qualified Data.Text.Lazy.Encoding as TLE (decodeUtf8) |
15 | | -import Data.Time (ZonedTime, defaultTimeLocale, formatTime, |
16 | | - nominalDiffTimeToSeconds, parseTimeM, |
17 | | - zonedTimeToUTC) |
18 | | -import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) |
19 | | -import Network.HTTP.Types.Status (Status (..)) |
20 | | -import Network.Wai (Middleware, rawPathInfo, requestMethod) |
21 | | -import Network.Wai.Logger (ZonedDate) |
22 | | -import Network.Wai.Middleware.RequestLogger (OutputFormat (..), |
23 | | - OutputFormatterWithDetails, mkRequestLogger, |
24 | | - outputFormat) |
25 | | -import System.BCD.Log (Level (..)) |
26 | | -import System.IO.Unsafe (unsafePerformIO) |
27 | | -import System.Log.FastLogger (toLogStr) |
| 9 | +import Data.Aeson (fromEncoding, pairs, (.=)) |
| 10 | +import Data.ByteString.Builder (hPutBuilder, toLazyByteString) |
| 11 | +import Data.Text as T (Text, pack, unwords) |
| 12 | +import Data.Text.Encoding (decodeUtf8) |
| 13 | +import qualified Data.Text.Encoding.Error as TE |
| 14 | +import qualified Data.Text.Lazy.Encoding as TLE (decodeUtf8With) |
| 15 | +import Data.Time (ZonedTime, defaultTimeLocale, formatTime, |
| 16 | + nominalDiffTimeToSeconds, utcToLocalZonedTime) |
| 17 | +import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime) |
| 18 | +import Network.HTTP.Types.Status (Status (..)) |
| 19 | +import Network.Wai (Middleware, rawPathInfo, requestMethod, responseStatus) |
| 20 | +import Network.Wai.Internal (Response (..)) |
| 21 | +import System.BCD.Log (Level (..)) |
| 22 | +import System.IO (stdout) |
28 | 23 |
|
29 | | -{-# NOINLINE bcdlog #-} |
30 | 24 | bcdlog :: Middleware |
31 | | -bcdlog = unsafePerformIO $ mkRequestLogger def |
32 | | - { outputFormat = CustomOutputFormatWithDetails $ formatter False |
33 | | - } |
| 25 | +bcdlog = logMiddleware False |
34 | 26 |
|
35 | | -{-# NOINLINE bcdlog400 #-} |
36 | 27 | bcdlog400 :: Middleware |
37 | | -bcdlog400 = unsafePerformIO $ mkRequestLogger def |
38 | | - { outputFormat = CustomOutputFormatWithDetails $ formatter True |
39 | | - } |
| 28 | +bcdlog400 = logMiddleware True |
40 | 29 |
|
41 | | -formatter :: Bool -> OutputFormatterWithDetails |
42 | | -formatter log400 zonedDate request status _ _ _ respBody = do |
43 | | - let |
44 | | - zonedTime = parseZonedDate zonedDate |
45 | | - statusC = statusCode status |
46 | | - method = decodeUtf8 $ requestMethod request |
47 | | - url = decodeUtf8 $ rawPathInfo request |
48 | | - msg' = T.unwords [method, url, pack (show statusC)] |
| 30 | +logMiddleware :: Bool -> Middleware |
| 31 | +logMiddleware log400 app request respond = do |
| 32 | + let |
| 33 | + url = decodeUtf8 $ rawPathInfo request |
| 34 | + method = decodeUtf8 $ requestMethod request |
| 35 | + start <- getPOSIXTime |
| 36 | + startZoned <- utcToLocalZonedTime $ posixSecondsToUTCTime start |
49 | 37 |
|
50 | | - -- Construct extended log record effectively by rendering directly to JSON, without |
51 | | - -- intermediate Value step. |
52 | | - res = pairs |
53 | | - ( "datetime" .= toIso zonedTime |
54 | | - <> "timestamp" .= toMs zonedTime |
55 | | - <> "level" .= INFO |
56 | | - <> "app" .= ("scotty" :: Text) |
57 | | - <> "msg" .= msg' |
58 | | - <> "status" .= statusC |
59 | | - <> "url" .= url |
| 38 | + app request $ \response -> do |
| 39 | + finishApp <- getPOSIXTime |
| 40 | + !rcv <- respond response |
| 41 | + finishNetwork <- getPOSIXTime |
| 42 | + let |
| 43 | + statusC = statusCode $ responseStatus response |
| 44 | + msg' = T.unwords [method, url, pack (show statusC)] |
| 45 | + responseBody = |
| 46 | + case response of |
| 47 | + -- Logger from wai-extra also reads streaming responses, |
| 48 | + -- but those may be big. |
| 49 | + ResponseBuilder _ _ b -> Just b |
| 50 | + _ -> Nothing |
| 51 | + logLine = pairs |
| 52 | + ( "datetime" .= toIso startZoned |
| 53 | + <> "timestamp" .= floor @_ @Int (toMs start) |
| 54 | + <> "duration" .= toMs (finishApp - start) |
| 55 | + <> "send_duration" .= toMs (finishNetwork - finishApp) |
| 56 | + <> "level" .= INFO |
| 57 | + <> "app" .= ("scotty" :: Text) |
| 58 | + <> "msg" .= msg' |
| 59 | + <> "status" .= statusC |
| 60 | + <> "url" .= url |
60 | 61 | <> if log400 && statusC >= 400 |
61 | | - then "response" .= (TLE.decodeUtf8 $ toLazyByteString respBody) |
| 62 | + then maybe mempty (\b -> "response" .= TLE.decodeUtf8With TE.lenientDecode (toLazyByteString b)) responseBody |
62 | 63 | else mempty |
63 | 64 | ) |
64 | 65 |
|
65 | | - toLogStr (encodingToLazyByteString res) <> "\n" |
66 | | - where |
67 | | - toIso :: Maybe ZonedTime -> Text |
68 | | - toIso = pack . maybe "1970-01-01T00:00:00+0000" (formatTime defaultTimeLocale "%FT%T%z") |
| 66 | + hPutBuilder stdout (fromEncoding logLine <> "\n") |
69 | 67 |
|
70 | | - toMs :: Maybe ZonedTime -> Int |
71 | | - toMs = maybe 0 (floor . (1000 *) . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds . zonedTimeToUTC) |
| 68 | + return rcv |
| 69 | + where |
| 70 | + toIso :: ZonedTime -> Text |
| 71 | + toIso = pack . formatTime defaultTimeLocale "%FT%T%z" |
72 | 72 |
|
73 | | - parseZonedDate :: ZonedDate -> Maybe ZonedTime |
74 | | - parseZonedDate = parseTimeM True defaultTimeLocale "%d/%b/%Y:%H:%M:%S %z" . unpack . decodeUtf8 |
| 73 | + toMs :: POSIXTime -> Double |
| 74 | + toMs = realToFrac . (1000 *) . nominalDiffTimeToSeconds |
0 commit comments