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