module Network.Wai.Middleware.Logging
  ( addThreadContext
  , addThreadContextFromRequest
  , requestLogger
  , requestLoggerWith

    -- * Configuration
  , Config
  , defaultConfig
  , setConfigLogSource
  , setConfigGetClientIp
  , setConfigGetDestinationIp
  ) where

import Prelude

import Blammo.Logging
import Blammo.Logging.Setup (HasLogger, runWithLogger)
import Blammo.Logging.ThreadContext (Pair, withThreadContext)
import Control.Applicative ((<|>))
import Control.Arrow ((***))
import Control.Monad.IO.Unlift (withRunInIO)
import Control.Monad.Logger.Aeson (SeriesElem)
import Data.Aeson
import qualified Data.Aeson.Compat as Key
import qualified Data.Aeson.Compat as KeyMap
import Data.ByteString (ByteString)
import qualified Data.CaseInsensitive as CI
import Data.List (find)
import Data.Maybe (fromMaybe)
import Data.Text (Text, pack)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Network.HTTP.Types.Header (Header, HeaderName)
import Network.HTTP.Types.Status (Status (..))
import Network.Wai
  ( Middleware
  , Request
  , rawPathInfo
  , rawQueryString
  , remoteHost
  , requestHeaders
  , requestMethod
  , responseHeaders
  , responseStatus
  )
import Network.Wai.Internal (Response (..))
import qualified System.Clock as Clock

-- | Add context to any logging done from the request-handling thread
addThreadContext :: [Pair] -> Middleware
addThreadContext :: [Pair] -> Middleware
addThreadContext = (Request -> [Pair]) -> Middleware
addThreadContextFromRequest ((Request -> [Pair]) -> Middleware)
-> ([Pair] -> Request -> [Pair]) -> [Pair] -> Middleware
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Pair] -> Request -> [Pair]
forall a b. a -> b -> a
const

-- | 'addThreadContext', but have the 'Request' available
addThreadContextFromRequest :: (Request -> [Pair]) -> Middleware
addThreadContextFromRequest :: (Request -> [Pair]) -> Middleware
addThreadContextFromRequest Request -> [Pair]
toContext Application
app Request
request Response -> IO ResponseReceived
respond = do
  [Pair] -> IO ResponseReceived -> IO ResponseReceived
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
[Pair] -> m a -> m a
withThreadContext (Request -> [Pair]
toContext Request
request) (IO ResponseReceived -> IO ResponseReceived)
-> IO ResponseReceived -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ do
    Application
app Request
request Response -> IO ResponseReceived
respond

-- | Log requests (more accurately, responses) as they happen
--
-- In JSON format, logged messages look like:
--
-- @
-- {
--   ...
--   message: {
--     text: "GET /foo/bar => 200 OK",
--     meta: {
--       method: "GET",
--       path: "/foo/bar",
--       query: "?baz=bat&quix=quo",
--       status: {
--         code: 200,
--         message: "OK"
--       },
--       durationMs: 1322.2,
--       requestHeaders: {
--         Authorization: "***",
--         Accept: "text/html",
--         Cookie: "***"
--       },
--       responseHeaders: {
--         Set-Cookie: "***",
--         Expires: "never"
--       }
--     }
--   }
-- }
-- @
requestLogger :: HasLogger env => env -> Middleware
requestLogger :: forall env. HasLogger env => env -> Middleware
requestLogger = Config -> env -> Middleware
forall env. HasLogger env => Config -> env -> Middleware
requestLoggerWith Config
defaultConfig

data Config = Config
  { Config -> Text
cLogSource :: LogSource
  , Config -> Request -> Text
cGetClientIp :: Request -> Text
  , Config -> Request -> Maybe Text
cGetDestinationIp :: Request -> Maybe Text
  }

defaultConfig :: Config
defaultConfig :: Config
defaultConfig =
  Config
    { cLogSource :: Text
cLogSource = Text
"requestLogger"
    , cGetClientIp :: Request -> Text
cGetClientIp = \Request
req ->
        Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe (String -> Text
pack (String -> Text) -> String -> Text
forall a b. (a -> b) -> a -> b
$ SockAddr -> String
forall a. Show a => a -> String
show (SockAddr -> String) -> SockAddr -> String
forall a b. (a -> b) -> a -> b
$ Request -> SockAddr
remoteHost Request
req) (Maybe Text -> Text) -> Maybe Text -> Text
forall a b. (a -> b) -> a -> b
$
          (Text -> Maybe Text
firstValue (Text -> Maybe Text) -> Maybe Text -> Maybe Text
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< HeaderName -> Request -> Maybe Text
lookupRequestHeader HeaderName
"x-forwarded-for" Request
req)
            Maybe Text -> Maybe Text -> Maybe Text
forall a. Maybe a -> Maybe a -> Maybe a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> HeaderName -> Request -> Maybe Text
lookupRequestHeader HeaderName
"x-real-ip" Request
req
    , cGetDestinationIp :: Request -> Maybe Text
cGetDestinationIp = HeaderName -> Request -> Maybe Text
lookupRequestHeader HeaderName
"x-real-ip"
    }
 where
  firstValue :: Text -> Maybe Text
firstValue = (Text -> Bool) -> [Text] -> Maybe Text
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (Bool -> Bool
not (Bool -> Bool) -> (Text -> Bool) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Bool
T.null) ([Text] -> Maybe Text) -> (Text -> [Text]) -> Text -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Text -> Text
T.strip ([Text] -> [Text]) -> (Text -> [Text]) -> Text -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HasCallStack => Text -> Text -> [Text]
Text -> Text -> [Text]
T.splitOn Text
","

lookupRequestHeader :: HeaderName -> Request -> Maybe Text
lookupRequestHeader :: HeaderName -> Request -> Maybe Text
lookupRequestHeader HeaderName
h = (ByteString -> Text) -> Maybe ByteString -> Maybe Text
forall a b. (a -> b) -> Maybe a -> Maybe b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ByteString -> Text
decodeUtf8 (Maybe ByteString -> Maybe Text)
-> (Request -> Maybe ByteString) -> Request -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HeaderName -> [(HeaderName, ByteString)] -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup HeaderName
h ([(HeaderName, ByteString)] -> Maybe ByteString)
-> (Request -> [(HeaderName, ByteString)])
-> Request
-> Maybe ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Request -> [(HeaderName, ByteString)]
requestHeaders

-- | Change the source used for log messages
--
-- Default is @requestLogger@.
setConfigLogSource :: LogSource -> Config -> Config
setConfigLogSource :: Text -> Config -> Config
setConfigLogSource Text
x Config
c = Config
c {cLogSource = x}

-- | Change how the @clientIp@ field is determined
--
-- Default is looking up the first value in @x-forwarded-for@, then the
-- @x-real-ip@ header, then finally falling back to 'Network.Wai.remoteHost'.
setConfigGetClientIp :: (Request -> Text) -> Config -> Config
setConfigGetClientIp :: (Request -> Text) -> Config -> Config
setConfigGetClientIp Request -> Text
x Config
c = Config
c {cGetClientIp = x}

-- | Change how the @destinationIp@ field is determined
--
-- Default is looking up the @x-real-ip@ header.
--
-- __NOTE__: Our default uses a somewhat loose definition of /destination/. It
-- would be more accurate to report the resolved IP address of the @Host@
-- header, but we don't have that available. Our default of @x-real-ip@ favors
-- containerized Warp on AWS/ECS, where this value holds the ECS target
-- container's IP address. This is valuable debugging information and could, if
-- you squint, be considered a /destination/.
setConfigGetDestinationIp :: (Request -> Maybe Text) -> Config -> Config
setConfigGetDestinationIp :: (Request -> Maybe Text) -> Config -> Config
setConfigGetDestinationIp Request -> Maybe Text
x Config
c = Config
c {cGetDestinationIp = x}

requestLoggerWith :: HasLogger env => Config -> env -> Middleware
requestLoggerWith :: forall env. HasLogger env => Config -> env -> Middleware
requestLoggerWith Config
config env
env Application
app Request
req Response -> IO ResponseReceived
respond =
  ((forall a. IO a -> IO a) -> IO ResponseReceived)
-> IO ResponseReceived
forall b. ((forall a. IO a -> IO a) -> IO b) -> IO b
forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO (((forall a. IO a -> IO a) -> IO ResponseReceived)
 -> IO ResponseReceived)
-> ((forall a. IO a -> IO a) -> IO ResponseReceived)
-> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ \forall a. IO a -> IO a
runInIO -> do
    TimeSpec
begin <- IO TimeSpec
getTime
    Application
app Request
req ((Response -> IO ResponseReceived) -> IO ResponseReceived)
-> (Response -> IO ResponseReceived) -> IO ResponseReceived
forall a b. (a -> b) -> a -> b
$ \Response
resp -> do
      ResponseReceived
recvd <- Response -> IO ResponseReceived
respond Response
resp
      Double
duration <- TimeSpec -> Double
toMillis (TimeSpec -> Double)
-> (TimeSpec -> TimeSpec) -> TimeSpec -> Double
forall b c a. (b -> c) -> (a -> b) -> a -> c
. TimeSpec -> TimeSpec -> TimeSpec
forall a. Num a => a -> a -> a
subtract TimeSpec
begin (TimeSpec -> Double) -> IO TimeSpec -> IO Double
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO TimeSpec
getTime
      IO () -> IO ()
forall a. IO a -> IO a
runInIO (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
        env -> WithLogger env IO () -> IO ()
forall env (m :: * -> *) a. env -> WithLogger env m a -> m a
runWithLogger env
env (WithLogger env IO () -> IO ()) -> WithLogger env IO () -> IO ()
forall a b. (a -> b) -> a -> b
$
          if Response -> Bool
isRaw Response
resp
            then Config -> Double -> Request -> WithLogger env IO ()
forall (m :: * -> *).
MonadLogger m =>
Config -> Double -> Request -> m ()
logRawResponse Config
config Double
duration Request
req
            else Config -> Double -> Request -> Response -> WithLogger env IO ()
forall (m :: * -> *).
MonadLogger m =>
Config -> Double -> Request -> Response -> m ()
logResponse Config
config Double
duration Request
req Response
resp
      ResponseReceived -> IO ResponseReceived
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ResponseReceived
recvd
 where
  getTime :: IO TimeSpec
getTime = Clock -> IO TimeSpec
Clock.getTime Clock
Clock.Monotonic
  toMillis :: TimeSpec -> Double
toMillis TimeSpec
x = Integer -> Double
forall a b. (Integral a, Num b) => a -> b
fromIntegral (TimeSpec -> Integer
Clock.toNanoSecs TimeSpec
x) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ Double
nsPerMs
  isRaw :: Response -> Bool
isRaw = \case
    ResponseFile {} -> Bool
False
    ResponseBuilder {} -> Bool
False
    ResponseStream {} -> Bool
False
    ResponseRaw {} -> Bool
True

logRawResponse :: MonadLogger m => Config -> Double -> Request -> m ()
logRawResponse :: forall (m :: * -> *).
MonadLogger m =>
Config -> Double -> Request -> m ()
logRawResponse config :: Config
config@Config {Text
Request -> Maybe Text
Request -> Text
cLogSource :: Config -> Text
cGetClientIp :: Config -> Request -> Text
cGetDestinationIp :: Config -> Request -> Maybe Text
cLogSource :: Text
cGetClientIp :: Request -> Text
cGetDestinationIp :: Request -> Maybe Text
..} Double
duration Request
req =
  Text -> Message -> m ()
forall (m :: * -> *).
(HasCallStack, MonadLogger m) =>
Text -> Message -> m ()
logDebugNS Text
cLogSource (Message -> m ()) -> Message -> m ()
forall a b. (a -> b) -> a -> b
$ Text
message Text -> [SeriesElem] -> Message
:# [SeriesElem]
details
 where
  message :: Text
message = Request -> Text -> Text
requestMessage Request
req Text
"<raw response>"
  details :: [SeriesElem]
details = Config -> Request -> [SeriesElem]
requestDetails Config
config Request
req [SeriesElem] -> [SeriesElem] -> [SeriesElem]
forall a. Semigroup a => a -> a -> a
<> [Key
"durationMs" Key -> Double -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Double
duration]

logResponse :: MonadLogger m => Config -> Double -> Request -> Response -> m ()
logResponse :: forall (m :: * -> *).
MonadLogger m =>
Config -> Double -> Request -> Response -> m ()
logResponse config :: Config
config@Config {Text
Request -> Maybe Text
Request -> Text
cLogSource :: Config -> Text
cGetClientIp :: Config -> Request -> Text
cGetDestinationIp :: Config -> Request -> Maybe Text
cLogSource :: Text
cGetClientIp :: Request -> Text
cGetDestinationIp :: Request -> Maybe Text
..} Double
duration Request
req Response
resp
  | Status -> Int
statusCode Status
status Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
500 = Text -> Message -> m ()
forall (m :: * -> *).
(HasCallStack, MonadLogger m) =>
Text -> Message -> m ()
logErrorNS Text
cLogSource (Message -> m ()) -> Message -> m ()
forall a b. (a -> b) -> a -> b
$ Text
message Text -> [SeriesElem] -> Message
:# [SeriesElem]
details
  | Status -> Int
statusCode Status
status Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
404 = Text -> Message -> m ()
forall (m :: * -> *).
(HasCallStack, MonadLogger m) =>
Text -> Message -> m ()
logDebugNS Text
cLogSource (Message -> m ()) -> Message -> m ()
forall a b. (a -> b) -> a -> b
$ Text
message Text -> [SeriesElem] -> Message
:# [SeriesElem]
details
  | Status -> Int
statusCode Status
status Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
400 = Text -> Message -> m ()
forall (m :: * -> *).
(HasCallStack, MonadLogger m) =>
Text -> Message -> m ()
logWarnNS Text
cLogSource (Message -> m ()) -> Message -> m ()
forall a b. (a -> b) -> a -> b
$ Text
message Text -> [SeriesElem] -> Message
:# [SeriesElem]
details
  | Bool
otherwise = Text -> Message -> m ()
forall (m :: * -> *).
(HasCallStack, MonadLogger m) =>
Text -> Message -> m ()
logDebugNS Text
cLogSource (Message -> m ()) -> Message -> m ()
forall a b. (a -> b) -> a -> b
$ Text
message Text -> [SeriesElem] -> Message
:# [SeriesElem]
details
 where
  message :: Text
message = Request -> Text -> Text
requestMessage Request
req (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ ByteString -> Text
decodeUtf8 (Status -> ByteString
statusMessage Status
status)
  details :: [SeriesElem]
details =
    Config -> Request -> [SeriesElem]
requestDetails Config
config Request
req
      [SeriesElem] -> [SeriesElem] -> [SeriesElem]
forall a. Semigroup a => a -> a -> a
<> Response -> [SeriesElem]
responseDetails Response
resp
      [SeriesElem] -> [SeriesElem] -> [SeriesElem]
forall a. Semigroup a => a -> a -> a
<> [Key
"durationMs" Key -> Double -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Double
duration]

  status :: Status
status = Response -> Status
responseStatus Response
resp

requestMessage :: Request -> Text -> Text
requestMessage :: Request -> Text -> Text
requestMessage Request
req Text
suffix =
  ByteString -> Text
decodeUtf8 (Request -> ByteString
requestMethod Request
req)
    Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" "
    Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> ByteString -> Text
decodeUtf8 (Request -> ByteString
rawPathInfo Request
req)
    Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" => "
    Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
suffix

requestDetails :: Config -> Request -> [SeriesElem]
requestDetails :: Config -> Request -> [SeriesElem]
requestDetails Config {Text
Request -> Maybe Text
Request -> Text
cLogSource :: Config -> Text
cGetClientIp :: Config -> Request -> Text
cGetDestinationIp :: Config -> Request -> Maybe Text
cLogSource :: Text
cGetClientIp :: Request -> Text
cGetDestinationIp :: Request -> Maybe Text
..} Request
req =
  [ Key
"method" Key -> Text -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= ByteString -> Text
decodeUtf8 (Request -> ByteString
requestMethod Request
req)
  , Key
"path" Key -> Text -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= ByteString -> Text
decodeUtf8 (Request -> ByteString
rawPathInfo Request
req)
  , Key
"query" Key -> Text -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= ByteString -> Text
decodeUtf8 (Request -> ByteString
rawQueryString Request
req)
  , Key
"clientIp" Key -> Text -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Request -> Text
cGetClientIp Request
req
  , Key
"destinationIp" Key -> Maybe Text -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Request -> Maybe Text
cGetDestinationIp Request
req
  , Key
"requestHeaders"
      Key -> Value -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= [HeaderName] -> [(HeaderName, ByteString)] -> Value
headerObject [HeaderName
"authorization", HeaderName
"cookie"] (Request -> [(HeaderName, ByteString)]
requestHeaders Request
req)
  ]

responseDetails :: Response -> [SeriesElem]
responseDetails :: Response -> [SeriesElem]
responseDetails Response
resp =
  [ Key
"status"
      Key -> Value -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= [Pair] -> Value
object
        [ Key
"code" Key -> Int -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= Status -> Int
statusCode Status
status
        , Key
"message" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= ByteString -> Text
decodeUtf8 (Status -> ByteString
statusMessage Status
status)
        ]
  , Key
"responseHeaders" Key -> Value -> SeriesElem
forall v. ToJSON v => Key -> v -> SeriesElem
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= [HeaderName] -> [(HeaderName, ByteString)] -> Value
headerObject [HeaderName
"set-cookie"] (Response -> [(HeaderName, ByteString)]
responseHeaders Response
resp)
  ]
 where
  status :: Status
status = Response -> Status
responseStatus Response
resp

headerObject :: [HeaderName] -> [Header] -> Value
headerObject :: [HeaderName] -> [(HeaderName, ByteString)] -> Value
headerObject [HeaderName]
redact = Object -> Value
Object (Object -> Value)
-> ([(HeaderName, ByteString)] -> Object)
-> [(HeaderName, ByteString)]
-> Value
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Pair] -> Object
forall v. [(Key, v)] -> KeyMap v
KeyMap.fromList ([Pair] -> Object)
-> ([(HeaderName, ByteString)] -> [Pair])
-> [(HeaderName, ByteString)]
-> Object
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((HeaderName, ByteString) -> Pair)
-> [(HeaderName, ByteString)] -> [Pair]
forall a b. (a -> b) -> [a] -> [b]
map ((HeaderName, ByteString) -> Pair
mung ((HeaderName, ByteString) -> Pair)
-> ((HeaderName, ByteString) -> (HeaderName, ByteString))
-> (HeaderName, ByteString)
-> Pair
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (HeaderName, ByteString) -> (HeaderName, ByteString)
forall {b}. IsString b => (HeaderName, b) -> (HeaderName, b)
hide)
 where
  mung :: (HeaderName, ByteString) -> Pair
mung = Text -> Key
Key.fromText (Text -> Key) -> (HeaderName -> Text) -> HeaderName -> Key
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Text
decodeUtf8 (ByteString -> Text)
-> (HeaderName -> ByteString) -> HeaderName -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HeaderName -> ByteString
forall s. CI s -> s
CI.foldedCase (HeaderName -> Key)
-> (ByteString -> Value) -> (HeaderName, ByteString) -> Pair
forall b c b' c'. (b -> c) -> (b' -> c') -> (b, b') -> (c, c')
forall (a :: * -> * -> *) b c b' c'.
Arrow a =>
a b c -> a b' c' -> a (b, b') (c, c')
*** Text -> Value
String (Text -> Value) -> (ByteString -> Text) -> ByteString -> Value
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Text
decodeUtf8
  hide :: (HeaderName, b) -> (HeaderName, b)
hide (HeaderName
k, b
v)
    | HeaderName
k HeaderName -> [HeaderName] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [HeaderName]
redact = (HeaderName
k, b
"***")
    | Bool
otherwise = (HeaderName
k, b
v)

nsPerMs :: Double
nsPerMs :: Double
nsPerMs = Double
1000000

decodeUtf8 :: ByteString -> Text
decodeUtf8 :: ByteString -> Text
decodeUtf8 = OnDecodeError -> ByteString -> Text
decodeUtf8With OnDecodeError
lenientDecode