log-elasticsearch 0.9.1.0 → 0.13.0.2
raw patch · 13 files changed
Files
- CHANGELOG.md +54/−5
- README.md +39/−2
- Setup.hs +0/−2
- log-elasticsearch.cabal +28/−23
- src/Log/Backend/ElasticSearch.hs +218/−3
- src/Log/Backend/ElasticSearch/Internal.hs +247/−3
- src/Log/Backend/ElasticSearch/Lens.hs +56/−3
- src/Log/Backend/ElasticSearch/V1.hs +0/−283
- src/Log/Backend/ElasticSearch/V1/Internal.hs +0/−31
- src/Log/Backend/ElasticSearch/V1/Lens.hs +0/−40
- src/Log/Backend/ElasticSearch/V5.hs +0/−254
- src/Log/Backend/ElasticSearch/V5/Internal.hs +0/−31
- src/Log/Backend/ElasticSearch/V5/Lens.hs +0/−40
CHANGELOG.md view
@@ -1,18 +1,67 @@+# log-elasticsearch-0.13.0.2 (2025-06-26)+* Switch back to `http-client-tls` by default since `HsOpenSSL` is based on a+ deprecated `openssl` API (soon to be removed).++# log-elasticsearch-0.13.0.1 (2023-01-31)+* Add support for OpenSearch.++# log-elasticsearch-0.13.0.0 (2022-09-21)+* Remove deprecated `elasticSearchLogger`.+* Use `http-client-openssl` by default instead of `http-client-tls`.+* Generalize logger related functions to `MonadUnliftIO`.++# log-elasticsearch-0.12.2.0 (2022-04-04)+* Add support for aeson 2.0.1.0.+* Add support for GHC 9.2.+* Drop support for GHC 8.0 and 8.2.++# log-elasticsearch-0.12.1.1 (2021-10-11)+* Make compatible with log-base-0.11.0.0.++# log-elasticsearch-0.12.1.0 (2021-06-23)+* Map `time` as `date_nanos` in ElasticSearch >= 7.0.++# log-elasticsearch-0.12.0.0 (2021-06-09)+* Stop putting `insertion_time` and `insertion_order` in ElasticSearch.++# log-elasticsearch-0.11.0.0 (2020-08-24)+* Drop dependency on bloodhound+* Unify V1 and V5 specific modules+* Add support for ElasticSearch 7.x+* Try harder to insert problematic messages into ElasticSearch+* Support building with GHC 8.8.4 and GHC 8.10.2++# log-elasticsearch-0.10.2.0 (2020-02-03)+* Modify data keys in deterministic manner in case of ES insertion failure++# log-elasticsearch-0.10.1.0 (2019-05-15)+* Add a Generic instance for ElasticSearchConfig.++# log-elasticsearch-0.10.0.1 (2019-01-24)+* Compatibility with ES 6.x by using "text" instead of deprecated "string"++# log-elasticsearch-0.10.0.0 (2018-03-28)+* Expose `checkElasticSearchConnection` and `checkElasticSearchLogin`.+* Add config parameters for number of shards and replicas to+ `ElasticSearchConfig` (and make default of 4 shards and 1 replica+ explicit in 'defaultElasticSearchConfig').+ # log-elasticsearch-0.9.1.0 (2017-08-10)-* Add 'toEncoding' to 'ToJSON LogsMapping' instance+* Add `toEncoding` to `ToJSON LogsMapping` instance # log-elasticsearch-0.9.0.1 (2017-06-19)-* 'withElasticSearchLogger' no longer fails when the Elasticsearch server is down.+* `withElasticSearchLogger` no longer fails when the Elasticsearch+ server is down. # log-elasticsearch-0.9.0.0 (2017-05-04) * Now works with bloodhound-0.14.0.0 (#30). # log-elasticsearch-0.8.1 (2017-03-27)-* Log.Backend.ElasticSearch.Internal now exports 'EsUsername' and- 'EsPassword'.+* `Log.Backend.ElasticSearch.Internal` now exports `EsUsername` and+ `EsPassword`. # log-elasticsearch-0.8 (2017-03-16)-* Made ElasticSearchConfig an abstract type (#27).+* Made `ElasticSearchConfig` an abstract type (#27). * Added support for HTTPS and basic auth (#26). # log-elasticsearch-0.7 (2016-11-25)
README.md view
@@ -1,3 +1,40 @@-# log-elasticsearch [](https://hackage.haskell.org/package/log-elasticsearch) [](http://travis-ci.org/scrive/log)+# log -Elasticsearch back end for the `log` library suite.+[](https://hackage.haskell.org/package/log-base)+[](https://github.com/scrive/log/actions?query=branch%3Amaster)++A set of libraries that provide a way to record structured log messages with+multiple backends.++Supported backends:++* Standard output via+ [`log-base`](https://hackage.haskell.org/package/log-base).+* Elasticsearch via+ [`log-elasticsearch`](https://hackage.haskell.org/package/log-elasticsearch).+* PostgreSQL via+ [`log-postgres`](https://hackage.haskell.org/package/log-postgres).++## Example++A sample usage for logging to both standard output and Elasticsearch:++```haskell+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Log+import Log.Backend.ElasticSearch+import Log.Backend.StandardOutput++main :: IO ()+main = do+ let config = defaultElasticSearchConfig+ { esServer = "http://localhost:9200"+ , esIndex = "logs"+ }+ withStdOutLogger $ \stdoutLogger -> do+ withElasticSearchLogger config $ \esLogger -> do+ runLogT "main" (stdoutLogger <> esLogger) defaultLogLevel $ do+ logInfo_ "Hi there"+```
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
log-elasticsearch.cabal view
@@ -1,12 +1,13 @@+cabal-version: 3.0 name: log-elasticsearch-version: 0.9.1.0+version: 0.13.0.2 synopsis: Structured logging solution (Elasticsearch back end) description: Elasticsearch back end for the 'log' library suite. Depends on 'log-base'. homepage: https://github.com/scrive/log-license: BSD3+license: BSD-3-Clause license-file: LICENSE author: Scrive AB maintainer: Andrzej Rybczak <andrzej@rybczak.net>,@@ -16,44 +17,42 @@ copyright: Scrive AB category: System build-type: Simple-cabal-version: >=1.10 extra-source-files: CHANGELOG.md, README.md-tested-with: GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.1+tested-with: GHC == { 8.10.7, 9.0.2, 9.2.8, 9.4.8, 9.6.7, 9.8.4, 9.10.2, 9.12.2 } -Source-repository head- Type: git- Location: https://github.com/scrive/log.git+source-repository head+ type: git+ location: https://github.com/scrive/log.git +flag openssl+ description: Use http-client-openssl instead of http-client-tls+ default: False+ library- 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+ exposed-modules: Log.Backend.ElasticSearch Log.Backend.ElasticSearch.Internal- build-depends: base <5,- log-base >= 0.7,- aeson >=1.0.0.0,+ Log.Backend.ElasticSearch.Lens+ build-depends: base >= 4.13 && <5,+ aeson >= 1.0, aeson-pretty >=0.8.2,- bytestring, base64-bytestring,- bloodhound >= 0.13 && < 0.15,+ bytestring, deepseq, http-client,- http-client-tls,+ http-types,+ log-base >= 0.10 && <0.13,+ network-uri, semigroups, text, text-show >= 2, time >= 1.5, transformers,+ unliftio-core >= 0.2, unordered-containers, vector hs-source-dirs: src - ghc-options: -O2 -Wall -funbox-strict-fields+ ghc-options: -Wall default-language: Haskell2010 default-extensions: BangPatterns@@ -62,10 +61,16 @@ , GeneralizedNewtypeDeriving , LambdaCase , MultiParamTypeClasses- , NoImplicitPrelude , OverloadedStrings , RankNTypes , RecordWildCards , ScopedTypeVariables+ , TupleSections , TypeFamilies , UndecidableInstances++ if flag(openssl)+ cpp-options: -DOPENSSL+ build-depends: http-client-openssl >= 0.3.2+ else+ build-depends: http-client-tls >= 0.3.6.3
src/Log/Backend/ElasticSearch.hs view
@@ -1,5 +1,220 @@+-- | Elasticsearch logging back-end. module Log.Backend.ElasticSearch- {-# DEPRECATED "Use directly Log.Backend.ElasticSearch.V1 or V5" #-}- ( module Log.Backend.ElasticSearch.V1 ) where+ ( ElasticSearchConfig+ , esServer+ , esIndex+ , esShardCount+ , esReplicaCount+ , esMapping+ , esLogin+ , esLoginInsecure+ , checkElasticSearchLogin+ , checkElasticSearchConnection+ , defaultElasticSearchConfig+ , withElasticSearchLogger+ ) where -import Log.Backend.ElasticSearch.V1+import Control.Applicative+import Control.Concurrent+import Control.Exception+import Control.Monad+import Control.Monad.IO.Unlift+import Data.Aeson+import Data.Aeson.Encode.Pretty+import Data.IORef+import Data.Maybe+import Data.Time+import Log+import Log.Internal.Logger+import Network.HTTP.Client+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as T+import qualified Data.Traversable as F+import qualified Data.Vector as V++import Log.Backend.ElasticSearch.Internal+import qualified Log.Internal.Aeson.Compat as AC++----------------------------------------+-- | 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 :: MonadUnliftIO m => ElasticSearchConfig -> (Logger -> m r) -> m r+withElasticSearchLogger conf act = withRunInIO $ \unlift -> do+ logger <- elasticSearchLogger conf+ withLogger logger (unlift . act)++-- | 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 Logger+elasticSearchLogger esConf@ElasticSearchConfig{..} = do+ checkElasticSearchLogin esConf+ env <- mkEsEnv esConf+ versionRef <- newIORef Nothing+ indexRef <- newIORef T.empty+ mkBulkLogger "ElasticSearch" (\msgs -> do+ now <- getCurrentTime+ oldIndex <- readIORef indexRef+ retryOnException versionRef $ do+ -- We need to consider version of ES because ES >= 5.0.0 and ES >= 7.0.0+ -- have slight differences in parts of API used for logging.+ version <- readIORef versionRef >>= \case+ Just version -> pure version+ Nothing -> serverInfo env >>= \case+ Left (ex :: HttpException) -> error+ $ "elasticSearchLogger: unexpected error: "+ <> show ex+ <> " (is ElasticSearch server running?)"+ Right reply -> case parseEsVersion $ responseBody reply of+ Nothing -> error+ $ "elasticSearchLogger: invalid response when parsing version number: "+ <> show reply+ Just version -> pure version+ -- 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 = T.concat+ [ esIndex+ , "-"+ , T.pack $ formatTime defaultTimeLocale "%F" now+ ]+ when (oldIndex /= index) $ do+ -- There is an obvious race condition in presence of more than one+ -- logger instance running, but it's irrelevant as attempting to create+ -- index that already exists is harmless.+ ixExists <- indexExists env index+ unless ixExists $ do+ reply <- createIndexWithMapping version env esConf index+ unless (isSuccess reply) $ do+ printEsError "error while creating index" $ responseBody reply+ writeIORef indexRef index+ let jsonMsgs = V.fromList $ map toJsonMsg msgs+ reply <- responseBody <$> bulkIndex version env esConf index jsonMsgs+ -- Try to parse parts of reply to get information about log messages that+ -- failed to be inserted for some reason.+ case checkForBulkErrors jsonMsgs reply of+ Nothing -> printEsError "unexpected response" reply+ Just (hasErrors, responses) -> when hasErrors $ do+ -- If any message failed to be inserted because of type mismatch, go+ -- back to them, log the insertion failure and add type suffix to each+ -- of the keys in their "data" fields to work around type errors.+ let newMsgs =+ let modifyData :: Maybe Value -> Value -> Value+ modifyData merr (Object hm) = Object $+ let newData = AC.foldrWithKey keyAddValueTypeSuffix AC.empty hm+ in case merr of+ -- We have the error message, i.e. we're at the top+ -- level object, so add it to the data.+ Just err -> newData `AC.union` AC.singleton "__es_error" err+ Nothing -> newData+ modifyData _ v = v++ keyAddValueTypeSuffix k v acc = AC.insert+ (case v of+ Object{} -> k <> "_object"+ Array{} -> k <> "_array"+ String{} -> k <> "_string"+ Number{} -> k <> "_number"+ Bool{} -> k <> "_bool"+ Null{} -> k <> "_null"+ ) (modifyData Nothing v) acc+ in adjustFailedMessagesWith modifyData jsonMsgs responses+ -- Attempt to put modified messages.+ newReply <- responseBody <$> bulkIndex version env esConf index newMsgs+ case checkForBulkErrors newMsgs newReply of+ Nothing -> printEsError "unexpected response" newReply+ Just (newHasErrors, newResponses) -> when newHasErrors $ do+ -- If some of the messages failed again (it might happen e.g. if+ -- data contains an array with elements of different types), drop+ -- their data field.+ let newerMsgs =+ let modifyData :: Maybe Value -> Value -> Value+ modifyData (Just err) Object{} = object [ "__es_error" .= err ]+ modifyData _ v = v+ in adjustFailedMessagesWith modifyData newMsgs newResponses+ -- Ignore any further errors.+ void $ bulkIndex version env esConf index newerMsgs)+ (refreshIndex env =<< readIORef indexRef)+ where+ -- Process reply of bulk indexing to get responses for each index operation+ -- and check whether any insertion failed.+ checkForBulkErrors+ :: V.Vector a+ -> Value+ -> Maybe (Bool, V.Vector Object)+ checkForBulkErrors jsonMsgs replyBody = do+ Object response <- pure replyBody+ Bool hasErrors <- "errors" `AC.lookup` response+ Array jsonItems <- "items" `AC.lookup` response+ items <- F.forM jsonItems $ \v -> do+ Object item <- pure v+ Object index_ <- "index" `AC.lookup` item+ -- ES <= 2.x returns 'create' for some reason, so consider both.+ <|> "create" `AC.lookup` item+ pure index_+ guard $ V.length items == V.length jsonMsgs+ pure (hasErrors, items)++ adjustFailedMessagesWith+ :: (Maybe err -> obj -> obj)+ -> V.Vector (AC.KeyMap obj)+ -> V.Vector (AC.KeyMap err)+ -> V.Vector (AC.KeyMap obj)+ adjustFailedMessagesWith f jsonMsgs responses =+ let failed = V.imapMaybe (\n item -> (n, ) <$> "error" `AC.lookup` item) responses+ adjust act key m = case AC.lookup key m of+ Nothing -> m+ Just v -> AC.insert key (act v) m+ in (`V.map` failed) $ \(n, err) -> adjust (f $ Just err) "data" $ jsonMsgs V.! n++ printEsError msg body =+ T.putStrLn $ "elasticSearchLogger: " <> msg <> " " <> prettyJson body++ retryOnException :: forall r. IORef (Maybe EsVersion) -> IO r -> IO r+ retryOnException versionRef m = try m >>= \case+ Left (ex::SomeException) -> do+ putStrLn $ "ElasticSearch: unexpected error: "+ <> show ex <> ", retrying in 10 seconds"+ -- If there was an exception, ElasticSearch version might've changed, so+ -- reset it.+ writeIORef versionRef Nothing+ threadDelay $ 10 * 1000000+ retryOnException versionRef m+ Right result -> return result++ prettyJson :: Value -> T.Text+ prettyJson = TL.toStrict+ . T.toLazyText+ . encodePrettyToTextBuilder' defConfig { confIndent = Spaces 2 }++ toJsonMsg :: LogMessage -> Object+ toJsonMsg msg = case toJSON msg of+ Object jMsg -> jMsg+ value -> error $ "unexpected non-object: " ++ show value++----------------------------------------++-- | Check that login credentials are specified properly.+--+-- @since 0.10.0.0+checkElasticSearchLogin :: MonadIO m => ElasticSearchConfig -> m ()+checkElasticSearchLogin ElasticSearchConfig{..} = liftIO $ do+ 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."++-- | Check that we can connect to the ES server.+--+-- @since 0.10.0.0+checkElasticSearchConnection :: MonadIO m => ElasticSearchConfig -> m (Either HttpException ())+checkElasticSearchConnection esConf = liftIO $ do+ fmap (const ()) <$> (serverInfo =<< mkEsEnv esConf)
src/Log/Backend/ElasticSearch/Internal.hs view
@@ -1,5 +1,249 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-} module Log.Backend.ElasticSearch.Internal- {-# DEPRECATED "Use directly Log.Backend.ElasticSearch.V1 or V5" #-}- ( module Log.Backend.ElasticSearch.V1.Internal ) where+ ( ElasticSearchConfig(..)+ , defaultElasticSearchConfig+ -- * ES version+ , EsVersion(..)+ , parseEsVersion+ , esV5, esV7+ -- * ES commands+ , serverInfo+ , indexExists+ , createIndexWithMapping+ , bulkIndex+ , refreshIndex+ -- * ES communication details+ , EsEnv(..)+ , mkEsEnv+ , dispatch+ , decodeReply+ , isSuccess+ ) where -import Log.Backend.ElasticSearch.V1.Internal+import Control.Exception+import Control.Monad+import Data.Aeson+import Data.Ix (inRange)+import Data.Maybe+import GHC.Generics (Generic)+import Network.HTTP.Client+import Network.HTTP.Types+#if OPENSSL+import Network.HTTP.Client.OpenSSL (newOpenSSLManager, withOpenSSL)+#else+import Network.HTTP.Client.TLS (tlsManagerSettings)+#endif+import qualified Data.ByteString.Builder as BSB+import qualified Data.ByteString.Lazy.Char8 as BSL+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Vector as V++import qualified Log.Internal.Aeson.Compat as AC++-- | 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.+ , esShardCount :: !Int+ -- ^ Elasticsearch shard count for the named index.+ --+ -- @since 0.10.0.0+ , esReplicaCount :: !Int+ -- ^ Elasticsearch replica count for the named index.+ --+ -- @since 0.10.0.0+ , esMapping :: !T.Text+ -- ^ Elasticsearch mapping name (unused with ES >= 7.0.0)+ , esLogin :: Maybe (T.Text, T.Text)+ -- ^ Elasticsearch basic authentication username and password.+ , esLoginInsecure :: !Bool+ -- ^ Allow basic authentication over non-TLS connections.+ } deriving (Eq, Show, Generic)++-- | Sensible defaults for 'ElasticSearchConfig'.+defaultElasticSearchConfig :: ElasticSearchConfig+defaultElasticSearchConfig = ElasticSearchConfig+ { esServer = "http://localhost:9200"+ , esIndex = "logs"+ , esShardCount = 4+ , esReplicaCount = 1+ , esMapping = "log"+ , esLogin = Nothing+ , esLoginInsecure = False+ }++----------------------------------------+-- ES communication++-- Most of the below code is taken from the bloodhound library+-- (https://github.com/bitemyapp/bloodhound).++data EsVersion = EsVersion !Int !Int !Int+ deriving (Eq, Ord)++parseEsVersion :: Value -> Maybe EsVersion+parseEsVersion js = do+ Object props <- pure js+ Object version <- "version" `AC.lookup` props+ case "distribution" `AC.lookup` version of+ Just "opensearch" -> do+ -- OpenSearch is compatible (so far) with esV7 mappings.+ pure esV7+ _ -> do+ String number <- "number" `AC.lookup` version+ [v1, v2, v3] <- mapM (maybeRead . T.unpack) $ T.splitOn "." number+ pure $ EsVersion v1 v2 v3+ where+ maybeRead s = do+ [(v, "")] <- pure $ reads s+ pure v++-- | Minimum version with split 'string' type.+esV5 :: EsVersion+esV5 = EsVersion 5 0 0++-- | Minimum version without mapping types.+esV7 :: EsVersion+esV7 = EsVersion 7 0 0++----------------------------------------++-- | Check the ElasticSearch server for info. Result can be fed to+-- 'parseEsVersion' to determine version of the server.+serverInfo :: EsEnv -> IO (Either HttpException (Response Value))+serverInfo env = try $ dispatch env methodGet [] Nothing++-- | Check that given index exists.+indexExists :: EsEnv -> T.Text -> IO Bool+indexExists env index =+ isSuccess <$> dispatch env methodHead [index] Nothing++-- | Create an index with given mapping.+createIndexWithMapping+ :: EsVersion+ -> EsEnv+ -> ElasticSearchConfig+ -> T.Text+ -> IO (Response Value)+createIndexWithMapping version env ElasticSearchConfig{..} index = do+ dispatch env methodPut [index] . Just . encode $ object+ [ "settings" .= object+ [ "number_of_shards" .= esShardCount+ , "number_of_replicas" .= esReplicaCount+ ]+ , "mappings" .= if version >= esV7+ then logsMapping+ else object [ AC.fromText esMapping .= logsMapping ]+ ]+ where+ logsMapping = object+ [ "properties" .= object+ [ "time" .= object+ [ "type" .= timeTy+ , "format" .= ("date_time"::T.Text)+ ]+ , "domain" .= object+ [ "type" .= textTy+ ]+ , "level" .= object+ [ "type" .= textTy+ ]+ , "component" .= object+ [ "type" .= textTy+ ]+ , "message" .= object+ [ "type" .= textTy+ ]+ ]+ ]+ where+ timeTy :: T.Text+ timeTy = if version >= esV7+ then "date_nanos"+ else "date"++ textTy :: T.Text+ textTy = if version >= esV5+ then "text"+ else "string"++-- Index multiple log messages.+bulkIndex+ :: EsVersion+ -> EsEnv+ -> ElasticSearchConfig+ -> T.Text+ -> V.Vector Object+ -> IO (Response Value)+bulkIndex version env conf index objs = do+ dispatch env methodPost route . Just . BSB.toLazyByteString $ foldMap ixOp objs+ where+ route = if version >= esV7+ then [index, "_bulk"]+ else [index, esMapping conf, "_bulk"]++ ixOp obj = ixCmd+ <> BSB.char8 '\n'+ <> BSB.lazyByteString (encode $ Object obj)+ <> BSB.char8 '\n'+ where+ ixCmd = BSB.lazyByteString . encode $ object+ [ "index" .= object []+ ]++-- Refresh given index.+refreshIndex :: EsEnv -> T.Text -> IO ()+refreshIndex env index =+ void $ dispatch env methodPost [index, "_refresh"] Nothing++----------------------------------------++data EsEnv = EsEnv+ { envServer :: !T.Text+ , envManager :: !Manager+ , envRequestHook :: !(Request -> Request)+ }++mkEsEnv :: ElasticSearchConfig -> IO EsEnv+mkEsEnv ElasticSearchConfig{..} = do+#if OPENSSL+ envManager <- withOpenSSL newOpenSSLManager+#else+ envManager <- newManager tlsManagerSettings+#endif+ let envServer = esServer+ envRequestHook = maybe id mkAuthHook esLogin+ pure EsEnv{..}+ where+ mkAuthHook (u, p) = applyBasicAuth (T.encodeUtf8 u) (T.encodeUtf8 p)++----------------------------------------++dispatch :: EsEnv+ -> Method+ -> [T.Text]+ -> Maybe BSL.ByteString+ -> IO (Response Value)+dispatch EsEnv{..} dMethod url body = do+ initReq <- parseRequest $ T.unpack $ T.intercalate "/" $ envServer : url+ let req = envRequestHook . setRequestIgnoreStatus $ initReq+ { method = dMethod+ , requestBody = RequestBodyLBS $ fromMaybe BSL.empty body+ , requestHeaders = ("Content-Type", "application/json") : requestHeaders initReq+ }+ fmap decodeReply <$> httpLbs req envManager++decodeReply :: BSL.ByteString -> Value+decodeReply bs = case eitherDecode' bs of+ Right js -> js+ Left err -> object ["decoding_error" .= err]++isSuccess :: Response a -> Bool+isSuccess = statusCheck (inRange (200, 299))+ where+ statusCheck :: (Int -> Bool) -> Response a -> Bool+ statusCheck p = p . statusCode . responseStatus
src/Log/Backend/ElasticSearch/Lens.hs view
@@ -1,5 +1,58 @@+{-# LANGUAGE RankNTypes #-}+-- | Lensified version of "Log.Backend.ElasticSearch". module Log.Backend.ElasticSearch.Lens- {-# DEPRECATED "Use directly Log.Backend.ElasticSearch.V1 or V5" #-}- ( module Log.Backend.ElasticSearch.V1.Lens ) where+ ( I.ElasticSearchConfig+ , esServer+ , esIndex+ , esShardCount+ , esReplicaCount+ , esMapping+ , esLogin+ , esLoginInsecure+ , I.defaultElasticSearchConfig+ , I.withElasticSearchLogger+ ) where -import Log.Backend.ElasticSearch.V1.Lens+import qualified Data.Text as T+import qualified Log.Backend.ElasticSearch as I++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 = (\x -> esc { I.esServer = x }) <$> f (I.esServer esc)+{-# INLINE esServer #-}++-- | Elasticsearch index name.+esIndex :: Lens' I.ElasticSearchConfig T.Text+esIndex f esc = (\x -> esc { I.esIndex = x }) <$> f (I.esIndex esc)+{-# INLINE esIndex #-}++-- | Elasticsearch shard count for the named index.+--+-- @since 0.10.0.0+esShardCount :: Lens' I.ElasticSearchConfig Int+esShardCount f esc = (\x -> esc { I.esShardCount = x }) <$> f (I.esShardCount esc)+{-# INLINE esShardCount #-}++-- | Elasticsearch replica count for the named index.+--+-- @since 0.10.0.0+esReplicaCount :: Lens' I.ElasticSearchConfig Int+esReplicaCount f esc = (\x -> esc { I.esReplicaCount = x }) <$> f (I.esReplicaCount esc)+{-# INLINE esReplicaCount #-}++-- | Elasticsearch mapping name.+esMapping :: Lens' I.ElasticSearchConfig T.Text+esMapping f esc = (\x -> esc { I.esMapping = x }) <$> f (I.esMapping esc)+{-# INLINE esMapping #-}++-- | Elasticsearch basic authentication username and password.+esLogin :: Lens' I.ElasticSearchConfig (Maybe (T.Text, T.Text))+esLogin f esc = (\x -> esc { I.esLogin = x }) <$> f (I.esLogin esc)+{-# INLINE esLogin #-}++-- | Allow basic authentication over non-TLS connections.+esLoginInsecure :: Lens' I.ElasticSearchConfig Bool+esLoginInsecure f esc = (\x -> esc { I.esLoginInsecure = x }) <$> f (I.esLoginInsecure esc)+{-# INLINE esLoginInsecure #-}
− src/Log/Backend/ElasticSearch/V1.hs
@@ -1,283 +0,0 @@--- | 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 System.IO-import TextShow-import qualified Data.Aeson.Encoding as Aeson-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) ->- hPutStrLn stderr $ "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)- ]- ]- ]-- toEncoding LogsMapping = Aeson.pairs $ mconcat- [ Aeson.pair "properties" $ Aeson.pairs $ mconcat- [ Aeson.pair "insertion_order" $ Aeson.pairs $ mconcat- [ "type" .= ("integer"::T.Text)- ]- , Aeson.pair "insertion_time" $ Aeson.pairs $ mconcat- [ "type" .= ("date"::T.Text)- , "format" .= ("date_time"::T.Text)- ]- , Aeson.pair "time" $ Aeson.pairs $ mconcat- [ "type" .= ("date"::T.Text)- , "format" .= ("date_time"::T.Text)- ]- , Aeson.pair "domain" $ Aeson.pairs $ mconcat- [ "type" .= ("string"::T.Text)- ]- , Aeson.pair "level" $ Aeson.pairs $ mconcat- [ "type" .= ("string"::T.Text)- ]- , Aeson.pair "component" $ Aeson.pairs $ mconcat- [ "type" .= ("string"::T.Text)- ]- , Aeson.pair "message" $ Aeson.pairs $ mconcat- [ "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
@@ -1,31 +0,0 @@-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
@@ -1,40 +0,0 @@-{-# 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
@@ -1,254 +0,0 @@--- | 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 System.IO-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) ->- hPutStrLn stderr $ "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
@@ -1,31 +0,0 @@-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
@@ -1,40 +0,0 @@-{-# 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)