on master help sign in
27 files changed+1202−269
since splitsnapshots
unifiedsplit
.gitignore+1 .travis.yml+8−11 katip-datadog/README.md+3−5 katip-datadog/changelog.md+6−1 katip-datadog/katip-datadog.cabal+2−1 katip-datadog/src/Katip/Scribes/Datadog/TCP.hs+1−1 katip-elasticsearch/bench/Main.hs+3−3 katip-elasticsearch/changelog.md+21 katip-elasticsearch/docker-compose.yml+10 katip-elasticsearch/examples/example.hs+1−1 katip-elasticsearch/katip-elasticsearch.cabal+15−18 katip-elasticsearch/src/Katip/Scribes/ElasticSearch.hs+5 katip-elasticsearch/src/Katip/Scribes/ElasticSearch/Internal.hs+18−7 katip-elasticsearch/test/Main.hs+23−7 katip-logzio/README.md−4 katip-logzio/changelog.md+8−1 katip-logzio/katip-logzio.cabal+38−7 katip-logzio/src/Katip/Scribes/LogzIO/HTTPS.hs+428−147 katip-logzio/test/Katip/Tests/Scribes/LogzIO/HTTPS.hs+466 katip-logzio/test/Main.hs+3−2 katip/README.md+1−1 katip/changelog.md+37 katip/katip.cabal+22−22 katip/src/Katip.hs+3−1 katip/src/Katip/Core.hs+46−20 katip/src/Katip/Monadic.hs+28−6 stack.yaml+5−3
.gitignore+1
@@ -7,3 +7,4 @@ TAGS
77*.hi
88*.dyn_o
99*.dyn_hi
10+stack.yaml.lock
.travis.yml+8−11
@@ -9,23 +9,19 @@ addons:
99 apt:
1010 packages:
1111 - libgmp-dev
12− - oracle-java8-installer
1312
1413# The different configurations we want to test. You could also do things like
1514# change flags or use --stack-yaml to point to a different file.
1615matrix:
1716 include:
18− - env: ARGS="--resolver=lts-8" ESVERSION=1.5.2 ESURL=https://download.elastic.co/elasticsearch/elasticsearch ESTESTARGS="--es-version 1"
19− - env: ARGS="--resolver=lts-9" ESVERSION=1.5.2 ESURL=https://download.elastic.co/elasticsearch/elasticsearch ESTESTARGS="--es-version 1"
20− - env: ARGS="--resolver=lts-10" ESVERSION=1.5.2 ESURL=https://download.elastic.co/elasticsearch/elasticsearch ESTESTARGS="--es-version 1"
21− - env: ARGS="--resolver=lts-11" ESVERSION=1.5.2 ESURL=https://download.elastic.co/elasticsearch/elasticsearch ESTESTARGS="--es-version 1"
22− - env: ARGS="--resolver=lts-12" ESVERSION=1.5.2 ESURL=https://download.elastic.co/elasticsearch/elasticsearch ESTESTARGS="--es-version 1"
23− - env: ARGS="--resolver=nightly" ESVERSION=5.5.0 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m" JAVA_HOME=/usr/lib/jvm/java-8-oracle
24− - env: ARGS="--resolver=nightly" ESVERSION=6.2.2 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m" JAVA_HOME=/usr/lib/jvm/java-8-oracle
25− # nightlies may fail from time to time
17+ - env: ARGS="--resolver=lts-16" ESVERSION=5.5.0 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m"
18+ - env: ARGS="--resolver=lts-16" ESVERSION=6.2.2 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m"
19+ - env: ARGS="--resolver=nightly" ESVERSION=5.5.0 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m"
20+ - env: ARGS="--resolver=nightly" ESVERSION=6.2.2 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m"
21+ # nightlies may fail usually due to lagging dependencies
2622 allow_failures:
27− - env: ARGS="--resolver=nightly" ESVERSION=5.5.0 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m" JAVA_HOME=/usr/lib/jvm/java-8-oracle
28− - env: ARGS="--resolver=nightly" ESVERSION=6.2.2 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m" JAVA_HOME=/usr/lib/jvm/java-8-oracle
23+ - env: ARGS="--resolver=nightly" ESVERSION=5.5.0 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m"
24+ - env: ARGS="--resolver=nightly" ESVERSION=6.2.2 ESURL=https://artifacts.elastic.co/downloads/elasticsearch ESTESTARGS="--es-version 5" ES_JAVA_OPTS="-Xms500m -Xmx750m"
2925
3026before_install:
3127# Download and unpack the stack executable
@@ -38,6 +34,7 @@ install:
3834 - tar xzf elasticsearch-$ESVERSION.tar.gz
3935 # set up a repo for snapshot testing. Required in ES >= 1.6
4036 - echo "path.repo = [\"/tmp\"]" >> ./elasticsearch-$ESVERSION/elasticsearch.yml
37+ - java -version
4138 - ./elasticsearch-$ESVERSION/bin/elasticsearch &
4239 # Bloodhound seems to take a while to build and can sometimes stall out the build when travis detects no output.
4340
katip-datadog/README.md+3−5
@@ -1,8 +1,6 @@
11# Katip Datadog [![Build Status](https://travis-ci.org/Soostone/katip.svg?branch=master)](https://travis-ci.org/Soostone/katip)
22
33katip-datadog is a scribe for the Katip logging framework that sends
4−structured logs to the Datadog logging service.
5−
6−## Warning
7−
8−This is an unreleased library and is not yet production-ready.
4+structured logs to the Datadog logging service. It supports both
5+directly sending to the ingestion API and through a locally installed
6+datadog agent.
katip-datadog/changelog.md+6−1
@@ -1,2 +1,7 @@
1−Unreleased
1+0.2.0.0
22==========
3+* Remove import of deprecated Network module
4+
5+0.1.0.0
6+==========
7+* Initial release
katip-datadog/katip-datadog.cabal+2−1
@@ -1,5 +1,6 @@
11name: katip-datadog
2−version: 0.0.1.0
2+version: 0.2.0.0
3+synopsis: Datadog scribe for the Katip logging framework
34description: See README.md for more details.
45license: BSD3
56license-file: LICENSE
katip-datadog/src/Katip/Scribes/Datadog/TCP.hs+1−1
@@ -33,8 +33,8 @@ import qualified Data.Text.Lazy.Builder as B
3333import Data.Time (NominalDiffTime)
3434import Katip
3535import Katip.Core (LocJs (..))
36−import qualified Network as Net
3736import qualified Network.Connection as C
37+import qualified Network.Socket as Net
3838import qualified System.Posix.Types as POSIX
3939-------------------------------------------------------------------------------
4040
katip-elasticsearch/bench/Main.hs+3−3
@@ -15,11 +15,11 @@ import qualified Data.HashMap.Strict as HM
1515import Data.Proxy (Proxy (..))
1616import System.Random
1717import qualified Data.Text as T
18−import Database.V1.Bloodhound.Types
18+import Database.Bloodhound.Types
1919import Numeric
2020-------------------------------------------------------------------------------
2121import Katip.Scribes.ElasticSearch
22−import Katip.Scribes.ElasticSearch.Internal (ESV1)
22+import Katip.Scribes.ElasticSearch.Internal (ESV5)
2323-------------------------------------------------------------------------------
2424
2525main :: IO ()
@@ -34,7 +34,7 @@ main = do
3434mkDocIdBenchmark :: Benchmark
3535mkDocIdBenchmark = bgroup "mkDocId"
3636 [
37− bench "mkDocId (randomIO)" $ nfIO (mkDocId (Proxy :: Proxy ESV1))
37+ bench "mkDocId (randomIO)" $ nfIO (mkDocId (Proxy :: Proxy ESV5))
3838 , bench "mkDocId' (shared )" $ nfIO mkDocId'
3939 ]
4040
katip-elasticsearch/changelog.md+21
@@ -1,3 +1,24 @@
1+0.7.0.0
2+=======
3+* Make compatible with unreleased bloodhound 0.17
4+* Deprecate use of ES version 1 which has been removed from bloodhound
5+ 0.17. All users should move over to ESV5 which should work for
6+ versions 5 and up.
7+* Default configuration to ESV5
8+* Add deprecation warnings for ESV1. Support for ESV1 will be removed
9+ in the future.
10+
11+0.6.0.0
12+=======
13+* Update to katip-0.8.0.0 `PermitFunc` convention. To update, for
14+ example, if you were passing `InfoS` to `mkEsScribe` to filter
15+ severities, instead you should pass `(permitItem InfoS)`.
16+* Update examples to add necessary type signature for the default
17+ scribe config.
18+* Bugfix: the default error retry policy was much more rapid than
19+ documented.
20+* Internally switch to safe-exceptions from encolosed-exceptions.
21+
1220.5.1.1
223=======
324* Loosen katip dep
katip-elasticsearch/docker-compose.ymlnew file+10
@@ -0,0 +1,10 @@
1+version: "3.7"
2+services:
3+ # https://hub.docker.com/_/elasticsearch
4+ elasticsearch:
5+ image: elasticsearch:5-alpine
6+ ports:
7+ - target: 9200
8+ published: 9200
9+ protocol: tcp
10+ mode: host
katip-elasticsearch/examples/example.hs+1−1
@@ -6,7 +6,7 @@ module Main
66
77-------------------------------------------------------------------------------
88import Control.Exception
9−import Database.V5.Bloodhound
9+import Database.Bloodhound
1010import Network.HTTP.Client
1111-------------------------------------------------------------------------------
1212import Katip
katip-elasticsearch/katip-elasticsearch.cabal+15−18
@@ -1,7 +1,7 @@
11name: katip-elasticsearch
22synopsis: ElasticSearch scribe for the Katip logging framework.
33description: See README.md for more details.
4−version: 0.6.0.0
4+version: 0.7.0.0
55license: BSD3
66license-file: LICENSE
77author: Ozgun Ataman, Michael Xavier
@@ -35,23 +35,23 @@ library
3535 Katip.Scribes.ElasticSearch.Internal
3636 build-depends:
3737 base >=4.6 && <5
38− , katip >= 0.2.0.0 && < 0.9
39− , bloodhound >= 0.13.0.0 && < 0.17
40− , uuid >= 1.3.12 && < 1.4
38+ , katip >= 0.8.0.0
39+ , bloodhound >= 0.13.0.0
40+ , uuid >= 1.3.12
4141 , aeson >=0.6
42− , stm >= 2.4.3 && < 2.6
43− , stm-chans >= 3.0.0.2 && < 3.1
44− , async >= 2.0.1.0 && < 3.0
42+ , stm >= 2.4.3
43+ , stm-chans >= 3.0.0.2
44+ , async >= 2.0.1.0
4545 , safe-exceptions >= 0.1.0.0
46− , http-client >= 0.3 && < 0.6
47− , text >= 0.11 && < 1.3
48− , retry >= 0.7 && < 0.8
46+ , http-client >= 0.3
47+ , text >= 0.11
48+ , retry >= 0.7
4949 , exceptions
50− , scientific >= 0.3.0.0 && < 0.4
51− , unordered-containers >= 0.1.0.0 && < 0.3
52− , transformers >= 0.2 && < 0.6
53− , http-types >= 0.8 && < 0.13
54− , time >= 1 && < 1.9
50+ , scientific >= 0.3.0.0
51+ , unordered-containers >= 0.1.0.0
52+ , transformers >= 0.2
53+ , http-types >= 0.8
54+ , time >= 1
5555 , bytestring
5656 , semigroups
5757 hs-source-dirs: src
@@ -108,7 +108,4 @@ test-suite test
108108 , bytestring
109109 , tagged
110110
111− if flag(lib-Werror)
112− ghc-options: -Wall -Werror
113−
114111 ghc-options: -threaded -rtsopts
katip-elasticsearch/src/Katip/Scribes/ElasticSearch.hs+5
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12-- | Includes a scribe that can be used to log structured, JSON log
23-- messages to ElasticSearch. These logs can be explored easily using
34-- <https://www.elastic.co/products/kibana kibana> or your tool of
@@ -67,12 +68,16 @@ module Katip.Scribes.ElasticSearch
6768 , essIndexSettings
6869 , essIndexSharding
6970 , defaultEsScribeCfg
71+#if !MIN_VERSION_bloodhound(0,17,0)
7072 , defaultEsScribeCfgV1
73+#endif
7174 , defaultEsScribeCfgV5
7275 -- ** Version-Proxied APIS
7376 -- $versionproxies
7477 , defaultEsScribeCfg'
78+#if !MIN_VERSION_bloodhound(0,17,0)
7579 , ESV1
80+#endif
7681 , ESV5
7782 -- * Utilities
7883 , mkDocId
katip-elasticsearch/src/Katip/Scribes/ElasticSearch/Internal.hs+18−7
@@ -28,7 +28,7 @@ import Control.Retry (RetryPolicy,
2828import Data.Aeson
2929import Data.ByteString.Lazy (ByteString)
3030import Data.List.NonEmpty (NonEmpty (..))
31−import Data.Monoid ((<>))
31+import Data.Monoid as Monoid
3232import Data.Text (Text)
3333import qualified Data.Text as T
3434import qualified Data.Text.Encoding as T
@@ -37,8 +37,12 @@ import Data.Time.Calendar.WeekDate
3737import Data.Typeable as Typeable
3838import Data.UUID
3939import qualified Data.UUID.V4 as UUID4
40+#if MIN_VERSION_bloodhound(0,17,0)
41+import qualified Database.Bloodhound as V5
42+#else
4043import qualified Database.V1.Bloodhound as V1
4144import qualified Database.V5.Bloodhound as V5
45+#endif
4246import Network.HTTP.Client
4347import Network.HTTP.Types.Status
4448import Text.Printf (printf)
@@ -95,7 +99,7 @@ data EsScribeCfg v = EsScribeCfg {
9599-- * DailyIndexSharding
96100defaultEsScribeCfg' :: ESVersion v => proxy v -> EsScribeCfg v
97101defaultEsScribeCfg' prx = EsScribeCfg {
98− essRetryPolicy = exponentialBackoff 25000 <> limitRetries 5
102+ essRetryPolicy = exponentialBackoff 25000 Monoid.<> limitRetries 5
99103 , essQueueSize = EsQueueSize 1000
100104 , essPoolSize = EsPoolSize 2
101105 , essAnnotateTypes = False
@@ -104,19 +108,22 @@ defaultEsScribeCfg' prx = EsScribeCfg {
104108 }
105109
106110
111+
112+
107113-------------------------------------------------------------------------------
108−-- | Alias of 'defaultEsScribeCfgV1' to minimize API
114+-- | Alias of 'defaultEsScribeCfgV5' to minimize API
109115-- breakage. Previous versions of katip-elasticsearch only supported
110−-- ES version 1.
111−defaultEsScribeCfg :: EsScribeCfg ESV1
112−defaultEsScribeCfg = defaultEsScribeCfgV1
116+-- ES version 1 and defaulted to it.
117+defaultEsScribeCfg :: EsScribeCfg ESV5
118+defaultEsScribeCfg = defaultEsScribeCfgV5
113119
114120
115121-------------------------------------------------------------------------------
122+#if !MIN_VERSION_bloodhound(0,17,0)
116123-- | EsScribeCfg that will use ElasticSearch V1
117124defaultEsScribeCfgV1 :: EsScribeCfg ESV1
118125defaultEsScribeCfgV1 = defaultEsScribeCfg' (Typeable.Proxy :: Typeable.Proxy ESV1)
119−
126+#endif
120127
121128-------------------------------------------------------------------------------
122129-- | EsScribeCfg that will use ElasticSearch V5
@@ -484,7 +491,10 @@ class ESVersion v where
484491
485492
486493
494+#if !MIN_VERSION_bloodhound(0,17,0)
487495data ESV1 = ESV1
496+{-# DEPRECATED ESV1 "ESV1 is deprecated and removed in bloodhound >= 0.17.0" #-}
497+
488498
489499instance ESVersion ESV1 where
490500 type BHEnv ESV1 = V1.BHEnv
@@ -524,6 +534,7 @@ instance ESVersion ESV1 where
524534 [ "type" .= String "string"
525535 , "index" .= String "analyzed"
526536 ]
537+#endif
527538
528539
529540data ESV5 = ESV5
katip-elasticsearch/test/Main.hs+23−7
@@ -34,8 +34,12 @@ import Data.Time
3434import Data.Time.Calendar.WeekDate
3535import Data.Typeable as Typeable
3636import qualified Data.Vector as V
37+#if MIN_VERSION_bloodhound(0,17,0)
38+import qualified Database.Bloodhound as V5
39+#else
3740import qualified Database.V1.Bloodhound as V1
3841import qualified Database.V5.Bloodhound as V5
42+#endif
3943import Network.HTTP.Client
4044import Network.HTTP.Types.Status
4145import Test.QuickCheck.Instances ()
@@ -54,8 +58,10 @@ main :: IO ()
5458main = defaultMainWithIngredients ings $ askOption $ \vers -> testGroup "katip-elasticsearch"
5559 [
5660 case vers of
57− TestV1 -> esTests (Typeable.Proxy :: Typeable.Proxy ESV1)
58− TestV5 -> esTests (Typeable.Proxy :: Typeable.Proxy ESV5)
61+#if !MIN_VERSION_bloodhound(0,17,0)
62+ TestV1 -> esTests (Typeable.Proxy :: Typeable.Proxy ESV1)
63+#endif
64+ TestV5 -> esTests (Typeable.Proxy :: Typeable.Proxy ESV5)
5965 , typeAnnotatedTests
6066 , roundToSundayTests
6167 ]
@@ -64,18 +70,26 @@ main = defaultMainWithIngredients ings $ askOption $ \vers -> testGroup "katip-e
6470
6571
6672-------------------------------------------------------------------------------
67−data TestWithESVersion = TestV1
68− | TestV5
69− deriving (Typeable)
73+data TestWithESVersion = TestV5
74+#if !MIN_VERSION_bloodhound(0,17,0)
75+ | TestV1
76+#endif
77+ deriving (Typeable)
7078
7179
7280instance IsOption TestWithESVersion where
73− defaultValue = TestV1
81+ defaultValue = TestV5
82+#if !MIN_VERSION_bloodhound(0,17,0)
7483 parseValue "1" = Just TestV1
84+#endif
7585 parseValue "5" = Just TestV5
7686 parseValue _ = Nothing
7787 optionName = Tagged "es-version"
78− optionHelp = Tagged "Version of ES to test against, either 1 or 5, defaulting to 1."
88+#if MIN_VERSION_bloodhound(0,17,0)
89+ optionHelp = Tagged "Version of ES to test against, either 1 or 5, defaulting to 5."
90+#else
91+ optionHelp = Tagged "Version of ES to test against, only v5 is supported, but should work for higher versions."
92+#endif
7993
8094
8195class ESVersion v => TestESVersion v where
@@ -101,6 +115,7 @@ class ESVersion v => TestESVersion v where
101115 searchByIndex :: proxy v -> IndexName v -> Search v -> BH v IO (Response ByteString)
102116
103117
118+#if !MIN_VERSION_bloodhound(0,17,0)
104119instance TestESVersion ESV1 where
105120 type Server ESV1 = V1.Server
106121 toServer _ = V1.Server
@@ -122,6 +137,7 @@ instance TestESVersion ESV1 where
122137 refreshIndex _ = V1.refreshIndex
123138 withBH _ = V1.withBH
124139 searchByIndex _ = V1.searchByIndex
140+#endif
125141
126142
127143instance TestESVersion ESV5 where
katip-logzio/README.md−4
@@ -2,7 +2,3 @@
22
33katip-logzio is a scribe for the Katip logging framework that sends
44structured logs to the logz.io centralized logging platform.
5−
6−## Warning
7−
8−This is an unreleased library and is not yet production-ready.
katip-logzio/changelog.md+8−1
@@ -1,2 +1,9 @@
1−Unreleased
1+0.1.1.0
2+=======
3+* Fixed bug where on scribe close, any items left in the buffer that
4+ hadn't timed out or been flushed already would be dropped rather
5+ than flushed. All users are encouraged to upgrade.
6+
7+0.1.0.0
28==========
9+* Initial release
katip-logzio/katip-logzio.cabal+38−7
@@ -1,5 +1,6 @@
11name: katip-logzio
2−version: 0.0.1.0
2+version: 0.1.1.0
3+synopsis: Logz.IO scribe for the Katip logging framework
34description: See README.md for more details.
45license: BSD3
56license-file: LICENSE
@@ -15,7 +16,6 @@ extra-source-files:
1516 README.md
1617 changelog.md
1718
18−--TODO: fill this in
1919tested-with: GHC == 8.4.3
2020
2121source-repository head
@@ -33,17 +33,27 @@ library
3333 build-depends:
3434 base >=4.8.0.0 && <5
3535 , katip >= 0.8.0.0
36− , safe-exceptions
3736 , stm >= 2.1.1
3837 , async
39− , time
38+ , time >= 1.8.0.2
4039 , aeson >= 0.10.0.0
4140 , unix
4241 , unordered-containers
42+ , http-client >= 0.1.0
43+ , http-client-tls >= 0.3.4
44+ , uri-bytestring
45+ , errors >= 2.0.0
46+ , safe-exceptions
47+ , retry
48+ , http-types
49+ , text
50+ , bytestring
51+ , scientific
52+ , stm-chans >= 2.0.0
4353 hs-source-dirs: src
4454 default-language: Haskell2010
4555 hs-source-dirs: src
46− ghc-options: -Wall
56+ ghc-options: -Wall -Wall -Wnoncanonical-monad-instances
4757 if flag(lib-Werror)
4858 ghc-options: -Werror
4959
@@ -53,11 +63,32 @@ test-suite test
5363 main-is: Main.hs
5464 hs-source-dirs: test
5565 default-language: Haskell2010
56− -- other-modules:
66+ other-modules:
67+ Katip.Tests.Scribes.LogzIO.HTTPS
5768 build-depends: base
5869 , katip
5970 , katip-logzio
6071 , tasty
61− ghc-options: -Wall
72+ , tasty-hedgehog
73+ , tasty-hunit
74+ , hedgehog
75+ , vector
76+ , unordered-containers
77+ , text
78+ , bytestring
79+ , time
80+ , template-haskell
81+ , aeson
82+ , scientific
83+ , unix
84+ , hostname
85+ , scotty
86+ , stm
87+ , async
88+ , safe-exceptions
89+ , uri-bytestring
90+ , warp
91+ , http-types
92+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -Wnoncanonical-monad-instances
6293 if flag(lib-Werror)
6394 ghc-options: -Werror
katip-logzio/src/Katip/Scribes/LogzIO/HTTPS.hs+428−147
@@ -1,3 +1,9 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE GADTs #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE TypeApplications #-}
17-- | This is a log scribe that writes logs to logz.io's bulk
28-- <https://app.logz.io/#/dashboard/data-sources/Bulk-HTTPS HTTPS
39-- API>.
@@ -7,25 +13,60 @@ module Katip.Scribes.LogzIO.HTTPS
713 -- * Types
814 , BulkAPIError(..)
915 , LogzIOScribeConfiguration(..)
16+ , Scheme(..)
17+ , APIToken(..)
18+ , LoggingError(..)
19+ -- ** Presets for configuration
20+ , usRegionHost
21+ , euRegionHost
22+ , httpsPort
23+ , httpPort
24+ , defaultRetryPolicy
25+ , defaultLogzIOScribeConfiguration
26+ -- * Internal API exported for testing
27+ , renderLineTruncated
28+ , renderLineTruncated'
29+ , maxPayloadBytes
30+ , maxLogLineLength
31+ , BulkBuffer (..)
32+ , LogAction (..)
33+ , bufferItem
34+ , bufferItem'
35+ , forceFlush
36+ , Bytes (..)
1037 ) where
1138
1239
1340-------------------------------------------------------------------------------
1441import Control.Applicative
15−import qualified Control.Concurrent.Async as Async
16−import qualified Control.Concurrent.STM as STM
17−import qualified Data.Aeson as A
18−import qualified Data.ByteString.Builder as BB
19−import qualified Data.ByteString.Lazy as LBS
20−import qualified Data.HashMap.Strict as HM
42+import qualified Control.Concurrent.Async as Async
43+import qualified Control.Concurrent.STM as STM
44+import qualified Control.Concurrent.STM.TBMQueue as STM
45+import qualified Control.Error as E
46+import qualified Control.Exception.Safe as EX
47+import Control.Monad
48+import qualified Control.Retry as Retry
49+import qualified Data.Aeson as A
50+import qualified Data.ByteString.Builder as BB
51+import qualified Data.ByteString.Lazy as LBS
52+import qualified Data.ByteString.Lazy.Char8 as LBS8
53+import qualified Data.HashMap.Strict as HM
2154import Data.Int
22−import qualified Data.Text.Lazy as TL
23−import qualified Data.Text.Lazy.Builder as TB
24−import qualified Data.Time as Time
25−import qualified Katip as K
26−import Katip.Core (LocJs (..))
27−import qualified System.Posix.Types as POSIX
28−-------------------------------------------------------------------------------
55+import qualified Data.Scientific as Scientific
56+import Data.Semigroup as Semigroup
57+import Data.String (IsString)
58+import qualified Data.Text as T
59+import qualified Data.Text.Encoding as TE
60+import qualified Data.Text.Lazy as TL
61+import qualified Data.Text.Lazy.Builder as TB
62+import qualified Data.Time as Time
63+import qualified Katip as K
64+import Katip.Core (LocJs (..))
65+import qualified Network.HTTP.Client as HTTP
66+import qualified Network.HTTP.Client.TLS as HTTPS
67+import qualified Network.HTTP.Types as HTypes
68+import qualified System.Posix.Types as POSIX
69+import qualified URI.ByteString as URIBS
2970-------------------------------------------------------------------------------
3071
3172
@@ -47,7 +88,21 @@ data BulkAPIError = BulkAPIError
4788 -- concept of a truly empty log line in katip, so this most likely
4889 -- indicates a __library bug__ and should be reported as an issue on
4990 -- GitHub.
50− }
91+ } deriving (Show, Eq)
92+
93+
94+instance A.FromJSON BulkAPIError where
95+ parseJSON = A.withObject "BulkAPIError" $ \o -> do
96+ malformedLines <- o A..: "malformedLines"
97+ successfulLines <- o A..: "successfulLines"
98+ oversizedLines <- o A..: "oversizedLines"
99+ emptyLogLines <- o A..: "emptyLogLines"
100+ pure $ BulkAPIError
101+ { bulkAPIError_malformedLines = malformedLines
102+ , bulkAPIError_successfulLines = successfulLines
103+ , bulkAPIError_oversizedLines = oversizedLines
104+ , bulkAPIError_emptyLogLines = emptyLogLines
105+ }
51106
52107
53108-------------------------------------------------------------------------------
@@ -59,10 +114,94 @@ data LogzIOScribeConfiguration = LogzIOScribeConfiguration
59114 , logzIOScribeConfiguration_bufferTimeout :: Time.NominalDiffTime
60115 -- ^ Will flush the buffer if it has been this long since the last
61116 -- flush __or__ 'logzIOScribeConfiguration_bufferItems' items are
62− -- accumulated, whichever is first.
63− --TODO: more
117+ -- accumulated, whichever is first. NominalDiffTime has a Num
118+ -- instance, so you can use a literal to specify seconds, e.g. 30 =
119+ -- 30s.
120+ , logzIOScribeConfiguration_scheme :: Scheme
121+ , logzIOScribeConfiguration_host :: URIBS.Host
122+ , logzIOScribeConfiguration_port :: URIBS.Port
123+ , logzIOScribeConfiguration_token :: APIToken
124+ , logzIOScribeConfiguration_retry :: Retry.RetryPolicyM IO
125+ -- ^ How should exceptions during writes be retried?
126+ , logzIOScribeConfiguration_onError :: LoggingError -> IO ()
64127 }
65128
129+
130+-- | A default configuration:
131+--
132+-- * 100 item buffering
133+--
134+-- * 30 second buffer timeout
135+--
136+-- * US, HTTPS logging endpoint (listener.logz.io:8071)
137+--
138+-- * 'defaultRetryPolicy' of 25ms exponential backoff, 5 retries
139+--
140+-- * Ignore logging errors
141+defaultLogzIOScribeConfiguration :: APIToken -> LogzIOScribeConfiguration
142+defaultLogzIOScribeConfiguration token = LogzIOScribeConfiguration
143+ { logzIOScribeConfiguration_bufferItems = 100
144+ , logzIOScribeConfiguration_bufferTimeout = 30
145+ , logzIOScribeConfiguration_scheme = HTTPS
146+ , logzIOScribeConfiguration_host = usRegionHost
147+ , logzIOScribeConfiguration_port = httpsPort
148+ , logzIOScribeConfiguration_token = token
149+ , logzIOScribeConfiguration_retry = defaultRetryPolicy
150+ , logzIOScribeConfiguration_onError = const (pure ())
151+ }
152+
153+
154+-------------------------------------------------------------------------------
155+-- | You can retrieve your account or sub-account's API token on the
156+-- <https://app.logz.io/#/dashboard/settings/manage-accounts manage
157+-- accounts page>. Note that APIToken has an IsString instance,
158+-- meaning that you can use a string literal with OverloadedStrings
159+-- enabled.
160+newtype APIToken = APIToken
161+ { apiToken :: T.Text
162+ } deriving (Show, Eq, IsString)
163+
164+
165+-- | This particular bulk API only supports HTTP or HTTPS. HTTPS is
166+-- strongly recommended for security reasons.
167+data Scheme
168+ = HTTPS
169+ -- ^ HTTPs should always be used except for local testing.
170+ | HTTP
171+ deriving (Show, Eq)
172+
173+
174+-- | See
175+-- <https://support.logz.io/hc/en-us/articles/210206365-What-IP-addresses-should-I-open-in-my-firewall-to-ship-logs-to-Logz-io-
176+-- this> for a list of listeners. This is the US region host,
177+-- listener.logz.io
178+usRegionHost :: URIBS.Host
179+usRegionHost = URIBS.Host "listener.logz.io"
180+
181+
182+-- | See
183+-- <https://support.logz.io/hc/en-us/articles/210206365-What-IP-addresses-should-I-open-in-my-firewall-to-ship-logs-to-Logz-io-
184+-- this> for a list of listeners. This is the EU region host,
185+-- listener.logz.io
186+euRegionHost :: URIBS.Host
187+euRegionHost = URIBS.Host "listener-eu.logz.io"
188+
189+
190+-- | Logz.io uses port 8071 for HTTPS
191+httpsPort :: URIBS.Port
192+httpsPort = URIBS.Port 8071
193+
194+
195+-- | Logz.io uses port 8070 for HTTP
196+httpPort :: URIBS.Port
197+httpPort = URIBS.Port 8070
198+
199+
200+-- | A reasonable retry policy: exponential backoff with 25ms base
201+-- delay up to 5 retries, for a total cumulative delay of 775ms.
202+defaultRetryPolicy :: (Monad m) => Retry.RetryPolicyM m
203+defaultRetryPolicy = Retry.exponentialBackoff 25000 `mappend` Retry.limitRetries 5
204+
66205-------------------------------------------------------------------------------
67206mkLogzIOScribe
68207 :: LogzIOScribeConfiguration
@@ -70,112 +209,113 @@ mkLogzIOScribe
70209 -> K.Verbosity
71210 -> IO K.Scribe
72211mkLogzIOScribe config permitItem verbosity = do
73− -- Accumulating buffer for each item
74− bufferState <- STM.newTVarIO (mempty :: BulkBuffer)
75− -- Single-slot TMVar when something is ready to write. Katip buffers
76− -- for us so we can simplify shutdown and flush guarantees this way
77− readyMVar <- STM.newEmptyTMVarIO :: IO (STM.TMVar BulkBuffer)
78−
79− -- We create a TVar that is set to ShutDown when its time to stop
80− killSwitch <- STM.newTVarIO Continue
81−
82− -- Create a mutable ref to whether the timer has expired. This lets
83− -- us replace the timer when its time to start timing again. We
84− -- reset the timer __after__ a write goes out, not as soon as it expires
85− timerRef <- STM.atomically $ do
86− -- If there's never been a log, no need to start the timer
87− never <- STM.newTVar False
88− STM.newTVar never
89−
90−
91− -- on every push, extend deadline
92− -- thread waits for deadline and pushes. need a way to be atomic though, cancelthread is heavy handed
93− --TODO: strictness
94−
95− -- in STM: resolve an action with the buffers
96− let processLogAction logAction = case logAction of
97− Buffered newBuffer -> STM.writeTVar bufferState newBuffer
98− FlushNow toFlush newBuffer -> do
99− STM.writeTVar bufferState newBuffer
100− STM.putTMVar readyMVar toFlush
101−
102− -- In recompute the new buffer state from a new log item and take action
103− let push logItem = do
104− curBuffer <- STM.readTVar bufferState
105− processLogAction (bufferItem itemBufferSize verbosity logItem curBuffer)
106−
107−
108− -- A transaction that retries until its time to shutdown
109− let waitShutdown = do
110− killStatus <- STM.readTVar killSwitch
111− STM.check (killStatus == ShutDown)
112−
113− --TODO: exception handling
114−
115− -- Retry transaction until timer has expired
116− let waitTimer = do
117− -- get the timer we'll be using
118− timer <- STM.readTVar timerRef
119− -- do not proceed until its ready
120− timerExpired <- STM.readTVar timer
121− STM.check timerExpired
122− -- do not wake again until this message is out the door
123− STM.writeTVar timer False
124− -- we waited long enough, grab the current buffer
125− curBuffer <- STM.readTVar bufferState
126− -- and tell it to flush right now
127− processLogAction (forceFlush curBuffer)
128−
129− -- loop until its time to shut down
130− let timedFlushLoop = do
131− res <- STM.atomically $
132− raceAlt
133− waitShutdown
134− waitTimer
212+ -- This size is actually somewhat arbitrary. We're just making sure
213+ -- it isn't infinite so we don't consume the whole backlog right
214+ -- away and take up lots of memory.. Katip above us does bounded
215+ -- buffering and load-shedding when there's too much of a
216+ -- backlog. Our writes are blocking to apply backpressure up to
217+ -- katip. This value just holds onto values temporarily while we
218+ -- concatenate them into a buffer.
219+ ingestionQueue <- STM.newTBMQueueIO (itemBufferSize * 10)
220+
221+ let newTimer = STM.registerDelay itemBufferTimeoutMicros
222+ timerRef <- STM.newTVarIO =<< newTimer
223+
224+ -- Set up a connection manager for requests
225+ mgr <- case scheme of
226+ HTTPS -> HTTPS.newTlsManager
227+ HTTP -> HTTP.newManager HTTP.defaultManagerSettings
228+
229+ -- An STM transaction that will return true when writes are stopped
230+ -- and backlog is emptied.
231+ let workExhausted :: STM.STM Bool
232+ workExhausted = (&&)
233+ <$> STM.isClosedTBMQueue ingestionQueue
234+ <*> STM.isEmptyTBMQueue ingestionQueue
235+ -- Block until there's no more work
236+ let waitWorkExhausted :: STM.STM ()
237+ waitWorkExhausted = STM.check =<< workExhausted
238+ let pop :: STM.STM (Tick AnyLogItem)
239+ pop = maybe WorkExhausted NewItem <$> STM.readTBMQueue ingestionQueue
240+
241+ let timeExpired :: STM.STM (Tick a)
242+ timeExpired = do
243+ isExpired <- STM.readTVar =<< STM.readTVar timerRef
244+ STM.check isExpired
245+ pure TimeExpired
246+
247+ -- Circular transaction that checks for completion of work, time
248+ -- expiration, or new events.
249+ let nextTick :: STM.STM (Tick AnyLogItem)
250+ nextTick =
251+ timeExpired <|>
252+ pop <|>
253+ (WorkExhausted <$ waitWorkExhausted)
254+
255+ -- Blocking push. This applies backpressure upstream to katip, which does its own buffering
256+ let push :: AnyLogItem -> STM.STM ()
257+ push = void . STM.writeTBMQueue ingestionQueue
258+
259+ let sealQueue = STM.atomically (STM.closeTBMQueue ingestionQueue)
260+
261+ -- Replace the timer with a new one
262+ let resetTimer = STM.atomically . STM.writeTVar timerRef =<< newTimer
263+
264+ -- Send the buffer and then reset the timer
265+ let flush curBuffer = do
266+ res <- flushBuffer config mgr curBuffer
135267 case res of
136− Left () -> pure () -- exit loop
137− Right () -> timedFlushLoop -- wait for another timer or shutdown
138−
139− timedFlusher <- Async.async timedFlushLoop
140−
141− let flushLoop = do
142− -- hold until something comes in to flush or we're told to stop. If we're told to stop but get a log first, we'll catch it on the next loop
143− readyBufferOrDie <- STM.atomically $
144− raceAlt
145− (STM.takeTMVar readyMVar)
146− waitShutdown
147− case readyBufferOrDie of
148− Right () -> pure () -- we caught shutdown before any new data arrived
149− -- There's some logs to write
150− Left readyBuffer -> do
151− -- flush in IO
152− flushBuffer readyBuffer
153− -- reset the timeout before the next flush on each write
154− newTimer <- STM.registerDelay itemBufferTimeoutMicros
155− STM.atomically $ do
156− STM.writeTVar timerRef newTimer
157− -- start over
158− flushLoop
159− flusher <- Async.async flushLoop
160−
161− let shutdown = do
162− -- tell workers this is the last loop
163− STM.atomically (STM.writeTVar killSwitch ShutDown)
164− -- wait for workers to finish
165− _ <- Async.waitCatch timedFlusher
166− _ <- Async.waitCatch flusher
167− -- stop blocking
268+ Left e -> onErrorSafe e
269+ Right () -> pure ()
270+ resetTimer
271+
272+ let flushLoop :: BulkBuffer -> IO ()
273+ flushLoop curBuffer = do
274+ tick <- STM.atomically nextTick
275+ case tick of
276+ WorkExhausted -> do
277+ flush curBuffer -- flush what you've got
278+ pure () -- stop looping
279+ TimeExpired -> do
280+ flush curBuffer
281+ flushLoop mempty
282+ NewItem (AnyLogItem item) -> do
283+ case bufferItem (logzIOScribeConfiguration_bufferItems config) verbosity item curBuffer of
284+ Buffered newBuffer -> flushLoop newBuffer
285+ FlushNow flushThis newBuffer -> do
286+ flush flushThis
287+ flushLoop newBuffer
288+
289+ flushThread <- Async.async (flushLoop mempty)
290+
291+ let close = do
292+ sealQueue
293+ _ <- Async.waitCatch flushThread
168294 pure ()
169295
170−
171296 pure $ K.Scribe
172− { liPush = STM.atomically . push
173− , scribeFinalizer = shutdown
174− , scribePermitItem = permitItem
297+ { K.liPush = STM.atomically . push . AnyLogItem
298+ , K.scribeFinalizer = close
299+ , K.scribePermitItem = permitItem
175300 }
176301 where
177302 itemBufferSize = logzIOScribeConfiguration_bufferItems config
178303 itemBufferTimeoutMicros = ndtToMicros (logzIOScribeConfiguration_bufferTimeout config)
304+ scheme = logzIOScribeConfiguration_scheme config
305+ onErrorSafe ex = do
306+ _ <- EX.tryAny (logzIOScribeConfiguration_onError config ex)
307+ pure ()
308+
309+
310+data AnyLogItem where
311+ AnyLogItem :: K.LogItem a => K.Item a -> AnyLogItem
312+
313+
314+-------------------------------------------------------------------------------
315+data Tick a =
316+ TimeExpired
317+ | NewItem !a
318+ | WorkExhausted
179319
180320
181321-------------------------------------------------------------------------------
@@ -196,26 +336,82 @@ ndtToMicros t = round (((fromIntegral (ndtPicos t)) :: Double) / picosInMicro)
196336
197337
198338-------------------------------------------------------------------------------
199−data KillSwitch =
200− Continue
201− | ShutDown
202− deriving (Eq)
203−
339+data LoggingError =
340+ URIError HTTP.HttpException
341+ -- ^ The URI generated was invalid. Check your configuration
342+ | RequestError HTTP.HttpException
343+ -- ^ We encountered an exception while sending the batch request
344+ | PartialFailure BulkAPIError
345+ -- ^ Some or all of the request was rejected. Check the logz.io UI
346+ -- for indexing errors.
347+ | BadToken
348+ -- ^ Your API token was rejected.
349+ | UnknownFailureResponse HTypes.Status LBS.ByteString
350+ -- ^ An error returned, but it could not be decoded into a
351+ -- 'BulkAPIError'. This may indicate a __library bug__, which should
352+ -- be reported to the issue tracker.
353+ deriving (Show)
204354
205355-------------------------------------------------------------------------------
206−raceAlt :: (Alternative m) => m a -> m b -> m (Either a b)
207−raceAlt a b = (Left <$> a) <|> (Right <$> b)
208−
356+flushBuffer
357+ :: LogzIOScribeConfiguration
358+ -> HTTP.Manager
359+ -> BulkBuffer
360+ -> IO (Either LoggingError ())
361+flushBuffer config mgr bulkBuffer
362+ | bulkBuffer_itemCount bulkBuffer <= 0 = pure (Right ())
363+ | otherwise = do
364+ E.runExceptT $ do
365+ req <- E.fmapLT URIError (E.ExceptT (EX.try (configureRequest <$> HTTP.parseRequest uriStr)))
366+ resp <- E.fmapLT RequestError $ E.ExceptT $ EX.try $
367+ Retry.recovering retryPolicy [\_stat -> EX.Handler handleHttpException] $ \_stat ->
368+ HTTP.httpLbs req mgr
369+ let respLBS = HTTP.responseBody resp
370+ let respStatus = HTTP.responseStatus resp
371+ if HTypes.statusIsSuccessful respStatus
372+ then pure ()
373+ else case A.decode @BulkAPIError respLBS of
374+ Nothing
375+ | HTypes.statusCode respStatus == 401 -> E.throwE BadToken
376+ | otherwise -> E.throwE (UnknownFailureResponse respStatus respLBS)
377+ Just bulkError -> E.throwE (PartialFailure bulkError)
378+ where
379+ configureRequest req = req
380+ { HTTP.method = HTypes.methodPost
381+ , HTTP.requestBody = HTTP.RequestBodyLBS (BB.toLazyByteString (bulkBuffer_payload bulkBuffer))
382+ }
383+
384+ retryPolicy = logzIOScribeConfiguration_retry config
385+ apiTokenBS = TE.encodeUtf8 (apiToken (logzIOScribeConfiguration_token config))
386+
387+ handleHttpException :: (Applicative m) => HTTP.HttpException -> m Bool
388+ handleHttpException _ = pure True
389+
390+ uriStr = LBS8.unpack (BB.toLazyByteString (URIBS.serializeURIRef uri))
391+
392+ authority = URIBS.Authority
393+ { URIBS.authorityUserInfo = Nothing
394+ , URIBS.authorityHost = logzIOScribeConfiguration_host config
395+ , URIBS.authorityPort = Just (logzIOScribeConfiguration_port config)
396+ }
397+
398+ uri = URIBS.URI
399+ { URIBS.uriScheme = case logzIOScribeConfiguration_scheme config of
400+ HTTPS -> URIBS.Scheme "https"
401+ HTTP -> URIBS.Scheme "http"
402+ , URIBS.uriAuthority = Just authority
403+ , URIBS.uriPath = "/"
404+ , URIBS.uriQuery = URIBS.Query
405+ [ ("token", apiTokenBS)
406+ ]
407+ , URIBS.uriFragment = Nothing
408+ }
209409
210−-------------------------------------------------------------------------------
211−-- TODO: noop on empty
212−flushBuffer :: BulkBuffer -> IO ()
213−flushBuffer = undefined
214410
215411-------------------------------------------------------------------------------
216412newtype Bytes = Bytes
217413 { bytes :: Int64
218− } deriving (Show, Eq, Num, Ord)
414+ } deriving (Show, Eq, Num, Ord, Bounded)
219415
220416
221417-- | How big of a body can we send? The limit is defined
@@ -231,10 +427,10 @@ maxLogLineLength :: Bytes
231427maxLogLineLength = 500000
232428
233429
234−measureJSON :: A.ToJSON a => a -> (BB.Builder, Bytes)
235−measureJSON a = (BB.lazyByteString lbs, Bytes (LBS.length lbs))
430+measureJSONLine :: A.ToJSON a => a -> (BB.Builder, Bytes)
431+measureJSONLine a = (BB.lazyByteString lbs, Bytes (LBS.length lbs))
236432 where
237− lbs = A.encode a
433+ lbs = A.encode a <> "\n"
238434
239435
240436-- | Fully-rendered JSON object for an item
@@ -246,35 +442,51 @@ fullItemObject verbosity item = HM.fromList
246442 , "thread" A..= K.getThreadIdText (K._itemThread item)
247443 , "host" A..= K._itemHost item
248444 , "pid" A..= pidInt
249− , "data" A..= K.payloadObject verbosity (K._itemPayload item)
445+ , "data" A..= annotateKeys (K.payloadObject verbosity (K._itemPayload item))
250446 -- Slight deviation, logz.io uses "message" instead of "msg"
251447 , "message" A..= TB.toLazyText (K.unLogStr (K._itemMessage item))
252− -- Another slight deviation, logz.io uses "@timestamp" instead
253− -- of "at". Note, your logs should be sent roughly close to when
254− -- they are created. They are assigned to indexes based on index
255− -- date, so time searches act strange if you backfill too far
256− -- from the past.
257− , "@timestamp" A..= K._itemTime item --TODO: explicit formatting as per spec?
448+ -- Another slight deviation, logz.io uses "@timestamp" instead of
449+ -- "at". Note, your logs should be sent roughly close to when they
450+ -- are created. They are assigned to indexes based on index date, so
451+ -- time searches act strange if you backfill too far from the
452+ -- past. They seem to support 3 decimal places of
453+ -- precision. Formatting like this requires time 1.8.0.2, which is
454+ -- reflected in the cabal file.
455+ , "@timestamp" A..= A.String (T.pack (Time.formatTime Time.defaultTimeLocale "%Y-%m-%dT%H:%M:%S%03QZ" (K._itemTime item)))
258456 , "ns" A..= K._itemNamespace item
259457 , "loc" A..= (LocJs <$> K._itemLoc item)
260458 ]
261459 where
262460 POSIX.CPid pidInt = K._itemProcess item
263461
264−--TODO: property test
265462
266463-- | A version of 'renderLine' which renders a line and stays under
267464-- the maximum line size of 500,000 bytes. If the default rendering is
268465-- too large, the log will be reduced to a timestamp and a potentially
269466-- truncated message.
270467renderLineTruncated :: K.LogItem a => K.Verbosity -> K.Item a -> (BB.Builder, Bytes)
271−renderLineTruncated verbosity item =
272− if fullSize <= maxLogLineLength
468+renderLineTruncated = renderLineTruncated' maxLogLineLength
469+
470+
471+-- | A generalized renderLineTruncated that takes a custom line length
472+-- limit. This is exclusively for testing.
473+renderLineTruncated'
474+ :: K.LogItem a
475+ => Bytes
476+ -- ^ Custom max log line length. Be careful, too low and no amount
477+ -- of shrinking can get under the size, breaking the invariant. For
478+ -- the production limit, we are guraanteed always able to come in
479+ -- under the limit.
480+ -> K.Verbosity
481+ -> K.Item a
482+ -> (BB.Builder, Bytes)
483+renderLineTruncated' customMaxLogLineLength verbosity item =
484+ if fullSize <= customMaxLogLineLength
273485 then (fullLine, fullSize)
274486 else (fallbackLine, fallbackSize)
275487 where
276488 fullObject = fullItemObject verbosity item
277− (fullLine, fullSize) = measureJSON fullObject
489+ (fullLine, fullSize) = measureJSONLine fullObject
278490 -- only the absolutely necessary keys, with message stripped out
279491 -- to help us calculate how much of the message we can keep. these
280492 -- are lazily evaluated and as such won't be computed unless the
@@ -284,9 +496,9 @@ renderLineTruncated verbosity item =
284496 [ "message" A..= A.String "" -- we'll start with a blank message
285497 , "@timestamp" A..= K._itemTime item
286498 ]
287− (_, blankObjectSize) = measureJSON blankObject
499+ (_, blankObjectSize) = measureJSONLine blankObject
288500 messageBytesAllowed = maxLogLineLength - blankObjectSize
289− (fallbackLine, fallbackSize) = measureJSON fallbackObject
501+ (fallbackLine, fallbackSize) = measureJSONLine fallbackObject
290502 fallbackObject :: A.Object
291503 fallbackObject = HM.fromList
292504 [ "message" A..= A.toJSON (TL.take (bytes messageBytesAllowed) (TB.toLazyText (K.unLogStr (K._itemMessage item))))
@@ -301,7 +513,7 @@ data BulkBuffer = BulkBuffer
301513 }
302514
303515
304−instance Semigroup BulkBuffer where
516+instance Semigroup.Semigroup BulkBuffer where
305517 (BulkBuffer bytesUsedA bufferA itemCountA) <>
306518 (BulkBuffer bytesUsedB bufferB itemCountB) = BulkBuffer
307519 (bytesUsedA + bytesUsedB)
@@ -332,11 +544,30 @@ bufferItem
332544 -> K.Item a
333545 -> BulkBuffer
334546 -> LogAction BulkBuffer
335−bufferItem maxItems verb item bulkBuffer =
336− let (encodedLine, spaceNeeded) = renderLineTruncated verb item
547+bufferItem = bufferItem' maxPayloadBytes maxLogLineLength
548+
549+
550+-- | An internal version with a configurable max item size and max
551+-- message size for testing. Be careful with this: if you set the
552+-- values too low, some of the guarantees about reducability of
553+-- messages break down.
554+bufferItem'
555+ :: (K.LogItem a)
556+ => Bytes
557+ -- ^ Max payload size
558+ -> Bytes
559+ -- ^ Max item size
560+ -> Int
561+ -- ^ Maximum items before flushing
562+ -> K.Verbosity
563+ -> K.Item a
564+ -> BulkBuffer
565+ -> LogAction BulkBuffer
566+bufferItem' customMaxPayload customMaxItem maxItems verb item bulkBuffer =
567+ let (encodedLine, spaceNeeded) = renderLineTruncated' customMaxItem verb item
337568 newBytesUsed = bulkBuffer_bytesUsed bulkBuffer + spaceNeeded
338569 newItemCount = bulkBuffer_itemCount bulkBuffer + 1
339− in if newItemCount >= maxItems || newBytesUsed >= maxPayloadBytes
570+ in if newItemCount >= maxItems || newBytesUsed >= customMaxPayload
340571 then FlushNow
341572 bulkBuffer
342573 BulkBuffer
@@ -356,3 +587,53 @@ bufferItem maxItems verb item bulkBuffer =
356587-- will always return 'FlushNow' with an empty new buffer.
357588forceFlush :: (Monoid buf) => buf -> LogAction buf
358589forceFlush buf = FlushNow buf mempty
590+
591+
592+-------------------------------------------------------------------------------
593+-- Annotation borrowed from katip-elasticsearch. There are fixed
594+-- fields in katip logs which should stay the same type forever and
595+-- thus won't need annotation. However, any code in userland may
596+-- choose to add akey to their log's metadata with a type that's
597+-- incompatible with the mapping. If the first value that's picked up
598+-- restrictive (e.g. a long), subsequent values will be rejected. By
599+-- differentiating types by their name, this is guaranteed not to
600+-- happen.
601+
602+annotateValue :: A.Value -> A.Value
603+annotateValue (A.Object o) = A.Object (annotateKeys o)
604+annotateValue (A.Array a) = A.Array (annotateValue <$> a)
605+annotateValue x = x
606+
607+
608+annotateKeys :: A.Object -> A.Object
609+annotateKeys = HM.fromList . map go . HM.toList
610+ where
611+ go (k, A.Object o) = (k, A.Object (annotateKeys o))
612+ go (k, A.Array a) = (k, A.Array (annotateValue <$> a))
613+ go (k, s@(A.String _)) = (k <> stringAnn, s)
614+ go (k, n@(A.Number sci)) = if Scientific.isFloating sci
615+ then (k <> doubleAnn, n)
616+ else (k <> longAnn, n)
617+ go (k, b@(A.Bool _)) = (k <> booleanAnn, b)
618+ go (k, A.Null) = (k <> nullAnn, A.Null)
619+
620+
621+-------------------------------------------------------------------------------
622+-- Annotation Constants
623+-------------------------------------------------------------------------------
624+
625+
626+stringAnn :: T.Text
627+stringAnn = "::s"
628+
629+doubleAnn :: T.Text
630+doubleAnn = "::d"
631+
632+longAnn :: T.Text
633+longAnn = "::l"
634+
635+booleanAnn :: T.Text
636+booleanAnn = "::b"
637+
638+nullAnn :: T.Text
639+nullAnn = "::n"
katip-logzio/test/Katip/Tests/Scribes/LogzIO/HTTPS.hsnew file+466
@@ -0,0 +1,466 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Katip.Tests.Scribes.LogzIO.HTTPS
3+ ( tests
4+ ) where
5+
6+
7+-------------------------------------------------------------------------------
8+import Control.Concurrent.Async
9+import Control.Concurrent.STM
10+import Control.Concurrent.STM.TSem
11+import Control.Exception.Safe
12+import Control.Monad
13+import Control.Monad.IO.Class
14+import Data.Aeson as A
15+import qualified Data.ByteString.Builder as BB
16+import qualified Data.ByteString.Lazy as LBS
17+import qualified Data.ByteString.Lazy.Char8 as LBS8
18+import qualified Data.HashMap.Strict as HM
19+import Data.Int
20+import Data.Scientific
21+import Data.Text (Text)
22+import Data.Time
23+import Data.Time.Clock.POSIX
24+import qualified Data.Vector as V
25+import Hedgehog as HH
26+import qualified Hedgehog.Gen as Gen
27+import qualified Hedgehog.Range as Range
28+import Katip
29+import Katip.Core
30+import Language.Haskell.TH (Loc (..))
31+import Network.HostName
32+import qualified Network.HTTP.Types.Status as HTypes
33+import qualified Network.Wai.Handler.Warp as Warp
34+import System.Posix
35+import System.Timeout
36+import Test.Tasty
37+import Test.Tasty.Hedgehog
38+import Test.Tasty.HUnit
39+import URI.ByteString
40+import qualified Web.Scotty as Scotty
41+-------------------------------------------------------------------------------
42+import Katip.Scribes.LogzIO.HTTPS
43+-------------------------------------------------------------------------------
44+
45+
46+tests :: TestTree
47+tests = testGroup "Katip.Scribes.LogzIO.HTTP"
48+ [ bufferItemTests
49+ , renderLineTruncatedTests
50+ , scribeTests
51+ ]
52+
53+
54+-------------------------------------------------------------------------------
55+-- We bind to a system port so we use a lock on these tests to avoid interleaved, concurrent requests
56+scribeTests :: TestTree
57+scribeTests = withLock $ \mkSem -> testGroup "scribe"
58+ [ testCase "writes well-formed log items as lines to the given API" $ do
59+ sem <- mkSem
60+ withServer sem $ \receivedMessages -> do
61+ withScribe (\cfg -> cfg { logzIOScribeConfiguration_bufferTimeout = 1}) $ \scribe -> do
62+ (liPush scribe) (genericLog "a message")
63+
64+ requestLog <- timeoutFailure (20 * second) (atomically (readTQueue receivedMessages))
65+ requestLog_token requestLog @?= testAPIToken
66+ let logs = requestLog_logs requestLog
67+ length logs @?= 1
68+ let firstLog = head logs
69+ receivedLog_message firstLog @?= "a message"
70+ , testCase "respects the items limit" $ do
71+ sem <- mkSem
72+ withServer sem $ \receivedMessages -> do
73+ withScribe (\cfg -> cfg { logzIOScribeConfiguration_bufferItems = 1, logzIOScribeConfiguration_bufferTimeout = 1}) $ \scribe -> do
74+ (liPush scribe) (genericLog "message 1")
75+ (liPush scribe) (genericLog "message 2")
76+
77+ log1 <- timeoutFailure (20 * second) (atomically (readTQueue receivedMessages))
78+ requestLog_token log1 @?= testAPIToken
79+ let logs1 = requestLog_logs log1
80+ length logs1 @?= 1
81+ let firstLog1 = head logs1
82+ receivedLog_message firstLog1 @?= "message 1"
83+
84+ log2 <- timeoutFailure (20 * second) (atomically (readTQueue receivedMessages))
85+ let logs2 = requestLog_logs log2
86+ length logs2 @?= 1
87+ let firstLog2 = head logs2
88+ receivedLog_message firstLog2 @?= "message 2"
89+ , testCase "respects the time limit" $ do
90+ sem <- mkSem
91+ withServer sem $ \receivedMessages -> do
92+ withScribe (\cfg -> cfg { logzIOScribeConfiguration_bufferItems = 2, logzIOScribeConfiguration_bufferTimeout = 1}) $ \scribe -> do
93+ (liPush scribe) (genericLog "a message")
94+ log1 <- timeoutFailure (20 * second) (atomically (readTQueue receivedMessages))
95+ requestLog_token log1 @?= testAPIToken
96+ let logs1 = requestLog_logs log1
97+ length logs1 @?= 1
98+ let firstLog1 = head logs1
99+ receivedLog_message firstLog1 @?= "a message"
100+ , testCase "reports errors" $ do
101+ sem <- mkSem
102+ let badToken = APIToken "badtoken"
103+ errorSignal <- newEmptyTMVarIO
104+ withServer sem $ \_receivedMessages -> do
105+ withScribe (\cfg -> cfg { logzIOScribeConfiguration_bufferItems = 1, logzIOScribeConfiguration_bufferTimeout = 1, logzIOScribeConfiguration_token = badToken, logzIOScribeConfiguration_onError = void . atomically . tryPutTMVar errorSignal}) $ \scribe -> do
106+ (liPush scribe) (genericLog "a message")
107+ loggingError <- timeoutFailure (20 * second) (atomically (takeTMVar errorSignal))
108+ case loggingError of
109+ BadToken -> pure ()
110+ _ -> assertFailure ("Expected BadToken error but got " <> show loggingError)
111+ , testCase "flushes all queued logs on close" $ do
112+ sem <- mkSem
113+ withServer sem $ \receivedMessages -> do
114+ withScribe (\cfg -> cfg { logzIOScribeConfiguration_bufferItems = 100, logzIOScribeConfiguration_bufferTimeout = 100}) $ \scribe -> do
115+ (liPush scribe) (genericLog "message 1")
116+ (liPush scribe) (genericLog "message 2")
117+ messages <- atomically (flushTQueue receivedMessages)
118+ let allLogs = receivedLog_message <$> mconcat (requestLog_logs <$> messages)
119+ allLogs @?= ["message 1", "message 2"]
120+ ]
121+ where
122+ second = 1000000
123+ timeoutFailure waitTime f = do
124+ res <- timeout waitTime f
125+ case res of
126+ Nothing -> assertFailure ("Operation timed out after " <> show waitTime <> "us")
127+ Just x -> pure x
128+ withLock = withResource (atomically (newTSem 1)) (const (return ()))
129+ withServer sem f = bracket_ (atomically (waitTSem sem)) (atomically (signalTSem sem)) $ do
130+ receivedMessages <- atomically newTQueue
131+ withAsync (startServer receivedMessages) $ \_async -> f receivedMessages
132+ testAPIToken = "bigSECRET"
133+ genericLog message = Item
134+ { _itemApp = "katip-logzio"
135+ , _itemEnv = "test"
136+ , _itemSeverity = InfoS
137+ , _itemThread = ThreadIdText "42"
138+ , _itemHost = "localhost"
139+ , _itemProcess = 111
140+ , _itemPayload = ()
141+ , _itemMessage = message
142+ , _itemTime = mkUTCTime 2019 1 2 3 4 5
143+ , _itemNamespace = "test"
144+ , _itemLoc = Nothing
145+ }
146+ startServer receivedMessages = Scotty.scottyOpts serverOpts $ do
147+ Scotty.post "/" $ do
148+ token <- APIToken <$> Scotty.param "token"
149+ if token == testAPIToken
150+ then do
151+ bodyLines <- LBS8.lines <$> Scotty.body
152+ receivedLogs <- mapM (either error pure . A.eitherDecode) bodyLines
153+ let requestLog = RequestLog
154+ { requestLog_token = token
155+ , requestLog_logs = receivedLogs
156+ }
157+ liftIO (atomically (writeTQueue receivedMessages requestLog))
158+ else Scotty.status HTypes.unauthorized401
159+ serverOpts = Scotty.Options
160+ { Scotty.verbose = 0
161+ , Scotty.settings = Warp.setPort 1337 Warp.defaultSettings
162+ }
163+ withScribe modConfig =
164+ bracket (mkLogzIOScribe (modConfig scribeConfig) (const (pure True)) V3) scribeFinalizer
165+ scribeConfig = LogzIOScribeConfiguration
166+ { logzIOScribeConfiguration_bufferItems = 5
167+ , logzIOScribeConfiguration_bufferTimeout = 10
168+ , logzIOScribeConfiguration_scheme = HTTP
169+ , logzIOScribeConfiguration_host = Host "127.0.0.1"
170+ , logzIOScribeConfiguration_port = Port 1337
171+ , logzIOScribeConfiguration_token = testAPIToken
172+ , logzIOScribeConfiguration_retry = defaultRetryPolicy
173+ , logzIOScribeConfiguration_onError = const (pure ())
174+ }
175+
176+
177+
178+data RequestLog = RequestLog
179+ { requestLog_token :: APIToken
180+ , requestLog_logs :: [ReceivedLog]
181+ }
182+
183+
184+-- | Simplified log that only parses the fields we want to assert on
185+data ReceivedLog = ReceivedLog
186+ { receivedLog_message :: Text
187+ }
188+
189+
190+instance FromJSON ReceivedLog where
191+ parseJSON = withObject "ReceivedLog" $ \o -> do
192+ message <- o .: "message"
193+ pure $ ReceivedLog
194+ { receivedLog_message = message
195+ }
196+
197+
198+-------------------------------------------------------------------------------
199+bufferItemTests :: TestTree
200+bufferItemTests = testGroup "bufferItem"
201+ [ testProperty "0 or negative maximum items always flushes immediately" $ property $ do
202+ item <- forAllWith showSimplePayloadItem genSimplePayloadItem
203+ verbosity <- forAll genVerbosity
204+ maxItems <- forAll (Gen.int (Range.linear minBound 0))
205+ bulkBuffer <- forAllWith inspectBulkBuffer genBulkBuffer
206+ let logAction = bufferItem' reducedMaxPayloadBytes reducedMaxLogLineLength maxItems verbosity item bulkBuffer
207+ assertFlushNow logAction
208+ , testProperty "never buffers too many items" $ property $ do
209+ item <- forAllWith showSimplePayloadItem genSimplePayloadItem
210+ verbosity <- forAll genVerbosity
211+ maxItems <- forAll (Gen.int (Range.linear 0 100))
212+ bulkBuffer <- forAllWith inspectBulkBuffer genBulkBuffer
213+ let logAction = bufferItem' reducedMaxPayloadBytes reducedMaxLogLineLength maxItems verbosity item bulkBuffer
214+ case logAction of
215+ Buffered newBuffer -> HH.assert (bulkBuffer_itemCount newBuffer <= maxItems)
216+ _ -> pure ()
217+ , testProperty "never produces too many bytes" $ property $ do
218+ item <- forAllWith showSimplePayloadItem genSimplePayloadItem
219+ verbosity <- forAll genVerbosity
220+ maxItems <- forAll (Gen.int (Range.linear 0 100))
221+ bulkBuffer <- forAllWith inspectBulkBuffer genBulkBuffer
222+ let logAction = bufferItem' reducedMaxPayloadBytes reducedMaxLogLineLength maxItems verbosity item bulkBuffer
223+ case logAction of
224+ Buffered newBuffer -> HH.assert (bulkBuffer_bytesUsed newBuffer <= reducedMaxPayloadBytes)
225+ _ -> pure ()
226+ , testProperty "never flushes more than 1 item" $ property $ do
227+ item <- forAllWith showSimplePayloadItem genSimplePayloadItem
228+ verbosity <- forAll genVerbosity
229+ maxItems <- forAll (Gen.int (Range.linear minBound 0))
230+ bulkBuffer <- forAllWith inspectBulkBuffer genBulkBuffer
231+ let logAction = bufferItem' reducedMaxPayloadBytes reducedMaxLogLineLength maxItems verbosity item bulkBuffer
232+ case logAction of
233+ FlushNow _ newBuffer -> bulkBuffer_itemCount newBuffer === 1
234+ _ -> pure ()
235+ , testProperty "flushes when the buffer is exactly at the limit" $ property $ do
236+ maxItems <- forAll (Gen.int (Range.linear 1 20))
237+ item <- forAllWith showSimplePayloadItem genSimplePayloadItem
238+ bulkBuffer <- forAllWith inspectBulkBuffer ((\b -> b { bulkBuffer_itemCount = maxItems - 1 }) <$> genBulkBuffer)
239+ verbosity <- forAll genVerbosity
240+ let logAction = bufferItem' maxBound maxBound maxItems verbosity item bulkBuffer
241+ case logAction of
242+ FlushNow _ _ -> pure ()
243+ Buffered _ -> fail "Expected a FlushNow but got a Buffered"
244+ ]
245+ where
246+ assertFlushNow (FlushNow _ _) = pure ()
247+ assertFlushNow (Buffered _) = fail ("Expceted FlushNow but got a Buffered")
248+ inspectBulkBuffer (BulkBuffer used _ itemCount) =
249+ "BulkBuffer (" <> show used <> ") ... " <> show itemCount
250+
251+
252+-------------------------------------------------------------------------------
253+-- | Reduced max log line length for more space and time efficient tests
254+reducedMaxLogLineLength :: Bytes
255+reducedMaxLogLineLength = Bytes (bytes maxLogLineLength `div` limitScalingFactor)
256+
257+
258+reducedMaxPayloadBytes :: Bytes
259+reducedMaxPayloadBytes = Bytes (bytes maxPayloadBytes `div` limitScalingFactor)
260+
261+
262+limitScalingFactor :: Int64
263+limitScalingFactor = 10
264+
265+-------------------------------------------------------------------------------
266+showSimplePayloadItem :: Item SimpleLogPayload -> String
267+showSimplePayloadItem item =
268+ show (item { _itemPayload = [(k, toJSON v) | (k, AnyLogPayload v) <- unSimpleLogPayload (_itemPayload item)]})
269+
270+
271+-------------------------------------------------------------------------------
272+renderLineTruncatedTests :: TestTree
273+renderLineTruncatedTests = testGroup "renderLineTruncated"
274+ [ testProperty "always produces an accurate byte count" $ property $ do
275+ item <- forAllWith showSimplePayloadItem genSimplePayloadItem
276+ verbosity <- forAll genVerbosity
277+ let (builder, Bytes byteCount) = renderLineTruncated' reducedMaxLogLineLength verbosity item
278+ builderLength builder === byteCount
279+ , testProperty "always produces a payload smaller than the line limit" $ property $ do
280+ let tinyMaxLineLength = 1024 -- set a really low one for speed
281+ let lowerBound = fromIntegral (bytes tinyMaxLineLength - 100)
282+ let upperBound = fromIntegral (bytes tinyMaxLineLength + 100)
283+ bigMessage <- forAll (genLogStr' (Range.linear lowerBound upperBound))
284+ item <- (\item -> item { _itemMessage = bigMessage}) <$> forAllWith showSimplePayloadItem genSimplePayloadItem
285+ verbosity <- forAll genVerbosity
286+ let (_, byteCount) = renderLineTruncated' tinyMaxLineLength verbosity item
287+ HH.assert (byteCount <= reducedMaxLogLineLength)
288+ ]
289+
290+
291+-------------------------------------------------------------------------------
292+builderLength :: BB.Builder -> Int64
293+builderLength = LBS.length . BB.toLazyByteString
294+
295+-------------------------------------------------------------------------------
296+genVerbosity :: Gen Verbosity
297+genVerbosity = Gen.enumBounded
298+
299+
300+-------------------------------------------------------------------------------
301+genBulkBuffer :: Gen BulkBuffer
302+genBulkBuffer = do
303+ bytesUsed <- genBytes
304+ -- generating a large bytestring string with hedghog seems quite slow, instead we'll generate a smaller string and replicate and merge it
305+ payloadMultiplier <- Gen.int (Range.linear 1 100)
306+ payloadSegment <- genBuilder (Range.linear 0 (round (toRational (bytes reducedMaxPayloadBytes) * 0.05)))
307+ let payload = mconcat (replicate payloadMultiplier payloadSegment)
308+ itemCount <- Gen.int (Range.linear 0 maxBound)
309+ pure $ BulkBuffer
310+ { bulkBuffer_bytesUsed = bytesUsed
311+ , bulkBuffer_payload = payload
312+ , bulkBuffer_itemCount = itemCount
313+ }
314+
315+
316+-------------------------------------------------------------------------------
317+genBytes :: Gen Bytes
318+genBytes = Bytes <$> Gen.int64 (Range.linear 0 maxBound)
319+
320+
321+-------------------------------------------------------------------------------
322+genBuilder :: Range Int -> Gen BB.Builder
323+genBuilder size = BB.byteString <$> Gen.bytes size
324+
325+
326+-------------------------------------------------------------------------------
327+genSimplePayloadItem :: Gen (Item SimpleLogPayload)
328+genSimplePayloadItem = do
329+ app <- genNamespace
330+ env <- genEnvironment
331+ severity <- genSeverity
332+ thread <- genThreadIdText
333+ host <- genHostName
334+ process <- genProcessID
335+ payload <- genSimpleLogPayload
336+ message <- genLogStr
337+ time <- genUTCTime
338+ namespace <- genNamespace
339+ loc <- Gen.maybe genLoc
340+ pure $ Item
341+ { _itemApp = app
342+ , _itemEnv = env
343+ , _itemSeverity = severity
344+ , _itemThread = thread
345+ , _itemHost = host
346+ , _itemProcess = process
347+ , _itemPayload = payload
348+ , _itemMessage = message
349+ , _itemTime = time
350+ , _itemNamespace = namespace
351+ , _itemLoc = loc
352+ }
353+
354+
355+genNamespace :: Gen Namespace
356+genNamespace = Namespace <$> Gen.list (Range.linear 0 20) genSmallText
357+
358+
359+genEnvironment :: Gen Environment
360+genEnvironment = Environment <$> genSmallText
361+
362+
363+genSeverity :: Gen Severity
364+genSeverity = Gen.enumBounded
365+
366+
367+genThreadIdText :: Gen ThreadIdText
368+genThreadIdText = ThreadIdText <$> genSmallText
369+
370+
371+genHostName :: Gen HostName
372+genHostName = genSmallString
373+
374+
375+genProcessID :: Gen ProcessID
376+genProcessID = Gen.enumBounded
377+
378+
379+genLogStr :: Gen LogStr
380+genLogStr = genLogStr' (Range.linear 0 1024)
381+
382+
383+genLogStr' :: Range Int -> Gen LogStr
384+genLogStr' sizeRange = ls <$> Gen.text sizeRange Gen.unicode
385+
386+
387+genUTCTime :: Gen UTCTime
388+genUTCTime = posixSecondsToUTCTime . fromIntegral
389+ <$> Gen.int (Range.linear 0 maxBound)
390+
391+
392+genLoc :: Gen Loc
393+genLoc = Loc
394+ <$> genSmallString
395+ <*> genSmallString
396+ <*> genSmallString
397+ <*> genCharPos
398+ <*> genCharPos
399+ where
400+ genCharPos = (,)
401+ <$> Gen.int (Range.linear 0 maxBound)
402+ <*> Gen.int (Range.linear 0 maxBound)
403+
404+
405+genSimpleLogPayload :: Gen SimpleLogPayload
406+genSimpleLogPayload = SimpleLogPayload
407+ <$> Gen.list (Range.linear 0 20) genPair
408+ where
409+ genPair = (,)
410+ <$> genSmallText
411+ <*> genAnyLogPayload
412+
413+
414+genAnyLogPayload :: Gen AnyLogPayload
415+genAnyLogPayload = AnyLogPayload
416+ <$> genValue
417+
418+
419+genValue :: Gen Value
420+genValue = Gen.recursive Gen.choice nonRecursiveGens recursiveGens
421+ where
422+ nonRecursiveGens =
423+ [ pure A.Null
424+ , A.String <$> genSmallText
425+ , A.Number <$> genScientific
426+ , A.Bool <$> Gen.bool
427+ ]
428+ recursiveGens =
429+ [ A.Object <$> genObject
430+ , A.Array <$> genArray
431+ ]
432+ genObject = HM.fromList <$> Gen.list (Range.linear 0 20) genPair
433+ where
434+ genPair = (,) <$> genSmallText <*> genValue
435+ genArray = V.fromList <$>
436+ Gen.list (Range.linear 0 20) genValue
437+
438+
439+
440+genScientific :: Gen Scientific
441+genScientific = scientific
442+ <$> Gen.integral (Range.linear (-999999) 999999)
443+ <*> Gen.int (Range.linear (-999) (999))
444+
445+
446+genSmallText :: Gen Text
447+genSmallText = Gen.text (Range.linear 0 128) Gen.unicode
448+
449+
450+genSmallString :: Gen String
451+genSmallString = Gen.string (Range.linear 0 128) Gen.unicode
452+
453+
454+-------------------------------------------------------------------------------
455+mkUTCTime :: Integer -> Int -> Int -> DiffTime -> DiffTime -> DiffTime -> UTCTime
456+mkUTCTime y m d hr mn s = UTCTime day dt
457+ where
458+ day = mkDay y m d
459+ dt = s + 60 * mn + 60 * 60 * hr
460+
461+
462+-------------------------------------------------------------------------------
463+mkDay :: Integer -> Int -> Int -> Day
464+mkDay y m d = day
465+ where
466+ Just day = fromGregorianValid y m d
katip-logzio/test/Main.hs+3−2
@@ -6,6 +6,7 @@ module Main
66-------------------------------------------------------------------------------
77import Test.Tasty
88-------------------------------------------------------------------------------
9+import qualified Katip.Tests.Scribes.LogzIO.HTTPS
910-------------------------------------------------------------------------------
1011
1112
@@ -16,6 +17,6 @@ main = defaultMain tests
1617
1718-------------------------------------------------------------------------------
1819tests :: TestTree
19−tests = testGroup "katip-datadog"
20− [
20+tests = testGroup "katip-logzio"
21+ [ Katip.Tests.Scribes.LogzIO.HTTPS.tests
2122 ]
katip/README.md+1−1
@@ -16,7 +16,7 @@ Kâtip (pronounced kah-tip) is the Turkish word for scribe.
1616 add context that will be automatically merged in with existing log
1717 context.
1818
19−* *Easy to Integration:* Katip was designed to be easily integrated
19+* *Easy Integration:* Katip was designed to be easily integrated
2020 into existing monads. By using typeclasses for logging facilities,
2121 individual subsystems and even libraries can easily add their own
2222 namespacing and context without having any knowledge of their
katip/changelog.md+37
@@ -1,3 +1,40 @@
1+0.8.5.0
2+=======
3+* Add MonadResource instances [#121](https://github.com/Soostone/katip/pull/121)
4+
5+0.8.4.0
6+=======
7+* Add support for unliftio-core 0.2 while maintaining backwards-compatible support for 0.1
8+
9+0.8.3.0
10+=======
11+
12+* Add `logKatipItem` function and reimplement `logItem` to use it. [vlatkoB](https://github.com/vlatkoB)
13+
14+0.8.2.0
15+=======
16+* Add `MonadFail` instances for `base` >= 4.9.0.0 [Koray Al](https://github.com/korayal)
17+
18+0.8.1.0
19+=======
20+* Export `logLoc`. Credit to [Brian McKenna](https://github.com/puffnfresh)
21+
22+0.8.0.0
23+=======
24+* **Breaking change**: switch from severity based log level filtering to
25+ user-definable `PermitFunc`. To upgrade, any time you initialize one
26+ of the standard scribes, rather than providing a severity like
27+ `InfoS`, instead pass in `(permitItem InfoS)`. Credit to
28+ [vlatkoB](https://github.com/vlatkoB).
29+* Update some misformatted and outdated docs.
30+* Set lower bound of base to 4.9.0.0, the version shipped with GHC
31+ 8.0.2. Support for GHC versions older than that is removed.
32+* Update default formatter of Handle scribe to format pid as `[PID
33+ 1234]` instead of `[1234]` and `[ThreadId 1234]` instead of
34+ `[1234]`.
35+* Add missing `Bounded` instance for `Verbosity`. This allows you to
36+ easily enumerate all verbosities via `[minBound..maxBound]`.
37+
1380.7.0.0
239=======
340* Exclusively use HasCallstack for location reporting in errors. Credit to [Domen Kožar](https://github.com/domenkozar)
katip/katip.cabal+22−22
@@ -1,5 +1,5 @@
11name: katip
2−version: 0.8.0.0
2+version: 0.8.5.0
33synopsis: A structured logging framework.
44description:
55 Katip is a structured logging framework. See README.md for more details.
@@ -56,29 +56,29 @@ library
5656 build-depends: base >=4.9.0.0 && <5
5757 , aeson >=0.6
5858 , async < 3.0.0.0
59− , auto-update >= 0.1 && < 0.2
60− , bytestring >= 0.9 && < 0.11
61− , containers >=0.4 && <0.7
62− , either >= 4 && < 5.1
59+ , auto-update >= 0.1
60+ , bytestring >= 0.9
61+ , containers >=0.4
62+ , either >= 4
6363 , safe-exceptions >= 0.1.0.0
64− , hostname >=1.0 && <1.1
65− , old-locale >= 1.0 && < 1.1
66− , string-conv >= 0.1 && < 0.2
67− , template-haskell >= 2.8 && < 2.15
68− , text >= 0.11 && <1.3
69− , time >= 1 && < 1.9
70− , transformers >= 0.3 && < 0.6
64+ , hostname >=1.0
65+ , old-locale >= 1.0
66+ , string-conv >= 0.1
67+ , template-haskell >= 2.8
68+ , text >= 0.11
69+ , time >= 1
70+ , transformers >= 0.3
7171 , transformers-compat
72− , unordered-containers >= 0.2 && < 0.3
73− , monad-control >= 1.0 && < 1.1
74− , mtl >= 2.0 && < 2.3
75− , transformers-base >= 0.3 && < 0.6
76− , resourcet >= 1.1 && < 1.3
72+ , unordered-containers >= 0.2
73+ , monad-control >= 1.0
74+ , mtl >= 2.0
75+ , transformers-base >= 0.3
76+ , resourcet >= 1.1
7777 , scientific >= 0.3.3.0
78− , microlens >= 0.2.0.0 && < 0.5
79− , microlens-th >= 0.1.0.0 && < 0.5
78+ , microlens >= 0.2.0.0
79+ , microlens-th >= 0.1.0.0
8080 , semigroups
81− , unliftio-core >= 0.1 && < 0.2
81+ , unliftio-core >= 0.1
8282 , stm >= 2.4
8383
8484 hs-source-dirs: src
@@ -87,7 +87,7 @@ library
8787 if flag(lib-Werror)
8888 ghc-options: -Werror
8989 if os(windows)
90− build-depends: Win32 >=2.3 && <2.6
90+ build-depends: Win32 >=2.3 && <2.9
9191 exposed-modules: Katip.Compat
9292 else
9393 build-depends: unix >= 2.5 && <2.8
@@ -114,7 +114,7 @@ test-suite test
114114 , template-haskell
115115 , text
116116 , time
117− , time-locale-compat >= 0.1.0.1 && < 0.2
117+ , time-locale-compat >= 0.1.0.1
118118 , directory
119119 , regex-tdfa
120120 , unordered-containers
katip/src/Katip.hs+3−1
@@ -44,7 +44,7 @@
4444--
4545-- newtype App m a = App {
4646-- unApp :: ReaderT Config m a
47−-- } deriving (Functor, Applicative, Monad) -- more instances as needed
47+-- } deriving (Functor, Applicative, Monad, MonadIO, MonadReader Config) -- these are necessary
4848--
4949--
5050-- -- These instances get even easier with lenses!
@@ -159,7 +159,9 @@ module Katip
159159 , logF
160160 , logMsg
161161 , logT
162+ , logLoc
162163 , logItem
164+ , logKatipItem
163165 , logException
164166 -- ** 'KatipContext': Logging With Context
165167 -- $katipcontextlogging
katip/src/Katip/Core.hs+46−20
@@ -31,6 +31,9 @@ import qualified Control.Concurrent.STM.TBQueue as BQ
3131import Control.Exception.Safe
3232import Control.Monad (unless, void, when)
3333import Control.Monad.Base
34+#if MIN_VERSION_base(4, 9, 0)
35+import qualified Control.Monad.Fail as MF
36+#endif
3437import Control.Monad.IO.Class
3538import Control.Monad.IO.Unlift
3639import Control.Monad.Trans.Class
@@ -41,7 +44,8 @@ import Control.Monad.Trans.Either
4144import Control.Monad.Trans.Except
4245import Control.Monad.Trans.Maybe
4346import Control.Monad.Trans.Reader
44−import Control.Monad.Trans.Resource (ResourceT, transResourceT)
47+import Control.Monad.Trans.Resource (MonadResource, ResourceT,
48+ transResourceT)
4549import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST)
4650import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST, mapRWST)
4751import Control.Monad.Trans.State.Lazy (StateT, mapStateT)
@@ -139,7 +143,7 @@ data Severity
139143-- - 'V3' implies the maximum amount of payload information.
140144-- - Anything in between is left to the discretion of the developer.
141145data Verbosity = V0 | V1 | V2 | V3
142− deriving (Eq, Ord, Show, Read, Generic, Enum)
146+ deriving (Eq, Ord, Show, Read, Generic, Enum, Bounded)
143147
144148
145149-------------------------------------------------------------------------------
@@ -661,7 +665,7 @@ makeLenses ''LogEnv
661665
662666
663667-------------------------------------------------------------------------------
664−-- | Create a reasonable default InitLogEnv. Uses an 'AutoUdate' which
668+-- | Create a reasonable default InitLogEnv. Uses an 'AutoUpdate' which
665669-- updates the timer every 1ms. If you need even more timestamp
666670-- precision at the cost of performance, consider setting
667671-- '_logEnvTimer' with 'getCurrentTime'.
@@ -876,7 +880,8 @@ instance (Katip m) => Katip (ResourceT m) where
876880-- opposed to implementing 'Katip' for your monad.
877881newtype KatipT m a = KatipT { unKatipT :: ReaderT LogEnv m a }
878882 deriving ( Functor, Applicative, Monad, MonadIO
879− , MonadMask, MonadCatch, MonadThrow, MonadTrans, MonadBase b)
883+ , MonadMask, MonadCatch, MonadThrow, MonadResource, MonadTrans
884+ , MonadBase b)
880885
881886
882887instance MonadIO m => Katip (KatipT m) where
@@ -898,9 +903,20 @@ instance (MonadBaseControl b m) => MonadBaseControl b (KatipT m) where
898903 restoreM = defaultRestoreM
899904
900905instance MonadUnliftIO m => MonadUnliftIO (KatipT m) where
906+#if MIN_VERSION_unliftio_core(0, 2, 0)
907+ withRunInIO inner = KatipT $ ReaderT $ \le -> withRunInIO $ \run ->
908+ inner (run . runKatipT le)
909+#else
901910 askUnliftIO = KatipT $
902− withUnliftIO $ \u ->
903− pure (UnliftIO (unliftIO u . unKatipT))
911+ withUnliftIO $ \u ->
912+ pure (UnliftIO (unliftIO u . unKatipT))
913+#endif
914+
915+#if MIN_VERSION_base(4, 9, 0)
916+instance MF.MonadFail m => MF.MonadFail (KatipT m) where
917+ fail msg = lift (MF.fail msg)
918+ {-# INLINE fail #-}
919+#endif
904920
905921-------------------------------------------------------------------------------
906922-- | Execute 'KatipT' on a log env.
@@ -932,19 +948,29 @@ logItem
932948 -> m ()
933949logItem a ns loc sev msg = do
934950 LogEnv{..} <- getLogEnv
935− liftIO $ do
936− item <- Item
937− <$> pure _logEnvApp
938− <*> pure _logEnvEnv
939− <*> pure sev
940− <*> (mkThreadIdText <$> myThreadId)
941− <*> pure _logEnvHost
942− <*> pure _logEnvPid
943− <*> pure a
944− <*> pure msg
945− <*> _logEnvTimer
946− <*> pure (_logEnvApp <> ns)
947− <*> pure loc
951+ logKatipItem =<< liftIO
952+ (Item <$> pure _logEnvApp
953+ <*> pure _logEnvEnv
954+ <*> pure sev
955+ <*> (mkThreadIdText <$> myThreadId)
956+ <*> pure _logEnvHost
957+ <*> pure _logEnvPid
958+ <*> pure a
959+ <*> pure msg
960+ <*> _logEnvTimer
961+ <*> pure (_logEnvApp <> ns)
962+ <*> pure loc)
963+
964+-- | Log already constructed 'Item'. This is the lowest level function that other log*
965+-- functions use.
966+-- It can be useful when implementing centralised logging services.
967+logKatipItem
968+ :: (A.Applicative m, LogItem a, Katip m)
969+ => Item a
970+ -> m ()
971+logKatipItem item = do
972+ LogEnv{..} <- getLogEnv
973+ liftIO $
948974 FT.forM_ (M.elems _logEnvScribes) $ \ ScribeHandle {..} -> do
949975 whenM (scribePermitItem shScribe item) $
950976 void $ atomically (tryWriteTBQueue shChan (NewItem item))
@@ -993,7 +1019,7 @@ logException
9931019logException a ns sev action = action `catchAny` \e -> f e >> throwM e
9941020 where
9951021 f e = logF a ns sev (msg e)
996− msg e = ls (T.pack "An exception has occured: ") <> showLS e
1022+ msg e = ls (T.pack "An exception has occurred: ") <> showLS e
9971023
9981024
9991025-------------------------------------------------------------------------------
katip/src/Katip/Monadic.hs+28−6
@@ -44,6 +44,9 @@ import Control.Applicative
4444import Control.Exception.Safe
4545import Control.Monad.Base
4646import Control.Monad.Error.Class
47+#if MIN_VERSION_base(4, 9, 0)
48+import qualified Control.Monad.Fail as MF
49+#endif
4750import Control.Monad.IO.Class
4851import Control.Monad.IO.Unlift
4952import Control.Monad.Reader
@@ -55,7 +58,8 @@ import Control.Monad.Trans.Either (EitherT, mapEitherT)
5558import Control.Monad.Trans.Except (ExceptT, mapExceptT)
5659import Control.Monad.Trans.Identity (IdentityT, mapIdentityT)
5760import Control.Monad.Trans.Maybe (MaybeT, mapMaybeT)
58−import Control.Monad.Trans.Resource (ResourceT, transResourceT)
61+import Control.Monad.Trans.Resource (MonadResource,
62+ ResourceT, transResourceT)
5963import Control.Monad.Trans.RWS (RWST, mapRWST)
6064import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST, mapRWST)
6165import qualified Control.Monad.Trans.State.Strict as Strict (StateT, mapStateT)
@@ -312,7 +316,7 @@ logExceptionM
312316logExceptionM action sev = action `catchAny` \e -> f e >> throwM e
313317 where
314318 f e = logFM sev (msg e)
315− msg e = ls ("An exception has occured: " :: Text) Semi.<> showLS e
319+ msg e = ls ("An exception has occurred: " :: Text) Semi.<> showLS e
316320
317321
318322-------------------------------------------------------------------------------
@@ -346,6 +350,7 @@ newtype KatipContextT m a = KatipContextT {
346350 , MonadWriter w
347351 , MonadError e
348352 , MonadPlus
353+ , MonadResource
349354 , Alternative
350355 , MonadFix
351356 , MonadTrans
@@ -392,10 +397,21 @@ instance (MonadIO m) => KatipContext (KatipContextT m) where
392397 localKatipNamespace f (KatipContextT m) = KatipContextT $ local (\s -> s { ltsNamespace = f (ltsNamespace s)}) m
393398
394399instance MonadUnliftIO m => MonadUnliftIO (KatipContextT m) where
400+#if MIN_VERSION_unliftio_core(0, 2, 0)
401+ withRunInIO inner = KatipContextT $ ReaderT $ \lts -> withRunInIO $ \run ->
402+ inner (run . runKatipContextT (ltsLogEnv lts) (ltsContext lts) (ltsNamespace lts))
403+#else
395404 askUnliftIO = KatipContextT $
396− withUnliftIO $ \u ->
397− pure (UnliftIO (unliftIO u . unKatipContextT))
405+ withUnliftIO $ \u ->
406+ pure (UnliftIO (unliftIO u . unKatipContextT))
407+#endif
408+
398409
410+#if MIN_VERSION_base(4, 9, 0)
411+instance MF.MonadFail m => MF.MonadFail (KatipContextT m) where
412+ fail msg = lift (MF.fail msg)
413+ {-# INLINE fail #-}
414+#endif
399415
400416-------------------------------------------------------------------------------
401417runKatipContextT :: (LogItem c) => LogEnv -> c -> Namespace -> KatipContextT m a -> m a
@@ -474,9 +490,15 @@ instance MonadBaseControl b m => MonadBaseControl b (NoLoggingT m) where
474490 restoreM = NoLoggingT . restoreM
475491
476492instance MonadUnliftIO m => MonadUnliftIO (NoLoggingT m) where
493+#if MIN_VERSION_unliftio_core(0, 2, 0)
494+ withRunInIO inner = NoLoggingT $ withRunInIO $ \run ->
495+ inner (run . runNoLoggingT)
496+#else
477497 askUnliftIO = NoLoggingT $
478− withUnliftIO $ \u ->
479− pure (UnliftIO (unliftIO u . runNoLoggingT))
498+ withUnliftIO $ \u ->
499+ pure (UnliftIO (unliftIO u . runNoLoggingT))
500+#endif
501+
480502
481503instance MonadIO m => Katip (NoLoggingT m) where
482504 getLogEnv = liftIO (initLogEnv "NoLoggingT" "no-logging")
stack.yaml+5−3
@@ -1,12 +1,14 @@
1−resolver: lts-12.26
1+resolver: lts-16.18
22packages:
33 - katip
44 - katip-elasticsearch
55 - katip-datadog
66 - katip-logzio
77extra-deps:
8− - bloodhound-0.16.0.0
9− - unliftio-core-0.1.2.0
8+ # ghc 8.8 support
9+ - github: wireapp/bloodhound
10+ commit: b409f52b518f20a4c20651b5893c31b7b85cb9b1
11+allow-newer: true
1012flags:
1113 katip:
1214 lib-Werror: true