@@ -5,6 +5,7 @@
5{-# LANGUAGE ExistentialQuantification #-}
5{-# LANGUAGE ExistentialQuantification #-}
6{-# LANGUAGE FlexibleContexts #-}
6{-# LANGUAGE FlexibleContexts #-}
7{-# LANGUAGE FlexibleInstances #-}
7{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9{-# LANGUAGE ImplicitParams #-}
10{-# LANGUAGE ImplicitParams #-}
10{-# LANGUAGE MultiParamTypeClasses #-}
11{-# LANGUAGE MultiParamTypeClasses #-}
@@ -22,9 +23,12 @@
22module Katip.Core where
23module Katip.Core where
24-------------------------------------------------------------------------------
25-------------------------------------------------------------------------------
25−import Control.Applicative as A
26+import Control.Applicative as A
26import Control.AutoUpdate
27import Control.AutoUpdate
27import Control.Concurrent
28import Control.Concurrent
29+import qualified Control.Concurrent.Async as Async
30+import qualified Control.Concurrent.Chan.Unagi.Bounded as UB
31+import Control.Monad (void)
28import Control.Monad.Base
32import Control.Monad.Base
29import Control.Monad.Catch
33import Control.Monad.Catch
30import Control.Monad.IO.Class
34import Control.Monad.IO.Class
@@ -37,21 +41,21 @@ import Control.Monad.Trans.Reader
37import Control.Monad.Trans.Resource
41import Control.Monad.Trans.Resource
38import Control.Monad.Trans.State
42import Control.Monad.Trans.State
39import Control.Monad.Trans.Writer
43import Control.Monad.Trans.Writer
40−import Data.Aeson (FromJSON (..), ToJSON (..),
44+import Data.Aeson (FromJSON (..),
41− object)
45+ ToJSON (..), object)
42−import qualified Data.Aeson as A
46+import qualified Data.Aeson as A
43−import Data.Foldable as FT
47+import Data.Foldable as FT
44−import qualified Data.HashMap.Strict as HM
48+import qualified Data.HashMap.Strict as HM
45import Data.List
49import Data.List
46−import qualified Data.Map.Strict as M
50+import qualified Data.Map.Strict as M
47import Data.Semigroup
51import Data.Semigroup
48import Data.String
52import Data.String
49import Data.String.Conv
53import Data.String.Conv
50−import Data.Text (Text)
54+import Data.Text (Text)
51−import qualified Data.Text as T
55+import qualified Data.Text as T
52−import qualified Data.Text.Lazy.Builder as B
56+import qualified Data.Text.Lazy.Builder as B
53import Data.Time
57import Data.Time
54−import GHC.Generics hiding (to)
58+import GHC.Generics hiding (to)
55#if MIN_VERSION_base(4, 8, 0)
59#if MIN_VERSION_base(4, 8, 0)
56#if !MIN_VERSION_base(4, 9, 0)
60#if !MIN_VERSION_base(4, 9, 0)
57import GHC.SrcLoc
61import GHC.SrcLoc
@@ -59,7 +63,7 @@ import GHC.SrcLoc
59import GHC.Stack
63import GHC.Stack
61import Language.Haskell.TH
65import Language.Haskell.TH
62−import qualified Language.Haskell.TH.Syntax as TH
66+import qualified Language.Haskell.TH.Syntax as TH
63import Lens.Micro
67import Lens.Micro
64import Lens.Micro.TH
68import Lens.Micro.TH
65import Network.HostName
69import Network.HostName
@@ -75,8 +79,8 @@ import System.Posix
75readMay :: Read a => String -> Maybe a
79readMay :: Read a => String -> Maybe a
76readMay s = case [x | (x,t) <- reads s, ("","") <- lex t] of
80readMay s = case [x | (x,t) <- reads s, ("","") <- lex t] of
77 [x] -> Just x
81 [x] -> Just x
78− [] -> Nothing -- no parse
82+ [] -> Nothing -- no parse
79− _ -> Nothing -- Ambiguous parse
83+ _ -> Nothing -- Ambiguous parse
82-------------------------------------------------------------------------------
86-------------------------------------------------------------------------------
@@ -132,13 +136,13 @@ data Verbosity = V0 | V1 | V2 | V3
132-------------------------------------------------------------------------------
136-------------------------------------------------------------------------------
133renderSeverity :: Severity -> Text
137renderSeverity :: Severity -> Text
134renderSeverity s = case s of
138renderSeverity s = case s of
135− DebugS -> "Debug"
139+ DebugS -> "Debug"
136− InfoS -> "Info"
140+ InfoS -> "Info"
137− NoticeS -> "Notice"
141+ NoticeS -> "Notice"
138− WarningS -> "Warning"
142+ WarningS -> "Warning"
139− ErrorS -> "Error"
143+ ErrorS -> "Error"
140− CriticalS -> "Critical"
144+ CriticalS -> "Critical"
141− AlertS -> "Alert"
145+ AlertS -> "Alert"
142 EmergencyS -> "Emergency"
146 EmergencyS -> "Emergency"
@@ -164,7 +168,7 @@ instance FromJSON Severity where
164 parseJSON = A.withText "Severity" parseSeverity
168 parseJSON = A.withText "Severity" parseSeverity
166 parseSeverity t = case textToSeverity t of
170 parseSeverity t = case textToSeverity t of
167− Just x -> return x
171+ Just x -> return x
168 Nothing -> fail $ "Invalid Severity " ++ toS t
172 Nothing -> fail $ "Invalid Severity " ++ toS t
@@ -343,7 +347,7 @@ instance FromJSON ProcessIDJs where
343 parseJSON = A.withText "ProcessID" parseProcessID
347 parseJSON = A.withText "ProcessID" parseProcessID
345 parseProcessID t = case textToProcessID t of
349 parseProcessID t = case textToProcessID t of
346− Just p -> return $ ProcessIDJs p
350+ Just p -> return $ ProcessIDJs p
347 Nothing -> fail $ "Invalid ProcessIDJs " ++ toS t
351 Nothing -> fail $ "Invalid ProcessIDJs " ++ toS t
@@ -380,7 +384,7 @@ class ToObject a where
380 default toObject :: ToJSON a => a -> A.Object
384 default toObject :: ToJSON a => a -> A.Object
381 toObject v = case toJSON v of
385 toObject v = case toJSON v of
382 A.Object o -> o
386 A.Object o -> o
383− _ -> mempty
387+ _ -> mempty
385instance ToObject ()
389instance ToObject ()
386instance ToObject A.Object
390instance ToObject A.Object
@@ -432,7 +436,7 @@ instance ToObject SimpleLogPayload
433instance LogItem SimpleLogPayload where
437instance LogItem SimpleLogPayload where
434 payloadKeys V0 _ = SomeKeys []
438 payloadKeys V0 _ = SomeKeys []
435− payloadKeys _ _ = AllKeys
439+ payloadKeys _ _ = AllKeys
438instance Semigroup SimpleLogPayload where
442instance Semigroup SimpleLogPayload where
@@ -455,7 +459,7 @@ sl a b = SimpleLogPayload [(a, AnyLogPayload b)]
455-- automatically bubble higher verbosity levels to lower ones.
459-- automatically bubble higher verbosity levels to lower ones.
456payloadObject :: LogItem a => Verbosity -> a -> A.Object
460payloadObject :: LogItem a => Verbosity -> a -> A.Object
457payloadObject verb a = case FT.foldMap (flip payloadKeys a) [(V0)..verb] of
461payloadObject verb a = case FT.foldMap (flip payloadKeys a) [(V0)..verb] of
458− AllKeys -> toObject a
462+ AllKeys -> toObject a
459 SomeKeys ks -> HM.filterWithKey (\ k _ -> k `FT.elem` ks) $ toObject a
463 SomeKeys ks -> HM.filterWithKey (\ k _ -> k `FT.elem` ks) $ toObject a
@@ -498,9 +502,9 @@ itemJson verb a = toJSON $ a & itemPayload %~ payloadObject verb
498-- down gracefully before returning. This can be hooked into your
502-- down gracefully before returning. This can be hooked into your
499-- application's shutdown routine to ensure you never miss any log
503-- application's shutdown routine to ensure you never miss any log
500-- messages on shutdown.
504-- messages on shutdown.
501−data Scribe = Scribe {
505+newtype Scribe = Scribe {
502− liPush :: forall a. LogItem a => Item a -> IO ()
506+ liPush :: forall a. LogItem a => Item a -> IO ()
506instance Semigroup Scribe where
510instance Semigroup Scribe where
@@ -514,6 +518,19 @@ instance Monoid Scribe where
514 mappend = (<>)
518 mappend = (<>)
521+-------------------------------------------------------------------------------
522+data ScribeHandle = ScribeHandle {
523+ shFinalizer :: IO ()
524+ , shChan :: UB.InChan WorkerMessage
528+-------------------------------------------------------------------------------
529+data WorkerMessage where
530+ NewItem :: LogItem a => Item a -> WorkerMessage
531+ PoisonPill :: WorkerMessage
517-------------------------------------------------------------------------------
534-------------------------------------------------------------------------------
518-- | Should this item be logged given the user's maximum severity?
535-- | Should this item be logged given the user's maximum severity?
519permitItem :: Severity -> Item a -> Bool
536permitItem :: Severity -> Item a -> Bool
@@ -536,7 +553,7 @@ data LogEnv = LogEnv {
536 -- 'AutoUpdate' for high volume logs but note that this may cause
553 -- 'AutoUpdate' for high volume logs but note that this may cause
537 -- some output forms to display logs out of order. Alternatively,
554 -- some output forms to display logs out of order. Alternatively,
538 -- you could just use 'getCurrentTime'.
555 -- you could just use 'getCurrentTime'.
539− , _logEnvScribes :: M.Map Text Scribe
556+ , _logEnvScribes :: M.Map Text ScribeHandle
541makeLenses ''LogEnv
558makeLenses ''LogEnv
@@ -568,20 +585,70 @@ registerScribe
569 -- ^ Name the scribe
586 -- ^ Name the scribe
570 -> Scribe
587 -> Scribe
571 -> LogEnv
589 -> LogEnv
572− -> LogEnv
590+ -> IO LogEnv
591+registerScribe nm scribe ScribeSettings {..} le = do
592+ (inChan, outChan) <- UB.newChan scribeBufferSize
593+ worker <- spawnScribeWorker scribe outChan
595+ UB.writeChan inChan PoisonPill
596+ -- wait for our worker to finish final write
597+ void (Async.waitCatch worker)
598+ --TODO: safe exceptions
599+ -- wait for scribe to finish final write
602+ let sh = ScribeHandle fin inChan
573−registerScribe nm h = logEnvScribes %~ M.insert nm h
603+ return (le & logEnvScribes %~ M.insert nm sh)
606+-------------------------------------------------------------------------------
607+--TODO: should do some reasonable exception handling in the scribe, don't want to crash the loop
608+spawnScribeWorker :: Scribe -> UB.OutChan WorkerMessage -> IO (Async.Async ())
609+spawnScribeWorker (Scribe write) outChan = Async.async go
612+ newCmd <- UB.readChan outChan
617+ PoisonPill -> return ()
620+-------------------------------------------------------------------------------
621+data ScribeSettings = ScribeSettings {
622+ scribeFinalizer :: IO ()
623+ -- ^ Provide a *blocking* finalizer to call when your scribe is
624+ -- removed. If this is not relevant to your scribe, return () is
626+ , scribeBufferSize :: Int
630+--TODO: standardize on american or british spelling of finali{z,s}er
631+-- | Reasonable defaults for a scribe. No-op finalizer and a buffer
633+defaultScribeSettings
637+defaultScribeSettings fin = ScribeSettings fin 4096
576-------------------------------------------------------------------------------
640-------------------------------------------------------------------------------
577-- | Remove a scribe from the list. All future log calls will no
641-- | Remove a scribe from the list. All future log calls will no
578−-- longer use this scribe. If the given scribe doesn't exist, its a no-op.
642+-- longer use this scribe. If there is a finalizer, it will be called
643+-- in a blocking manner.
579unregisterScribe
644unregisterScribe
581 -- ^ Name of the scribe
646 -- ^ Name of the scribe
582 -> LogEnv
647 -> LogEnv
583− -> LogEnv
648+ -> IO LogEnv
649+unregisterScribe nm le = do
650+ maybe (return ()) shFinalizer (M.lookup nm (_logEnvScribes le))
584−unregisterScribe nm = logEnvScribes %~ M.delete nm
651+ return (le & logEnvScribes %~ M.delete nm)
587-------------------------------------------------------------------------------
654-------------------------------------------------------------------------------
@@ -592,8 +659,9 @@ unregisterScribe nm = logEnvScribes %~ M.delete nm
592-- example.
659-- example.
593clearScribes
660clearScribes
594 :: LogEnv
661 :: LogEnv
595− -> LogEnv
662+ -> IO LogEnv
596−clearScribes = logEnvScribes .~ mempty
663+clearScribes le =
664+ foldrM unregisterScribe le (M.keys (_logEnvScribes le))
599-------------------------------------------------------------------------------
667-------------------------------------------------------------------------------
@@ -692,7 +760,7 @@ logItem a ns loc sev msg = do
692 <*> _logEnvTimer
760 <*> _logEnvTimer
693 <*> pure (_logEnvApp <> ns)
761 <*> pure (_logEnvApp <> ns)
694 <*> pure loc
762 <*> pure loc
695− FT.forM_ (M.elems _logEnvScribes) $ \ (Scribe h) -> h item
763+ FT.forM_ (M.elems _logEnvScribes) $ \ ScribeHandle {..} -> UB.tryWriteChan shChan (NewItem item)
698-------------------------------------------------------------------------------
766-------------------------------------------------------------------------------
@@ -755,14 +823,14 @@ instance TH.Lift Verbosity where
757instance TH.Lift Severity where
825instance TH.Lift Severity where
758− lift DebugS = [| DebugS |]
826+ lift DebugS = [| DebugS |]
759− lift InfoS = [| InfoS |]
827+ lift InfoS = [| InfoS |]
760− lift NoticeS = [| NoticeS |]
828+ lift NoticeS = [| NoticeS |]
761− lift WarningS = [| WarningS |]
829+ lift WarningS = [| WarningS |]
762− lift ErrorS = [| ErrorS |]
830+ lift ErrorS = [| ErrorS |]
763 lift CriticalS = [| CriticalS |]
831 lift CriticalS = [| CriticalS |]
764− lift AlertS = [| AlertS |]
832+ lift AlertS = [| AlertS |]
765− lift EmergencyS = [| EmergencyS |]
833+ lift EmergencyS = [| EmergencyS |]
768-- | Lift a location into an Exp.
836-- | Lift a location into an Exp.