1{-# LANGUAGE TypeFamilies #-}
2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE FunctionalDependencies #-}
4{-# LANGUAGE MultiParamTypeClasses #-}
5{-# LANGUAGE CPP #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE NoImplicitPrelude #-}
8{-# LANGUAGE BangPatterns #-}
9module RIO.Prelude.Logger
10  ( -- ** Running with logging
11    withLogFunc
12  , newLogFunc
13  , LogFunc
14  , HasLogFunc (..)
15  , logOptionsHandle
16    -- *** Log options
17  , LogOptions
18  , setLogMinLevel
19  , setLogMinLevelIO
20  , setLogVerboseFormat
21  , setLogVerboseFormatIO
22  , setLogTerminal
23  , setLogUseTime
24  , setLogUseColor
25  , setLogUseLoc
26  , setLogFormat
27  , setLogLevelColors
28  , setLogSecondaryColor
29  , setLogAccentColors
30    -- ** Standard logging functions
31  , logDebug
32  , logInfo
33  , logWarn
34  , logError
35  , logOther
36    -- ** Advanced logging functions
37    -- *** Sticky logging
38  , logSticky
39  , logStickyDone
40    -- *** With source
41    --
42    -- $withSource
43  , logDebugS
44  , logInfoS
45  , logWarnS
46  , logErrorS
47  , logOtherS
48    -- *** Generic log function
49  , logGeneric
50    -- ** Advanced running functions
51  , mkLogFunc
52  , logOptionsMemory
53    -- ** Data types
54  , LogLevel (..)
55  , LogSource
56  , CallStack
57    -- ** Convenience functions
58  , displayCallStack
59  , noLogging
60    -- ** Accessors
61  , logFuncUseColorL
62  , logFuncLogLevelColorsL
63  , logFuncSecondaryColorL
64  , logFuncAccentColorsL
65    -- * Type-generic logger
66    -- $type-generic-intro
67  , glog
68  , GLogFunc
69  , gLogFuncClassic
70  , mkGLogFunc
71  , contramapMaybeGLogFunc
72  , contramapGLogFunc
73  , HasGLogFunc(..)
74  , HasLogLevel(..)
75  , HasLogSource(..)
76  ) where
77
78import RIO.Prelude.Reexports hiding ((<>))
79import RIO.Prelude.Renames
80import RIO.Prelude.Display
81import RIO.Prelude.Lens
82import Data.Text (Text)
83import qualified Data.Text as T
84import Control.Monad.IO.Class (MonadIO, liftIO)
85import GHC.Stack (HasCallStack, CallStack, SrcLoc (..), getCallStack, callStack)
86import Data.Time
87import qualified Data.Text.IO as TIO
88import Data.Bits
89import Data.ByteString.Builder (toLazyByteString, char7, byteString, hPutBuilder)
90import Data.ByteString.Builder.Extra (flush)
91import           GHC.IO.Handle.Internals         (wantWritableHandle)
92import           GHC.IO.Encoding.Types           (textEncodingName)
93import           GHC.IO.Handle.Types             (Handle__ (..))
94import qualified Data.ByteString as B
95import           System.IO                  (localeEncoding)
96import           GHC.Foreign                (peekCString, withCString)
97import Data.Semigroup (Semigroup (..))
98
99#if MIN_VERSION_base(4,12,0)
100import Data.Functor.Contravariant
101#endif
102
103-- | The log level of a message.
104--
105-- @since 0.0.0.0
106data LogLevel = LevelDebug | LevelInfo | LevelWarn | LevelError | LevelOther !Text
107    deriving (Eq, Show, Read, Ord)
108
109-- | Where in the application a log message came from. Used for
110-- display purposes only.
111--
112-- @since 0.0.0.0
113type LogSource = Text
114
115-- | Environment values with a logging function.
116--
117-- @since 0.0.0.0
118class HasLogFunc env where
119  logFuncL :: Lens' env LogFunc
120instance HasLogFunc LogFunc where
121  logFuncL = id
122
123-- | A logging function, wrapped in a newtype for better error messages.
124--
125-- An implementation may choose any behavior of this value it wishes,
126-- including printing to standard output or no action at all.
127--
128-- @since 0.0.0.0
129data LogFunc = LogFunc
130  { unLogFunc :: !(CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ())
131  , lfOptions :: !(Maybe LogOptions)
132  }
133
134-- | Perform both sets of actions per log entry.
135--
136-- @since 0.0.0.0
137instance Semigroup LogFunc where
138  LogFunc f o1 <> LogFunc g o2 = LogFunc
139    { unLogFunc = \a b c d -> f a b c d *> g a b c d
140    , lfOptions = o1 `mplus` o2
141    }
142
143-- | 'mempty' peforms no logging.
144--
145-- @since 0.0.0.0
146instance Monoid LogFunc where
147  mempty = mkLogFunc $ \_ _ _ _ -> return ()
148  mappend = (<>)
149
150-- | Create a 'LogFunc' from the given function.
151--
152-- @since 0.0.0.0
153mkLogFunc :: (CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ()) -> LogFunc
154mkLogFunc f = LogFunc f Nothing
155
156-- | Generic, basic function for creating other logging functions.
157--
158-- @since 0.0.0.0
159logGeneric
160  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
161  => LogSource
162  -> LogLevel
163  -> Utf8Builder
164  -> m ()
165logGeneric src level str = do
166  LogFunc logFunc _ <- view logFuncL
167  liftIO $ logFunc callStack src level str
168
169-- | Log a debug level message with no source.
170--
171-- @since 0.0.0.0
172logDebug
173  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
174  => Utf8Builder
175  -> m ()
176logDebug = logGeneric "" LevelDebug
177
178-- | Log an info level message with no source.
179--
180-- @since 0.0.0.0
181logInfo
182  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
183  => Utf8Builder
184  -> m ()
185logInfo = logGeneric "" LevelInfo
186
187-- | Log a warn level message with no source.
188--
189-- @since 0.0.0.0
190logWarn
191  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
192  => Utf8Builder
193  -> m ()
194logWarn = logGeneric "" LevelWarn
195
196-- | Log an error level message with no source.
197--
198-- @since 0.0.0.0
199logError
200  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
201  => Utf8Builder
202  -> m ()
203logError = logGeneric "" LevelError
204
205-- | Log a message with the specified textual level and no source.
206--
207-- @since 0.0.0.0
208logOther
209  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
210  => Text -- ^ level
211  -> Utf8Builder
212  -> m ()
213logOther = logGeneric "" . LevelOther
214
215-- $withSource
216--
217-- There is a set of logging functions that take an extra 'LogSource'
218-- argument to provide context, typically detailing what part of an
219-- application the message comes from.
220--
221-- For example, in verbose mode, @infoLogS "database" "connected"@ will
222-- result in
223--
224-- > [info] (database) connected
225
226-- | Log a debug level message with the given source.
227--
228-- @since 0.0.0.0
229logDebugS
230  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
231  => LogSource
232  -> Utf8Builder
233  -> m ()
234logDebugS src = logGeneric src LevelDebug
235
236-- | Log an info level message with the given source.
237--
238-- @since 0.0.0.0
239logInfoS
240  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
241  => LogSource
242  -> Utf8Builder
243  -> m ()
244logInfoS src = logGeneric src LevelInfo
245
246-- | Log a warn level message with the given source.
247--
248-- @since 0.0.0.0
249logWarnS
250  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
251  => LogSource
252  -> Utf8Builder
253  -> m ()
254logWarnS src = logGeneric src LevelWarn
255
256-- | Log an error level message with the given source.
257--
258-- @since 0.0.0.0
259logErrorS
260  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
261  => LogSource
262  -> Utf8Builder
263  -> m ()
264logErrorS src = logGeneric src LevelError
265
266-- | Log a message with the specified textual level and the given
267-- source.
268--
269-- @since 0.0.0.0
270logOtherS
271  :: (MonadIO m, MonadReader env m, HasLogFunc env, HasCallStack)
272  => Text -- ^ level
273  -> LogSource
274  -> Utf8Builder
275  -> m ()
276logOtherS src = logGeneric src . LevelOther
277
278-- | Write a "sticky" line to the terminal. Any subsequent lines will
279-- overwrite this one, and that same line will be repeated below
280-- again. In other words, the line sticks at the bottom of the output
281-- forever. Running this function again will replace the sticky line
282-- with a new sticky line. When you want to get rid of the sticky
283-- line, run 'logStickyDone'.
284--
285-- Note that not all 'LogFunc' implementations will support sticky
286-- messages as described. However, the 'withLogFunc' implementation
287-- provided by this module does.
288--
289-- @since 0.0.0.0
290logSticky :: (MonadIO m, HasCallStack, MonadReader env m, HasLogFunc env) => Utf8Builder -> m ()
291logSticky = logOther "sticky"
292
293-- | This will print out the given message with a newline and disable
294-- any further stickiness of the line until a new call to 'logSticky'
295-- happens.
296--
297-- @since 0.0.0.0
298logStickyDone :: (MonadIO m, HasCallStack, MonadReader env m, HasLogFunc env) => Utf8Builder -> m ()
299logStickyDone = logOther "sticky-done"
300
301-- TODO It might be better at some point to have a 'runSticky' function
302-- that encompasses the logSticky->logStickyDone pairing.
303
304canUseUtf8 :: MonadIO m => Handle -> m Bool
305canUseUtf8 h = liftIO $ wantWritableHandle "canUseUtf8" h $ \h_ -> do
306  -- TODO also handle haOutputNL for CRLF
307  return $ (textEncodingName <$> haCodec h_) == Just "UTF-8"
308
309-- | Create a 'LogOptions' value which will store its data in
310-- memory. This is primarily intended for testing purposes. This will
311-- return both a 'LogOptions' value and an 'IORef' containing the
312-- resulting 'Builder' value.
313--
314-- This will default to non-verbose settings and assume there is a
315-- terminal attached. These assumptions can be overridden using the
316-- appropriate @set@ functions.
317--
318-- @since 0.0.0.0
319logOptionsMemory :: MonadIO m => m (IORef Builder, LogOptions)
320logOptionsMemory = do
321  ref <- newIORef mempty
322  let options = LogOptions
323        { logMinLevel = return LevelInfo
324        , logVerboseFormat = return False
325        , logTerminal = True
326        , logUseTime = False
327        , logUseColor = False
328        , logColors = defaultLogColors
329        , logUseLoc = False
330        , logFormat = id
331        , logSend = \new -> atomicModifyIORef' ref $ \old -> (old <> new, ())
332        }
333  return (ref, options)
334
335-- | Create a 'LogOptions' value from the given 'Handle' and whether
336-- to perform verbose logging or not. Individiual settings can be
337-- overridden using appropriate @set@ functions.
338--
339-- When Verbose Flag is @True@, the following happens:
340--
341--     * @setLogVerboseFormat@ is called with @True@
342--     * @setLogUseColor@ is called with @True@ (except on Windows)
343--     * @setLogUseLoc@ is called with @True@
344--     * @setLogUseTime@ is called with @True@
345--     * @setLogMinLevel@ is called with 'Debug' log level
346--
347-- @since 0.0.0.0
348logOptionsHandle
349  :: MonadIO m
350  => Handle
351  -> Bool -- ^ Verbose Flag
352  -> m LogOptions
353logOptionsHandle handle' verbose = liftIO $ do
354  terminal <- hIsTerminalDevice handle'
355  useUtf8 <- canUseUtf8 handle'
356  unicode <- if useUtf8 then return True else getCanUseUnicode
357  return LogOptions
358    { logMinLevel = return $ if verbose then LevelDebug else LevelInfo
359    , logVerboseFormat = return verbose
360    , logTerminal = terminal
361    , logUseTime = verbose
362#if WINDOWS
363    , logUseColor = False
364#else
365    , logUseColor = verbose && terminal
366#endif
367    , logColors = defaultLogColors
368    , logUseLoc = verbose
369    , logFormat = id
370    , logSend = \builder ->
371        if useUtf8 && unicode
372          then hPutBuilder handle' (builder <> flush)
373          else do
374            let lbs = toLazyByteString builder
375                bs = toStrictBytes lbs
376            case decodeUtf8' bs of
377              Left e -> error $ "mkLogOptions: invalid UTF8 sequence: " ++ show (e, bs)
378              Right text -> do
379                let text'
380                      | unicode = text
381                      | otherwise = T.map replaceUnicode text
382                TIO.hPutStr handle' text'
383                hFlush handle'
384    }
385
386-- | Taken from GHC: determine if we should use Unicode syntax
387getCanUseUnicode :: IO Bool
388getCanUseUnicode = do
389    let enc = localeEncoding
390        str = "\x2018\x2019"
391        test = withCString enc str $ \cstr -> do
392            str' <- peekCString enc cstr
393            return (str == str')
394    test `catchIO` \_ -> return False
395
396
397-- | Given a 'LogOptions' value, returns both a new 'LogFunc' and a sub-routine that
398-- disposes it.
399--
400-- Intended for use if you want to deal with the teardown of 'LogFunc' yourself,
401-- otherwise prefer the 'withLogFunc' function instead.
402--
403--  @since 0.1.3.0
404newLogFunc :: (MonadIO n, MonadIO m) => LogOptions -> n (LogFunc, m ())
405newLogFunc options =
406  if logTerminal options then do
407    var <- newMVar (mempty,0)
408    return (LogFunc
409             { unLogFunc = stickyImpl var options (simpleLogFunc options)
410             , lfOptions = Just options
411             }
412           , do (state,_) <- takeMVar var
413                unless (B.null state) (liftIO $ logSend options "\n")
414           )
415  else
416    return (LogFunc
417            { unLogFunc = \cs src level str ->
418                simpleLogFunc options cs src (noSticky level) str
419            , lfOptions = Just options
420            }
421           , return ()
422           )
423
424-- | Given a 'LogOptions' value, run the given function with the
425-- specified 'LogFunc'. A common way to use this function is:
426--
427-- @
428-- let isVerbose = False -- get from the command line instead
429-- logOptions' <- logOptionsHandle stderr isVerbose
430-- let logOptions = setLogUseTime True logOptions'
431-- withLogFunc logOptions $ \\lf -> do
432--   let app = App -- application specific environment
433--         { appLogFunc = lf
434--         , appOtherStuff = ...
435--         }
436--   runRIO app $ do
437--     logInfo "Starting app"
438--     myApp
439-- @
440--
441-- @since 0.0.0.0
442withLogFunc :: MonadUnliftIO m => LogOptions -> (LogFunc -> m a) -> m a
443withLogFunc options inner = withRunInIO $ \run -> do
444  bracket (newLogFunc options)
445          snd
446          (run . inner . fst)
447
448
449-- | Replace Unicode characters with non-Unicode equivalents
450replaceUnicode :: Char -> Char
451replaceUnicode '\x2018' = '`'
452replaceUnicode '\x2019' = '\''
453replaceUnicode c = c
454
455noSticky :: LogLevel -> LogLevel
456noSticky (LevelOther "sticky-done") = LevelInfo
457noSticky (LevelOther "sticky") = LevelInfo
458noSticky level = level
459
460-- | Configuration for how to create a 'LogFunc'. Intended to be used
461-- with the 'withLogFunc' function.
462--
463-- @since 0.0.0.0
464data LogOptions = LogOptions
465  { logMinLevel :: !(IO LogLevel)
466  , logVerboseFormat :: !(IO Bool)
467  , logTerminal :: !Bool
468  , logUseTime :: !Bool
469  , logUseColor :: !Bool
470  , logColors :: !LogColors
471  , logUseLoc :: !Bool
472  , logFormat :: !(Utf8Builder -> Utf8Builder)
473  , logSend :: !(Builder -> IO ())
474  }
475
476-- | ANSI color codes for use in the configuration of the creation of a
477-- 'LogFunc'.
478--
479-- @since 0.1.18.0
480data LogColors = LogColors
481  { -- | The color associated with each 'LogLevel'.
482    logColorLogLevels :: !(LogLevel -> Utf8Builder)
483    -- | The color of secondary content.
484  , logColorSecondary :: !Utf8Builder
485    -- | The color of accents, which are indexed by 'Int'.
486  , logColorAccents :: !(Int -> Utf8Builder)
487  }
488
489defaultLogColors :: LogColors
490defaultLogColors = LogColors
491  { logColorLogLevels = defaultLogLevelColors
492  , logColorSecondary = defaultLogSecondaryColor
493  , logColorAccents = defaultLogAccentColors
494  }
495
496defaultLogLevelColors :: LogLevel -> Utf8Builder
497defaultLogLevelColors LevelDebug = "\ESC[32m" -- Green
498defaultLogLevelColors LevelInfo = "\ESC[34m" -- Blue
499defaultLogLevelColors LevelWarn = "\ESC[33m" -- Yellow
500defaultLogLevelColors LevelError = "\ESC[31m" -- Red
501defaultLogLevelColors (LevelOther _) = "\ESC[35m" -- Magenta
502
503defaultLogSecondaryColor :: Utf8Builder
504defaultLogSecondaryColor = "\ESC[90m"  -- Bright black (gray)
505
506defaultLogAccentColors :: Int -> Utf8Builder
507defaultLogAccentColors = const "\ESC[92m" -- Bright green
508
509-- | Set the minimum log level. Messages below this level will not be
510-- printed.
511--
512-- Default: in verbose mode, 'LevelDebug'. Otherwise, 'LevelInfo'.
513--
514-- @since 0.0.0.0
515setLogMinLevel :: LogLevel -> LogOptions -> LogOptions
516setLogMinLevel level options = options { logMinLevel = return level }
517
518-- | Refer to 'setLogMinLevel'. This modifier allows to alter the verbose format
519-- value dynamically at runtime.
520--
521-- Default: in verbose mode, 'LevelDebug'. Otherwise, 'LevelInfo'.
522--
523-- @since 0.1.3.0
524setLogMinLevelIO :: IO LogLevel -> LogOptions -> LogOptions
525setLogMinLevelIO getLevel options = options { logMinLevel = getLevel }
526
527-- | Use the verbose format for printing log messages.
528--
529-- Default: follows the value of the verbose flag.
530--
531-- @since 0.0.0.0
532setLogVerboseFormat :: Bool -> LogOptions -> LogOptions
533setLogVerboseFormat v options = options { logVerboseFormat = return v }
534
535-- | Refer to 'setLogVerboseFormat'. This modifier allows to alter the verbose
536--   format value dynamically at runtime.
537--
538-- Default: follows the value of the verbose flag.
539--
540-- @since 0.1.3.0
541setLogVerboseFormatIO :: IO Bool -> LogOptions -> LogOptions
542setLogVerboseFormatIO getVerboseLevel options =
543  options { logVerboseFormat = getVerboseLevel }
544
545-- | Do we treat output as a terminal. If @True@, we will enabled
546-- sticky logging functionality.
547--
548-- Default: checks if the @Handle@ provided to 'logOptionsHandle' is a
549-- terminal with 'hIsTerminalDevice'.
550--
551-- @since 0.0.0.0
552setLogTerminal :: Bool -> LogOptions -> LogOptions
553setLogTerminal t options = options { logTerminal = t }
554
555-- | Include the time when printing log messages.
556--
557-- Default: `True` in debug mode, `False` otherwise.
558--
559-- @since 0.0.0.0
560setLogUseTime :: Bool -> LogOptions -> LogOptions
561setLogUseTime t options = options { logUseTime = t }
562
563-- | Use ANSI color codes in the log output.
564--
565-- Default: `True` if in verbose mode /and/ the 'Handle' is a terminal device.
566--
567-- @since 0.0.0.0
568setLogUseColor :: Bool -> LogOptions -> LogOptions
569setLogUseColor c options = options { logUseColor = c }
570
571-- | ANSI color codes for 'LogLevel' in the log output.
572--
573-- Default: 'LevelDebug'   = \"\\ESC[32m\" -- Green
574--          'LevelInfo'    = \"\\ESC[34m\" -- Blue
575--          'LevelWarn'    = \"\\ESC[33m\" -- Yellow
576--          'LevelError'   = \"\\ESC[31m\" -- Red
577--          'LevelOther' _ = \"\\ESC[35m\" -- Magenta
578--
579-- @since 0.1.18.0
580setLogLevelColors :: (LogLevel -> Utf8Builder) -> LogOptions -> LogOptions
581setLogLevelColors logLevelColors options =
582  let lc = (logColors options){ logColorLogLevels = logLevelColors }
583  in  options { logColors = lc }
584
585-- | ANSI color codes for secondary content in the log output.
586--
587-- Default: \"\\ESC[90m\" -- Bright black (gray)
588--
589-- @since 0.1.18.0
590setLogSecondaryColor :: Utf8Builder -> LogOptions -> LogOptions
591setLogSecondaryColor c options =
592  let lc = (logColors options){ logColorSecondary = c }
593  in  options { logColors = lc }
594
595-- | ANSI color codes for accents in the log output. Accent colors are indexed
596-- by 'Int'.
597--
598-- Default: 'const' \"\\ESC[92m\" -- Bright green, for all indicies
599--
600-- @since 0.1.18.0
601setLogAccentColors
602  :: (Int -> Utf8Builder)  -- ^ This should be a total function.
603  -> LogOptions
604  -> LogOptions
605setLogAccentColors accentColors options =
606  let lc = (logColors options){ logColorAccents = accentColors }
607  in  options { logColors = lc }
608
609-- | Use code location in the log output.
610--
611-- Default: `True` if in verbose mode, `False` otherwise.
612--
613-- @since 0.1.2.0
614setLogUseLoc :: Bool -> LogOptions -> LogOptions
615setLogUseLoc l options = options { logUseLoc = l }
616
617-- | Set format method for messages
618--
619-- Default: `id`
620--
621-- @since 0.1.13.0
622setLogFormat :: (Utf8Builder -> Utf8Builder) -> LogOptions -> LogOptions
623setLogFormat f options = options { logFormat = f }
624
625simpleLogFunc :: LogOptions -> CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ()
626simpleLogFunc lo cs src level msg = do
627    logLevel   <- logMinLevel lo
628    logVerbose <- logVerboseFormat lo
629
630    when (level >= logLevel) $ do
631      timestamp <- getTimestamp logVerbose
632      logSend lo $ getUtf8Builder $
633        timestamp <>
634        getLevel logVerbose <>
635        ansi reset <>
636        getSource <>
637        logFormat lo msg <>
638        getLoc <>
639        ansi reset <>
640        "\n"
641  where
642   reset = "\ESC[0m"
643   lc = logColors lo
644   levelColor = logColorLogLevels lc level
645   timestampColor = logColorSecondary lc
646   locColor = logColorSecondary lc
647
648   ansi :: Utf8Builder -> Utf8Builder
649   ansi xs | logUseColor lo = xs
650           | otherwise = mempty
651
652   getTimestamp :: Bool -> IO Utf8Builder
653   getTimestamp logVerbose
654     | logVerbose && logUseTime lo =
655       do now <- getZonedTime
656          return $ ansi timestampColor <> fromString (formatTime' now) <> ": "
657     | otherwise = return mempty
658     where
659       formatTime' =
660           take timestampLength . formatTime defaultTimeLocale "%F %T.%q"
661
662   getLevel :: Bool -> Utf8Builder
663   getLevel logVerbose
664     | logVerbose = ansi levelColor <>
665         case level of
666           LevelDebug -> "[debug] "
667           LevelInfo -> "[info] "
668           LevelWarn -> "[warn] "
669           LevelError -> "[error] "
670           LevelOther name ->
671             "[" <>
672             display name <>
673             "] "
674     | otherwise = mempty
675
676   getSource :: Utf8Builder
677   getSource = case src of
678     "" -> ""
679     _  -> "(" <> display src <> ") "
680
681   getLoc :: Utf8Builder
682   getLoc
683     | logUseLoc lo = ansi locColor <> "\n@(" <> displayCallStack cs <> ")"
684     | otherwise = mempty
685
686-- | Convert a 'CallStack' value into a 'Utf8Builder' indicating
687-- the first source location.
688--
689-- TODO Consider showing the entire call stack instead.
690--
691-- @since 0.0.0.0
692displayCallStack :: CallStack -> Utf8Builder
693displayCallStack cs =
694     case reverse $ getCallStack cs of
695       [] -> "<no call stack found>"
696       (_desc, loc):_ ->
697         let file = srcLocFile loc
698          in fromString file <>
699             ":" <>
700             displayShow (srcLocStartLine loc) <>
701             ":" <>
702             displayShow (srcLocStartCol loc)
703
704-- | The length of a timestamp in the format "YYYY-MM-DD hh:mm:ss.μμμμμμ".
705-- This definition is top-level in order to avoid multiple reevaluation at runtime.
706timestampLength :: Int
707timestampLength =
708  length (formatTime defaultTimeLocale "%F %T.000000" (UTCTime (ModifiedJulianDay 0) 0))
709
710stickyImpl
711    :: MVar (ByteString,Int) -> LogOptions
712    -> (CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ())
713    -> CallStack -> LogSource -> LogLevel -> Utf8Builder -> IO ()
714stickyImpl ref lo logFunc loc src level msgOrig = modifyMVar_ ref $ \(sticky,stickyLen) -> do
715  let backSpaceChar = '\8'
716      repeating = mconcat . replicate stickyLen . char7
717      clear = logSend lo
718        (repeating backSpaceChar <>
719        repeating ' ' <>
720        repeating backSpaceChar)
721
722  logLevel <- logMinLevel lo
723
724  case level of
725    LevelOther "sticky-done" -> do
726      clear
727      logFunc loc src LevelInfo msgOrig
728      return (mempty,0)
729    LevelOther "sticky" -> do
730      clear
731      let bs = toStrictBytes $ toLazyByteString $ getUtf8Builder msgOrig
732      logSend lo (byteString bs <> flush)
733      return (bs, utf8CharacterCount bs)
734    _
735      | level >= logLevel -> do
736          clear
737          logFunc loc src level msgOrig
738          unless (B.null sticky) $ logSend lo (byteString sticky <> flush)
739          return (sticky,stickyLen)
740      | otherwise -> return (sticky,stickyLen)
741
742-- | The number of Unicode characters in a UTF-8 encoded byte string,
743-- excluding ANSI CSI sequences.
744utf8CharacterCount :: ByteString -> Int
745utf8CharacterCount = go 0
746  where
747    go !n bs = case B.uncons bs of
748        Nothing -> n
749        Just (c,bs)
750            | c .&. 0xC0 == 0x80 -> go n bs            -- UTF-8 continuation
751            | c == 0x1B          -> go n $ dropCSI bs  -- ANSI escape
752            | otherwise          -> go (n+1) bs
753
754    dropCSI bs = case B.uncons bs of
755        Just (0x5B,bs2) -> B.drop 1 $ B.dropWhile isSequenceByte bs2
756        _               -> bs
757
758    isSequenceByte c = c >= 0x20 && c <= 0x3F
759
760-- | Is the log func configured to use color output?
761--
762-- Intended for use by code which wants to optionally add additional color to
763-- its log messages.
764--
765-- @since 0.1.0.0
766logFuncUseColorL :: HasLogFunc env => SimpleGetter env Bool
767logFuncUseColorL = logFuncL.to (maybe False logUseColor . lfOptions)
768
769-- | What color is the log func configured to use for each 'LogLevel'?
770--
771-- Intended for use by code which wants to optionally add additional color to
772-- its log messages.
773--
774-- @since 0.1.18.0
775logFuncLogLevelColorsL :: HasLogFunc env
776                       => SimpleGetter env (LogLevel -> Utf8Builder)
777logFuncLogLevelColorsL = logFuncL.to
778                           (maybe defaultLogLevelColors
779                                  (logColorLogLevels . logColors) . lfOptions)
780
781-- | What color is the log func configured to use for secondary content?
782--
783-- Intended for use by code which wants to optionally add additional color to
784-- its log messages.
785--
786-- @since 0.1.18.0
787logFuncSecondaryColorL :: HasLogFunc env
788                       => SimpleGetter env Utf8Builder
789logFuncSecondaryColorL = logFuncL.to
790                           (maybe defaultLogSecondaryColor
791                                  (logColorSecondary . logColors) . lfOptions)
792
793-- | What accent colors, indexed by 'Int', is the log func configured to use?
794--
795-- Intended for use by code which wants to optionally add additional color to
796-- its log messages.
797--
798-- @since 0.1.18.0
799logFuncAccentColorsL :: HasLogFunc env
800                       => SimpleGetter env (Int -> Utf8Builder)
801logFuncAccentColorsL = logFuncL.to
802                           (maybe defaultLogAccentColors
803                                  (logColorAccents . logColors) . lfOptions)
804
805-- | Disable logging capabilities in a given sub-routine
806--
807-- Intended to skip logging in general purpose implementations, where secrets
808-- might be logged accidently.
809--
810-- @since 0.1.5.0
811noLogging :: (HasLogFunc env, MonadReader env m) => m a -> m a
812noLogging = local (set logFuncL mempty)
813
814--------------------------------------------------------------------------------
815--
816-- $type-generic-intro
817--
818-- When logging takes on a more semantic meaning and the logs need to
819-- be digested, acted upon, translated or serialized upstream (to
820-- e.g. a JSON logging server), we have 'GLogFunc' (as in "generic log
821-- function"), and is accessed via 'HasGLogFunc'.
822--
823-- There is only one function to log in this system: the 'glog'
824-- function, which can log any message. You determine the log levels
825-- or severity of messages when needed.
826--
827-- Using 'RIO.Prelude.mapRIO' and 'contramapGLogFunc' (or
828-- 'contramapMaybeGLogFunc'), you can build hierarchies of loggers.
829--
830-- Example:
831--
832-- @
833-- import RIO
834--
835-- data DatabaseMsg = Connected String | Query String | Disconnected deriving Show
836-- data WebMsg = Request String | Error String | DatabaseMsg DatabaseMsg deriving Show
837-- data AppMsg = InitMsg String | WebMsg WebMsg deriving Show
838--
839-- main :: IO ()
840-- main =
841--   runRIO
842--     (mkGLogFunc (\stack msg -> print msg))
843--     (do glog (InitMsg "Ready to go!")
844--         runWeb
845--           (do glog (Request "/foo")
846--               runDB (do glog (Connected "127.0.0.1")
847--                         glog (Query "SELECT 1"))
848--               glog (Error "Oh noes!")))
849--
850-- runDB :: RIO (GLogFunc DatabaseMsg) () -> RIO (GLogFunc WebMsg) ()
851-- runDB = mapRIO (contramapGLogFunc DatabaseMsg)
852--
853-- runWeb :: RIO (GLogFunc WebMsg) () -> RIO (GLogFunc AppMsg) ()
854-- runWeb = mapRIO (contramapGLogFunc WebMsg)
855-- @
856--
857-- If we instead decided that we only wanted to log database queries,
858-- and not bother the upstream with connect/disconnect messages, we
859-- could simplify the constructor to @DatabaseQuery String@:
860--
861-- @
862-- data WebMsg = Request String | Error String | DatabaseQuery String deriving Show
863-- @
864--
865-- And then @runDB@ could use 'contramapMaybeGLogFunc' to parse only queries:
866--
867-- @
868-- runDB =
869--   mapRIO
870--     (contramapMaybeGLogFunc
871--        (\msg ->
872--           case msg of
873--             Query string -> pure (DatabaseQuery string)
874--             _ -> Nothing))
875-- @
876--
877-- This way, upstream only has to care about queries and not
878-- connect/disconnect constructors.
879
880-- | An app is capable of generic logging if it implements this.
881--
882-- @since 0.1.13.0
883class HasGLogFunc env where
884  type GMsg env
885  gLogFuncL :: Lens' env (GLogFunc (GMsg env))
886
887-- | Quick way to run a RIO that only has a logger in its environment.
888--
889-- @since 0.1.13.0
890instance HasGLogFunc (GLogFunc msg) where
891  type GMsg (GLogFunc msg) = msg
892  gLogFuncL = id
893
894-- | A generic logger of some type @msg@.
895--
896-- Your 'GLocFunc' can re-use the existing classical logging framework
897-- of RIO, and/or implement additional transforms,
898-- filters. Alternatively, you may log to a JSON source in a database,
899-- or anywhere else as needed. You can decide how to log levels or
900-- severities based on the constructors in your type. You will
901-- normally determine this in your main app entry point.
902--
903-- @since 0.1.13.0
904newtype GLogFunc msg = GLogFunc (CallStack -> msg -> IO ())
905
906#if MIN_VERSION_base(4,12,0)
907-- https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor-Contravariant.html
908
909-- | Use this instance to wrap sub-loggers via 'RIO.mapRIO'.
910--
911-- The 'Contravariant' class is available in base 4.12.0.
912--
913-- @since 0.1.13.0
914instance Contravariant GLogFunc where
915  contramap = contramapGLogFunc
916  {-# INLINABLE contramap #-}
917#endif
918
919-- | Perform both sets of actions per log entry.
920--
921-- @since 0.1.13.0
922instance Semigroup (GLogFunc msg) where
923  GLogFunc f <> GLogFunc g = GLogFunc (\a b -> f a b *> g a b)
924
925-- | 'mempty' peforms no logging.
926--
927-- @since 0.1.13.0
928instance Monoid (GLogFunc msg) where
929  mempty = mkGLogFunc $ \_ _ -> return ()
930  mappend = (<>)
931
932-- | A vesion of 'contramapMaybeGLogFunc' which supports filering.
933--
934-- @since 0.1.13.0
935contramapMaybeGLogFunc :: (a -> Maybe b) -> GLogFunc b -> GLogFunc a
936contramapMaybeGLogFunc f (GLogFunc io) =
937  GLogFunc (\stack msg -> maybe (pure ()) (io stack) (f msg))
938{-# INLINABLE contramapMaybeGLogFunc #-}
939
940-- | A contramap. Use this to wrap sub-loggers via 'RIO.mapRIO'.
941--
942-- If you are on base > 4.12.0, you can just use 'contramap'.
943--
944-- @since 0.1.13.0
945contramapGLogFunc :: (a -> b) -> GLogFunc b -> GLogFunc a
946contramapGLogFunc f (GLogFunc io) = GLogFunc (\stack msg -> io stack (f msg))
947{-# INLINABLE contramapGLogFunc #-}
948
949-- | Make a custom generic logger. With this you could, for example,
950-- write to a database or a log digestion service. For example:
951--
952-- > mkGLogFunc (\stack msg -> send (Data.Aeson.encode (JsonLog stack msg)))
953--
954-- @since 0.1.13.0
955mkGLogFunc :: (CallStack -> msg -> IO ()) -> GLogFunc msg
956mkGLogFunc = GLogFunc
957
958-- | Log a value generically.
959--
960-- @since 0.1.13.0
961glog ::
962     (MonadIO m, HasCallStack, HasGLogFunc env, MonadReader env m)
963  => GMsg env
964  -> m ()
965glog t = do
966  GLogFunc gLogFunc <- view gLogFuncL
967  liftIO (gLogFunc callStack t)
968{-# INLINABLE glog #-}
969
970--------------------------------------------------------------------------------
971-- Integration with classical logger framework
972
973-- | Level, if any, of your logs. If unknown, use 'LogOther'. Use for
974-- your generic log data types that want to sit inside the classic log
975-- framework.
976--
977-- @since 0.1.13.0
978class HasLogLevel msg where
979  getLogLevel :: msg -> LogLevel
980
981-- | Source of a log. This can be whatever you want. Use for your
982-- generic log data types that want to sit inside the classic log
983-- framework.
984--
985-- @since 0.1.13.0
986class HasLogSource msg where
987  getLogSource :: msg -> LogSource
988
989-- | Make a 'GLogFunc' via classic 'LogFunc'. Use this if you'd like
990-- to log your generic data type via the classic RIO terminal logger.
991--
992-- @since 0.1.13.0
993gLogFuncClassic ::
994     (HasLogLevel msg, HasLogSource msg, Display msg) => LogFunc -> GLogFunc msg
995gLogFuncClassic (LogFunc {unLogFunc = io}) =
996  mkGLogFunc
997    (\theCallStack msg ->
998       liftIO
999         (io theCallStack (getLogSource msg) (getLogLevel msg) (display msg)))
1000