@@ -0,0 +1,189 @@
1+{-# LANGUAGE ImplicitParams #-}
2+{-# OPTIONS_GHC -fno-warn-orphans #-}
3+
4+module Control.Logger.Katip
5+ ( ElasticServer(..)
6+ , logSeverityToKSeverity
7+ , registerElastic
8+ , maybeElastic
9+ , KatipContextTState(..)
10+ , getKatipLogger
11+ , ourFormatter
12+ , ourMapping
13+ , katipIndexNameString
14+ , reopenableFileLog
15+ ) where
16+
17+import Control.EnvT
18+import Control.Lens hiding ((.=))
19+import Control.Logger.Internal
20+import Control.Logger.Katip.Scribes.Reopenable
21+import Data.Aeson hiding (Error)
22+import Data.ByteString (ByteString)
23+import qualified Data.HashMap.Strict as HM
24+import Data.Proxy as P
25+import Data.Text (Text)
26+import qualified Data.Text as T
27+import qualified Data.Text.Encoding as T
28+import qualified Data.Text.Lazy.Builder as T.Builder
29+import Database.V5.Bloodhound
30+import Katip as K hiding (logMsg)
31+import Katip.Core (ItemFunc(..), LocJs(..), ProcessIDJs(..))
32+import Katip.Monadic (KatipContextTState(..))
33+import Katip.Scribes.ElasticSearch
34+import Katip.Scribes.ElasticSearch.Internal as K
35+import Network.HTTP.Client
36+
37+
38+data ElasticServer = ElasticServer
39+ { _esServer :: !(Maybe Server)
40+ , _esUsername :: !(Maybe ByteString)
41+ , _esPassword :: !(Maybe ByteString)
42+ } deriving (Eq, Show)
43+
44+instance FromJSON ElasticServer where
45+ parseJSON = withObject "ElasticServer" $ \obj -> ElasticServer
46+ <$> (obj .: "server")
47+ <*> (fmap T.encodeUtf8 <$> (obj .: "username"))
48+ <*> (fmap T.encodeUtf8 <$> (obj .: "password"))
49+
50+instance ToJSON ElasticServer where
51+ toJSON es = object
52+ [ "server" .= fmap fromServer (_esServer es)
53+ , "username" .= fmap T.decodeUtf8 (_esUsername es)
54+ , "password" .= fmap T.decodeUtf8 (_esPassword es)
55+ ]
56+ where
57+ fromServer (Server s) = s
58+
59+logSeverityToKSeverity :: LogSeverity -> Severity
60+logSeverityToKSeverity = \case
61+ Debug -> DebugS
62+ Info -> InfoS
63+ Warn -> WarningS
64+ Error -> ErrorS
65+
66+registerElastic
67+ :: Text
68+ -- ^ git revision
69+ -> Verbosity
70+ -> ElasticServer
71+ -> PermitFunc
72+ -> LogEnv
73+ -> IO LogEnv
74+registerElastic gitRev verbosity ElasticServer{..} permitF env =
75+ case _esServer of
76+ Nothing -> return env
77+ Just server -> do
78+ elastic <- do
79+ mgr <- newManager defaultManagerSettings
80+ let
81+ bloodhoundEnv = (mkBHEnv server mgr)
82+ { bhRequestHook = requestHook }
83+ mkEsScribe cfg bloodhoundEnv permitF verbosity
84+ registerScribe "elastic" elastic defaultScribeSettings env
85+ where
86+ cfg = (defaultEsScribeCfgV5 ixName mappingName)
87+ { essItemFormatter = ourFormatter gitRev
88+ , essIndexMappingValue = ourMapping mappingName }
89+ where
90+ ixName = IndexName katipIndexNameString
91+ mappingName = MappingName "logs"
92+ requestHook = return . case (_esUsername, _esPassword) of
93+ (Just u, Just p) -> applyBasicAuth u p
94+ _ -> id
95+
96+-- | Doubles the value of @msg@ key into the @msg-keyword@ and adds
97+-- the @rev-hash@ value to each record. The @msg-keyword@ is needed to
98+-- group errors by message. The @rev-hash@ is needed for many purposes
99+-- and for grouping as well.
100+ourFormatter
101+ :: Text
102+ -- ^ git revision string
103+ -> Verbosity
104+ -> ItemFunc Value
105+ourFormatter gitRev verb = ItemFunc $ \item -> go $ ourItemJson verb item
106+ where
107+ go :: Value -> Value
108+ go = \case
109+ Object h -> Object $ setRev $ dupMsg h
110+ x -> x -- drop exception here?
111+ where
112+ setRev :: HM.HashMap Text Value -> HM.HashMap Text Value
113+ setRev = HM.insert "rev-hash" (toJSON gitRev)
114+ dupMsg :: HM.HashMap Text Value -> HM.HashMap Text Value
115+ dupMsg m = HM.alter (const $ HM.lookup "msg" m) "msg-keyword" m
116+
117+ourMapping :: K.MappingName ESV5 -> Value
118+ourMapping mn = object
119+ [fromMappingName prx mn .= object ["properties" .= object prs]]
120+ where
121+ prs =
122+ [ unanalyzedString "thread"
123+ , unanalyzedString "sev"
124+ , unanalyzedString "pid"
125+ -- ns is frequently fulltext searched
126+ , analyzedString "ns"
127+ -- we want message to be fulltext searchable
128+ , analyzedString "msg"
129+ -- we want to group by the message as well
130+ , unanalyzedString "msg-keyword"
131+ -- we want to group by revision hash
132+ , unanalyzedString "rev-hash"
133+ , "loc" .= locType
134+ , unanalyzedString "host"
135+ , unanalyzedString "env"
136+ , "at" .= dateType
137+ , unanalyzedString "app"
138+ ]
139+ prx = P.Proxy :: P.Proxy ESV5
140+ unanalyzedString k = k .= unanalyzedStringSpec prx
141+ analyzedString k = k .= analyzedStringSpec prx
142+ locType = object ["properties" .= object locPairs]
143+ locPairs =
144+ [ unanalyzedString "loc_pkg"
145+ , unanalyzedString "loc_mod"
146+ , unanalyzedString "loc_ln"
147+ , unanalyzedString "loc_fn"
148+ , unanalyzedString "loc_col"
149+ ]
150+ dateType = object ["format" .= esDateFormat, "type" .= String "date"]
151+
152+katipIndexNameString :: Text
153+katipIndexNameString = "antorica-b2b-logs"
154+
155+ourItemJson :: LogItem a => Verbosity -> Item a -> Value
156+ourItemJson verb item = formatItem $
157+ item & itemPayload %~ payloadObject verb
158+ where
159+ formatItem Item{..} = object
160+ [ "app" .= T.intercalate "." (unNamespace _itemApp)
161+ , "env" .= _itemEnv
162+ , "sev" .= _itemSeverity
163+ , "thread" .= getThreadIdText _itemThread
164+ , "host" .= _itemHost
165+ , "pid" .= ProcessIDJs _itemProcess
166+ , "data" .= _itemPayload
167+ , "msg" .= T.Builder.toLazyText (unLogStr _itemMessage)
168+ , "at" .= _itemTime
169+ , "ns" .= T.intercalate "." (unNamespace _itemNamespace)
170+ , "loc" .= fmap LocJs _itemLoc
171+ ]
172+
173+-- | Return empty server if Nothing
174+maybeElastic :: Maybe ElasticServer -> ElasticServer
175+maybeElastic = \case
176+ Nothing -> ElasticServer Nothing Nothing Nothing
177+ Just a -> a
178+
179+instance LogItem Object where
180+ payloadKeys V0 _ = SomeKeys []
181+ payloadKeys _ _ = AllKeys
182+
183+getKatipLogger :: KatipContextTState -> Logger
184+getKatipLogger katipCtx = Logger (toObject . ltsContext $ katipCtx)
185+ $ \ ctx stack s msg ->
186+ let ?callStack = stack
187+ in
188+ flip runEnvT katipCtx {ltsContext = liftPayload ctx}
189+ $ logLocM (logSeverityToKSeverity s) (logStr msg)