Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add debugLog and defaultExceptionResponse #34

Merged
merged 5 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions .github/workflows/haskell-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
#
# For more information, see https://github.com/haskell-CI/haskell-ci
#
# version: 0.15.20230115
# version: 0.16.3
#
# REGENDATA ("0.15.20230115",["github","cabal.project"])
# REGENDATA ("0.16.3",["github","cabal.project"])
#
name: Haskell-CI
on:
Expand All @@ -32,14 +32,19 @@ jobs:
strategy:
matrix:
include:
- compiler: ghc-9.4.4
- compiler: ghc-9.6.2
compilerKind: ghc
compilerVersion: 9.4.4
compilerVersion: 9.6.2
setup-method: ghcup
allow-failure: true
- compiler: ghc-9.2.5
- compiler: ghc-9.4.5
compilerKind: ghc
compilerVersion: 9.2.5
compilerVersion: 9.4.5
setup-method: ghcup
allow-failure: true
- compiler: ghc-9.2.7
compilerKind: ghc
compilerVersion: 9.2.7
setup-method: ghcup
allow-failure: true
- compiler: ghc-9.0.2
Expand All @@ -59,10 +64,10 @@ jobs:
apt-get update
apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
mkdir -p "$HOME/.ghcup/bin"
curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup"
curl -sL https://downloads.haskell.org/ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 > "$HOME/.ghcup/bin/ghcup"
chmod a+x "$HOME/.ghcup/bin/ghcup"
"$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false)
"$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
"$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
env:
HCKIND: ${{ matrix.compilerKind }}
HCNAME: ${{ matrix.compiler }}
Expand All @@ -78,7 +83,7 @@ jobs:
echo "HC=$HC" >> "$GITHUB_ENV"
echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV"
echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV"
echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV"
echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV"
HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
Expand Down Expand Up @@ -128,8 +133,8 @@ jobs:
- name: install cabal-plan
run: |
mkdir -p $HOME/.cabal/bin
curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.6.2.0/cabal-plan-0.6.2.0-x86_64-linux.xz > cabal-plan.xz
echo 'de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz' | sha256sum -c -
curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.7.3.0/cabal-plan-0.7.3.0-x86_64-linux.xz > cabal-plan.xz
echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2 cabal-plan.xz' | sha256sum -c -
xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
rm -f cabal-plan.xz
chmod a+x $HOME/.cabal/bin/cabal-plan
Expand Down Expand Up @@ -162,6 +167,9 @@ jobs:
echo "package web-template" >> cabal.project
echo " ghc-options: -Werror=missing-methods" >> cabal.project
cat >> cabal.project <<EOF
allow-newer: openid-connect:text
allow-newer: openid-connect:mtl

source-repository-package
type: git
location: https://github.com/biocad/bcd-log
Expand Down Expand Up @@ -213,7 +221,7 @@ jobs:
${CABAL} -vnormal check
- name: haddock
run: |
$CABAL v2-haddock --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
$CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
- name: unconstrained build
run: |
rm -f cabal.project.local
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.3.14] - 2023-06-14
### Added
- Debug log formatter `debugLogHandler`, customizable log middleware `logMiddlewareCustom`.

## [0.1.3.13] - 2023-01-23
### Changed
- Add check for aeson 2, for GHC 9.2.5.
Expand Down
2 changes: 2 additions & 0 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ source-repository-package
tag: 79432eaa084705d6ac3f5b877287a74815a8eb71
subdir: generic-override-aeson
--sha256: 1qqb39lw71q7637bh2xpdcxrkyh9r6r55z2xmkgcgbwvx0wi5l9l

allow-newer: openid-connect:text, openid-connect:mtl
113 changes: 89 additions & 24 deletions src/Web/Template/Log.hs
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Web.Template.Log
( bcdlog
, bcdlog400
, debugLog
, debugLogHandler

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

пустое место!!!!!!!!!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

или это специально так?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ага, типа на группы делю

, logMiddlewareCustom
, AccessLogRecord(..)
, formatTimeIso

, userIdVaultKey
, tokenVaultKey
, pTokenVaultKey
) where

import Control.Monad (forM_, when)
import Crypto.JWT (ClaimsSet)
import Data.Aeson (fromEncoding, pairs, (.=))
import Data.ByteString.Builder (hPutBuilder, toLazyByteString)
import Data.IORef (IORef, newIORef, readIORef)
import Data.Text as T (Text, pack, unwords)
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.Encoding.Error as TE
import qualified Data.Text.IO as TIO
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE (decodeUtf8With)
import qualified Data.Text.Lazy.IO as TLIO
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
nominalDiffTimeToSeconds, utcToLocalZonedTime)
import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime)
import Data.Vault.Lazy (Key, insert, newKey)
import GHC.Generics (Generic)
import Network.HTTP.Types.Status (Status (..))
import Network.Wai (Middleware, rawPathInfo, requestMethod, responseStatus,
vault)
Expand All @@ -42,14 +54,33 @@ pTokenVaultKey :: Key (IORef (Maybe ClaimsSet))
pTokenVaultKey = unsafePerformIO newKey
{-# NOINLINE pTokenVaultKey #-}

data AccessLogRecord
= AccessLogRecord
{ alStart :: !POSIXTime
, alFinishApp :: !POSIXTime
, alFinishNetwork :: !POSIXTime
, alMsg :: !Text
, alStatus :: !Int
, alURL :: !Text
, alUserId :: !(Maybe Text)
, alResponseBody :: Maybe TL.Text
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а зачем тут специально ленивый текст?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Потому что из респонса возвращается ленивый

}
deriving (Show, Generic)

bcdlog :: Middleware
bcdlog = logMiddleware False

bcdlog400 :: Middleware
bcdlog400 = logMiddleware True

debugLog :: Middleware
debugLog = logMiddlewareCustom True $ Just debugLogHandler

logMiddleware :: Bool -> Middleware
logMiddleware log400 app request respond = do
logMiddleware log400 = logMiddlewareCustom log400 Nothing

logMiddlewareCustom :: Bool -> Maybe (AccessLogRecord -> IO ()) -> Middleware
logMiddlewareCustom log400 mLogAction app request respond = do
let
url = decodeUtf8 $ rawPathInfo request
method = decodeUtf8 $ requestMethod request
Expand Down Expand Up @@ -85,29 +116,63 @@ logMiddleware log400 app request respond = do
-- but those may be big.
ResponseBuilder _ _ b -> Just b
_ -> Nothing
logLine = pairs
( "datetime" .= toIso startZoned
<> "timestamp" .= floor @_ @Int (toMs start)
<> "duration" .= toMs (finishApp - start)
<> "send_duration" .= toMs (finishNetwork - finishApp)
<> "level" .= INFO
<> "app" .= ("scotty" :: Text)
<> "msg" .= msg'
<> "status" .= statusC
<> "url" .= url
<> maybe mempty ("userId" .=) userId
<> if log400 && statusC >= 400
then maybe mempty (\b -> "response" .= TLE.decodeUtf8With TE.lenientDecode (toLazyByteString b)) responseBody
else mempty
)

hPutBuilder stdout (fromEncoding logLine <> "\n")
hFlush stdout
responseBodyText = TLE.decodeUtf8With TE.lenientDecode . toLazyByteString <$> responseBody

case mLogAction of
Nothing -> do
let
logLine = pairs
( "datetime" .= formatTimeIso startZoned
<> "timestamp" .= floor @_ @Int (toMs start)
<> "duration" .= toMs (finishApp - start)
<> "send_duration" .= toMs (finishNetwork - finishApp)
<> "level" .= INFO
<> "app" .= ("scotty" :: Text)
<> "msg" .= msg'
<> "status" .= statusC
<> "url" .= url
<> maybe mempty ("userId" .=) userId
<> if log400 && statusC >= 400
then maybe mempty ("response" .=) responseBodyText
else mempty
)

hPutBuilder stdout (fromEncoding logLine <> "\n")
hFlush stdout
Just logAction -> do
logAction AccessLogRecord
{ alStart = start
, alFinishApp = finishApp
, alFinishNetwork = finishNetwork
, alMsg = msg'
, alStatus = statusC
, alURL = url
, alUserId = userId
, alResponseBody = responseBodyText
}

return rcv
where
toIso :: ZonedTime -> Text
toIso = pack . formatTime defaultTimeLocale "%FT%T%z"

toMs :: POSIXTime -> Double
toMs = realToFrac . (1000 *) . nominalDiffTimeToSeconds
formatTimeIso :: ZonedTime -> Text
formatTimeIso = pack . formatTime defaultTimeLocale "%FT%T%z"

toMs :: POSIXTime -> Double
toMs = realToFrac . (1000 *) . nominalDiffTimeToSeconds

debugLogHandler :: AccessLogRecord -> IO ()
debugLogHandler AccessLogRecord{..} = do
let
duration = toMs (alFinishApp - alStart)
sendDuration = toMs (alFinishNetwork - alFinishApp)

startZoned <- utcToLocalZonedTime $ posixSecondsToUTCTime alStart
TIO.putStrLn $
formatTimeIso startZoned
<> " INFO "
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

если это debug log
может лучше тут не INFO а DEBUG ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug в смысле форматирования, а не уровня)

<> alMsg
<> " " <> pack (show duration)
<> " " <> pack (show sendDuration)
when (alStatus >= 400) $
forM_ alResponseBody TLIO.putStrLn

hFlush stdout
2 changes: 2 additions & 0 deletions src/Web/Template/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module Web.Template.Server
, defaultHandleLog400
, defaultHeaderCORS
, defaultOnException
, debugOnException
, debugLog
, toApplication
) where

Expand Down
64 changes: 52 additions & 12 deletions src/Web/Template/Wai.hs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
module Web.Template.Wai
where
( logMiddlewareCustom
, debugLogHandler
, debugLog
, formatTimeIso

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Пустота холодна..."

, module Web.Template.Wai
) where

import Control.Exception (SomeException, fromException)
import Data.Text (Text)
import Network.HTTP.Types (Header)
import Network.Wai (Middleware, Request, mapResponseHeaders, modifyResponse)
import Network.Wai.Handler.Warp (InvalidRequest (..), Port, Settings, defaultSettings,
exceptionResponseForDebug, setOnException, setOnExceptionResponse,
setPort)
import Control.Exception (SomeException, displayException, fromException)
import Data.Aeson (fromEncoding, object, pairs, (.=))
import Data.Text (Text, pack)
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.IO as TIO
import Data.Time (defaultTimeLocale, formatTime, utcToLocalZonedTime)
import Data.Time.Clock.POSIX (getPOSIXTime, posixSecondsToUTCTime)
import Network.HTTP.Types (Header, status500)
import Network.Wai (Middleware, Request (..), Response, mapResponseHeaders,
modifyResponse, responseBuilder)
import Network.Wai.Handler.Warp (InvalidRequest (..), Port, Settings, defaultSettings,
setOnException, setOnExceptionResponse, setPort)
import System.IO (hFlush, stdout)

import System.BCD.Log (error')

import Web.Template.Log (bcdlog, bcdlog400)
import Web.Template.Log (bcdlog, bcdlog400, debugLog, debugLogHandler, logMiddlewareCustom, formatTimeIso)

defaultHandleLog :: Middleware
defaultHandleLog = bcdlog
Expand All @@ -37,10 +49,38 @@ defaultOnException _ e =
Just ConnectionClosedByPeer -> return ()
_ -> error' ("scotty" :: Text) $ show e

debugOnException :: Maybe Request -> SomeException -> IO ()
debugOnException req e =
case fromException e of
Just ConnectionClosedByPeer -> return ()
_ -> do
time <- getPOSIXTime >>= utcToLocalZonedTime . posixSecondsToUTCTime
let
msg = pack (formatTime defaultTimeLocale "%FT%T%z" time) <> " ERROR"
exc = pack $ displayException e
reqMsg =
case req of
Nothing -> ""
Just request -> let
url = decodeUtf8 $ rawPathInfo request
method = decodeUtf8 $ requestMethod request
in " " <> method <> " " <> url

TIO.putStrLn $ msg <> reqMsg <> "\n" <> exc
hFlush stdout

defaultExceptionResponse :: SomeException -> Response
defaultExceptionResponse e = responseBuilder status500 [] $ fromEncoding $ pairs
( "error" .= ("exception" :: Text)
<> ("params" .= object
[ "message" .= displayException e
])
)

warpSettings :: Port -> (Settings -> Settings) -> Settings
warpSettings port userSettings =
setOnException defaultOnException
. setOnExceptionResponse exceptionResponseForDebug
userSettings
. setOnException defaultOnException
. setOnExceptionResponse defaultExceptionResponse
. setPort port
. userSettings
$ defaultSettings
7 changes: 4 additions & 3 deletions web-template.cabal
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: web-template
version: 0.1.3.13
version: 0.1.3.14
synopsis: Web template
description:
Web template includes:
Expand All @@ -22,8 +22,9 @@ cabal-version: >=1.10
tested-with:
GHC ==8.10.7
|| ==9.0.2
|| ==9.2.5
|| ==9.4.4
|| ==9.2.7
|| ==9.4.5
|| ==9.6.2

library
hs-source-dirs: src
Expand Down