packages feed

log-elasticsearch 0.8.1 → 0.9.0.0

raw patch · 10 files changed

+669/−324 lines, 10 filesdep ~bloodhoundPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: bloodhound

API changes (from Hackage documentation)

- Log.Backend.ElasticSearch: data ElasticSearchConfig
- Log.Backend.ElasticSearch: defaultElasticSearchConfig :: ElasticSearchConfig
- Log.Backend.ElasticSearch: elasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> IO Logger
- Log.Backend.ElasticSearch: esIndex :: ElasticSearchConfig -> Text
- Log.Backend.ElasticSearch: esLogin :: ElasticSearchConfig -> Maybe (EsUsername, EsPassword)
- Log.Backend.ElasticSearch: esLoginInsecure :: ElasticSearchConfig -> Bool
- Log.Backend.ElasticSearch: esMapping :: ElasticSearchConfig -> Text
- Log.Backend.ElasticSearch: esServer :: ElasticSearchConfig -> Text
- Log.Backend.ElasticSearch: instance Data.Aeson.Types.ToJSON.ToJSON Log.Backend.ElasticSearch.LogsMapping
- Log.Backend.ElasticSearch: withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r) -> IO r
- Log.Backend.ElasticSearch.Internal: ElasticSearchConfig :: !Text -> !Text -> !Text -> Maybe (EsUsername, EsPassword) -> !Bool -> ElasticSearchConfig
- Log.Backend.ElasticSearch.Internal: EsPassword :: Text -> EsPassword
- Log.Backend.ElasticSearch.Internal: EsUsername :: Text -> EsUsername
- Log.Backend.ElasticSearch.Internal: [esIndex] :: ElasticSearchConfig -> !Text
- Log.Backend.ElasticSearch.Internal: [esLoginInsecure] :: ElasticSearchConfig -> !Bool
- Log.Backend.ElasticSearch.Internal: [esLogin] :: ElasticSearchConfig -> Maybe (EsUsername, EsPassword)
- Log.Backend.ElasticSearch.Internal: [esMapping] :: ElasticSearchConfig -> !Text
- Log.Backend.ElasticSearch.Internal: [esPassword] :: EsPassword -> Text
- Log.Backend.ElasticSearch.Internal: [esServer] :: ElasticSearchConfig -> !Text
- Log.Backend.ElasticSearch.Internal: [esUsername] :: EsUsername -> Text
- Log.Backend.ElasticSearch.Internal: data ElasticSearchConfig
- Log.Backend.ElasticSearch.Internal: defaultElasticSearchConfig :: ElasticSearchConfig
- Log.Backend.ElasticSearch.Internal: instance GHC.Classes.Eq Log.Backend.ElasticSearch.Internal.ElasticSearchConfig
- Log.Backend.ElasticSearch.Internal: instance GHC.Show.Show Log.Backend.ElasticSearch.Internal.ElasticSearchConfig
- Log.Backend.ElasticSearch.Internal: newtype EsPassword :: *
- Log.Backend.ElasticSearch.Internal: newtype EsUsername :: *
- Log.Backend.ElasticSearch.Lens: data ElasticSearchConfig
- Log.Backend.ElasticSearch.Lens: defaultElasticSearchConfig :: ElasticSearchConfig
- Log.Backend.ElasticSearch.Lens: esIndex :: Lens' ElasticSearchConfig Text
- Log.Backend.ElasticSearch.Lens: esLogin :: Lens' ElasticSearchConfig (Maybe (EsUsername, EsPassword))
- Log.Backend.ElasticSearch.Lens: esLoginInsecure :: Lens' ElasticSearchConfig Bool
- Log.Backend.ElasticSearch.Lens: esMapping :: Lens' ElasticSearchConfig Text
- Log.Backend.ElasticSearch.Lens: esServer :: Lens' ElasticSearchConfig Text
- Log.Backend.ElasticSearch.Lens: withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r) -> IO r
+ Log.Backend.ElasticSearch.V1: data ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1: defaultElasticSearchConfig :: ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1: elasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> IO Logger
+ Log.Backend.ElasticSearch.V1: esIndex :: ElasticSearchConfig -> Text
+ Log.Backend.ElasticSearch.V1: esLogin :: ElasticSearchConfig -> Maybe (EsUsername, EsPassword)
+ Log.Backend.ElasticSearch.V1: esLoginInsecure :: ElasticSearchConfig -> Bool
+ Log.Backend.ElasticSearch.V1: esMapping :: ElasticSearchConfig -> Text
+ Log.Backend.ElasticSearch.V1: esServer :: ElasticSearchConfig -> Text
+ Log.Backend.ElasticSearch.V1: instance Data.Aeson.Types.ToJSON.ToJSON Log.Backend.ElasticSearch.V1.LogsMapping
+ Log.Backend.ElasticSearch.V1: withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r) -> IO r
+ Log.Backend.ElasticSearch.V1.Internal: ElasticSearchConfig :: !Text -> !Text -> !Text -> Maybe (EsUsername, EsPassword) -> !Bool -> ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Internal: EsPassword :: Text -> EsPassword
+ Log.Backend.ElasticSearch.V1.Internal: EsUsername :: Text -> EsUsername
+ Log.Backend.ElasticSearch.V1.Internal: [esIndex] :: ElasticSearchConfig -> !Text
+ Log.Backend.ElasticSearch.V1.Internal: [esLoginInsecure] :: ElasticSearchConfig -> !Bool
+ Log.Backend.ElasticSearch.V1.Internal: [esLogin] :: ElasticSearchConfig -> Maybe (EsUsername, EsPassword)
+ Log.Backend.ElasticSearch.V1.Internal: [esMapping] :: ElasticSearchConfig -> !Text
+ Log.Backend.ElasticSearch.V1.Internal: [esPassword] :: EsPassword -> Text
+ Log.Backend.ElasticSearch.V1.Internal: [esServer] :: ElasticSearchConfig -> !Text
+ Log.Backend.ElasticSearch.V1.Internal: [esUsername] :: EsUsername -> Text
+ Log.Backend.ElasticSearch.V1.Internal: data ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Internal: defaultElasticSearchConfig :: ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Internal: instance GHC.Classes.Eq Log.Backend.ElasticSearch.V1.Internal.ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Internal: instance GHC.Show.Show Log.Backend.ElasticSearch.V1.Internal.ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Internal: newtype EsPassword :: *
+ Log.Backend.ElasticSearch.V1.Internal: newtype EsUsername :: *
+ Log.Backend.ElasticSearch.V1.Lens: data ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Lens: defaultElasticSearchConfig :: ElasticSearchConfig
+ Log.Backend.ElasticSearch.V1.Lens: esIndex :: Lens' ElasticSearchConfig Text
+ Log.Backend.ElasticSearch.V1.Lens: esLogin :: Lens' ElasticSearchConfig (Maybe (EsUsername, EsPassword))
+ Log.Backend.ElasticSearch.V1.Lens: esLoginInsecure :: Lens' ElasticSearchConfig Bool
+ Log.Backend.ElasticSearch.V1.Lens: esMapping :: Lens' ElasticSearchConfig Text
+ Log.Backend.ElasticSearch.V1.Lens: esServer :: Lens' ElasticSearchConfig Text
+ Log.Backend.ElasticSearch.V1.Lens: withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r) -> IO r
+ Log.Backend.ElasticSearch.V5: data ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5: defaultElasticSearchConfig :: ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5: elasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> IO Logger
+ Log.Backend.ElasticSearch.V5: esIndex :: ElasticSearchConfig -> Text
+ Log.Backend.ElasticSearch.V5: esLogin :: ElasticSearchConfig -> Maybe (EsUsername, EsPassword)
+ Log.Backend.ElasticSearch.V5: esLoginInsecure :: ElasticSearchConfig -> Bool
+ Log.Backend.ElasticSearch.V5: esMapping :: ElasticSearchConfig -> Text
+ Log.Backend.ElasticSearch.V5: esServer :: ElasticSearchConfig -> Text
+ Log.Backend.ElasticSearch.V5: instance Data.Aeson.Types.ToJSON.ToJSON Log.Backend.ElasticSearch.V5.LogsMapping
+ Log.Backend.ElasticSearch.V5: withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r) -> IO r
+ Log.Backend.ElasticSearch.V5.Internal: ElasticSearchConfig :: !Text -> !Text -> !Text -> Maybe (EsUsername, EsPassword) -> !Bool -> ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Internal: EsPassword :: Text -> EsPassword
+ Log.Backend.ElasticSearch.V5.Internal: EsUsername :: Text -> EsUsername
+ Log.Backend.ElasticSearch.V5.Internal: [esIndex] :: ElasticSearchConfig -> !Text
+ Log.Backend.ElasticSearch.V5.Internal: [esLoginInsecure] :: ElasticSearchConfig -> !Bool
+ Log.Backend.ElasticSearch.V5.Internal: [esLogin] :: ElasticSearchConfig -> Maybe (EsUsername, EsPassword)
+ Log.Backend.ElasticSearch.V5.Internal: [esMapping] :: ElasticSearchConfig -> !Text
+ Log.Backend.ElasticSearch.V5.Internal: [esPassword] :: EsPassword -> Text
+ Log.Backend.ElasticSearch.V5.Internal: [esServer] :: ElasticSearchConfig -> !Text
+ Log.Backend.ElasticSearch.V5.Internal: [esUsername] :: EsUsername -> Text
+ Log.Backend.ElasticSearch.V5.Internal: data ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Internal: defaultElasticSearchConfig :: ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Internal: instance GHC.Classes.Eq Log.Backend.ElasticSearch.V5.Internal.ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Internal: instance GHC.Show.Show Log.Backend.ElasticSearch.V5.Internal.ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Internal: newtype EsPassword :: *
+ Log.Backend.ElasticSearch.V5.Internal: newtype EsUsername :: *
+ Log.Backend.ElasticSearch.V5.Lens: data ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Lens: defaultElasticSearchConfig :: ElasticSearchConfig
+ Log.Backend.ElasticSearch.V5.Lens: esIndex :: Lens' ElasticSearchConfig Text
+ Log.Backend.ElasticSearch.V5.Lens: esLogin :: Lens' ElasticSearchConfig (Maybe (EsUsername, EsPassword))
+ Log.Backend.ElasticSearch.V5.Lens: esLoginInsecure :: Lens' ElasticSearchConfig Bool
+ Log.Backend.ElasticSearch.V5.Lens: esMapping :: Lens' ElasticSearchConfig Text
+ Log.Backend.ElasticSearch.V5.Lens: esServer :: Lens' ElasticSearchConfig Text
+ Log.Backend.ElasticSearch.V5.Lens: withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r) -> IO r

Files

log-elasticsearch.cabal view
@@ -1,5 +1,5 @@ name:                log-elasticsearch-version:             0.8.1+version:             0.9.0.0 synopsis:            Structured logging solution (Elasticsearch back end)  description:         Elasticsearch back end for the 'log' library.@@ -24,16 +24,22 @@   Location: https://github.com/scrive/log.git  library-  exposed-modules:     Log.Backend.ElasticSearch-                       Log.Backend.ElasticSearch.Internal+  exposed-modules:     Log.Backend.ElasticSearch.V1+                       Log.Backend.ElasticSearch.V1.Internal+                       Log.Backend.ElasticSearch.V1.Lens+                       Log.Backend.ElasticSearch.V5+                       Log.Backend.ElasticSearch.V5.Internal+                       Log.Backend.ElasticSearch.V5.Lens+                       Log.Backend.ElasticSearch                        Log.Backend.ElasticSearch.Lens+                       Log.Backend.ElasticSearch.Internal   build-depends:       base <5,                        log-base >= 0.7,                        aeson >=0.11.0.0,                        aeson-pretty >=0.8.2,                        bytestring,                        base64-bytestring,-                       bloodhound >= 0.11.1 && < 0.13,+                       bloodhound >= 0.13 && < 0.15,                        deepseq,                        http-client,                        http-client-tls,
src/Log/Backend/ElasticSearch.hs view
@@ -1,253 +1,5 @@--- | Elasticsearch logging back-end.-module Log.Backend.ElasticSearch (-    ElasticSearchConfig-  , esServer-  , esIndex-  , esMapping-  , esLogin-  , esLoginInsecure-  , defaultElasticSearchConfig-  , withElasticSearchLogger-  , elasticSearchLogger-  ) where--import Control.Applicative-import Control.Arrow (second)-import Control.Concurrent-import Control.Exception-import Control.Monad-import Control.Monad.IO.Class-import Data.Aeson-import Data.Aeson.Encode.Pretty-import Data.Bits-import Data.IORef-import Data.Maybe (isJust)-import Data.Semigroup-import Data.Time-import Data.Time.Clock.POSIX-import Data.Word-import Database.Bloodhound hiding (Status)-import Log-import Log.Internal.Logger-import Network.HTTP.Client-import Network.HTTP.Client.TLS (tlsManagerSettings)-import Prelude-import TextShow-import qualified Data.ByteString as BS-import qualified Data.ByteString.Base64 as B64-import qualified Data.ByteString.Lazy.Char8 as BSL-import qualified Data.HashMap.Strict as H-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import qualified Data.Traversable as F-import qualified Data.Vector as V--import Log.Backend.ElasticSearch.Internal--------------------------------------------- | Create an 'elasticSearchLogger' for the duration of the given--- action, and shut it down afterwards, making sure that all buffered--- messages are actually written to the Elasticsearch store.-withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r)-                        -> IO r-withElasticSearchLogger conf randGen act = do-  logger <- elasticSearchLogger conf randGen-  withLogger logger act--{-# DEPRECATED elasticSearchLogger "Use 'withElasticSearchLogger' instead!" #-}---- | Start an asynchronous logger thread that stores messages using--- Elasticsearch.------ Please use 'withElasticSearchLogger' instead, which is more--- exception-safe (see the note attached to 'mkBulkLogger').-elasticSearchLogger ::-  ElasticSearchConfig -- ^ Configuration.-  -> IO Word32        -- ^ Generate a random 32-bit word for use in-                      -- document IDs.-  -> IO Logger-elasticSearchLogger ElasticSearchConfig{..} genRandomWord = do-  checkElasticSearchLogin-  checkElasticSearchConnection-  indexRef <- newIORef $ IndexName T.empty-  mkBulkLogger "ElasticSearch" (\msgs -> do-    now <- getCurrentTime-    oldIndex <- readIORef indexRef-    -- Bloodhound doesn't support letting ES autogenerate IDs, so let's generate-    -- them ourselves. An ID of a log message is 12 bytes (4 bytes: random, 4-    -- bytes: current time as epoch, 4 bytes: insertion order) encoded as-    -- Base64. This makes eventual collisions practically impossible.-    baseID <- (<>)-      <$> (littleEndianRep <$> liftIO genRandomWord)-      <*> pure (littleEndianRep . floor $ timeToDouble now)-    retryOnException . runBH_ $ do-      -- Elasticsearch index names are additionally indexed by date so that each-      -- day is logged to a separate index to make log management easier.-      let index = IndexName $ T.concat [-              esIndex-            , "-"-            , T.pack $ formatTime defaultTimeLocale "%F" now-            ]-      when (oldIndex /= index) $ do-        -- There is an obvious race condition in the presence of more than one-        -- logger instance running, but it's irrelevant as attempting to create-        -- index that already exists is harmless.-        indexExists' <- indexExists index-        unless indexExists' $ do-          -- Bloodhound is weird and won't let us create index using default-          -- settings, so pass these as the default ones.-          let indexSettings = IndexSettings {-                  indexShards   = ShardCount 4-                , indexReplicas = ReplicaCount 1-                }-          void $ createIndex indexSettings index-          reply <- putMapping index mapping LogsMapping-          when (not $ isSuccess reply) $ do-            error $ "ElasticSearch: error while creating mapping: "-              <> T.unpack (T.decodeUtf8 . BSL.toStrict . jsonToBSL-                            $ decodeReply reply)-        liftIO $ writeIORef indexRef index-      let jsonMsgs = V.fromList $ map (toJsonMsg now) $ zip [1..] msgs-      reply <- bulk $ V.map (toBulk index baseID) jsonMsgs-      -- Try to parse parts of reply to get information about log messages that-      -- failed to be inserted for some reason.-      let replyBody = decodeReply reply-          result = do-            Object response <- return replyBody-            Bool hasErrors  <- "errors" `H.lookup` response-            Array jsonItems <- "items"  `H.lookup` response-            items <- F.forM jsonItems $ \v -> do-              Object item   <- return v-              Object index_ <- "index" `H.lookup` item-              return index_-            guard $ V.length items == V.length jsonMsgs-            return (hasErrors, items)-      case result of-        Nothing -> liftIO . BSL.putStrLn-          $ "ElasticSearch: unexpected response: " <> jsonToBSL replyBody-        Just (hasErrors, items) -> when hasErrors $ do-          -- If any message failed to be inserted because of type mismatch, go-          -- back to them, replace their data with elastic search error and put-          -- old data into its own namespace to work around insertion errors.-          let failed = V.findIndices (H.member "error") items-          dummyMsgs <- V.forM failed $ \n -> do-            dataNamespace <- liftIO genRandomWord-            let modifyData oldData = object [-                    "__es_error" .= H.lookup "error" (items V.! n)-                  , "__es_modified" .= True-                  , ("__data_" <> showt dataNamespace) .= oldData-                  ]-            return . second (H.adjust modifyData "data") $ jsonMsgs V.! n-          -- Attempt to put modified messages and ignore any further errors.-          void $ bulk (V.map (toBulk index baseID) dummyMsgs))-    (elasticSearchSync indexRef)-  where-    server  = Server esServer-    mapping = MappingName esMapping--    elasticSearchSync :: IORef IndexName -> IO ()-    elasticSearchSync indexRef = do-      indexName <- readIORef indexRef-      void . runBH_ $ refreshIndex indexName--    checkElasticSearchLogin :: IO ()-    checkElasticSearchLogin =-      when (isJust esLogin-            && not esLoginInsecure-            && not ("https:" `T.isPrefixOf` esServer)) $-        error $ "ElasticSearch: insecure login: "-          <> "Attempting to send login credentials over an insecure connection. "-          <> "Set esLoginInsecure = True to disable this check."--    checkElasticSearchConnection :: IO ()-    checkElasticSearchConnection = try (void $ runBH_ listIndices) >>= \case-      Left (ex::HttpException) -> error $ "ElasticSearch: unexpected error: "-                                  <> show ex-                                  <> " (is ElasticSearch server running?)"-      Right () -> return ()--    retryOnException :: forall r. IO r -> IO r-    retryOnException m = try m >>= \case-      Left (ex::SomeException) -> do-        putStrLn $ "ElasticSearch: unexpected error: "-          <> show ex <> ", retrying in 10 seconds"-        threadDelay $ 10 * 1000000-        retryOnException m-      Right result -> return result--    timeToDouble :: UTCTime -> Double-    timeToDouble = realToFrac . utcTimeToPOSIXSeconds--    runBH_ :: forall r. BH IO r -> IO r-    runBH_ f = do-      mgr <- newManager tlsManagerSettings-      let hook = maybe return (uncurry basicAuthHook) esLogin-      let env = (mkBHEnv server mgr) { bhRequestHook = hook }-      runBH env f---    jsonToBSL :: Value -> BSL.ByteString-    jsonToBSL = encodePretty' defConfig { confIndent = Spaces 2 }--    toJsonMsg :: UTCTime -> (Word32, LogMessage)-              -> (Word32, H.HashMap T.Text Value)-    toJsonMsg now (n, msg) = (n, H.union jMsg $ H.fromList [-        ("insertion_order", toJSON n)-      , ("insertion_time",  toJSON now)-      ])-      where-        Object jMsg = toJSON msg--    mkDocId :: BS.ByteString -> Word32 -> DocId-    mkDocId baseID insertionOrder = DocId . T.decodeUtf8-                                    . B64.encode $ BS.concat [-        baseID-      , littleEndianRep insertionOrder-      ]--    toBulk :: IndexName -> BS.ByteString -> (Word32, H.HashMap T.Text Value)-           -> BulkOperation-    toBulk index baseID (n, obj) =-      BulkIndex index mapping (mkDocId baseID n) $ Object obj--data LogsMapping = LogsMapping-instance ToJSON LogsMapping where-  toJSON LogsMapping = object [-    "properties" .= object [-        "insertion_order" .= object [-            "type" .= ("integer"::T.Text)-          ]-        , "insertion_time" .= object [-            "type"   .= ("date"::T.Text)-          , "format" .= ("date_time"::T.Text)-          ]-        , "time" .= object [-            "type"   .= ("date"::T.Text)-          , "format" .= ("date_time"::T.Text)-          ]-        , "domain" .= object [-            "type" .= ("string"::T.Text)-          ]-        , "level" .= object [-            "type" .= ("string"::T.Text)-          ]-        , "component" .= object [-            "type" .= ("string"::T.Text)-          ]-        , "message" .= object [-            "type" .= ("string"::T.Text)-          ]-        ]-    ]--------------------------------------------littleEndianRep :: Word32 -> BS.ByteString-littleEndianRep = fst . BS.unfoldrN 4 step-  where-    step n = Just (fromIntegral $ n .&. 0xff, n `shiftR` 8)+module Log.Backend.ElasticSearch+    {-# DEPRECATED "Use directly Log.Backend.ElasticSearch.V1 or V5" #-}+    ( module Log.Backend.ElasticSearch.V1 ) where -decodeReply :: Reply -> Value-decodeReply reply = case eitherDecode' $ responseBody reply of-  Right body -> body-  Left  err  -> object ["decoding_error" .= err]+import Log.Backend.ElasticSearch.V1
src/Log/Backend/ElasticSearch/Internal.hs view
@@ -1,31 +1,5 @@ module Log.Backend.ElasticSearch.Internal-  (ElasticSearchConfig(..)-  ,defaultElasticSearchConfig-  ,EsUsername(..)-  ,EsPassword(..))-where--import Database.Bloodhound hiding (Status)-import Prelude-import qualified Data.Text as T---- | Configuration for the Elasticsearch 'Logger'. See--- <https://www.elastic.co/guide/en/elasticsearch/reference/current/glossary.html>--- for the explanation of terms.-data ElasticSearchConfig = ElasticSearchConfig {-    esServer        :: !T.Text -- ^ Elasticsearch server address.-  , esIndex         :: !T.Text -- ^ Elasticsearch index name.-  , esMapping       :: !T.Text -- ^ Elasticsearch mapping name.-  , esLogin         :: Maybe (EsUsername, EsPassword) -- ^ Elasticsearch basic authentication username and password.-  , esLoginInsecure :: !Bool   -- ^ Allow basic authentication over non-TLS connections.-  } deriving (Eq, Show)+    {-# DEPRECATED "Use directly Log.Backend.ElasticSearch.V1 or V5" #-}+    ( module Log.Backend.ElasticSearch.V1.Internal ) where --- | Sensible defaults for 'ElasticSearchConfig'.-defaultElasticSearchConfig :: ElasticSearchConfig-defaultElasticSearchConfig = ElasticSearchConfig {-  esServer        = "http://localhost:9200",-  esIndex         = "logs",-  esMapping       = "log",-  esLogin         = Nothing,-  esLoginInsecure = False-  }+import Log.Backend.ElasticSearch.V1.Internal
src/Log/Backend/ElasticSearch/Lens.hs view
@@ -1,40 +1,5 @@-{-# LANGUAGE RankNTypes #-}--- | Lensified version of "Log.Backend.ElasticSearch".-module Log.Backend.ElasticSearch.Lens (-    I.ElasticSearchConfig-  , esServer-  , esIndex-  , esMapping-  , esLogin-  , esLoginInsecure-  , I.defaultElasticSearchConfig-  , I.withElasticSearchLogger-  ) where--import Database.Bloodhound hiding (Status)-import Prelude-import qualified Data.Text as T-import qualified Log.Backend.ElasticSearch as I-import qualified Log.Backend.ElasticSearch.Internal ()--type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s---- | Elasticsearch server address.-esServer :: Lens' I.ElasticSearchConfig T.Text-esServer f esc = fmap (\x -> esc { I.esServer = x }) $ f (I.esServer esc)---- | Elasticsearch index name.-esIndex :: Lens' I.ElasticSearchConfig T.Text-esIndex f esc = fmap (\x -> esc { I.esIndex = x }) $ f (I.esIndex esc)---- | Elasticsearch mapping name.-esMapping :: Lens' I.ElasticSearchConfig T.Text-esMapping f esc = fmap (\x -> esc { I.esMapping = x }) $ f (I.esMapping esc)---- |  Elasticsearch basic authentication username and password.-esLogin :: Lens' I.ElasticSearchConfig (Maybe (EsUsername, EsPassword))-esLogin f esc = fmap (\x -> esc { I.esLogin = x }) $ f (I.esLogin esc)+module Log.Backend.ElasticSearch.Lens+    {-# DEPRECATED "Use directly Log.Backend.ElasticSearch.V1 or V5" #-}+    ( module Log.Backend.ElasticSearch.V1.Lens ) where --- | Allow basic authentication over non-TLS connections.-esLoginInsecure :: Lens' I.ElasticSearchConfig Bool-esLoginInsecure f esc = fmap (\x -> esc { I.esLoginInsecure = x }) $ f (I.esLoginInsecure esc)+import Log.Backend.ElasticSearch.V1.Lens
+ src/Log/Backend/ElasticSearch/V1.hs view
@@ -0,0 +1,253 @@+-- | Elasticsearch logging back-end.+module Log.Backend.ElasticSearch.V1 (+    ElasticSearchConfig+  , esServer+  , esIndex+  , esMapping+  , esLogin+  , esLoginInsecure+  , defaultElasticSearchConfig+  , withElasticSearchLogger+  , elasticSearchLogger+  ) where++import Control.Applicative+import Control.Arrow (second)+import Control.Concurrent+import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Data.Aeson+import Data.Aeson.Encode.Pretty+import Data.Bits+import Data.IORef+import Data.Maybe (isJust)+import Data.Semigroup+import Data.Time+import Data.Time.Clock.POSIX+import Data.Word+import Database.V1.Bloodhound hiding (Status)+import Log+import Log.Internal.Logger+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Prelude+import TextShow+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Lazy.Char8 as BSL+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Traversable as F+import qualified Data.Vector as V++import Log.Backend.ElasticSearch.V1.Internal++----------------------------------------+-- | Create an 'elasticSearchLogger' for the duration of the given+-- action, and shut it down afterwards, making sure that all buffered+-- messages are actually written to the Elasticsearch store.+withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r)+                        -> IO r+withElasticSearchLogger conf randGen act = do+  logger <- elasticSearchLogger conf randGen+  withLogger logger act++{-# DEPRECATED elasticSearchLogger "Use 'withElasticSearchLogger' instead!" #-}++-- | Start an asynchronous logger thread that stores messages using+-- Elasticsearch.+--+-- Please use 'withElasticSearchLogger' instead, which is more+-- exception-safe (see the note attached to 'mkBulkLogger').+elasticSearchLogger ::+  ElasticSearchConfig -- ^ Configuration.+  -> IO Word32        -- ^ Generate a random 32-bit word for use in+                      -- document IDs.+  -> IO Logger+elasticSearchLogger ElasticSearchConfig{..} genRandomWord = do+  checkElasticSearchLogin+  checkElasticSearchConnection+  indexRef <- newIORef $ IndexName T.empty+  mkBulkLogger "ElasticSearch" (\msgs -> do+    now <- getCurrentTime+    oldIndex <- readIORef indexRef+    -- Bloodhound doesn't support letting ES autogenerate IDs, so let's generate+    -- them ourselves. An ID of a log message is 12 bytes (4 bytes: random, 4+    -- bytes: current time as epoch, 4 bytes: insertion order) encoded as+    -- Base64. This makes eventual collisions practically impossible.+    baseID <- (<>)+      <$> (littleEndianRep <$> liftIO genRandomWord)+      <*> pure (littleEndianRep . floor $ timeToDouble now)+    retryOnException . runBH_ $ do+      -- Elasticsearch index names are additionally indexed by date so that each+      -- day is logged to a separate index to make log management easier.+      let index = IndexName $ T.concat [+              esIndex+            , "-"+            , T.pack $ formatTime defaultTimeLocale "%F" now+            ]+      when (oldIndex /= index) $ do+        -- There is an obvious race condition in the presence of more than one+        -- logger instance running, but it's irrelevant as attempting to create+        -- index that already exists is harmless.+        indexExists' <- indexExists index+        unless indexExists' $ do+          -- Bloodhound is weird and won't let us create index using default+          -- settings, so pass these as the default ones.+          let indexSettings = IndexSettings {+                  indexShards   = ShardCount 4+                , indexReplicas = ReplicaCount 1+                }+          void $ createIndex indexSettings index+          reply <- putMapping index mapping LogsMapping+          when (not $ isSuccess reply) $ do+            error $ "ElasticSearch: error while creating mapping: "+              <> T.unpack (T.decodeUtf8 . BSL.toStrict . jsonToBSL+                            $ decodeReply reply)+        liftIO $ writeIORef indexRef index+      let jsonMsgs = V.fromList $ map (toJsonMsg now) $ zip [1..] msgs+      reply <- bulk $ V.map (toBulk index baseID) jsonMsgs+      -- Try to parse parts of reply to get information about log messages that+      -- failed to be inserted for some reason.+      let replyBody = decodeReply reply+          result = do+            Object response <- return replyBody+            Bool hasErrors  <- "errors" `H.lookup` response+            Array jsonItems <- "items"  `H.lookup` response+            items <- F.forM jsonItems $ \v -> do+              Object item   <- return v+              Object index_ <- "index" `H.lookup` item+              return index_+            guard $ V.length items == V.length jsonMsgs+            return (hasErrors, items)+      case result of+        Nothing -> liftIO . BSL.putStrLn+          $ "ElasticSearch: unexpected response: " <> jsonToBSL replyBody+        Just (hasErrors, items) -> when hasErrors $ do+          -- If any message failed to be inserted because of type mismatch, go+          -- back to them, replace their data with elastic search error and put+          -- old data into its own namespace to work around insertion errors.+          let failed = V.findIndices (H.member "error") items+          dummyMsgs <- V.forM failed $ \n -> do+            dataNamespace <- liftIO genRandomWord+            let modifyData oldData = object [+                    "__es_error" .= H.lookup "error" (items V.! n)+                  , "__es_modified" .= True+                  , ("__data_" <> showt dataNamespace) .= oldData+                  ]+            return . second (H.adjust modifyData "data") $ jsonMsgs V.! n+          -- Attempt to put modified messages and ignore any further errors.+          void $ bulk (V.map (toBulk index baseID) dummyMsgs))+    (elasticSearchSync indexRef)+  where+    server  = Server esServer+    mapping = MappingName esMapping++    elasticSearchSync :: IORef IndexName -> IO ()+    elasticSearchSync indexRef = do+      indexName <- readIORef indexRef+      void . runBH_ $ refreshIndex indexName++    checkElasticSearchLogin :: IO ()+    checkElasticSearchLogin =+      when (isJust esLogin+            && not esLoginInsecure+            && not ("https:" `T.isPrefixOf` esServer)) $+        error $ "ElasticSearch: insecure login: "+          <> "Attempting to send login credentials over an insecure connection. "+          <> "Set esLoginInsecure = True to disable this check."++    checkElasticSearchConnection :: IO ()+    checkElasticSearchConnection = try (void $ runBH_ listIndices) >>= \case+      Left (ex::HttpException) -> error $ "ElasticSearch: unexpected error: "+                                  <> show ex+                                  <> " (is ElasticSearch server running?)"+      Right () -> return ()++    retryOnException :: forall r. IO r -> IO r+    retryOnException m = try m >>= \case+      Left (ex::SomeException) -> do+        putStrLn $ "ElasticSearch: unexpected error: "+          <> show ex <> ", retrying in 10 seconds"+        threadDelay $ 10 * 1000000+        retryOnException m+      Right result -> return result++    timeToDouble :: UTCTime -> Double+    timeToDouble = realToFrac . utcTimeToPOSIXSeconds++    runBH_ :: forall r. BH IO r -> IO r+    runBH_ f = do+      mgr <- newManager tlsManagerSettings+      let hook = maybe return (uncurry basicAuthHook) esLogin+      let env = (mkBHEnv server mgr) { bhRequestHook = hook }+      runBH env f+++    jsonToBSL :: Value -> BSL.ByteString+    jsonToBSL = encodePretty' defConfig { confIndent = Spaces 2 }++    toJsonMsg :: UTCTime -> (Word32, LogMessage)+              -> (Word32, H.HashMap T.Text Value)+    toJsonMsg now (n, msg) = (n, H.union jMsg $ H.fromList [+        ("insertion_order", toJSON n)+      , ("insertion_time",  toJSON now)+      ])+      where+        Object jMsg = toJSON msg++    mkDocId :: BS.ByteString -> Word32 -> DocId+    mkDocId baseID insertionOrder = DocId . T.decodeUtf8+                                    . B64.encode $ BS.concat [+        baseID+      , littleEndianRep insertionOrder+      ]++    toBulk :: IndexName -> BS.ByteString -> (Word32, H.HashMap T.Text Value)+           -> BulkOperation+    toBulk index baseID (n, obj) =+      BulkIndex index mapping (mkDocId baseID n) $ Object obj++data LogsMapping = LogsMapping+instance ToJSON LogsMapping where+  toJSON LogsMapping = object [+    "properties" .= object [+        "insertion_order" .= object [+            "type" .= ("integer"::T.Text)+          ]+        , "insertion_time" .= object [+            "type"   .= ("date"::T.Text)+          , "format" .= ("date_time"::T.Text)+          ]+        , "time" .= object [+            "type"   .= ("date"::T.Text)+          , "format" .= ("date_time"::T.Text)+          ]+        , "domain" .= object [+            "type" .= ("string"::T.Text)+          ]+        , "level" .= object [+            "type" .= ("string"::T.Text)+          ]+        , "component" .= object [+            "type" .= ("string"::T.Text)+          ]+        , "message" .= object [+            "type" .= ("string"::T.Text)+          ]+        ]+    ]++----------------------------------------++littleEndianRep :: Word32 -> BS.ByteString+littleEndianRep = fst . BS.unfoldrN 4 step+  where+    step n = Just (fromIntegral $ n .&. 0xff, n `shiftR` 8)++decodeReply :: Reply -> Value+decodeReply reply = case eitherDecode' $ responseBody reply of+  Right body -> body+  Left  err  -> object ["decoding_error" .= err]
+ src/Log/Backend/ElasticSearch/V1/Internal.hs view
@@ -0,0 +1,31 @@+module Log.Backend.ElasticSearch.V1.Internal+  (ElasticSearchConfig(..)+  ,defaultElasticSearchConfig+  ,EsUsername(..)+  ,EsPassword(..))+where++import Database.V1.Bloodhound hiding (Status)+import Prelude+import qualified Data.Text as T++-- | Configuration for the Elasticsearch 'Logger'. See+-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/glossary.html>+-- for the explanation of terms.+data ElasticSearchConfig = ElasticSearchConfig {+    esServer        :: !T.Text -- ^ Elasticsearch server address.+  , esIndex         :: !T.Text -- ^ Elasticsearch index name.+  , esMapping       :: !T.Text -- ^ Elasticsearch mapping name.+  , esLogin         :: Maybe (EsUsername, EsPassword) -- ^ Elasticsearch basic authentication username and password.+  , esLoginInsecure :: !Bool   -- ^ Allow basic authentication over non-TLS connections.+  } deriving (Eq, Show)++-- | Sensible defaults for 'ElasticSearchConfig'.+defaultElasticSearchConfig :: ElasticSearchConfig+defaultElasticSearchConfig = ElasticSearchConfig {+  esServer        = "http://localhost:9200",+  esIndex         = "logs",+  esMapping       = "log",+  esLogin         = Nothing,+  esLoginInsecure = False+  }
+ src/Log/Backend/ElasticSearch/V1/Lens.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE RankNTypes #-}+-- | Lensified version of "Log.Backend.ElasticSearch".+module Log.Backend.ElasticSearch.V1.Lens (+    I.ElasticSearchConfig+  , esServer+  , esIndex+  , esMapping+  , esLogin+  , esLoginInsecure+  , I.defaultElasticSearchConfig+  , I.withElasticSearchLogger+  ) where++import Database.V1.Bloodhound hiding (Status)+import Prelude+import qualified Data.Text as T+import qualified Log.Backend.ElasticSearch.V1 as I+import qualified Log.Backend.ElasticSearch.V1.Internal ()++type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s++-- | Elasticsearch server address.+esServer :: Lens' I.ElasticSearchConfig T.Text+esServer f esc = fmap (\x -> esc { I.esServer = x }) $ f (I.esServer esc)++-- | Elasticsearch index name.+esIndex :: Lens' I.ElasticSearchConfig T.Text+esIndex f esc = fmap (\x -> esc { I.esIndex = x }) $ f (I.esIndex esc)++-- | Elasticsearch mapping name.+esMapping :: Lens' I.ElasticSearchConfig T.Text+esMapping f esc = fmap (\x -> esc { I.esMapping = x }) $ f (I.esMapping esc)++-- |  Elasticsearch basic authentication username and password.+esLogin :: Lens' I.ElasticSearchConfig (Maybe (EsUsername, EsPassword))+esLogin f esc = fmap (\x -> esc { I.esLogin = x }) $ f (I.esLogin esc)++-- | Allow basic authentication over non-TLS connections.+esLoginInsecure :: Lens' I.ElasticSearchConfig Bool+esLoginInsecure f esc = fmap (\x -> esc { I.esLoginInsecure = x }) $ f (I.esLoginInsecure esc)
+ src/Log/Backend/ElasticSearch/V5.hs view
@@ -0,0 +1,253 @@+-- | Elasticsearch logging back-end.+module Log.Backend.ElasticSearch.V5 (+    ElasticSearchConfig+  , esServer+  , esIndex+  , esMapping+  , esLogin+  , esLoginInsecure+  , defaultElasticSearchConfig+  , withElasticSearchLogger+  , elasticSearchLogger+  ) where++import Control.Applicative+import Control.Arrow (second)+import Control.Concurrent+import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Data.Aeson+import Data.Aeson.Encode.Pretty+import Data.Bits+import Data.IORef+import Data.Maybe (isJust)+import Data.Semigroup+import Data.Time+import Data.Time.Clock.POSIX+import Data.Word+import Database.V5.Bloodhound hiding (Status)+import Log+import Log.Internal.Logger+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Prelude+import TextShow+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Lazy.Char8 as BSL+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Traversable as F+import qualified Data.Vector as V++import Log.Backend.ElasticSearch.V5.Internal++----------------------------------------+-- | Create an 'elasticSearchLogger' for the duration of the given+-- action, and shut it down afterwards, making sure that all buffered+-- messages are actually written to the Elasticsearch store.+withElasticSearchLogger :: ElasticSearchConfig -> IO Word32 -> (Logger -> IO r)+                        -> IO r+withElasticSearchLogger conf randGen act = do+  logger <- elasticSearchLogger conf randGen+  withLogger logger act++{-# DEPRECATED elasticSearchLogger "Use 'withElasticSearchLogger' instead!" #-}++-- | Start an asynchronous logger thread that stores messages using+-- Elasticsearch.+--+-- Please use 'withElasticSearchLogger' instead, which is more+-- exception-safe (see the note attached to 'mkBulkLogger').+elasticSearchLogger ::+  ElasticSearchConfig -- ^ Configuration.+  -> IO Word32        -- ^ Generate a random 32-bit word for use in+                      -- document IDs.+  -> IO Logger+elasticSearchLogger ElasticSearchConfig{..} genRandomWord = do+  checkElasticSearchLogin+  checkElasticSearchConnection+  indexRef <- newIORef $ IndexName T.empty+  mkBulkLogger "ElasticSearch" (\msgs -> do+    now <- getCurrentTime+    oldIndex <- readIORef indexRef+    -- Bloodhound doesn't support letting ES autogenerate IDs, so let's generate+    -- them ourselves. An ID of a log message is 12 bytes (4 bytes: random, 4+    -- bytes: current time as epoch, 4 bytes: insertion order) encoded as+    -- Base64. This makes eventual collisions practically impossible.+    baseID <- (<>)+      <$> (littleEndianRep <$> liftIO genRandomWord)+      <*> pure (littleEndianRep . floor $ timeToDouble now)+    retryOnException . runBH_ $ do+      -- Elasticsearch index names are additionally indexed by date so that each+      -- day is logged to a separate index to make log management easier.+      let index = IndexName $ T.concat [+              esIndex+            , "-"+            , T.pack $ formatTime defaultTimeLocale "%F" now+            ]+      when (oldIndex /= index) $ do+        -- There is an obvious race condition in the presence of more than one+        -- logger instance running, but it's irrelevant as attempting to create+        -- index that already exists is harmless.+        indexExists' <- indexExists index+        unless indexExists' $ do+          -- Bloodhound is weird and won't let us create index using default+          -- settings, so pass these as the default ones.+          let indexSettings = IndexSettings {+                  indexShards   = ShardCount 4+                , indexReplicas = ReplicaCount 1+                }+          void $ createIndex indexSettings index+          reply <- putMapping index mapping LogsMapping+          when (not $ isSuccess reply) $ do+            error $ "ElasticSearch: error while creating mapping: "+              <> T.unpack (T.decodeUtf8 . BSL.toStrict . jsonToBSL+                            $ decodeReply reply)+        liftIO $ writeIORef indexRef index+      let jsonMsgs = V.fromList $ map (toJsonMsg now) $ zip [1..] msgs+      reply <- bulk $ V.map (toBulk index baseID) jsonMsgs+      -- Try to parse parts of reply to get information about log messages that+      -- failed to be inserted for some reason.+      let replyBody = decodeReply reply+          result = do+            Object response <- return replyBody+            Bool hasErrors  <- "errors" `H.lookup` response+            Array jsonItems <- "items"  `H.lookup` response+            items <- F.forM jsonItems $ \v -> do+              Object item   <- return v+              Object index_ <- "index" `H.lookup` item+              return index_+            guard $ V.length items == V.length jsonMsgs+            return (hasErrors, items)+      case result of+        Nothing -> liftIO . BSL.putStrLn+          $ "ElasticSearch: unexpected response: " <> jsonToBSL replyBody+        Just (hasErrors, items) -> when hasErrors $ do+          -- If any message failed to be inserted because of type mismatch, go+          -- back to them, replace their data with elastic search error and put+          -- old data into its own namespace to work around insertion errors.+          let failed = V.findIndices (H.member "error") items+          dummyMsgs <- V.forM failed $ \n -> do+            dataNamespace <- liftIO genRandomWord+            let modifyData oldData = object [+                    "__es_error" .= H.lookup "error" (items V.! n)+                  , "__es_modified" .= True+                  , ("__data_" <> showt dataNamespace) .= oldData+                  ]+            return . second (H.adjust modifyData "data") $ jsonMsgs V.! n+          -- Attempt to put modified messages and ignore any further errors.+          void $ bulk (V.map (toBulk index baseID) dummyMsgs))+    (elasticSearchSync indexRef)+  where+    server  = Server esServer+    mapping = MappingName esMapping++    elasticSearchSync :: IORef IndexName -> IO ()+    elasticSearchSync indexRef = do+      indexName <- readIORef indexRef+      void . runBH_ $ refreshIndex indexName++    checkElasticSearchLogin :: IO ()+    checkElasticSearchLogin =+      when (isJust esLogin+            && not esLoginInsecure+            && not ("https:" `T.isPrefixOf` esServer)) $+        error $ "ElasticSearch: insecure login: "+          <> "Attempting to send login credentials over an insecure connection. "+          <> "Set esLoginInsecure = True to disable this check."++    checkElasticSearchConnection :: IO ()+    checkElasticSearchConnection = try (void $ runBH_ listIndices) >>= \case+      Left (ex::HttpException) -> error $ "ElasticSearch: unexpected error: "+                                  <> show ex+                                  <> " (is ElasticSearch server running?)"+      Right () -> return ()++    retryOnException :: forall r. IO r -> IO r+    retryOnException m = try m >>= \case+      Left (ex::SomeException) -> do+        putStrLn $ "ElasticSearch: unexpected error: "+          <> show ex <> ", retrying in 10 seconds"+        threadDelay $ 10 * 1000000+        retryOnException m+      Right result -> return result++    timeToDouble :: UTCTime -> Double+    timeToDouble = realToFrac . utcTimeToPOSIXSeconds++    runBH_ :: forall r. BH IO r -> IO r+    runBH_ f = do+      mgr <- newManager tlsManagerSettings+      let hook = maybe return (uncurry basicAuthHook) esLogin+      let env = (mkBHEnv server mgr) { bhRequestHook = hook }+      runBH env f+++    jsonToBSL :: Value -> BSL.ByteString+    jsonToBSL = encodePretty' defConfig { confIndent = Spaces 2 }++    toJsonMsg :: UTCTime -> (Word32, LogMessage)+              -> (Word32, H.HashMap T.Text Value)+    toJsonMsg now (n, msg) = (n, H.union jMsg $ H.fromList [+        ("insertion_order", toJSON n)+      , ("insertion_time",  toJSON now)+      ])+      where+        Object jMsg = toJSON msg++    mkDocId :: BS.ByteString -> Word32 -> DocId+    mkDocId baseID insertionOrder = DocId . T.decodeUtf8+                                    . B64.encode $ BS.concat [+        baseID+      , littleEndianRep insertionOrder+      ]++    toBulk :: IndexName -> BS.ByteString -> (Word32, H.HashMap T.Text Value)+           -> BulkOperation+    toBulk index baseID (n, obj) =+      BulkIndex index mapping (mkDocId baseID n) $ Object obj++data LogsMapping = LogsMapping+instance ToJSON LogsMapping where+  toJSON LogsMapping = object [+    "properties" .= object [+        "insertion_order" .= object [+            "type" .= ("integer"::T.Text)+          ]+        , "insertion_time" .= object [+            "type"   .= ("date"::T.Text)+          , "format" .= ("date_time"::T.Text)+          ]+        , "time" .= object [+            "type"   .= ("date"::T.Text)+          , "format" .= ("date_time"::T.Text)+          ]+        , "domain" .= object [+            "type" .= ("string"::T.Text)+          ]+        , "level" .= object [+            "type" .= ("string"::T.Text)+          ]+        , "component" .= object [+            "type" .= ("string"::T.Text)+          ]+        , "message" .= object [+            "type" .= ("string"::T.Text)+          ]+        ]+    ]++----------------------------------------++littleEndianRep :: Word32 -> BS.ByteString+littleEndianRep = fst . BS.unfoldrN 4 step+  where+    step n = Just (fromIntegral $ n .&. 0xff, n `shiftR` 8)++decodeReply :: Reply -> Value+decodeReply reply = case eitherDecode' $ responseBody reply of+  Right body -> body+  Left  err  -> object ["decoding_error" .= err]
+ src/Log/Backend/ElasticSearch/V5/Internal.hs view
@@ -0,0 +1,31 @@+module Log.Backend.ElasticSearch.V5.Internal+  (ElasticSearchConfig(..)+  ,defaultElasticSearchConfig+  ,EsUsername(..)+  ,EsPassword(..))+where++import Database.V5.Bloodhound hiding (Status)+import Prelude+import qualified Data.Text as T++-- | Configuration for the Elasticsearch 'Logger'. See+-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/glossary.html>+-- for the explanation of terms.+data ElasticSearchConfig = ElasticSearchConfig {+    esServer        :: !T.Text -- ^ Elasticsearch server address.+  , esIndex         :: !T.Text -- ^ Elasticsearch index name.+  , esMapping       :: !T.Text -- ^ Elasticsearch mapping name.+  , esLogin         :: Maybe (EsUsername, EsPassword) -- ^ Elasticsearch basic authentication username and password.+  , esLoginInsecure :: !Bool   -- ^ Allow basic authentication over non-TLS connections.+  } deriving (Eq, Show)++-- | Sensible defaults for 'ElasticSearchConfig'.+defaultElasticSearchConfig :: ElasticSearchConfig+defaultElasticSearchConfig = ElasticSearchConfig {+  esServer        = "http://localhost:9200",+  esIndex         = "logs",+  esMapping       = "log",+  esLogin         = Nothing,+  esLoginInsecure = False+  }
+ src/Log/Backend/ElasticSearch/V5/Lens.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE RankNTypes #-}+-- | Lensified version of "Log.Backend.ElasticSearch".+module Log.Backend.ElasticSearch.V5.Lens (+    I.ElasticSearchConfig+  , esServer+  , esIndex+  , esMapping+  , esLogin+  , esLoginInsecure+  , I.defaultElasticSearchConfig+  , I.withElasticSearchLogger+  ) where++import Database.V5.Bloodhound hiding (Status)+import Prelude+import qualified Data.Text as T+import qualified Log.Backend.ElasticSearch.V5 as I+import qualified Log.Backend.ElasticSearch.V5.Internal ()++type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s++-- | Elasticsearch server address.+esServer :: Lens' I.ElasticSearchConfig T.Text+esServer f esc = fmap (\x -> esc { I.esServer = x }) $ f (I.esServer esc)++-- | Elasticsearch index name.+esIndex :: Lens' I.ElasticSearchConfig T.Text+esIndex f esc = fmap (\x -> esc { I.esIndex = x }) $ f (I.esIndex esc)++-- | Elasticsearch mapping name.+esMapping :: Lens' I.ElasticSearchConfig T.Text+esMapping f esc = fmap (\x -> esc { I.esMapping = x }) $ f (I.esMapping esc)++-- |  Elasticsearch basic authentication username and password.+esLogin :: Lens' I.ElasticSearchConfig (Maybe (EsUsername, EsPassword))+esLogin f esc = fmap (\x -> esc { I.esLogin = x }) $ f (I.esLogin esc)++-- | Allow basic authentication over non-TLS connections.+esLoginInsecure :: Lens' I.ElasticSearchConfig Bool+esLoginInsecure f esc = fmap (\x -> esc { I.esLoginInsecure = x }) $ f (I.esLoginInsecure esc)