on master help sign in
52 files changed+6738−679
since splitsnapshots
unifiedsplit
.gitignore+6 .travis.yml+61 README.md+10−7 TODO.org+9−1 katip-datadog/LICENSE+30 katip-datadog/README.md+6 katip-datadog/changelog.md+7 katip-datadog/katip-datadog.cabal+78 katip-datadog/src/Katip/Scribes/Datadog/TCP.hs+191 katip-datadog/test/Katip/Tests/Scribes/Datadog/TCP.hs+105 katip-datadog/test/Main.hs+22 katip-elasticsearch/LICENSE+1−1 katip-elasticsearch/README.md+24 katip-elasticsearch/bench/Main.hs+38−14 katip-elasticsearch/changelog.md+131 katip-elasticsearch/docker-compose.yml+10 katip-elasticsearch/examples/example.hs+34 katip-elasticsearch/katip-elasticsearch.cabal+78−20 katip-elasticsearch/src/Katip/Scribes/ElasticSearch.hs+76−136 katip-elasticsearch/src/Katip/Scribes/ElasticSearch/Annotations.hs+112 katip-elasticsearch/src/Katip/Scribes/ElasticSearch/Internal.hs+577 katip-elasticsearch/test/Main.hs+545 katip-logzio/LICENSE+30 katip-logzio/README.md+4 katip-logzio/changelog.md+9 katip-logzio/katip-logzio.cabal+94 katip-logzio/src/Katip/Scribes/LogzIO/HTTPS.hs+639 katip-logzio/test/Katip/Tests/Scribes/LogzIO/HTTPS.hs+466 katip-logzio/test/Main.hs+22 katip/LICENSE+1−1 katip/README.md+89 katip/bench/Main.hs+70−42 katip/changelog.md+189 katip/examples/Main.hs−122 katip/examples/add_context_loop.hs+69 katip/examples/example.hs+128 katip/examples/example_lens.hs+134 katip/katip.cabal+108−32 katip/katip.jpg katip/src/Katip.hs+253−14 katip/src/Katip/Compat.hs+30 katip/src/Katip/Core.hs+761−90 katip/src/Katip/Format/Time.hs+213 katip/src/Katip/Monadic.hs+364−145 katip/src/Katip/Scribes/Handle.hs+129−54 katip/test/Katip/Tests.hs+319 katip/test/Katip/Tests/Format/Time.hs+81 katip/test/Katip/Tests/Scribes/Handle-json.golden+38 katip/test/Katip/Tests/Scribes/Handle-text.golden+40 katip/test/Katip/Tests/Scribes/Handle.hs+266 katip/test/Main.hs+21 stack.yaml+20
.gitignore+6
@@ -2,3 +2,9 @@
22dist
33cabal.sandbox.config
44TAGS
5+.stack-work/
6+*.o
7+*.hi
8+*.dyn_o
9+*.dyn_hi
10+stack.yaml.lock
.travis.ymlnew file+61
@@ -0,0 +1,61 @@
1+# Use new container infrastructure to enable caching
2+sudo: false
3+
4+# Choose a lightweight base image; we provide our own build tools.
5+language: c
6+
7+# GHC depends on GMP. You can add other dependencies here as well.
8+addons:
9+ apt:
10+ packages:
11+ - libgmp-dev
12+
13+# The different configurations we want to test. You could also do things like
14+# change flags or use --stack-yaml to point to a different file.
15+matrix:
16+ include:
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
22+ allow_failures:
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"
25+
26+before_install:
27+# Download and unpack the stack executable
28+- mkdir -p ~/.local/bin
29+- export PATH=$HOME/.local/bin:$PATH
30+- travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
31+
32+install:
33+ - wget --no-check-certificate $ESURL/elasticsearch-$ESVERSION.tar.gz
34+ - tar xzf elasticsearch-$ESVERSION.tar.gz
35+ # set up a repo for snapshot testing. Required in ES >= 1.6
36+ - echo "path.repo = [\"/tmp\"]" >> ./elasticsearch-$ESVERSION/elasticsearch.yml
37+ - java -version
38+ - ./elasticsearch-$ESVERSION/bin/elasticsearch &
39+ # Bloodhound seems to take a while to build and can sometimes stall out the build when travis detects no output.
40+
41+
42+# This line does all of the work: installs GHC if necessary, build the library,
43+# executables, and test suites, and runs the test suites. --no-terminal works
44+# around some quirks in Travis's terminal implementation.
45+script:
46+ - stack $ARGS setup
47+ # Prevent Inactivity timeouts. See https://github.com/travis-ci/travis-ci/issues/7020#issuecomment-288961580
48+ - travis_wait 45 sleep 1800 &
49+ - stack $ARGS test katip:test --no-terminal --haddock --no-haddock-deps
50+ - stack $ARGS test katip-elasticsearch:test --no-terminal --test-arguments="$ESTESTARGS"
51+ - stack $ARGS bench
52+ - stack $ARGS build
53+ - for f in katip/examples/*.hs; do stack $ARGS ghc -- -Wall -Werror $f -o /dev/null; done
54+ - for f in katip-elasticsearch/examples/*.hs; do stack $ARGS ghc -- -Wall -Werror $f -o /dev/null; done
55+ - stack $ARGS sdist
56+
57+# Caching so the next build will be fast too.
58+cache:
59+ directories:
60+ - $HOME/.stack
61+ - .stack-work
README.md+10−7
@@ -1,18 +1,21 @@
1+# Katip [![Build Status](https://travis-ci.org/Soostone/katip.svg?branch=master)](https://travis-ci.org/Soostone/katip)
12
2−# Katip
3+Katip is a structured logging framework for Haskell.
34
4−A structured logging framework for tagging each log message with its
5−surrounding context.
5+![Katip](https://github.com/Soostone/katip/blob/master/katip/katip.jpg)
66
7+Kâtip (pronounced kah-tip) is the Turkish word for scribe.
78
89## katip
910
10−This is the core katip logging library.
11+This is the core katip logging library. It includes a file handle
12+logger.
1113
14+See [katip's README](https://github.com/Soostone/katip/blob/master/katip/README.md) for more details.
1215
13−## katip-elasticsearch
14−
15−Elasticsearch backend for katip using the bloodhound bindings package.
1616
17+## katip-elasticsearch
1718
19+ElasticSearch backend for katip using the `bloodhound` package.
1820
21+See [katip-elasticsearch's README](https://github.com/Soostone/katip/blob/master/katip-elasticsearch/README.md) for more details.
TODO.org+9−1
@@ -72,7 +72,15 @@
7272 Seems to be a limitation of http-client that we'll have to live
7373 with for now.
7474
75−
75+** DONE update index settings on setup (when the API becomes available in bloodhound)
76+ CLOSED: [2015-11-12 Thu 08:33]
77+
78+** TODO by-day or user scheduled index sharding
79+ Consider using templates and write to indexes per-day. So rather
80+ than building the direct index the user asks for, set up a template
81+ that automatically gets applied to indices matching the prefix,
82+ then create an index per day. Apparently this allows more efficient
83+ searches. Its yet to be seen when one-big-index breaks down
7684* katip-rotated-log
7785
7886** TODO Create a rotated-log scribe
katip-datadog/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2018, Soostone Inc
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Ozgun Ataman nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
katip-datadog/README.mdnew file+6
@@ -0,0 +1,6 @@
1+# Katip Datadog [![Build Status](https://travis-ci.org/Soostone/katip.svg?branch=master)](https://travis-ci.org/Soostone/katip)
2+
3+katip-datadog is a scribe for the Katip logging framework that sends
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.mdnew file+7
@@ -0,0 +1,7 @@
1+0.2.0.0
2+==========
3+* Remove import of deprecated Network module
4+
5+0.1.0.0
6+==========
7+* Initial release
katip-datadog/katip-datadog.cabalnew file+78
@@ -0,0 +1,78 @@
1+name: katip-datadog
2+version: 0.2.0.0
3+synopsis: Datadog scribe for the Katip logging framework
4+description: See README.md for more details.
5+license: BSD3
6+license-file: LICENSE
7+author: Michael Xavier
8+maintainer: michael.xavier@soostone.com
9+copyright: Soostone Inc, 2018
10+category: Data, Text, Logging
11+homepage: https://github.com/Soostone/katip
12+bug-reports: https://github.com/Soostone/katip/issues
13+build-type: Simple
14+cabal-version: >=1.10
15+extra-source-files:
16+ README.md
17+ changelog.md
18+
19+--TODO: fill this in
20+tested-with: GHC == 8.4.3
21+
22+source-repository head
23+ type: git
24+ location: https://github.com/Soostone/katip.git
25+
26+flag lib-Werror
27+ default: False
28+ manual: True
29+
30+
31+library
32+ exposed-modules:
33+ Katip.Scribes.Datadog.TCP
34+ build-depends:
35+ base >=4.4.0.0 && <5
36+ , aeson >= 1.0.0.0
37+ , bytestring
38+ , katip >= 0.8.0.0
39+ , text
40+ , connection
41+ , resource-pool
42+ , time
43+ , binary
44+ , network
45+ , safe-exceptions
46+ , retry >= 0.7
47+ hs-source-dirs: src
48+ default-language: Haskell2010
49+ hs-source-dirs: src
50+ ghc-options: -Wall
51+ if flag(lib-Werror)
52+ ghc-options: -Werror
53+
54+
55+test-suite test
56+ type: exitcode-stdio-1.0
57+ main-is: Main.hs
58+ hs-source-dirs: test
59+ default-language: Haskell2010
60+ other-modules:
61+ Katip.Tests.Scribes.Datadog.TCP
62+ build-depends: base
63+ , katip
64+ , katip-datadog
65+ , conduit
66+ , conduit-extra
67+ , tasty
68+ , tasty-hunit
69+ , aeson
70+ , unordered-containers
71+ , containers
72+ , safe-exceptions
73+ , async
74+ , attoparsec
75+ , text
76+ ghc-options: -Wall
77+ if flag(lib-Werror)
78+ ghc-options: -Werror
katip-datadog/src/Katip/Scribes/Datadog/TCP.hsnew file+191
@@ -0,0 +1,191 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE RankNTypes #-}
4+-- | Creates a scribe as a Custom Forwarder for Datadog that sends log
5+-- messages over TCP via their TLS endpoint.
6+module Katip.Scribes.Datadog.TCP
7+ ( -- * Types
8+ APIKey(..)
9+ , DatadogScribeSettings(..)
10+ , DatadogAuth(..)
11+ , directAPIConnectionParams
12+ , localAgentConnectionParams
13+ -- * Scribe construction
14+ , mkDatadogScribeSettings
15+ , mkDatadogScribe
16+ ) where
17+
18+
19+
20+-------------------------------------------------------------------------------
21+import qualified Control.Concurrent as Conc
22+import qualified Control.Exception.Safe as EX
23+import Control.Monad (void)
24+import qualified Control.Retry as Retry
25+import qualified Data.Aeson as A
26+import qualified Data.Binary.Builder as BB
27+import qualified Data.ByteString.Lazy as BSL
28+import Data.Monoid as Monoid
29+import qualified Data.Pool as Pool
30+import Data.Text (Text)
31+import qualified Data.Text.Encoding as T
32+import qualified Data.Text.Lazy.Builder as B
33+import Data.Time (NominalDiffTime)
34+import Katip
35+import Katip.Core (LocJs (..))
36+import qualified Network.Connection as C
37+import qualified Network.Socket as Net
38+import qualified System.Posix.Types as POSIX
39+-------------------------------------------------------------------------------
40+
41+
42+-- | When writing directly to the main intake API, you must specify an
43+-- API key
44+newtype APIKey = APIKey
45+ { apiKey :: Text
46+ } deriving (Show, Eq)
47+
48+
49+data DatadogAuth =
50+ NoAuthLocal
51+ -- ^ Writing to the local agent, no auth required
52+ | DirectAuth APIKey
53+ -- ^ Writing directly to the main intake API over TLS . API Key is
54+ -- required. Using a local agent is recommended.
55+
56+
57+data DatadogScribeSettings = DatadogScribeSettings
58+ { datadogScribeSettings_connectionParams :: C.ConnectionParams
59+ -- ^ Specify where the logs should go. If writing directly to the
60+ -- main intake API, you can use 'directAPIConnectionParams'. If
61+ -- writing to a local agent, we recommend
62+ -- 'localAgentConnectionParams'
63+ , datadogScribeSettings_auth :: DatadogAuth
64+ , datadogScribeSettings_poolStripes :: Int
65+ -- ^ How many stripes should be used in the connection pool. 1 is a reasonable default
66+ , datadogScribeSettings_connsPerStripe :: Int
67+ -- ^ How many TCP connections per stripe should be maintained in the pool.
68+ , datadogScribeSettings_connectionKeepalive :: NominalDiffTime
69+ -- ^ How long should a TCP connection be kept open. 30 (seconds) is a reasonable default. Anything around 60 will tend to time out a lot.
70+ , datadogScribeSettings_retry :: Retry.RetryPolicyM IO
71+ -- ^ How should exceptions during write be retried?
72+ }
73+
74+
75+-- | Configured for secure TLS communication to the main intake API at
76+-- intake.logs.datadoghq.com:10516. Must be combined with DirectAuth
77+-- authentication.
78+directAPIConnectionParams :: C.ConnectionParams
79+directAPIConnectionParams = C.ConnectionParams
80+ { C.connectionHostname = "intake.logs.datadoghq.com"
81+ , C.connectionPort = 10516
82+ , C.connectionUseSecure = Just $ C.TLSSettingsSimple
83+ { C.settingDisableCertificateValidation = False
84+ , C.settingDisableSession = False
85+ , C.settingUseServerName = False
86+ }
87+ , C.connectionUseSocks = Nothing
88+ }
89+
90+
91+-- | Configure to talk to a local Datadog agent at 127.0.0.1. TLS is not
92+-- utilized. This is the preferred type of connection because
93+-- authentication, buffering, and other more advanced features are
94+-- handled by the agent configuration.
95+localAgentConnectionParams :: Net.PortNumber -> C.ConnectionParams
96+localAgentConnectionParams port = C.ConnectionParams
97+ { C.connectionHostname = "127.0.0.1"
98+ , C.connectionPort = port
99+ , C.connectionUseSecure = Nothing
100+ , C.connectionUseSocks = Nothing
101+ }
102+
103+
104+-- | Reasonable defaults. Sets a 30s connection timeout, 1 stripe of
105+-- connections with a number of connections per stripe equal to
106+-- 'getNumCapabilities'. Retry policy will do an exponential backoff
107+-- with a 25ms base delay for up to 5 retries for a total cumulative delay of 775ms
108+mkDatadogScribeSettings :: C.ConnectionParams -> DatadogAuth -> IO DatadogScribeSettings
109+mkDatadogScribeSettings connectionParams auth = do
110+ capabilities <- Conc.getNumCapabilities
111+ pure $ DatadogScribeSettings
112+ { datadogScribeSettings_connectionParams = connectionParams
113+ , datadogScribeSettings_auth = auth
114+ , datadogScribeSettings_poolStripes = 1
115+ , datadogScribeSettings_connsPerStripe = capabilities
116+ , datadogScribeSettings_connectionKeepalive = 30
117+ , datadogScribeSettings_retry = Retry.exponentialBackoff 25000 <> Retry.limitRetries 5
118+ }
119+
120+
121+-------------------------------------------------------------------------------
122+mkDatadogScribe
123+ :: DatadogScribeSettings
124+ -> PermitFunc
125+ -- ^ Function on whether to permit items, e.g. @permitItem InfoS@
126+ -> Verbosity
127+ -- ^ Verbosity level to observe
128+ -> IO Scribe
129+mkDatadogScribe settings permit verb = do
130+ connectionContext <- C.initConnectionContext
131+ pool <- Pool.createPool
132+ (C.connectTo connectionContext (datadogScribeSettings_connectionParams settings))
133+ C.connectionClose
134+ (datadogScribeSettings_poolStripes settings)
135+ (datadogScribeSettings_connectionKeepalive settings)
136+ (datadogScribeSettings_connsPerStripe settings)
137+ pure $ Scribe
138+ { liPush = pushPool (datadogScribeSettings_retry settings) pool keyBuilder verb
139+ , scribeFinalizer = Pool.destroyAllResources pool
140+ , scribePermitItem = permit
141+ }
142+ where
143+ !keyBuilder = case datadogScribeSettings_auth settings of
144+ NoAuthLocal -> Nothing
145+ DirectAuth (APIKey k) -> Just (BB.fromByteString (T.encodeUtf8 k))
146+
147+
148+pushPool
149+ :: LogItem a
150+ => Retry.RetryPolicyM IO
151+ -> Pool.Pool C.Connection
152+ -> Maybe BB.Builder
153+ -- ^ Rendered API key
154+ -> Verbosity
155+ -> Item a
156+ -> IO ()
157+pushPool retryPolicy pool token verb item =
158+ void $ Retry.retrying retryPolicy (\_status shouldRetry -> pure shouldRetry) $ \_status -> do
159+ res <- EX.tryAny $ Pool.withResource pool $ \conn -> do
160+ C.connectionPut conn rendered
161+ pure $ case res of
162+ Left _ -> True
163+ Right _ -> False
164+ where
165+ payloadBuilder = A.fromEncoding (encodeDatadog verb item) Monoid.<> "\n"
166+ messageBuilder = case token of
167+ --TODO: lots of conversion going on here. could use a bench
168+ Nothing -> payloadBuilder
169+ Just directKey -> directKey <> " " <> payloadBuilder
170+ rendered = BSL.toStrict (BB.toLazyByteString messageBuilder)
171+
172+
173+encodeDatadog :: LogItem a => Verbosity -> Item a -> A.Encoding
174+encodeDatadog verb i = A.pairs $
175+ -- datadog seems to use "service" to denote appname
176+ "service" A..= _itemApp i <>
177+ -- datadog uses severity (or some other alternatives for sev)
178+ "severity" A..= _itemSeverity i <>
179+ "env" A..= _itemEnv i <>
180+ "thread" A..= getThreadIdText (_itemThread i) <>
181+ "host" A..= _itemHost i <>
182+ "pid" A..= pidInt <>
183+ "data" A..= payloadObject verb (_itemPayload i) <>
184+ -- datadog uses "message" instead of msg
185+ "message" A..= (B.toLazyText (unLogStr (_itemMessage i))) <>
186+ -- datadog uses "date" instead of "at"
187+ "date" A..= _itemTime i <>
188+ "ns" A..= _itemNamespace i <>
189+ "loc" A..= fmap LocJs (_itemLoc i)
190+ where
191+ POSIX.CPid pidInt = _itemProcess i
katip-datadog/test/Katip/Tests/Scribes/Datadog/TCP.hsnew file+105
@@ -0,0 +1,105 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Katip.Tests.Scribes.Datadog.TCP
3+ ( tests
4+ ) where
5+
6+
7+-------------------------------------------------------------------------------
8+import Control.Concurrent.Async
9+import qualified Control.Exception.Safe as EX
10+import qualified Data.Aeson as A
11+import qualified Data.Char as Char
12+import qualified Data.Conduit as C
13+import qualified Data.Conduit.Binary as CB
14+import qualified Data.Conduit.List as CL
15+import qualified Data.Conduit.Network as CN
16+import Data.Foldable (toList)
17+import qualified Data.HashMap.Strict as HM
18+import Data.IORef
19+import Data.Sequence ((|>))
20+import Data.Text (Text)
21+import qualified Data.Text as T
22+import Test.Tasty
23+import Test.Tasty.HUnit
24+-------------------------------------------------------------------------------
25+import Katip
26+import Katip.Scribes.Datadog.TCP
27+-------------------------------------------------------------------------------
28+
29+
30+
31+tests :: TestTree
32+tests = testGroup "Katip.Scribes.Datadog.TCP"
33+ [ testCase "logs well-formed messages in order" $ do
34+ logs <- collectLogs $ do
35+ logItem (sl "foo" ("bar" :: Text)) "mynamespace" noLoc InfoS "a message"
36+ length logs @?= 1
37+ let (CapturedLog ns dta sev msg thread) = head logs
38+ ns @?= Namespace ["katip-datadog-tests", "mynamespace"]
39+ dta @?= A.Object (HM.singleton "foo" (A.String "bar"))
40+ sev @?= InfoS
41+ msg @?= "a message"
42+ if T.all Char.isNumber thread && T.length thread > 0
43+ then pure ()
44+ else assertFailure ("Expected thread id to just be a number but it was " <> T.unpack thread)
45+ ]
46+ where
47+ noLoc = Nothing
48+
49+
50+-------------------------------------------------------------------------------
51+-- | A log type parsed out of the fake datadog agent's buffer
52+data CapturedLog = CapturedLog
53+ { capturedLog_namespace :: Namespace
54+ , capturedLog_data :: A.Value
55+ , capturedLog_sev :: Severity
56+ , capturedLog_message :: Text
57+ , capturedLog_thread :: Text
58+ } deriving (Show, Eq)
59+
60+
61+instance A.FromJSON CapturedLog where
62+ parseJSON = A.withObject "CapturedLog" $ \o -> do
63+ message <- o A..: "message"
64+ sev <- o A..: "severity"
65+ dta <- o A..: "data"
66+ namespace <- o A..: "ns"
67+ thread <- o A..: "thread"
68+ pure $ CapturedLog
69+ { capturedLog_namespace = namespace
70+ , capturedLog_data = dta
71+ , capturedLog_sev = sev
72+ , capturedLog_message = message
73+ , capturedLog_thread = thread
74+ }
75+
76+
77+-------------------------------------------------------------------------------
78+collectLogs :: KatipT IO () -> IO [CapturedLog]
79+collectLogs f = do
80+ capturedRef <- newIORef mempty
81+ withAsync (startServer capturedRef) $ \_worker -> do
82+ EX.bracket mkLE closeLE $ \le -> do
83+ runKatipT le f
84+ toList <$> readIORef capturedRef
85+ where
86+ closeLE = closeScribes
87+ port = 1337
88+ mkLE = do
89+ datadogScribeSettings <- mkDatadogScribeSettings (localAgentConnectionParams (fromInteger port)) NoAuthLocal
90+ scribe <- mkDatadogScribe datadogScribeSettings (permitItem DebugS) V3
91+ registerScribe "datadog" scribe defaultScribeSettings =<< initLogEnv "katip-datadog-tests" "test"
92+ startServer capturedRef = CN.runTCPServer (CN.serverSettings (fromInteger port) "*") $ \appData -> C.runConduit $
93+ CN.appSource appData C..|
94+ CB.lines C..|
95+ CL.map A.eitherDecodeStrict C..|
96+ CL.mapM_ (\parseResult -> case parseResult of
97+ Left e -> EX.throwM (ParseError e)
98+ (Right capturedLog) -> modifyIORef' capturedRef (\captured -> captured |> capturedLog))
99+
100+
101+-------------------------------------------------------------------------------
102+newtype ParseError = ParseError String
103+ deriving (Show)
104+
105+instance EX.Exception ParseError
katip-datadog/test/Main.hsnew file+22
@@ -0,0 +1,22 @@
1+module Main
2+ ( main
3+ ) where
4+
5+
6+-------------------------------------------------------------------------------
7+import Test.Tasty
8+-------------------------------------------------------------------------------
9+import qualified Katip.Tests.Scribes.Datadog.TCP
10+-------------------------------------------------------------------------------
11+
12+
13+
14+main :: IO ()
15+main = defaultMain tests
16+
17+
18+-------------------------------------------------------------------------------
19+tests :: TestTree
20+tests = testGroup "katip-datadog"
21+ [ Katip.Tests.Scribes.Datadog.TCP.tests
22+ ]
katip-elasticsearch/LICENSE+1−1
@@ -1,4 +1,4 @@
1−Copyright (c) 2015, Soostone Inc
1+Copyright (c) 2015-2016, Soostone Inc
22
33All rights reserved.
44
katip-elasticsearch/README.mdnew file+24
@@ -0,0 +1,24 @@
1+# Katip Elasticsearch [![Build Status](https://travis-ci.org/Soostone/katip.svg?branch=master)](https://travis-ci.org/Soostone/katip)
2+
3+katip-elasticsearch is a scribe for the Katip logging framework that
4+sends structured logs to ElasticSearch.
5+
6+## Features
7+
8+* Built in bounded buffering.
9+
10+* Configurable pool of logging workers to help with high write
11+ volume.
12+
13+* Optional field type annotation to avoid mistyping values.
14+
15+* Optional automatic date sharding, so logs can be filed into monthly,
16+ weekly, daily, hourly, every minute indices. You can even specify
17+ your own index routing logic. This pattern can be seen in the ELK
18+ stack as a way of keeping indexes reasonably sized and easy to
19+ optimize, rotate, and manage.
20+
21+* Customizable retry policy for temporary outages and errors.
22+
23+* Automatic index and mapping setup.
24+
katip-elasticsearch/bench/Main.hs+38−14
@@ -10,34 +10,58 @@ module Main
1010import Control.DeepSeq
1111import Control.Monad
1212import Criterion.Main
13−import Data.RNG
14−import qualified Data.Text as T
13+import Data.Aeson
14+import qualified Data.HashMap.Strict as HM
15+import Data.Proxy (Proxy (..))
16+import System.Random
17+import qualified Data.Text as T
1518import Database.Bloodhound.Types
1619import Numeric
1720-------------------------------------------------------------------------------
1821import Katip.Scribes.ElasticSearch
22+import Katip.Scribes.ElasticSearch.Internal (ESV5)
1923-------------------------------------------------------------------------------
2024
2125main :: IO ()
2226main = do
23− rng <- mkRNG
24− defaultMain [mkDocIdBenchmark rng]
27+ defaultMain
28+ [
29+ mkDocIdBenchmark
30+ , deannotateValueBenchmark
31+ ]
2532
2633-------------------------------------------------------------------------------
27−mkDocIdBenchmark :: RNG -> Benchmark
28−mkDocIdBenchmark rng = bgroup "mkDocId"
29− [ bench "mkDocId (randomIO)" $ nfIO mkDocId
30− , bench "mkDocId' (shared )" $ nfIO $ mkDocId' rng
34+mkDocIdBenchmark :: Benchmark
35+mkDocIdBenchmark = bgroup "mkDocId"
36+ [
37+ bench "mkDocId (randomIO)" $ nfIO (mkDocId (Proxy :: Proxy ESV5))
38+ , bench "mkDocId' (shared )" $ nfIO mkDocId'
3139 ]
3240
3341
34−mkDocId' :: RNG -> IO DocId
35−mkDocId' rng = do
36− is <- withRNG rng $ \gen -> replicateM len $ mk gen
37− return . DocId . T.pack . concatMap (`showHex` "") $ is
42+-------------------------------------------------------------------------------
43+deannotateValueBenchmark :: Benchmark
44+deannotateValueBenchmark = bgroup "deannotateValue"
45+ [
46+ bench "deannotateValue" $ nf deannotateValue annotatedValue
47+ ]
48+
49+
50+-------------------------------------------------------------------------------
51+annotatedValue :: Value
52+annotatedValue = Object $ HM.fromList [ ("a::string", String "whatever")
53+ , ("b::double", Number 4.5)
54+ , ("c::long", Number 4)
55+ , ("d::boolean", Bool True)
56+ , ("e::null", Null)
57+ ]
58+
59+-------------------------------------------------------------------------------
60+mkDocId' :: IO DocId
61+mkDocId' = do
62+ is <- replicateM len (randomRIO (0, 15))
63+ return (DocId (T.pack (concatMap (`showHex` "") (is :: [Int]))))
3864 where
3965 len = 32
40− mk :: GenIO -> IO Int
41− mk = uniformR (0,15)
4266
4367deriving instance NFData DocId
katip-elasticsearch/changelog.mdnew file+131
@@ -0,0 +1,131 @@
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+
22+0.5.1.1
23+=======
24+* Loosen katip dep
25+
26+0.5.1.0
27+=======
28+* Loosen deps
29+
30+0.5.0.0
31+=======
32+* Update template for ESv1 to not analyze certain fields like host and
33+ namespace. These are not fields that benefit from tokenization. If
34+ you're on ESv1, after running `mkEsScribe`, the logs template will
35+ be updated to stop analyzing those fields. That means the next
36+ index that is cut (e.g. tomorrow's logs if you're using daily index
37+ sharding) will no longer analyze some fields.
38+
39+0.4.2.0
40+=======
41+* Allow newer bloodhound, aeson, async.
42+
43+0.4.1.0
44+=======
45+* Fix bug where index was created in `mkEsScribe` when it would not be used due to index sharding.
46+* Update some index settings if index already exists and sharding is not used.
47+* For ES V5 and higher, stop using the deprecated (and in 6.x, removed) `string` type for index templates, instead using `text` and `keyword` as appropriate. This makes `katip-elasticsearch` compatible with ES 6.x.
48+
49+0.4.0.4
50+=======
51+* Allow http-types 0.12
52+
53+0.4.0.3
54+=======
55+* Fix benchmark and test builds for stackage nightly
56+
57+0.4.0.2
58+=======
59+* Add repository/homepage info to cabal file
60+
61+0.4.0.1
62+=======
63+* Bump dependencies to allow GHC 8.2.1
64+
65+0.4.0.0
66+=======
67+* Update to bloodhound >= 0.13.0.0. This version adds support for both ElasticSearch versions 1 and 5. Previously, we implicitly supported one and maybe would work on 5. The types in `EsScribeCfg` had to change to be able to specify which version was being targeted.
68+* Improved documentation.
69+
70+0.3.1.0
71+=======
72+* Widen dependency on katip
73+
74+0.3.0.2
75+=======
76+* Loosen deps on aeson to allow 1.1.0.0
77+
78+0.3.0.1
79+=======
80+* Loosen deps on bloodhound, aeson, and http-client.
81+
82+0.3.0.0
83+==============
84+* Added zero padding to date-based indices. This shouldn't negatively
85+ impact most users but to be safe, this was put behind a breaking
86+ version number. Previously, you may see indices
87+ `log-index-prefix-2016-1-2`. That index will now be
88+ `log-index-prefix-2016-01-02`, so at worst when you deploy, the day
89+ of the change will have 2 indices: one zero-padded and one not. If
90+ you are using custom index sharding, you will not be affected by
91+ this. The reasoning here is that most existing elasticsearch tools
92+ such as `curator` expects date indices to be zero padded. By
93+ switching to the standard, users can easily get log rotation and
94+ other features from `curator` rather than having to roll their own
95+ or add them to Katip.
96+
97+0.2.1.0
98+==============
99+
100+* Drop direct dependency on random, upgrade uuid to >= 1.3.12 for
101+ safer id generation. Previously, UUID was using randomIO, which uses
102+ the system clock as a seed. So if multiple nodes happened to start
103+ at the same time, they would produce conflicting UUID sequences.
104+
105+0.2.0.0
106+==============
107+
108+* Default index sharding policy to daily. Previously it was no
109+ sharding. The reasoning here is that no sharding creates very large
110+ indices which become very difficult to manage in
111+ production. Rotating data out on a time basis is very slow compared
112+ to deleting date-based indices.
113+
114+ Upgrade note: if you were using the defaults before and switch to
115+ daily, rather than having the index name of `my-index`, you'll start
116+ seeing `my-index-2016-3-14`. The good news is that whatever you're
117+ using to use to search against your logs (such as kibana) will
118+ support index patterns, so just use the pattern of `my-index*` to
119+ get everything. Eventually if you have a retention period, you can
120+ manually delete the `my-index` index without disruption.
121+
122+
123+0.1.1.0
124+==============
125+
126+* Set upper bounds for a few dependencies.
127+
128+0.1.0.0
129+==============
130+
131+* Initial release
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.hsnew file+34
@@ -0,0 +1,34 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Main
3+ ( main
4+ ) where
5+
6+
7+-------------------------------------------------------------------------------
8+import Control.Exception
9+import Database.Bloodhound
10+import Network.HTTP.Client
11+-------------------------------------------------------------------------------
12+import Katip
13+import Katip.Scribes.ElasticSearch
14+-------------------------------------------------------------------------------
15+
16+
17+main :: IO ()
18+main = do
19+ mgr <- newManager defaultManagerSettings
20+ let bhe = mkBHEnv (Server "http://localhost:9200") mgr
21+ -- You may need to specify the version in a type signature when customizing your index settings
22+ let _customizedExample = defaultEsScribeCfgV5 { essIndexSettings = IndexSettings (ShardCount 1) (ReplicaCount 0)} :: EsScribeCfg ESV5
23+ esScribe <- mkEsScribe
24+ -- Reasonable for production
25+ defaultEsScribeCfgV5
26+ -- Reasonable for single-node in development
27+ bhe
28+ (IndexName "all-indices-prefixed-with")
29+ (MappingName "application-logs")
30+ (permitItem DebugS)
31+ V3
32+ let mkLogEnv = registerScribe "es" esScribe defaultScribeSettings =<< initLogEnv "MyApp" "production"
33+ bracket mkLogEnv closeScribes $ \le -> runKatipT le $ do
34+ logMsg "ns" InfoS "This goes to elasticsearch"
katip-elasticsearch/katip-elasticsearch.cabal+78−20
@@ -1,13 +1,28 @@
11name: katip-elasticsearch
2−version: 0.1.0.0
2+synopsis: ElasticSearch scribe for the Katip logging framework.
3+description: See README.md for more details.
4+version: 0.7.0.0
35license: BSD3
46license-file: LICENSE
5−author: Ozgun Ataman
6−maintainer: ozgun.ataman@soostone.com
7−copyright: Soostone Inc, 2015
8−category: Data
7+author: Ozgun Ataman, Michael Xavier
8+maintainer: michael.xavier@soostone.com
9+copyright: Soostone Inc, 2015-2017
10+category: Data, Text, Logging
11+homepage: https://github.com/Soostone/katip
12+bug-reports: https://github.com/Soostone/katip/issues
913build-type: Simple
1014cabal-version: >=1.10
15+extra-source-files:
16+ README.md
17+ changelog.md
18+ bench/Main.hs
19+ examples/example.hs
20+ test/Main.hs
21+tested-with: GHC == 7.8.4, GHC== 7.10.3, GHC == 8.0.1, GHC == 8.2.2, GHC == 8.4.3
22+
23+source-repository head
24+ type: git
25+ location: https://github.com/Soostone/katip.git
1126
1227flag lib-Werror
1328 default: False
@@ -16,23 +31,32 @@ flag lib-Werror
1631library
1732 exposed-modules:
1833 Katip.Scribes.ElasticSearch
34+ Katip.Scribes.ElasticSearch.Annotations
35+ Katip.Scribes.ElasticSearch.Internal
1936 build-depends:
20− base >=4.6 && <4.7
21− , katip
22− , bloodhound >= 0.5 && < 0.6
23− , random >= 1.0 && < 1.2
24− , uuid >= 1.3 && < 1.4
25− , aeson >=0.6 && <0.9
26− , stm >= 2.4.3 && < 2.5
27− , async >= 2.0.1.0 && < 2.1
28− , enclosed-exceptions >= 1.0.0 && < 1.1
29− , http-client
30− , text
31− , retry >= 0.6 && < 0.8
37+ base >=4.6 && <5
38+ , katip >= 0.8.0.0
39+ , bloodhound >= 0.13.0.0
40+ , uuid >= 1.3.12
41+ , aeson >=0.6
42+ , stm >= 2.4.3
43+ , stm-chans >= 3.0.0.2
44+ , async >= 2.0.1.0
45+ , safe-exceptions >= 0.1.0.0
46+ , http-client >= 0.3
47+ , text >= 0.11
48+ , retry >= 0.7
49+ , exceptions
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
55+ , bytestring
56+ , semigroups
3257 hs-source-dirs: src
3358 default-language: Haskell2010
3459 hs-source-dirs: src
35− default-language: Haskell2010
3660 if flag(lib-Werror)
3761 ghc-options: -Wall -Werror
3862
@@ -47,7 +71,41 @@ benchmark bench
4771 , katip-elasticsearch
4872 , bloodhound
4973 , deepseq
50− , criterion >= 0.8 && < 1.0
51− , rng-utils
74+ , criterion >= 1.1.0.0
75+ , random
5276 , uuid
5377 , text
78+ , unordered-containers
79+ , aeson
80+
81+test-suite test
82+ type: exitcode-stdio-1.0
83+ main-is: Main.hs
84+ hs-source-dirs: test
85+ default-language: Haskell2010
86+ build-depends:
87+ katip-elasticsearch
88+ , katip
89+ , http-client
90+ , bloodhound
91+ , base
92+ , tasty >= 0.11
93+ , tasty-hunit
94+ , tasty-quickcheck
95+ , quickcheck-instances
96+ , containers
97+ , transformers
98+ , lens
99+ , lens-aeson
100+ , text
101+ , http-types
102+ , aeson
103+ , vector
104+ , unordered-containers
105+ , scientific
106+ , time
107+ , stm
108+ , bytestring
109+ , tagged
110+
111+ ghc-options: -threaded -rtsopts
katip-elasticsearch/src/Katip/Scribes/ElasticSearch.hs+76−136
@@ -1,159 +1,99 @@
1−{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE RecordWildCards #-}
1+{-# LANGUAGE CPP #-}
2+-- | Includes a scribe that can be used to log structured, JSON log
3+-- messages to ElasticSearch. These logs can be explored easily using
4+-- <https://www.elastic.co/products/kibana kibana> or your tool of
5+-- choice. Supports ElasticSearch servers with version 1.x or 5.x by
6+-- way of different configs.
7+--
8+-- Example of configuring for ES5:
9+--
10+-- @
11+--
12+-- import Control.Exception
13+-- import Database.V5.Bloodhound
14+-- import Network.HTTP.Client
15+-- import Katip
16+-- import Katip.Scribes.ElasticSearch
17+--
18+-- main :: IO ()
19+-- main = do
20+-- mgr <- newManager defaultManagerSettings
21+-- let bhe = mkBHEnv (Server "http://localhost:9200") mgr
22+-- esScribe <- mkEsScribe
23+-- -- Reasonable for production
24+-- defaultEsScribeCfgV5
25+-- -- Reasonable for single-node in development
26+-- -- defaultEsScribeCfgV5 { essIndexSettings = IndexSettings (ShardCound 1) (ReplicaCount 0)} :: EsScribeCfg ESV5
27+-- bhe
28+-- (IndexName "all-indices-prefixed-with")
29+-- (MappingName "application-logs")
30+-- DebugS
31+-- V3
32+-- let mkLogEnv = registerScribe "es" esScribe defaultScribeSettings =<< initLogEnv \"MyApp\" "production"
33+-- bracket mkLogEnv closeScribes $ \\le -> runKatipT le $ do
34+-- logMsg "ns" InfoS "This goes to elasticsearch"
35+--
36+-- @
37+--
38+-- __Important Note on Index Settings__
39+--
40+-- 'defaultEsScribeCfg' inherits a set of default index settings from
41+-- the @bloodhound@ package. These settings at this time of writing
42+-- set the indices up to have 3 shards and 2 replicas. This is an
43+-- arguably reasonable default setting for production but may cause
44+-- problems for development. In development, your cluster may be
45+-- configured to seek a write quorum greater than 1. If you're running
46+-- ElasticSearch on a single node, this could cause your writes to
47+-- wait for a bit and then fail due to a lack of quorum. __For development, we recommend setting your replica count to 0 or modifying your write quorum settings__. For production, we recommend reading the
48+-- <https://www.elastic.co/guide/en/elasticsearch/guide/current/scale.html ElasticSearch Scaling Guide> and choosing the appropriate settings,
49+-- keeping in mind that you can chage replica counts on a live index
50+-- but that changing shard counts requires recreating the index.
351module Katip.Scribes.ElasticSearch
452 (-- * Building a scribe
553 mkEsScribe
654 -- * Scribe configuration
55+ , EsScribeSetupError(..)
756 , EsQueueSize
857 , mkEsQueueSize
958 , EsPoolSize
1059 , mkEsPoolSize
60+ , IndexShardingPolicy(..)
61+ , IndexNameSegment(..)
62+ -- ** EsScribeCfg and fields
1163 , EsScribeCfg
12− , essManagerSettings
1364 , essRetryPolicy
1465 , essQueueSize
1566 , essPoolSize
67+ , essAnnotateTypes
68+ , essIndexSettings
69+ , essIndexSharding
1670 , defaultEsScribeCfg
71+#if !MIN_VERSION_bloodhound(0,17,0)
72+ , defaultEsScribeCfgV1
73+#endif
74+ , defaultEsScribeCfgV5
75+ -- ** Version-Proxied APIS
76+ -- $versionproxies
77+ , defaultEsScribeCfg'
78+#if !MIN_VERSION_bloodhound(0,17,0)
79+ , ESV1
80+#endif
81+ , ESV5
1782 -- * Utilities
1883 , mkDocId
84+ , module Katip.Scribes.ElasticSearch.Annotations
1985 ) where
2086
2187
2288-------------------------------------------------------------------------------
23−import Control.Applicative
24−import Control.Concurrent
25−import Control.Concurrent.STM
26−import Control.Exception.Enclosed
27−import Control.Monad
28−import Control.Retry
29−import Data.Aeson
30−import qualified Data.Text.Encoding as T
31−import Data.Typeable
32−import Data.UUID
33−import Database.Bloodhound
34−import Network.HTTP.Client
35−import System.Random
36−-------------------------------------------------------------------------------
37−import Katip.Core
38−-------------------------------------------------------------------------------
39−
40−
41−data EsScribeCfg = EsScribeCfg {
42− essManagerSettings :: ManagerSettings
43− -- ^ Connection manager settings
44− , essRetryPolicy :: RetryPolicy
45− -- ^ Retry policy when there are errors sending logs to the server
46− , essQueueSize :: EsQueueSize
47− -- ^ Maximum size of the bounded log queue
48− , essPoolSize :: EsPoolSize
49− -- ^ Worker pool size limit for sending data to the
50− } deriving (Typeable)
51−
52−
53−-- | Reasonable defaults for a config:
54−--
55−-- * defaultManagerSettings
56−--
57−-- * exponential backoff with 25ms base delay up to 5 retries
58−--
59−-- * Queue size of 1000
60−--
61−-- * Pool size of 2
62−defaultEsScribeCfg :: EsScribeCfg
63−defaultEsScribeCfg = EsScribeCfg {
64− essManagerSettings = defaultManagerSettings
65− , essRetryPolicy = exponentialBackoff 25 <> limitRetries 5
66− , essQueueSize = EsQueueSize 1000
67− , essPoolSize = EsPoolSize 2
68− }
69−
70−
89+import Katip.Scribes.ElasticSearch.Annotations
90+import Katip.Scribes.ElasticSearch.Internal
7191-------------------------------------------------------------------------------
72−mkEsScribe
73− :: EsScribeCfg
74− -> Server
75− -> IndexName
76− -> MappingName
77− -> Severity
78− -> Verbosity
79− -> IO Scribe
80−mkEsScribe cfg@EsScribeCfg {..} server ix mapping sev verb = do
81− q <- newTBQueueIO $ unEsQueueSize essQueueSize
82− withManager essManagerSettings $ \mgr -> do
83− let env = BHEnv { bhServer = server
84− , bhManager = mgr
85− }
86− replicateM_ (unEsPoolSize essPoolSize) $ forkIO $
87− startWorker cfg env ix mapping q
88−
89− return $ Scribe $ \ i ->
90− when (_itemSeverity i >= sev) $
91− void $ atomically $ tryWriteTBQueue q (itemJson verb i)
9292
9393
94−-------------------------------------------------------------------------------
95−mkDocId :: IO DocId
96−mkDocId = (DocId . T.decodeUtf8 . toASCIIBytes) `fmap` randomIO
97−
94+{- $versionproxies
9895
99−-------------------------------------------------------------------------------
100−newtype EsQueueSize = EsQueueSize {
101− unEsQueueSize :: Int
102− } deriving (Show, Eq, Ord)
103−
104−
105−instance Bounded EsQueueSize where
106− minBound = EsQueueSize 1
107− maxBound = EsQueueSize maxBound
108−
109−
110−mkEsQueueSize :: Int -> Maybe EsQueueSize
111−mkEsQueueSize = mkNonZero EsQueueSize
112−
113−
114−-------------------------------------------------------------------------------
115−newtype EsPoolSize = EsPoolSize {
116− unEsPoolSize :: Int
117− } deriving (Show, Eq, Ord)
118−
119−
120−instance Bounded EsPoolSize where
121− minBound = EsPoolSize 1
122− maxBound = EsPoolSize maxBound
123−
124−
125−mkEsPoolSize :: Int -> Maybe EsPoolSize
126−mkEsPoolSize = mkNonZero EsPoolSize
127−
128−
129−-------------------------------------------------------------------------------
130−mkNonZero :: (Int -> a) -> Int -> Maybe a
131−mkNonZero ctor n
132− | n > 0 = Just $ ctor n
133− | otherwise = Nothing
134−
135−
136−-------------------------------------------------------------------------------
137−startWorker
138− :: EsScribeCfg
139− -> BHEnv
140− -> IndexName
141− -> MappingName
142− -> TBQueue Value
143− -> IO ()
144−startWorker EsScribeCfg {..} env ix mapping q = forever $ do
145− v <- atomically $ readTBQueue q
146− sendLog v `catchAny` eat
147− where
148− sendLog v = void $ recoverAll essRetryPolicy $ do
149− did <- mkDocId
150− runBH env $ indexDocument ix mapping v did
151− eat _ = return ()
152−
153−
154−-------------------------------------------------------------------------------
155−tryWriteTBQueue :: TBQueue a -> a -> STM Bool
156−tryWriteTBQueue q v = do
157− ok <- not <$> isFullTBQueue q
158− when ok $ writeTBQueue q v
159− return ok
96+ You may need these these functions and types if type inference
97+ fails. For instance, you may need to hint to the compiler that a
98+ config is @:: EsScribeCfg ESV5@, for instance.
99+-}
katip-elasticsearch/src/Katip/Scribes/ElasticSearch/Annotations.hsnew file+112
@@ -0,0 +1,112 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Katip.Scribes.ElasticSearch.Annotations
3+ ( TypeAnnotated(..)
4+ -- * Exported for benchmarking
5+ , deannotateValue
6+ ) where
7+
8+
9+-------------------------------------------------------------------------------
10+import Control.Applicative as A
11+import Data.Aeson
12+import qualified Data.Foldable as FT
13+import qualified Data.HashMap.Strict as HM
14+import Data.Maybe
15+import Data.Monoid as M
16+import Data.Scientific (isFloating)
17+import Data.Text (Text)
18+import qualified Data.Text as T
19+-------------------------------------------------------------------------------
20+import Katip
21+-------------------------------------------------------------------------------
22+
23+
24+-- | Represents a value that can be converted to and from JSON that
25+-- will type annotate object keys when serializing and strip them out when deserializating
26+newtype TypeAnnotated a = TypeAnnotated {
27+ typeAnnotatedValue :: a
28+ }
29+
30+instance ToJSON a => ToJSON (TypeAnnotated a) where
31+ toJSON = annotateValue . toJSON . typeAnnotatedValue
32+
33+
34+instance ToObject a => ToObject (TypeAnnotated a) where
35+ toObject = annotateKeys . toObject . typeAnnotatedValue
36+
37+
38+instance FromJSON a => FromJSON (TypeAnnotated a) where
39+ parseJSON v = TypeAnnotated A.<$> parseJSON (deannotateValue v)
40+
41+
42+instance LogItem a => LogItem (TypeAnnotated a) where
43+ payloadKeys v (TypeAnnotated x) = case payloadKeys v x of
44+ AllKeys -> AllKeys
45+ -- Take the key selection, overlap it with the actual keys
46+ -- produced and annotate them
47+ SomeKeys ks -> let o = toObject x
48+ oInFocus = HM.fromList $ zip ks (repeat Null)
49+ final = annotateKeys $ HM.intersection o oInFocus
50+ in SomeKeys $ HM.keys final
51+
52+-------------------------------------------------------------------------------
53+-- Conversion Functions
54+-------------------------------------------------------------------------------
55+
56+
57+annotateValue :: Value -> Value
58+annotateValue (Object o) = Object $ annotateKeys o
59+annotateValue (Array a) = Array (annotateValue <$> a)
60+annotateValue x = x
61+
62+
63+annotateKeys :: Object -> Object
64+annotateKeys = HM.fromList . map go . HM.toList
65+ where
66+ go (k, Object o) = (k, Object $ annotateKeys o)
67+ go (k, Array a) = (k, Array (annotateValue <$> a))
68+ go (k, s@(String _)) = (k M.<> stringAnn, s)
69+ go (k, n@(Number sci)) = if isFloating sci
70+ then (k <> doubleAnn, n)
71+ else (k <> longAnn, n)
72+ go (k, b@(Bool _)) = (k <> booleanAnn, b)
73+ go (k, Null) = (k <> nullAnn, Null)
74+
75+
76+deannotateValue :: Value -> Value
77+deannotateValue (Object o) = Object $ deannotateKeys o
78+deannotateValue (Array a) = Array (deannotateValue <$> a)
79+deannotateValue x = x
80+
81+
82+deannotateKeys :: Object -> Object
83+deannotateKeys = HM.fromList . map go . HM.toList
84+ where
85+ go (k, Object o) = (k, Object $ deannotateKeys o)
86+ go (k, Array a) = (k, Array (deannotateValue <$> a))
87+ go (k, v) = (fromMaybe k k', v)
88+ where
89+ k' = FT.asum (stripSuffix <$> suffixes)
90+ suffixes = [stringAnn, doubleAnn, longAnn, booleanAnn, nullAnn]
91+ stripSuffix suffix = T.stripSuffix suffix k
92+
93+
94+-------------------------------------------------------------------------------
95+-- Annotation Constants
96+-------------------------------------------------------------------------------
97+
98+
99+stringAnn :: Text
100+stringAnn = "::s"
101+
102+doubleAnn :: Text
103+doubleAnn = "::d"
104+
105+longAnn :: Text
106+longAnn = "::l"
107+
108+booleanAnn :: Text
109+booleanAnn = "::b"
110+
111+nullAnn :: Text
112+nullAnn = "::n"
katip-elasticsearch/src/Katip/Scribes/ElasticSearch/Internal.hsnew file+577
@@ -0,0 +1,577 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE RecordWildCards #-}
7+{-# LANGUAGE ScopedTypeVariables #-}
8+{-# LANGUAGE TypeFamilies #-}
9+-- | This is an internal module. No guarantees are made in this module
10+-- about API stability.
11+module Katip.Scribes.ElasticSearch.Internal where
12+
13+
14+-------------------------------------------------------------------------------
15+import Control.Applicative as A
16+import Control.Concurrent
17+import Control.Concurrent.Async
18+import Control.Concurrent.STM.TBMQueue
19+import qualified Control.Exception.Safe as EX
20+import Control.Monad
21+import Control.Monad.Catch
22+import Control.Monad.IO.Class
23+import Control.Monad.STM
24+import Control.Retry (RetryPolicy,
25+ exponentialBackoff,
26+ limitRetries,
27+ recovering)
28+import Data.Aeson
29+import Data.ByteString.Lazy (ByteString)
30+import Data.List.NonEmpty (NonEmpty (..))
31+import Data.Monoid as Monoid
32+import Data.Text (Text)
33+import qualified Data.Text as T
34+import qualified Data.Text.Encoding as T
35+import Data.Time
36+import Data.Time.Calendar.WeekDate
37+import Data.Typeable as Typeable
38+import Data.UUID
39+import qualified Data.UUID.V4 as UUID4
40+#if MIN_VERSION_bloodhound(0,17,0)
41+import qualified Database.Bloodhound as V5
42+#else
43+import qualified Database.V1.Bloodhound as V1
44+import qualified Database.V5.Bloodhound as V5
45+#endif
46+import Network.HTTP.Client
47+import Network.HTTP.Types.Status
48+import Text.Printf (printf)
49+-------------------------------------------------------------------------------
50+import Katip.Core
51+import Katip.Scribes.ElasticSearch.Annotations
52+-------------------------------------------------------------------------------
53+
54+
55+-- | EsScribeCfg now carries a type variable for the version of
56+-- ElasticSearch it targets, either 'ESV1' or 'ESV5'. You can use
57+-- 'defaultEsScribeCfgV1' and 'defaultESScribeCfgV5' for a good
58+-- starting point depending on the ES version you have.
59+data EsScribeCfg v = EsScribeCfg {
60+ essRetryPolicy :: RetryPolicy
61+ -- ^ Retry policy when there are errors sending logs to the server
62+ , essQueueSize :: EsQueueSize
63+ -- ^ Maximum size of the bounded log queue
64+ , essPoolSize :: EsPoolSize
65+ -- ^ Worker pool size limit for sending data to the
66+ , essAnnotateTypes :: Bool
67+ -- ^ Different payload items coexist in the "data" attribute in
68+ -- ES. It is possible for different payloads to have different
69+ -- types for the same key, e.g. an "id" key that is sometimes a
70+ -- number and sometimes a string. If you're having ES do dynamic
71+ -- mapping, the first log item will set the type and any that
72+ -- don't conform will be *discarded*. If you set this to True,
73+ -- keys will recursively be appended with their ES core
74+ -- type. e.g. "id" would become "id::l" and "id::s"
75+ -- automatically, so they won't conflict. When this library
76+ -- exposes a querying API, we will try to make deserialization and
77+ -- querying transparently remove the type annotations if this is
78+ -- enabled.
79+ , essIndexSettings :: IndexSettings v
80+ -- ^ This will be the IndexSettings type from the appropriate
81+ -- bloodhound module, either @Database.V1.Bloodhound@ or
82+ -- @Database.V5.Bloodhound@
83+ , essIndexSharding :: IndexShardingPolicy
84+ } deriving (Typeable)
85+
86+
87+-- | Reasonable defaults for a config:
88+--
89+-- * defaultManagerSettings
90+--
91+-- * exponential backoff with 25ms base delay up to 5 retries, for a total cumulative delay of 775ms
92+--
93+-- * Queue size of 1000
94+--
95+-- * Pool size of 2
96+--
97+-- * Annotate types set to False
98+--
99+-- * DailyIndexSharding
100+defaultEsScribeCfg' :: ESVersion v => proxy v -> EsScribeCfg v
101+defaultEsScribeCfg' prx = EsScribeCfg {
102+ essRetryPolicy = exponentialBackoff 25000 Monoid.<> limitRetries 5
103+ , essQueueSize = EsQueueSize 1000
104+ , essPoolSize = EsPoolSize 2
105+ , essAnnotateTypes = False
106+ , essIndexSettings = defaultIndexSettings prx
107+ , essIndexSharding = DailyIndexSharding
108+ }
109+
110+
111+
112+
113+-------------------------------------------------------------------------------
114+-- | Alias of 'defaultEsScribeCfgV5' to minimize API
115+-- breakage. Previous versions of katip-elasticsearch only supported
116+-- ES version 1 and defaulted to it.
117+defaultEsScribeCfg :: EsScribeCfg ESV5
118+defaultEsScribeCfg = defaultEsScribeCfgV5
119+
120+
121+-------------------------------------------------------------------------------
122+#if !MIN_VERSION_bloodhound(0,17,0)
123+-- | EsScribeCfg that will use ElasticSearch V1
124+defaultEsScribeCfgV1 :: EsScribeCfg ESV1
125+defaultEsScribeCfgV1 = defaultEsScribeCfg' (Typeable.Proxy :: Typeable.Proxy ESV1)
126+#endif
127+
128+-------------------------------------------------------------------------------
129+-- | EsScribeCfg that will use ElasticSearch V5
130+defaultEsScribeCfgV5 :: EsScribeCfg ESV5
131+defaultEsScribeCfgV5 = defaultEsScribeCfg' (Typeable.Proxy :: Typeable.Proxy ESV5)
132+
133+
134+-------------------------------------------------------------------------------
135+-- | How should katip store your log data?
136+--
137+-- * NoIndexSharding will store all logs in one index name. This is
138+-- the simplest option but is not advised in production. In practice,
139+-- the index will grow very large and will get slower to
140+-- search. Deleting records based on some sort of retention period is
141+-- also extremely slow.
142+--
143+-- * MonthlyIndexSharding, DailyIndexSharding, HourlyIndexSharding,
144+-- EveryMinuteIndexSharding will generate indexes based on the time of
145+-- the log. Index name is treated as a prefix. So if your index name
146+-- is @foo@ and DailySharding is used, logs will be stored in
147+-- @foo-2016-2-25@, @foo-2016-2-26@ and so on. Index templating will
148+-- be used to set up mappings automatically. Deletes based on date are
149+-- very fast and queries can be restricted to date ranges for better
150+-- performance. Queries against all dates should use @foo-*@ as an
151+-- index name. Note that index aliasing's glob feature is not suitable
152+-- for these date ranges as it matches index names as they are
153+-- declared, so new dates will be excluded. DailyIndexSharding is a
154+-- reasonable choice. Changing index sharding strategies is not
155+-- advisable.
156+--
157+-- * CustomSharding: supply your own function that decomposes an item
158+-- into its index name hierarchy which will be appended to the index
159+-- name. So for instance if your function return ["arbitrary",
160+-- "prefix"], the index will be @foo-arbitrary-prefix@ and the index
161+-- template will be set to match @foo-*@. In general, you want to use
162+-- segments of increasing granularity (like year, month, day for
163+-- dates). This makes it easier to address groups of indexes
164+-- (e.g. @foo-2016-*@).
165+data IndexShardingPolicy = NoIndexSharding
166+ | MonthlyIndexSharding
167+ | WeeklyIndexSharding
168+ -- ^ A special case of daily which shards to sunday
169+ | DailyIndexSharding
170+ | HourlyIndexSharding
171+ | EveryMinuteIndexSharding
172+ | CustomIndexSharding (forall a. Item a -> [IndexNameSegment])
173+
174+
175+instance Show IndexShardingPolicy where
176+ show NoIndexSharding = "NoIndexSharding"
177+ show MonthlyIndexSharding = "MonthlyIndexSharding"
178+ show WeeklyIndexSharding = "WeeklyIndexSharding"
179+ show DailyIndexSharding = "DailyIndexSharding"
180+ show HourlyIndexSharding = "HourlyIndexSharding"
181+ show EveryMinuteIndexSharding = "EveryMinuteIndexSharding"
182+ show (CustomIndexSharding _) = "CustomIndexSharding λ"
183+
184+
185+-------------------------------------------------------------------------------
186+newtype IndexNameSegment = IndexNameSegment {
187+ indexNameSegment :: Text
188+ } deriving (Show, Eq, Ord)
189+
190+
191+-------------------------------------------------------------------------------
192+shardPolicySegs :: IndexShardingPolicy -> Item a -> [IndexNameSegment]
193+shardPolicySegs NoIndexSharding _ = []
194+shardPolicySegs MonthlyIndexSharding Item {..} = [sis y, sis m]
195+ where
196+ (y, m, _) = toGregorian (utctDay _itemTime)
197+shardPolicySegs WeeklyIndexSharding Item {..} = [sis y, sis m, sis d]
198+ where
199+ (y, m, d) = toGregorian (roundToSunday (utctDay _itemTime))
200+shardPolicySegs DailyIndexSharding Item {..} = [sis y, sis m, sis d]
201+ where
202+ (y, m, d) = toGregorian (utctDay _itemTime)
203+shardPolicySegs HourlyIndexSharding Item {..} = [sis y, sis m, sis d, sis h]
204+ where
205+ (y, m, d) = toGregorian (utctDay _itemTime)
206+ (h, _) = splitTime (utctDayTime _itemTime)
207+shardPolicySegs EveryMinuteIndexSharding Item {..} = [sis y, sis m, sis d, sis h, sis mn]
208+ where
209+ (y, m, d) = toGregorian (utctDay _itemTime)
210+ (h, mn) = splitTime (utctDayTime _itemTime)
211+shardPolicySegs (CustomIndexSharding f) i = f i
212+
213+
214+-------------------------------------------------------------------------------
215+-- | If the given day is sunday, returns the input, otherwise returns
216+-- the previous sunday
217+roundToSunday :: Day -> Day
218+roundToSunday d
219+ | dow == 7 = d
220+ | w > 1 = fromWeekDate y (w - 1) 7
221+ | otherwise = fromWeekDate (y - 1) 53 7
222+ where
223+ (y, w, dow) = toWeekDate d
224+
225+
226+-------------------------------------------------------------------------------
227+chooseIxn :: ESVersion v => proxy v -> IndexName v -> IndexShardingPolicy -> Item a -> IndexName v
228+chooseIxn prx ixn p i =
229+ toIndexName prx (T.intercalate "-" ((fromIndexName prx ixn):segs))
230+ where
231+ segs = indexNameSegment A.<$> shardPolicySegs p i
232+
233+
234+-------------------------------------------------------------------------------
235+sis :: Integral a => a -> IndexNameSegment
236+sis = IndexNameSegment . T.pack . fmt
237+ where
238+ fmt = printf "%02d" . toInteger
239+
240+
241+-------------------------------------------------------------------------------
242+splitTime :: DiffTime -> (Int, Int)
243+splitTime t = asMins `divMod` 60
244+ where
245+ asMins = floor t `div` 60
246+
247+
248+-------------------------------------------------------------------------------
249+data EsScribeSetupError = CouldNotCreateIndex !(Response ByteString)
250+ | CouldNotUpdateIndexSettings !(Response ByteString)
251+ | CouldNotCreateMapping !(Response ByteString)
252+ | CouldNotPutTemplate !(Response ByteString)
253+ deriving (Typeable, Show)
254+
255+
256+instance Exception EsScribeSetupError
257+
258+
259+-------------------------------------------------------------------------------
260+-- | The Any field tagged with a @v@ corresponds to the type of the
261+-- same name in the corresponding @bloodhound@ module. For instance,
262+-- if you are configuring for ElasticSearch version 1, import
263+-- @Database.V1.Bloodhound@ and @BHEnv v@ will refer to @BHEnv@ from
264+-- that module, @IndexName v@ will repsond to @IndexName@ from that
265+-- module, etc.
266+mkEsScribe
267+ :: forall v. ( ESVersion v
268+ , MonadIO (BH v IO)
269+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
270+ , Functor (BH v IO)
271+#endif
272+ )
273+ => EsScribeCfg v
274+ -> BHEnv v
275+ -> IndexName v
276+ -- ^ Treated as a prefix if index sharding is enabled
277+ -> MappingName v
278+ -> PermitFunc
279+ -> Verbosity
280+ -> IO Scribe
281+mkEsScribe cfg@EsScribeCfg {..} env ix mapping permit verb = do
282+ q <- newTBMQueueIO $ unEsQueueSize essQueueSize
283+ endSig <- newEmptyMVar
284+
285+ runBH prx env $ do
286+ if shardingEnabled
287+ then do
288+ -- create or update
289+ res <- putTemplate prx tpl tplName
290+ unless (statusIsSuccessful (responseStatus res)) $
291+ liftIO $ EX.throwIO (CouldNotPutTemplate res)
292+ else do
293+ ixExists <- indexExists prx ix
294+ if ixExists
295+ then do
296+ res <- updateIndexSettings prx (toUpdatabaleIndexSettings prx essIndexSettings) ix
297+ unless (statusIsSuccessful (responseStatus res)) $
298+ liftIO $ EX.throwIO (CouldNotUpdateIndexSettings res)
299+ else do
300+ r1 <- createIndex prx essIndexSettings ix
301+ unless (statusIsSuccessful (responseStatus r1)) $
302+ liftIO $ EX.throwIO (CouldNotCreateIndex r1)
303+ r2 <- putMapping prx ix mapping base
304+ unless (statusIsSuccessful (responseStatus r2)) $
305+ liftIO $ EX.throwIO (CouldNotCreateMapping r2)
306+
307+ workers <- replicateM (unEsPoolSize essPoolSize) $ async $
308+ startWorker cfg env mapping q
309+
310+ _ <- async $ do
311+ takeMVar endSig
312+ atomically $ closeTBMQueue q
313+ mapM_ waitCatch workers
314+ putMVar endSig ()
315+
316+ let finalizer = putMVar endSig () >> takeMVar endSig
317+ return (Scribe (logger q) finalizer permit)
318+ where
319+ logger :: forall a. LogItem a => TBMQueue (IndexName v, Value) -> Item a -> IO ()
320+ logger q i =
321+ void $ atomically $ tryWriteTBMQueue q (chooseIxn prx ix essIndexSharding i, itemJson' i)
322+ prx :: Typeable.Proxy v
323+ prx = Typeable.Proxy
324+ tplName = toTemplateName prx ixn
325+ shardingEnabled = case essIndexSharding of
326+ NoIndexSharding -> False
327+ _ -> True
328+ tpl = toIndexTemplate prx (toTemplatePattern prx (ixn <> "-*")) (Just essIndexSettings) [toJSON base]
329+ base = baseMapping prx mapping
330+ ixn = fromIndexName prx ix
331+ itemJson' :: LogItem a => Item a -> Value
332+ itemJson' i
333+ | essAnnotateTypes = itemJson verb (TypeAnnotated <$> i)
334+ | otherwise = itemJson verb i
335+
336+
337+-------------------------------------------------------------------------------
338+baseMapping :: ESVersion v => proxy v -> MappingName v -> Value
339+baseMapping prx mn =
340+ object [ fromMappingName prx mn .= object ["properties" .= object prs] ]
341+ where prs = [ unanalyzedString "thread"
342+ , unanalyzedString "sev"
343+ , unanalyzedString "pid"
344+ -- ns is frequently fulltext searched
345+ , analyzedString "ns"
346+ -- we want message to be fulltext searchable
347+ , analyzedString "msg"
348+ , "loc" .= locType
349+ , unanalyzedString "host"
350+ , unanalyzedString "env"
351+ , "at" .= dateType
352+ , unanalyzedString "app"
353+ ]
354+ unanalyzedString k = k .= unanalyzedStringSpec prx
355+ analyzedString k = k .= analyzedStringSpec prx
356+ locType = object ["properties" .= object locPairs]
357+ locPairs = [ unanalyzedString "loc_pkg"
358+ , unanalyzedString "loc_mod"
359+ , unanalyzedString "loc_ln"
360+ , unanalyzedString "loc_fn"
361+ , unanalyzedString "loc_col"
362+ ]
363+ dateType = object [ "format" .= esDateFormat
364+ , "type" .= String "date"
365+ ]
366+
367+
368+-------------------------------------------------------------------------------
369+-- | Handle both old-style aeson and picosecond-level precision
370+esDateFormat :: Text
371+esDateFormat = "yyyy-MM-dd'T'HH:mm:ssZ||yyyy-MM-dd'T'HH:mm:ss.SSSZ||yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSSSSZ"
372+
373+
374+-------------------------------------------------------------------------------
375+mkDocId :: ESVersion v => proxy v -> IO (DocId v)
376+mkDocId prx = (toDocId prx . T.decodeUtf8 . toASCIIBytes) `fmap` UUID4.nextRandom
377+
378+
379+-------------------------------------------------------------------------------
380+newtype EsQueueSize = EsQueueSize {
381+ unEsQueueSize :: Int
382+ } deriving (Show, Eq, Ord)
383+
384+
385+instance Bounded EsQueueSize where
386+ minBound = EsQueueSize 1
387+ maxBound = EsQueueSize maxBound
388+
389+
390+mkEsQueueSize :: Int -> Maybe EsQueueSize
391+mkEsQueueSize = mkNonZero EsQueueSize
392+
393+
394+-------------------------------------------------------------------------------
395+newtype EsPoolSize = EsPoolSize {
396+ unEsPoolSize :: Int
397+ } deriving (Show, Eq, Ord)
398+
399+
400+instance Bounded EsPoolSize where
401+ minBound = EsPoolSize 1
402+ maxBound = EsPoolSize maxBound
403+
404+
405+mkEsPoolSize :: Int -> Maybe EsPoolSize
406+mkEsPoolSize = mkNonZero EsPoolSize
407+
408+
409+-------------------------------------------------------------------------------
410+mkNonZero :: (Int -> a) -> Int -> Maybe a
411+mkNonZero ctor n
412+ | n > 0 = Just $ ctor n
413+ | otherwise = Nothing
414+
415+
416+-------------------------------------------------------------------------------
417+startWorker
418+ :: forall v. (ESVersion v)
419+ => EsScribeCfg v
420+ -> BHEnv v
421+ -> MappingName v
422+ -> TBMQueue (IndexName v, Value)
423+ -> IO ()
424+startWorker EsScribeCfg {..} env mapping q = go
425+ where
426+ go = do
427+ popped <- atomically $ readTBMQueue q
428+ case popped of
429+ Just (ixn, v) -> do
430+ sendLog ixn v `EX.catchAny` eat
431+ go
432+ Nothing -> return ()
433+ prx :: Typeable.Proxy v
434+ prx = Typeable.Proxy
435+ sendLog :: IndexName v -> Value -> IO ()
436+ sendLog ixn v = void $ recovering essRetryPolicy [handler] $ const $ do
437+ did <- mkDocId prx
438+ res <- runBH prx env $ indexDocument prx ixn mapping (defaultIndexDocumentSettings prx) v did
439+ return res
440+ eat _ = return ()
441+ handler _ = Handler $ \e ->
442+ case fromException e of
443+ Just (_ :: EX.SomeAsyncException) -> return False
444+ _ -> return True
445+
446+
447+-------------------------------------------------------------------------------
448+-- We are spanning multiple versions of ES which use completely
449+-- separate types and APIs, but the subset we use is the same for both
450+-- versions. This will be kept up to date with bloodhound's supported
451+-- versions and should be minimally visible to the end user.
452+class ESVersion v where
453+ -- Types
454+ type BHEnv v
455+ type IndexSettings v
456+ defaultIndexSettings :: proxy v -> IndexSettings v
457+ type UpdatableIndexSetting v
458+ type IndexName v
459+ toIndexName :: proxy v -> Text -> IndexName v
460+ fromIndexName :: proxy v -> IndexName v -> Text
461+ type MappingName v
462+ fromMappingName :: proxy v -> MappingName v -> Text
463+ type DocId v
464+ toDocId :: proxy v -> Text -> DocId v
465+ type BH v :: (* -> *) -> * -> *
466+ runBH :: proxy v -> BHEnv v -> BH v m a -> m a
467+ type TemplateName v
468+ toTemplateName :: proxy v -> Text -> TemplateName v
469+ type TemplatePattern v
470+ toTemplatePattern :: proxy v -> Text -> TemplatePattern v
471+ type IndexTemplate v
472+ toIndexTemplate :: proxy v -> TemplatePattern v -> Maybe (IndexSettings v) -> [Value] -> IndexTemplate v
473+ type IndexDocumentSettings v
474+ defaultIndexDocumentSettings :: proxy v -> IndexDocumentSettings v
475+
476+ toUpdatabaleIndexSettings :: proxy v -> IndexSettings v -> NonEmpty (UpdatableIndexSetting v)
477+
478+ -- Operations
479+ -- We're deciding on IO here, but it isn't necessary
480+ indexExists :: proxy v -> IndexName v -> BH v IO Bool
481+ indexDocument :: ToJSON doc => proxy v -> IndexName v -> MappingName v -> IndexDocumentSettings v -> doc -> DocId v -> BH v IO (Response ByteString)
482+ createIndex :: proxy v -> IndexSettings v -> IndexName v -> BH v IO (Response ByteString)
483+ updateIndexSettings :: proxy v -> NonEmpty (UpdatableIndexSetting v) -> IndexName v -> BH v IO (Response ByteString)
484+ putTemplate :: proxy v -> IndexTemplate v -> TemplateName v -> BH v IO (Response ByteString)
485+ putMapping :: (ToJSON a) => proxy v -> IndexName v -> MappingName v -> a -> BH v IO (Response ByteString)
486+
487+ -- In ES 5 and beyond, "string" was deprecated in favor of text for
488+ -- fulltext and keyword for unanalyzed tokens
489+ unanalyzedStringSpec :: proxy v -> Value
490+ analyzedStringSpec :: proxy v -> Value
491+
492+
493+
494+#if !MIN_VERSION_bloodhound(0,17,0)
495+data ESV1 = ESV1
496+{-# DEPRECATED ESV1 "ESV1 is deprecated and removed in bloodhound >= 0.17.0" #-}
497+
498+
499+instance ESVersion ESV1 where
500+ type BHEnv ESV1 = V1.BHEnv
501+ type IndexSettings ESV1 = V1.IndexSettings
502+ defaultIndexSettings _ = V1.defaultIndexSettings
503+ type IndexName ESV1 = V1.IndexName
504+ type UpdatableIndexSetting ESV1 = V1.UpdatableIndexSetting
505+ toIndexName _ = V1.IndexName
506+ fromIndexName _ (V1.IndexName x) = x
507+ type MappingName ESV1 = V1.MappingName
508+ fromMappingName _ (V1.MappingName x) = x
509+ type DocId ESV1 = V1.DocId
510+ toDocId _ = V1.DocId
511+ type BH ESV1 = V1.BH
512+ runBH _ = V1.runBH
513+ type TemplateName ESV1 = V1.TemplateName
514+ toTemplateName _ = V1.TemplateName
515+ type TemplatePattern ESV1 = V1.TemplatePattern
516+ toTemplatePattern _ = V1.TemplatePattern
517+ type IndexTemplate ESV1 = V1.IndexTemplate
518+ toIndexTemplate _ = V1.IndexTemplate
519+ type IndexDocumentSettings ESV1 = V1.IndexDocumentSettings
520+ toUpdatabaleIndexSettings _ s =
521+ (V1.NumberOfReplicas (V1.indexReplicas s)) :| []
522+ defaultIndexDocumentSettings _ = V1.defaultIndexDocumentSettings
523+ indexExists _ = V1.indexExists
524+ indexDocument _ = V1.indexDocument
525+ createIndex _ = V1.createIndex
526+ updateIndexSettings _ = V1.updateIndexSettings
527+ putTemplate _ = V1.putTemplate
528+ putMapping _ = V1.putMapping
529+ unanalyzedStringSpec _ = object
530+ [ "type" .= String "string"
531+ , "index" .= String "not_analyzed"
532+ ]
533+ analyzedStringSpec _ = object
534+ [ "type" .= String "string"
535+ , "index" .= String "analyzed"
536+ ]
537+#endif
538+
539+
540+data ESV5 = ESV5
541+
542+instance ESVersion ESV5 where
543+ type BHEnv ESV5 = V5.BHEnv
544+ type IndexSettings ESV5 = V5.IndexSettings
545+ type UpdatableIndexSetting ESV5 = V5.UpdatableIndexSetting
546+ defaultIndexSettings _ = V5.defaultIndexSettings
547+ type IndexName ESV5 = V5.IndexName
548+ toIndexName _ = V5.IndexName
549+ fromIndexName _ (V5.IndexName x) = x
550+ type MappingName ESV5 = V5.MappingName
551+ fromMappingName _ (V5.MappingName x) = x
552+ type DocId ESV5 = V5.DocId
553+ toDocId _ = V5.DocId
554+ type BH ESV5 = V5.BH
555+ runBH _ = V5.runBH
556+ type TemplateName ESV5 = V5.TemplateName
557+ toTemplateName _ = V5.TemplateName
558+ type TemplatePattern ESV5 = V5.TemplatePattern
559+ toTemplatePattern _ = V5.TemplatePattern
560+ type IndexTemplate ESV5 = V5.IndexTemplate
561+ toIndexTemplate _ = V5.IndexTemplate
562+ type IndexDocumentSettings ESV5 = V5.IndexDocumentSettings
563+ toUpdatabaleIndexSettings _ s =
564+ (V5.NumberOfReplicas (V5.indexReplicas s)) :| []
565+ defaultIndexDocumentSettings _ = V5.defaultIndexDocumentSettings
566+ indexExists _ = V5.indexExists
567+ indexDocument _ = V5.indexDocument
568+ createIndex _ = V5.createIndex
569+ updateIndexSettings _ = V5.updateIndexSettings
570+ putTemplate _ = V5.putTemplate
571+ putMapping _ = V5.putMapping
572+ unanalyzedStringSpec _ = object
573+ [ "type" .= String "keyword"
574+ ]
575+ analyzedStringSpec _ = object
576+ [ "type" .= String "text"
577+ ]
katip-elasticsearch/test/Main.hsnew file+545
@@ -0,0 +1,545 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE KindSignatures #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE RankNTypes #-}
7+{-# LANGUAGE RecordWildCards #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TemplateHaskell #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# OPTIONS_GHC -fno-warn-orphans #-}
12+module Main
13+ ( main
14+ ) where
15+
16+
17+-------------------------------------------------------------------------------
18+import Control.Applicative as A
19+import Control.Concurrent.STM
20+import Control.Exception
21+import Control.Lens hiding (mapping, (.=))
22+import Control.Monad
23+import Control.Monad.IO.Class
24+import Data.Aeson
25+import Data.Aeson.Lens
26+import Data.Aeson.Types
27+import Data.ByteString.Lazy (ByteString)
28+import qualified Data.HashMap.Strict as HM
29+import Data.Monoid as M
30+import Data.Scientific
31+import Data.Tagged
32+import Data.Text (Text)
33+import Data.Time
34+import Data.Time.Calendar.WeekDate
35+import Data.Typeable as Typeable
36+import qualified Data.Vector as V
37+#if MIN_VERSION_bloodhound(0,17,0)
38+import qualified Database.Bloodhound as V5
39+#else
40+import qualified Database.V1.Bloodhound as V1
41+import qualified Database.V5.Bloodhound as V5
42+#endif
43+import Network.HTTP.Client
44+import Network.HTTP.Types.Status
45+import Test.QuickCheck.Instances ()
46+import Test.Tasty
47+import Test.Tasty.HUnit
48+import Test.Tasty.Options
49+import Test.Tasty.QuickCheck
50+-------------------------------------------------------------------------------
51+import Katip
52+import Katip.Scribes.ElasticSearch.Annotations
53+import Katip.Scribes.ElasticSearch.Internal
54+-------------------------------------------------------------------------------
55+
56+
57+main :: IO ()
58+main = defaultMainWithIngredients ings $ askOption $ \vers -> testGroup "katip-elasticsearch"
59+ [
60+ case vers of
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)
65+ , typeAnnotatedTests
66+ , roundToSundayTests
67+ ]
68+ where
69+ ings = (includingOptions [Option (Typeable.Proxy :: Typeable.Proxy TestWithESVersion)]):defaultIngredients
70+
71+
72+-------------------------------------------------------------------------------
73+data TestWithESVersion = TestV5
74+#if !MIN_VERSION_bloodhound(0,17,0)
75+ | TestV1
76+#endif
77+ deriving (Typeable)
78+
79+
80+instance IsOption TestWithESVersion where
81+ defaultValue = TestV5
82+#if !MIN_VERSION_bloodhound(0,17,0)
83+ parseValue "1" = Just TestV1
84+#endif
85+ parseValue "5" = Just TestV5
86+ parseValue _ = Nothing
87+ optionName = Tagged "es-version"
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
93+
94+
95+class ESVersion v => TestESVersion v where
96+ type Server v
97+ toServer :: proxy v -> Text -> Server v
98+ toMappingName :: proxy v -> Text -> MappingName v
99+ type Search v
100+ type Query v
101+ type Filter v
102+ mkSearch :: proxy v -> Maybe (Query v) -> Maybe (Filter v) -> Search v
103+ mkBHEnv :: proxy v -> Server v -> Manager -> BHEnv v
104+ type ShardCount v
105+ toShardCount :: proxy v -> Int -> ShardCount v
106+ type ReplicaCount v
107+ toReplicaCount :: proxy v -> Int -> ReplicaCount v
108+ indexShards :: proxy v -> Lens' (IndexSettings v) (ShardCount v)
109+ indexReplicas :: proxy v -> Lens' (IndexSettings v) (ReplicaCount v)
110+
111+ deleteIndex :: proxy v -> IndexName v -> BH v IO (Response ByteString)
112+ deleteTemplate :: proxy v -> TemplateName v -> BH v IO (Response ByteString)
113+ refreshIndex :: proxy v -> IndexName v -> BH v IO (Response ByteString)
114+ withBH :: proxy v -> ManagerSettings -> Server v -> BH v IO a -> IO a
115+ searchByIndex :: proxy v -> IndexName v -> Search v -> BH v IO (Response ByteString)
116+
117+
118+#if !MIN_VERSION_bloodhound(0,17,0)
119+instance TestESVersion ESV1 where
120+ type Server ESV1 = V1.Server
121+ toServer _ = V1.Server
122+ toMappingName _ = V1.MappingName
123+ type Search ESV1 = V1.Search
124+ type Query ESV1 = V1.Query
125+ type Filter ESV1 = V1.Filter
126+ type ShardCount ESV1 = V1.ShardCount
127+ toShardCount _ = V1.ShardCount
128+ type ReplicaCount ESV1 = V1.ReplicaCount
129+ toReplicaCount _ = V1.ReplicaCount
130+ mkSearch _ = V1.mkSearch
131+ mkBHEnv _ = V1.mkBHEnv
132+ indexShards _ = lens V1.indexShards (\s v -> s { V1.indexShards = v})
133+ indexReplicas _ = lens V1.indexReplicas (\r v -> r { V1.indexReplicas = v})
134+
135+ deleteIndex _ = V1.deleteIndex
136+ deleteTemplate _ = V1.deleteTemplate
137+ refreshIndex _ = V1.refreshIndex
138+ withBH _ = V1.withBH
139+ searchByIndex _ = V1.searchByIndex
140+#endif
141+
142+
143+instance TestESVersion ESV5 where
144+ type Server ESV5 = V5.Server
145+ toServer _ = V5.Server
146+ toMappingName _ = V5.MappingName
147+ type Search ESV5 = V5.Search
148+ type Query ESV5 = V5.Query
149+ type Filter ESV5 = V5.Filter
150+ type ShardCount ESV5 = V5.ShardCount
151+ toShardCount _ = V5.ShardCount
152+ type ReplicaCount ESV5 = V5.ReplicaCount
153+ toReplicaCount _ = V5.ReplicaCount
154+ mkSearch _ = V5.mkSearch
155+ mkBHEnv _ = V5.mkBHEnv
156+ indexShards _ = lens V5.indexShards (\s v -> s { V5.indexShards = v})
157+ indexReplicas _ = lens V5.indexReplicas (\r v -> r { V5.indexReplicas = v})
158+
159+ deleteIndex _ = V5.deleteIndex
160+ deleteTemplate _ = V5.deleteTemplate
161+ refreshIndex _ = V5.refreshIndex
162+ withBH _ = V5.withBH
163+ searchByIndex _ = V5.searchByIndex
164+
165+
166+-------------------------------------------------------------------------------
167+setupSearch
168+ :: forall proxy v. ( TestESVersion v
169+ , MonadIO (BH v IO)
170+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
171+ , Functor (BH v IO)
172+#endif
173+ )
174+ => proxy v
175+ -> (EsScribeCfg v -> EsScribeCfg v)
176+ -> IO Scribe
177+setupSearch prx modScribeCfg = do
178+ bh prx (dropESSchema prx)
179+ mgr <- newManager defaultManagerSettings
180+ mkEsScribe cfg (mkBHEnv prx (svr prx) mgr) (ixn prx) (mn prx) (permitItem DebugS) V3
181+ where
182+ cfg :: EsScribeCfg v
183+ cfg = modScribeCfg $
184+ (defaultEsScribeCfg' prx)
185+ { essAnnotateTypes = True
186+ , essIndexSettings = ixs prx
187+ }
188+
189+
190+-------------------------------------------------------------------------------
191+teardownSearch
192+ :: ( TestESVersion v
193+ , Monad (BH v IO)
194+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
195+ , Functor (BH v IO)
196+#endif
197+ )
198+ => proxy v
199+ -> IO ()
200+teardownSearch prx = do
201+ bh prx $ do
202+ dropESSchema prx
203+ dropESSTemplate prx
204+
205+
206+-------------------------------------------------------------------------------
207+withSearch
208+ :: ( TestESVersion v
209+ , MonadIO (BH v IO)
210+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
211+ , Functor (BH v IO)
212+#endif
213+ )
214+ => proxy v
215+ -> (IO Scribe -> TestTree)
216+ -> TestTree
217+withSearch = withSearch' id
218+
219+
220+-------------------------------------------------------------------------------
221+withSearch'
222+ :: ( TestESVersion v
223+ , MonadIO (BH v IO)
224+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
225+ , Functor (BH v IO)
226+#endif
227+ )
228+ => (EsScribeCfg v -> EsScribeCfg v)
229+ -> proxy v
230+ -> (IO Scribe -> TestTree)
231+ -> TestTree
232+withSearch' modScribeCfg prx = withResource (setupSearch prx modScribeCfg) (const (teardownSearch prx))
233+
234+
235+-------------------------------------------------------------------------------
236+esTests
237+ :: ( TestESVersion v
238+ , MonadIO (BH v IO)
239+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
240+ , Functor (BH v IO)
241+#endif
242+ , Show (IndexName v)
243+ )
244+ => proxy v
245+ -> TestTree
246+esTests prx = testGroup "elasticsearch scribe"
247+ [
248+ withSearch' (\c -> c { essIndexSharding = NoIndexSharding}) prx $ \setup -> testCase "it flushes to elasticsearch" $ withTestLogging prx setup $ \done -> do
249+ $(logT) (ExampleCtx True) M.mempty InfoS "A test message"
250+ liftIO $ do
251+ void done
252+ logs <- getLogs prx
253+ length logs @?= 1
254+ let l = head logs
255+ l ^? key "_source" . key "msg" . _String @?= Just "A test message"
256+ l ^? key "_source" . key "data" . key "whatever::b" . _Bool @?= Just True
257+ , testCase "can set up twice with no sharding" $ do
258+ let setup = bracket_ (setupSearch prx (\c -> c { essIndexSharding = NoIndexSharding })) (teardownSearch prx)
259+ setup $ setup $
260+ return ()
261+ , testCase "can set up twice with sharding" $ do
262+ let setup = bracket_ (setupSearch prx (\c -> c { essIndexSharding = DailyIndexSharding })) (teardownSearch prx)
263+ setup $ setup $
264+ return ()
265+ , withSearch prx $ \setup -> testCase "date-based index sharding" $ do
266+ let t1 = mkTime 2016 1 2 3 4 5
267+ fakeClock <- newTVarIO t1
268+ withTestLogging' (set logEnvTimer (readTVarIO fakeClock)) prx setup $ \done -> do
269+ $(logT) (ExampleCtx True) mempty InfoS "today"
270+ let t2 = mkTime 2016 1 3 3 4 5
271+ liftIO (atomically (writeTVar fakeClock t2))
272+ $(logT) (ExampleCtx True) mempty InfoS "tomorrow"
273+ liftIO $ do
274+ void done
275+ todayLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-01-02")
276+ tomorrowLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-01-03")
277+ assertBool ("todayLogs has " <> show (length todayLogs) <> " items") (length todayLogs == 1)
278+ assertBool ("tomorrowLogs has " <> show (length tomorrowLogs) <> " items") (length tomorrowLogs == 1)
279+ let logToday = head todayLogs
280+ let logTomorrow = head tomorrowLogs
281+ logToday ^? key "_source" . key "msg" . _String @?= Just "today"
282+ logTomorrow ^? key "_source" . key "msg" . _String @?= Just "tomorrow"
283+ , withSearch' (\c -> c { essIndexSharding = WeeklyIndexSharding}) prx $ \setup -> testCase "weekly index sharding rounds to previous sunday" $ do
284+ let t1 = mkTime 2016 3 5 0 0 0 -- saturday, march 5th
285+ fakeClock <- newTVarIO t1
286+ withTestLogging' (set logEnvTimer (readTVarIO fakeClock)) prx setup $ \done -> do
287+ $(logT) (ExampleCtx True) mempty InfoS "today"
288+ let t2 = mkTime 2016 3 6 0 0 0 -- sunday march 6th
289+ liftIO (atomically (writeTVar fakeClock t2))
290+ $(logT) (ExampleCtx True) mempty InfoS "tomorrow"
291+ liftIO $ do
292+ void done
293+ todayLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-02-28") -- rounds back to previous sunday
294+ tomorrowLogs <- getLogsByIndex prx (toIndexName prx "katip-elasticsearch-tests-2016-03-06") -- is on sunday, so uses current date
295+ assertBool ("todayLogs has " <> show (length todayLogs) <> " items") (length todayLogs == 1)
296+ assertBool ("tomorrowLogs has " <> show (length tomorrowLogs) <> " items") (length tomorrowLogs == 1)
297+ let logToday = head todayLogs
298+ let logTomorrow = head tomorrowLogs
299+ logToday ^? key "_source" . key "msg" . _String @?= Just "today"
300+ logTomorrow ^? key "_source" . key "msg" . _String @?= Just "tomorrow"
301+ ]
302+
303+
304+-------------------------------------------------------------------------------
305+mkTime :: Integer -> Int -> Int -> DiffTime -> DiffTime -> DiffTime -> UTCTime
306+mkTime y m d hr minute s = UTCTime day dt
307+ where
308+ day = mkDay y m d
309+ dt = s + 60 * minute + 60 * 60 * hr
310+
311+
312+-------------------------------------------------------------------------------
313+mkDay :: Integer -> Int -> Int -> Day
314+mkDay y m d = day
315+ where
316+ Just day = fromGregorianValid y m d
317+
318+
319+-------------------------------------------------------------------------------
320+data ExampleCtx = ExampleCtx {
321+ ecBool :: Bool
322+ }
323+
324+instance ToJSON ExampleCtx where
325+ toJSON c = object ["whatever" .= ecBool c]
326+
327+
328+instance ToObject ExampleCtx
329+
330+
331+instance LogItem ExampleCtx where
332+ payloadKeys _ _ = AllKeys
333+
334+-------------------------------------------------------------------------------
335+typeAnnotatedTests :: TestTree
336+typeAnnotatedTests = testGroup "TypeAnnotated"
337+ [
338+ testCase "annotates values on toJSON" $
339+ toJSON (TypeAnnotated exampleValue) @?= annotatedExampleValue
340+
341+ , testCase "annotates values on toObject" $
342+ toObject (TypeAnnotated exampleObject) @?= annotatedExampleObject
343+
344+ , testCase "deannotates on parseJSON" $
345+ parseEither parseJSON (toJSON exampleValue) @?= Right exampleValue
346+
347+ , testProperty "roundtrips the same as raw" $ \(v :: Value) ->
348+ let res = typeAnnotatedValue
349+ <$> parseEither parseJSON (toJSON (TypeAnnotated v))
350+ in res === Right v
351+ ]
352+
353+
354+-------------------------------------------------------------------------------
355+roundToSundayTests :: TestTree
356+roundToSundayTests = testGroup "roundToSunday"
357+ [
358+ testProperty "always returns a sunday" $ \d ->
359+ getDOW (roundToSunday d) === 7
360+ , testProperty "returns input on sunday" $ \d -> getDOW d == 7 ==>
361+ roundToSunday d === d
362+ , testProperty "goes back a week when not sunday" $ \d -> getDOW d /= 7 ==>
363+ roundToSunday d < d
364+ ]
365+ where
366+ getDOW = view _3 . toWeekDate
367+
368+
369+-------------------------------------------------------------------------------
370+exampleObject :: Object
371+exampleObject = HM.fromList
372+ [ ("a bool", Bool False)
373+ , ("a long", Number 24)
374+ , ("a double", Number 52.3)
375+ , ("a string", String "s")
376+ , ("a null", Null)
377+ , ("a map", Object (HM.singleton "baz" (Bool True)))
378+ ]
379+
380+
381+-------------------------------------------------------------------------------
382+annotatedExampleObject :: Object
383+annotatedExampleObject = HM.fromList
384+ [ ("a map",Object $ HM.fromList [("baz::b", Bool True)])
385+ , ("a bool::b", Bool False)
386+ , ("a null::n", Null)
387+ , ("a string::s", String "s")
388+ , ("a double::d", Number 52.3)
389+ , ("a long::l", Number 24.0)
390+ ]
391+
392+
393+-------------------------------------------------------------------------------
394+exampleValue :: Value
395+exampleValue = Array $ V.fromList [Null, Object exampleObject]
396+
397+
398+-------------------------------------------------------------------------------
399+annotatedExampleValue :: Value
400+annotatedExampleValue = Array $ V.fromList
401+ [ Null
402+ , Object annotatedExampleObject
403+ ]
404+
405+
406+-------------------------------------------------------------------------------
407+getLogs
408+ :: ( TestESVersion v
409+ , Monad (BH v IO)
410+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
411+ , Functor (BH v IO)
412+#endif
413+ , Show (IndexName v)
414+ )
415+ => proxy v
416+ -> IO [Value]
417+getLogs prx = getLogsByIndex prx (ixn prx)
418+
419+
420+-------------------------------------------------------------------------------
421+getLogsByIndex
422+ :: ( TestESVersion v
423+ , Monad (BH v IO)
424+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
425+ , Functor (BH v IO)
426+#endif
427+ , Show (IndexName v)
428+ )
429+ => proxy v
430+ -> IndexName v
431+ -> IO [Value]
432+getLogsByIndex prx i = do
433+ r <- bh prx $ do
434+ void (refreshIndex prx i)
435+ searchByIndex prx i (mkSearch prx Nothing Nothing)
436+ let actualCode = statusCode (responseStatus r)
437+ assertBool ("search by " <> show i <> " " <> show actualCode <> " /= 200") (actualCode == 200)
438+ return $ responseBody r ^.. key "hits" . key "hits" . values
439+
440+
441+-------------------------------------------------------------------------------
442+bh :: TestESVersion v => proxy v -> BH v IO a -> IO a
443+bh prx = withBH prx defaultManagerSettings (svr prx)
444+
445+
446+-------------------------------------------------------------------------------
447+withTestLogging
448+ :: TestESVersion v
449+ => proxy v
450+ -> IO Scribe
451+ -> (IO (Response ByteString) -> KatipT IO b)
452+ -> IO b
453+withTestLogging = withTestLogging' id
454+
455+
456+-------------------------------------------------------------------------------
457+withTestLogging'
458+ :: (TestESVersion v)
459+ => (LogEnv -> LogEnv)
460+ -> proxy v
461+ -> IO Scribe
462+ -> (IO (Response ByteString) -> KatipT IO b)
463+ -> IO b
464+withTestLogging' modEnv prx setup f = do
465+ scr <- setup
466+ le <- modEnv <$> initLogEnv ns env
467+ le' <- registerScribe "es" scr defaultScribeSettings le
468+ let done' = do
469+ _ <- closeScribes le'
470+ bh prx (refreshIndex prx (ixn prx))
471+ runKatipT le' (f done')
472+ where
473+ ns = Namespace ["katip-test"]
474+ env = Environment "test"
475+
476+
477+-------------------------------------------------------------------------------
478+svr :: TestESVersion v => proxy v -> Server v
479+svr prx = toServer prx "http://localhost:9200"
480+
481+
482+-------------------------------------------------------------------------------
483+ixn :: TestESVersion v => proxy v -> IndexName v
484+ixn prx = toIndexName prx "katip-elasticsearch-tests"
485+
486+
487+-------------------------------------------------------------------------------
488+ixs :: TestESVersion v => proxy v -> IndexSettings v
489+ixs prx = defaultIndexSettings prx
490+ & indexShards prx .~ toShardCount prx 1
491+ & indexReplicas prx .~ toReplicaCount prx 1
492+
493+
494+-------------------------------------------------------------------------------
495+tn :: TestESVersion v => proxy v -> TemplateName v
496+tn prx = toTemplateName prx "katip-elasticsearch-tests"
497+
498+
499+-------------------------------------------------------------------------------
500+mn :: TestESVersion v => proxy v -> MappingName v
501+mn prx = toMappingName prx "logs"
502+
503+
504+-------------------------------------------------------------------------------
505+dropESSchema
506+ :: ( TestESVersion v
507+ , Monad (BH v IO)
508+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
509+ , Functor (BH v IO)
510+#endif
511+ )
512+ => proxy v
513+ -> BH v IO ()
514+dropESSchema prx = void $ deleteIndex prx (toIndexName prx "katip-elasticsearch-tests*")
515+
516+
517+-------------------------------------------------------------------------------
518+dropESSTemplate
519+ :: ( TestESVersion v
520+ , Monad (BH v IO)
521+#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ < 800
522+ , Functor (BH v IO)
523+#endif
524+ )
525+ => proxy v
526+ -> BH v IO ()
527+dropESSTemplate prx = void $ deleteTemplate prx (tn prx)
528+
529+
530+-------------------------------------------------------------------------------
531+instance Arbitrary Value where
532+ arbitrary = oneof
533+ [ Object <$> reduceSize arbitrary
534+ , Array . V.fromList <$> reduceSize arbitrary
535+ , String <$> arbitrary
536+ , Number <$> (scientific <$> arbitrary <*> arbitrary)
537+ , Bool <$> arbitrary
538+ , A.pure Null
539+ ]
540+
541+
542+-------------------------------------------------------------------------------
543+-- | Reduce the size of Arbitrary input for the given generator
544+reduceSize :: Gen a -> Gen a
545+reduceSize f = sized $ \ n -> resize (n `div` 2) f
katip-logzio/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2018, Soostone Inc
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Ozgun Ataman nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
katip-logzio/README.mdnew file+4
@@ -0,0 +1,4 @@
1+# Katip Logz.IO [![Build Status](https://travis-ci.org/Soostone/katip.svg?branch=master)](https://travis-ci.org/Soostone/katip)
2+
3+katip-logzio is a scribe for the Katip logging framework that sends
4+structured logs to the logz.io centralized logging platform.
katip-logzio/changelog.mdnew file+9
@@ -0,0 +1,9 @@
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
8+==========
9+* Initial release
katip-logzio/katip-logzio.cabalnew file+94
@@ -0,0 +1,94 @@
1+name: katip-logzio
2+version: 0.1.1.0
3+synopsis: Logz.IO scribe for the Katip logging framework
4+description: See README.md for more details.
5+license: BSD3
6+license-file: LICENSE
7+author: Michael Xavier
8+maintainer: michael.xavier@soostone.com
9+copyright: Soostone Inc, 2018
10+category: Data, Text, Logging
11+homepage: https://github.com/Soostone/katip
12+bug-reports: https://github.com/Soostone/katip/issues
13+build-type: Simple
14+cabal-version: >=1.10
15+extra-source-files:
16+ README.md
17+ changelog.md
18+
19+tested-with: GHC == 8.4.3
20+
21+source-repository head
22+ type: git
23+ location: https://github.com/Soostone/katip.git
24+
25+flag lib-Werror
26+ default: False
27+ manual: True
28+
29+
30+library
31+ exposed-modules:
32+ Katip.Scribes.LogzIO.HTTPS
33+ build-depends:
34+ base >=4.8.0.0 && <5
35+ , katip >= 0.8.0.0
36+ , stm >= 2.1.1
37+ , async
38+ , time >= 1.8.0.2
39+ , aeson >= 0.10.0.0
40+ , unix
41+ , 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
53+ hs-source-dirs: src
54+ default-language: Haskell2010
55+ hs-source-dirs: src
56+ ghc-options: -Wall -Wall -Wnoncanonical-monad-instances
57+ if flag(lib-Werror)
58+ ghc-options: -Werror
59+
60+
61+test-suite test
62+ type: exitcode-stdio-1.0
63+ main-is: Main.hs
64+ hs-source-dirs: test
65+ default-language: Haskell2010
66+ other-modules:
67+ Katip.Tests.Scribes.LogzIO.HTTPS
68+ build-depends: base
69+ , katip
70+ , katip-logzio
71+ , tasty
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
93+ if flag(lib-Werror)
94+ ghc-options: -Werror
katip-logzio/src/Katip/Scribes/LogzIO/HTTPS.hsnew file+639
@@ -0,0 +1,639 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE GADTs #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE TypeApplications #-}
7+-- | This is a log scribe that writes logs to logz.io's bulk
8+-- <https://app.logz.io/#/dashboard/data-sources/Bulk-HTTPS HTTPS
9+-- API>.
10+module Katip.Scribes.LogzIO.HTTPS
11+ ( -- * Scribe construction
12+ mkLogzIOScribe
13+ -- * Types
14+ , BulkAPIError(..)
15+ , 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 (..)
37+ ) where
38+
39+
40+-------------------------------------------------------------------------------
41+import Control.Applicative
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
54+import Data.Int
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
70+-------------------------------------------------------------------------------
71+
72+
73+
74+-- | This is returned when the bulk import was a partial success
75+data BulkAPIError = BulkAPIError
76+ { bulkAPIError_malformedLines :: Int
77+ -- ^ The number of log lines which are not well-formed JSON. This
78+ -- indicates a __library bug__. Please file an issue on GitHub.
79+ , bulkAPIError_successfulLines :: Int
80+ -- ^ The number of log lines received successfully.
81+ , bulkAPIError_oversizedLines :: Int
82+ -- ^ The number of log lines which exceed the line length
83+ -- limit. katip-logzio makes a best effort to truncate logs that
84+ -- exceed the size limit. This probably indicates a __library bug__
85+ -- and should be reported as an issue on GitHub.
86+ , bulkAPIError_emptyLogLines :: Int
87+ -- ^ The number of log lines which were empty. There isnt' really a
88+ -- concept of a truly empty log line in katip, so this most likely
89+ -- indicates a __library bug__ and should be reported as an issue on
90+ -- GitHub.
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+ }
106+
107+
108+-------------------------------------------------------------------------------
109+data LogzIOScribeConfiguration = LogzIOScribeConfiguration
110+ { logzIOScribeConfiguration_bufferItems :: Int
111+ -- ^ Will flush the log buffer if this many items is in the buffer
112+ -- __or__ 'logzIOScribeConfiguration_bufferTimeout' is reached,
113+ -- whichever is first
114+ , logzIOScribeConfiguration_bufferTimeout :: Time.NominalDiffTime
115+ -- ^ Will flush the buffer if it has been this long since the last
116+ -- flush __or__ 'logzIOScribeConfiguration_bufferItems' items are
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 ()
127+ }
128+
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+
205+-------------------------------------------------------------------------------
206+mkLogzIOScribe
207+ :: LogzIOScribeConfiguration
208+ -> K.PermitFunc
209+ -> K.Verbosity
210+ -> IO K.Scribe
211+mkLogzIOScribe config permitItem verbosity = do
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
267+ case res of
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
294+ pure ()
295+
296+ pure $ K.Scribe
297+ { K.liPush = STM.atomically . push . AnyLogItem
298+ , K.scribeFinalizer = close
299+ , K.scribePermitItem = permitItem
300+ }
301+ where
302+ itemBufferSize = logzIOScribeConfiguration_bufferItems config
303+ 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
319+
320+
321+-------------------------------------------------------------------------------
322+-- Match the native resolution of NominalDiffTime, which is excessive
323+-- for our needs
324+ndtPicos :: Time.NominalDiffTime -> Int64
325+ndtPicos = round . (* picos)
326+ where
327+ picos :: Time.NominalDiffTime
328+ picos = 10 ^ (9 :: Int)
329+
330+
331+-------------------------------------------------------------------------------
332+ndtToMicros :: Time.NominalDiffTime -> Int
333+ndtToMicros t = round (((fromIntegral (ndtPicos t)) :: Double) / picosInMicro)
334+ where
335+ picosInMicro = 10 ^ (3 :: Int)
336+
337+
338+-------------------------------------------------------------------------------
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)
354+
355+-------------------------------------------------------------------------------
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+ }
409+
410+
411+-------------------------------------------------------------------------------
412+newtype Bytes = Bytes
413+ { bytes :: Int64
414+ } deriving (Show, Eq, Num, Ord, Bounded)
415+
416+
417+-- | How big of a body can we send? The limit is defined
418+-- <https://app.logz.io/#/dashboard/data-sources/Bulk-HTTPS here>.
419+maxPayloadBytes :: Bytes
420+maxPayloadBytes = 10485760
421+
422+
423+-- | How long can each serialized payload be (let's assume including
424+-- the trailing newline). The limit is defined
425+-- <https://app.logz.io/#/dashboard/data-sources/Bulk-HTTPS here>.
426+maxLogLineLength :: Bytes
427+maxLogLineLength = 500000
428+
429+
430+measureJSONLine :: A.ToJSON a => a -> (BB.Builder, Bytes)
431+measureJSONLine a = (BB.lazyByteString lbs, Bytes (LBS.length lbs))
432+ where
433+ lbs = A.encode a <> "\n"
434+
435+
436+-- | Fully-rendered JSON object for an item
437+fullItemObject :: K.LogItem a => K.Verbosity -> K.Item a -> A.Object
438+fullItemObject verbosity item = HM.fromList
439+ [ "app" A..= K._itemApp item
440+ , "env" A..= K._itemEnv item
441+ , "sev" A..= K._itemSeverity item
442+ , "thread" A..= K.getThreadIdText (K._itemThread item)
443+ , "host" A..= K._itemHost item
444+ , "pid" A..= pidInt
445+ , "data" A..= annotateKeys (K.payloadObject verbosity (K._itemPayload item))
446+ -- Slight deviation, logz.io uses "message" instead of "msg"
447+ , "message" A..= TB.toLazyText (K.unLogStr (K._itemMessage item))
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)))
456+ , "ns" A..= K._itemNamespace item
457+ , "loc" A..= (LocJs <$> K._itemLoc item)
458+ ]
459+ where
460+ POSIX.CPid pidInt = K._itemProcess item
461+
462+
463+-- | A version of 'renderLine' which renders a line and stays under
464+-- the maximum line size of 500,000 bytes. If the default rendering is
465+-- too large, the log will be reduced to a timestamp and a potentially
466+-- truncated message.
467+renderLineTruncated :: K.LogItem a => K.Verbosity -> K.Item a -> (BB.Builder, Bytes)
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
485+ then (fullLine, fullSize)
486+ else (fallbackLine, fallbackSize)
487+ where
488+ fullObject = fullItemObject verbosity item
489+ (fullLine, fullSize) = measureJSONLine fullObject
490+ -- only the absolutely necessary keys, with message stripped out
491+ -- to help us calculate how much of the message we can keep. these
492+ -- are lazily evaluated and as such won't be computed unless the
493+ -- item is too big
494+ blankObject :: A.Object
495+ blankObject = HM.fromList
496+ [ "message" A..= A.String "" -- we'll start with a blank message
497+ , "@timestamp" A..= K._itemTime item
498+ ]
499+ (_, blankObjectSize) = measureJSONLine blankObject
500+ messageBytesAllowed = maxLogLineLength - blankObjectSize
501+ (fallbackLine, fallbackSize) = measureJSONLine fallbackObject
502+ fallbackObject :: A.Object
503+ fallbackObject = HM.fromList
504+ [ "message" A..= A.toJSON (TL.take (bytes messageBytesAllowed) (TB.toLazyText (K.unLogStr (K._itemMessage item))))
505+ , "@timestamp" A..= A.toJSON (K._itemTime item)
506+ ]
507+
508+
509+data BulkBuffer = BulkBuffer
510+ { bulkBuffer_bytesUsed :: !Bytes
511+ , bulkBuffer_payload :: !BB.Builder
512+ , bulkBuffer_itemCount :: !Int
513+ }
514+
515+
516+instance Semigroup.Semigroup BulkBuffer where
517+ (BulkBuffer bytesUsedA bufferA itemCountA) <>
518+ (BulkBuffer bytesUsedB bufferB itemCountB) = BulkBuffer
519+ (bytesUsedA + bytesUsedB)
520+ (bufferA <> bufferB)
521+ (itemCountA + itemCountB)
522+
523+
524+instance Monoid BulkBuffer where
525+ mempty = BulkBuffer 0 mempty 0
526+ mappend = (<>)
527+
528+
529+data LogAction buf =
530+ Buffered
531+ buf
532+ -- ^ New buffer
533+ | FlushNow
534+ buf
535+ -- ^ Buffer to flush
536+ buf
537+ -- ^ New buffer
538+
539+bufferItem
540+ :: K.LogItem a
541+ => Int
542+ -- ^ Maximum items before flushing
543+ -> K.Verbosity
544+ -> K.Item a
545+ -> BulkBuffer
546+ -> LogAction BulkBuffer
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
568+ newBytesUsed = bulkBuffer_bytesUsed bulkBuffer + spaceNeeded
569+ newItemCount = bulkBuffer_itemCount bulkBuffer + 1
570+ in if newItemCount >= maxItems || newBytesUsed >= customMaxPayload
571+ then FlushNow
572+ bulkBuffer
573+ BulkBuffer
574+ { bulkBuffer_bytesUsed = spaceNeeded
575+ , bulkBuffer_payload = encodedLine
576+ , bulkBuffer_itemCount = 1
577+ }
578+ else Buffered $
579+ BulkBuffer
580+ { bulkBuffer_bytesUsed = newBytesUsed
581+ , bulkBuffer_payload = bulkBuffer_payload bulkBuffer <> encodedLine
582+ , bulkBuffer_itemCount = newItemCount
583+ }
584+
585+
586+-- | When time has run out on a flush, splits the buffer. n.b. this
587+-- will always return 'FlushNow' with an empty new buffer.
588+forceFlush :: (Monoid buf) => buf -> LogAction buf
589+forceFlush 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.hsnew file+22
@@ -0,0 +1,22 @@
1+module Main
2+ ( main
3+ ) where
4+
5+
6+-------------------------------------------------------------------------------
7+import Test.Tasty
8+-------------------------------------------------------------------------------
9+import qualified Katip.Tests.Scribes.LogzIO.HTTPS
10+-------------------------------------------------------------------------------
11+
12+
13+
14+main :: IO ()
15+main = defaultMain tests
16+
17+
18+-------------------------------------------------------------------------------
19+tests :: TestTree
20+tests = testGroup "katip-logzio"
21+ [ Katip.Tests.Scribes.LogzIO.HTTPS.tests
22+ ]
katip/LICENSE+1−1
@@ -1,4 +1,4 @@
1−Copyright (c) 2015, Soostone Inc
1+Copyright (c) 2015-2016, Soostone Inc
22
33All rights reserved.
44
katip/README.mdnew file+89
@@ -0,0 +1,89 @@
1+# Katip [![Build Status](https://travis-ci.org/Soostone/katip.svg?branch=master)](https://travis-ci.org/Soostone/katip)
2+
3+Katip is a structured logging framework for Haskell.
4+
5+![Katip](https://github.com/Soostone/katip/blob/master/katip/katip.jpg)
6+
7+Kâtip (pronounced kah-tip) is the Turkish word for scribe.
8+
9+## Features
10+
11+* *Structured:* Logs are structured, meaning they can be individually
12+ tagged with key value data (JSON Objects). This helps you add
13+ critical details to log messages before you need them so that when
14+ you do, they are available. Katip exposes a typeclass for log
15+ payloads so that you can use rich, domain-specific Haskell types to
16+ add context that will be automatically merged in with existing log
17+ context.
18+
19+* *Easy Integration:* Katip was designed to be easily integrated
20+ into existing monads. By using typeclasses for logging facilities,
21+ individual subsystems and even libraries can easily add their own
22+ namespacing and context without having any knowledge of their
23+ logging environment.
24+
25+* *Practical Use:* Katip comes with a set of convenience facilities
26+ built-in, so it can be used without much headache even in small
27+ projects.
28+
29+ * A `Handle` backend for logging to files in simple settings.
30+
31+ * A `AnyLogPayload` key-value type that makes it easy to log
32+ structured columns on the fly without having to define new data
33+ types.
34+
35+ * A `Monadic` interface where logging namespace can be obtained
36+ from the monad context.
37+
38+ * Multiple variants of the fundamental logging functions for
39+ optionally including fields and line-number information.
40+
41+* *Extensible:* Can be easily extended (even at runtime) to output to
42+ multiple backends at once (known as scribes). See
43+ `katip-elasticsearch` as an example. Backends for other forms of
44+ storage are trivial to write, including both hosted database systems
45+ and SaaS logging providers.
46+
47+* *Debug-Friendly:* Critical details for monitoring production systems
48+ such as host, PID, thread id, module and line location are
49+ automatically captured. User-specified attributes such as
50+ environment (e.g. Production, Test, Dev) and system name are also
51+ captured.
52+
53+* *Configurable:* Can be adjusted on a per-scribe basis both with
54+ verbosity and severity.
55+
56+ * *Verbosity* dictates how much of the log structure should
57+ actually get logged. In code you can capture highly detailed
58+ metadata and decide how much of that gets emitted to each backend.
59+
60+ * *Severity* AKA "log level" is specified with each message and
61+ individual scribes can decide whether or not to record that
62+ severity. It is even possible to at runtime swap out and replace
63+ loggers, allowing for swapping in verbose debug logging at runtime
64+ if you want.
65+
66+* *Battle-Tested:* Katip has been integrated into several production
67+ systems since 2015 and has logged hundreds of millions of messages
68+ to files and ElasticSearch.
69+
70+
71+## Examples
72+
73+Be sure to look in the
74+[examples](https://github.com/Soostone/katip/blob/master/katip/examples)
75+directory for some examples of how to integrate Katip into your own
76+stack.
77+
78+
79+## Contributors
80+
81+* [Ozgun Ataman](https://github.com/ozataman)
82+* [Michael Xavier](https://github.com/MichaelXavier)
83+* [Doug Beardsley](https://github.com/mightybyte)
84+* [Leonid Onokhov](https://github.com/sopvop)
85+* [Alexander Vershilov](https://github.com/qnikst)
86+* [Chris Martin](https://github.com/chris-martin)
87+* [Domen Kožar](https://github.com/domenkozar)
88+* [Tristan Bull](https://github.com/tmbull)
89+* [Aleksey Khudyakov](https://github.com/Shimuuar)
katip/bench/Main.hs+70−42
@@ -1,19 +1,23 @@
1−{-# LANGUAGE OverloadedStrings #-}
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE StandaloneDeriving #-}
5+{-# OPTIONS_GHC -fno-warn-orphans #-}
26module Main
37 ( main
48 ) where
59
610
711-------------------------------------------------------------------------------
8−import Blaze.ByteString.Builder
912import Control.Concurrent
10−import Control.Monad.IO.Class
11−import CriterionPlus
13+import Control.DeepSeq
14+import Control.Exception.Safe
15+import Control.Monad
16+import Criterion.Main
1217import Data.Aeson
13−import Data.Monoid
14−import Data.Text (Text)
15−import Data.Time.Calendar
16−import Data.Time.Clock
18+import Data.Monoid as M
19+import System.Directory
20+import System.FilePath
1721import System.IO
1822import System.Posix
1923-------------------------------------------------------------------------------
@@ -22,58 +26,82 @@ import Katip.Scribes.Handle
2226-------------------------------------------------------------------------------
2327
2428main :: IO ()
25−main = benchmark $
26− handleScribeBench
29+main = defaultMain [
30+ handleScribeBench
31+ ]
2732
2833
2934-------------------------------------------------------------------------------
30−handleScribeBench :: Benchmark ()
31−handleScribeBench = standoff "Katip.Scribes.Handle" $
32− subject "Bytestring Builder" $ do
33− pause
34− (Scribe push) <- liftIO setup
35− tid <- liftIO myThreadId
36− continue
37− whnfIO $ push $ exItem tid
35+handleScribeBench :: Benchmark
36+handleScribeBench = bgroup "Katip.Scribes.Handle" $
37+ flip map destinations $ \dest -> bgroup (show dest) [
38+ -- This is variably wildly on disk performance but should be a
39+ -- better test since a push test basically just tests how fast your
40+ -- queue structure is.
41+ bench "full env, flush 1000 writes" $
42+ whnfIO $ do
43+ le <- setupHandleLogEnv dest
44+ runKatipT le $ replicateM_ 1000 $ logItem ExPayload "namespace" Nothing InfoS "example"
45+ closeScribes le
46+ ]
47+ where
48+ destinations = [DevNull, TempFile]
49+ --destinations = [DevNull]
50+
51+
52+-------------------------------------------------------------------------------
53+data HandleDest = DevNull | TempFile deriving (Show, Eq)
3854
3955
4056-------------------------------------------------------------------------------
41−exItem :: ThreadId -> Item ExPayload
42−exItem tid = Item {
43− itemApp = Namespace ["app"]
44− , itemEnv = Environment "production"
45− , itemSeverity = Warning
46− , itemThread = tid
47− , itemHost = "example"
48− , itemProcess = CPid 123
49− , itemPayload = ExPayload
50− , itemMessage = "message"
51− , itemTime = mkUTCTime 2015 3 14 1 5 9
52− , itemNamespace = Namespace ["foo"]
53− , itemLoc = Nothing
54− }
57+instance NFData LogEnv where
58+ rnf (LogEnv !_ !_ !_ !_ !_ !_) = ()
59+
60+setupHandleLogEnv :: HandleDest -> IO LogEnv
61+setupHandleLogEnv hd = do
62+ (scr, _) <- setupHandleEnv hd
63+ registerScribe "handle" scr defaultScribeSettings =<< initLogEnv "katip-bench" "bench"
64+
65+
66+-------------------------------------------------------------------------------
67+setupHandleEnv :: HandleDest -> IO (Scribe, ThreadIdText)
68+setupHandleEnv dest = do
69+ scribe <- setup dest
70+ tid <- myThreadId
71+ return (scribe, mkThreadIdText tid)
5572
5673
5774-------------------------------------------------------------------------------
5875data ExPayload = ExPayload
5976
6077instance ToJSON ExPayload where
61− toJSON _ = Object mempty
78+ toJSON _ = Object M.mempty
79+
80+instance ToObject ExPayload
6281
6382instance LogItem ExPayload where
6483 payloadKeys _ _ = AllKeys
6584
6685
6786-------------------------------------------------------------------------------
68−mkUTCTime :: Integer -> Int -> Int -> DiffTime -> DiffTime -> DiffTime -> UTCTime
69−mkUTCTime y mt d h mn s = UTCTime day dt
70− where
71− day = fromGregorian y mt d
72− dt = h * 60 * 60 + mn * 60 + s
87+setup :: HandleDest -> IO Scribe
88+setup dest = do
89+ outFile <- case dest of
90+ TempFile -> do
91+ tmp <- getTemporaryDirectory
92+ return (tmp </> "katip-bench.log")
93+ DevNull -> return ("/dev/null")
94+ h <- openFile outFile WriteMode
95+ s <- mkHandleScribe ColorIfTerminal h (permitItem DebugS) V0
96+ let cleanupHandle = hClose h `finally` (when (dest == TempFile) (removeLink outFile))
97+ return s { scribeFinalizer = scribeFinalizer s `finally` cleanupHandle}
7398
7499
75100-------------------------------------------------------------------------------
76−setup :: IO Scribe
77−setup = do
78− h <- openFile "/dev/null" WriteMode
79− mkHandleScribe h Debug V0
101+deriving instance NFData ThreadIdText
102+
103+instance NFData Scribe where
104+ rnf (Scribe a b p) = (a :: Item ExPayload -> IO ())
105+ `seq` b
106+ `seq` (p :: Item ExPayload -> IO Bool)
107+ `seq` ()
katip/changelog.mdnew file+189
@@ -0,0 +1,189 @@
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+
38+0.7.0.0
39+=======
40+* Exclusively use HasCallstack for location reporting in errors. Credit to [Domen Kožar](https://github.com/domenkozar)
41+* Drop support for GHC 7.x
42+
43+0.6.3.0
44+=======
45+* Add `ToJSON`/`FromJSON` instances for Verbosity. Credit to [Aleksey Khudyakov](https://github.com/Shimuuar).
46+
47+0.6.2.1
48+=======
49+* Add `mkHandleScribeWithFormatter`. This allows control over the format of log items going to a handle. Credit to [Tristan Bull](https://github.com/tmbull) for the implementation.
50+* Documentation fixes. Credit to [Domen Kožar](https://github.com/domenkozar) for the implementation.
51+* Add `jsonFormat` handle formatter.
52+* Deprecate `formatItem` which is now replaced by `bracketFormat`.
53+
54+0.6.1.0
55+=======
56+* Loosen deps
57+* Support latest STM
58+
59+0.6.0.0
60+=======
61+* Drop ListT instance due to deprecations.
62+* Loosen dependencies.
63+
64+0.5.5.1
65+=======
66+* Decrease default timer resolution from 1s to 1ms.
67+
68+0.5.5.0
69+=======
70+* Export `runNoLoggingT` and `NoLoggingT` constructor.
71+* Delegate MonadReader instance for `NoLoggingT`.
72+
73+0.5.4.0
74+=======
75+* Loosen bounds on resourcet and template-haskell.
76+* Add convenience function askLoggerIO.
77+
78+0.5.3.0
79+=======
80+* Add MonadUnliftIO instances.
81+* Add NoLoggingT
82+
83+0.5.2.0
84+=======
85+* Allow newer versions of either by conditionally adding instances for the removed EitherT interface.
86+
87+0.5.1.0
88+=======
89+* Add mkFileScribe, a specialization of mkHandleScribe for files that manages the handle automatically.
90+
91+0.5.0.4
92+=======
93+* Loosen Win32 upper bound to run with GHC 8.2 on Windows.
94+
95+0.5.0.3
96+=======
97+* Add worked example of Katip/KatipContext to the haddocks.
98+
99+0.5.0.2
100+=======
101+* Export Katip.Compat for Windows users.
102+
103+0.5.0.1
104+=======
105+* Fix numeric formatting in Handle scribe.
106+* Bump deps for GHC 8.2.1
107+
108+0.5.0.0
109+=======
110+* Improved documentation.
111+* Add built-in buffering to scribes.
112+ Scribes now allocate a bounded queue (with configurable size). Rather than writes being synchronous to all scribes, they simply attempt to write into the bounded queue of each scribe. If any of the scribes is too far behind and the queue is full, the write is dropped. This also means that closing scribes is now an IO operation that happens synchrounsly.
113+* Added local-like functions to Katip and KatipContext typeclasses. This allows us to generalize `katipNoLogging`, `katipAddNamespace`, and `katipAddContext` to be available to anything with a `Katip` or `KatipContext` instance rather than having to reimplement these functions all the time.
114+
115+0.4.1.0
116+=======
117+* Add Katip instances for Strict StateT, WriterT, RWST.
118+* Add Katip instances for Lazy RWST.
119+
120+0.4.0.0
121+=======
122+* Drop unsafe _ioLogEnv for safe ioLogEnv
123+
124+0.3.1.5
125+=======
126+* Add Semigroup instance for LogStr.
127+
128+0.3.1.4
129+=======
130+* Loosen deps on aeson to allow 1.1.0.0
131+
132+0.3.1.3
133+=======
134+* Fix build on windows
135+
136+0.3.1.2
137+=======
138+* Add some missing test files
139+
140+0.3.1.1
141+=======
142+* Fix some example code that wasn't building
143+* Make FromJSON instance for Severity case insensitive.
144+
145+0.3.1.0
146+=======
147+* Add support for aeson 1.0.x
148+* Add Katip.Format.Time module and use much more efficient time formatting code in the Handle scribe.
149+
150+0.3.0.0
151+=======
152+* Switch from `regex-tdfa-rc` to `regex-tdfa`.
153+* Add `katipNoLogging` combinator.
154+* Add `Semigroup` instances.
155+* Drop `ToJSON` superclass requirement fro `ToObject`. Instead,
156+ `ToObject` will provide a default instance for types with an
157+ instance for `ToJSON`. This gets us to the same place as before
158+ without having to add a broader instance for something that's only
159+ going to show up in logs as an Object.
160+* Add a simple MVar lock for file handle scribes to avoid interleaved
161+ log lines from concurrent inputs.
162+
163+0.2.0.0
164+=======
165+
166+* Add GHC implicit callstack support, add logLoc.
167+* Drop lens in favor of type-compatible, lighter microlens.
168+* Renamed `logEnvNs` to clearer `logEnvApp`
169+* Added `katipAddNamespace` and `katipAddContext`
170+* Fixed nested objects not rendering in Handle scribe.
171+* LogContexts Monoid instance is now right-biased rather than left
172+ biased. This better fits the use case. For instance `ctx1 <> ctx2`
173+ will prefer keys in `ctx2` if there are conflicts. This makes the
174+ most sense because functions like `katipAddContext` will `mappend`
175+ on the right side.
176+* LogContext internally uses a `Seq` instead of a list for better
177+ complexity on context add.
178+* Improved documentation.
179+
180+0.1.1.0
181+==============
182+
183+* Set upper bounds for a few dependencies.
184+* Add ExceptT instance for Katip typeclass
185+
186+0.1.0.0
187+==============
188+
189+* Initial release
katip/examples/Main.hsdeleted−122
@@ -1,122 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE MultiParamTypeClasses #-}
3−{-# LANGUAGE TemplateHaskell #-}
4−
5−{-# LANGUAGE NoMonomorphismRestriction #-}
6−
7−
8−module Main where
9−
10−-------------------------------------------------------------------------------
11−import Control.Applicative
12−import Control.Monad.Reader
13−import Data.Aeson
14−import Data.Text
15−-------------------------------------------------------------------------------
16−import Katip
17−import Katip.Scribes.Handle
18−-------------------------------------------------------------------------------
19−
20−
21−
22−main :: IO ()
23−main = runKatipT _ioLogEnv $ do
24− logMsg "example" InfoS "Easy to emit from IO directly!"
25− logF myContext "example" InfoS "Here's a more stateful item."
26− $(logT) myContext "example" InfoS "Here's one with code location."
27− runHandler handler
28−
29−
30−myContext :: MyContext
31−myContext = MyContext "blah" 3
32−
33−data MyContext = MyContext {
34− foo :: Text
35− , bar :: Int
36− }
37−
38−
39−instance ToJSON MyContext where
40− toJSON mc = object ["foo" .= foo mc, "bar" .= bar mc]
41−
42−instance ToObject MyContext
43−
44−instance LogItem MyContext where
45− payloadKeys _ _ = AllKeys
46−
47−-------------------------------------------------------------------------------
48−type Handler a = WebT (KatipT IO) a
49−
50−
51−runHandler :: Handler a -> KatipT IO a
52−runHandler h = runReaderT (unWebT h) req
53− where
54− req = Request "GET" "/foo/bar" "example.com"
55−
56−withDB :: (ConnInfo -> Handler ()) -> Handler ()
57−withDB f = f $ ConnInfo "1234" "exampledb" ()
58−
59−
60−handler :: Handler ()
61−handler = do
62− $(logTM) InfoS "got request"
63− withDB $ \ci -> do
64− let dbctx = DBLog (ciConnId ci)
65− let ns = Namespace ["db", ciConnDB ci]
66− runLogT (return dbctx) (return ns) $ do
67− liftIO $ putStrLn "DB things!"
68− $(logTM) InfoS "calling db"
69−
70−-------------------------------------------------------------------------------
71−data Request = Request {
72− reqMethod :: Text
73− , reqPath :: Text
74− , reqReferer :: Text
75− }
76−
77−instance ToJSON Request where
78− toJSON req = object [ "method" .= reqMethod req
79− , "path" .= reqPath req
80− , "referer" .= reqReferer req
81− ]
82−
83−instance ToObject Request
84−
85−instance LogItem Request where
86− payloadKeys V3 _ = AllKeys
87− payloadKeys _ _ = SomeKeys ["method", "path"]
88−
89−newtype WebT m a = WebT {
90− unWebT :: ReaderT Request m a
91− } deriving ( Monad
92− , Functor
93− , Applicative
94− , MonadReader Request
95− , MonadTrans
96− , MonadIO
97− , Katip
98− )
99−
100−instance Katip m => KatipContext (WebT m) where
101− getKatipContext = liftM liftPayload ask
102− getKatipNamespace = return $ Namespace ["myApp"]
103−
104−
105−-------------------------------------------------------------------------------
106−data ConnInfo = ConnInfo {
107− ciConnId :: Text
108− , ciConnDB :: Text
109− , ciConn :: ()
110− }
111−
112−data DBLog = DBLog {
113− dblConnId :: Text
114− }
115−
116−instance ToJSON DBLog where
117− toJSON dbl = object ["connId" .= dblConnId dbl]
118−
119−instance ToObject DBLog
120−
121−instance LogItem DBLog where
122− payloadKeys _ _ = AllKeys
katip/examples/add_context_loop.hsnew file+69
@@ -0,0 +1,69 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+-- | This executable is a test which runs addContext with
3+-- key-compatible log contexts in a tight loop. At time of writing, I
4+-- suspect this will result in a memory leak as we just naively
5+-- concatenate it into a sequence. If instead we eagerly merge the log
6+-- objects, each loop will overwrite the previous context and there
7+-- will be no memory leak. You can pass a numeric argument to this
8+-- executable to specify how many loops to do. -1 goes infinite.
9+--
10+-- If we didn't have to retain the log context and could eagerly merge
11+-- it, this wouldn't be an issue, but unfortunately we don't know
12+-- which keys to keep until log time, not context add time. Further,
13+-- we couldn't accumulate and compose key retention functions for the
14+-- same reason.
15+module Main
16+ ( main
17+ ) where
18+
19+
20+-------------------------------------------------------------------------------
21+import Control.Monad (unless)
22+import qualified Data.Aeson as A
23+import qualified Data.HashMap.Strict as HM
24+import qualified Data.String.Conv as SC
25+import qualified System.Environment as SE
26+-------------------------------------------------------------------------------
27+import qualified Katip as K
28+-------------------------------------------------------------------------------
29+
30+
31+main :: IO ()
32+main = do
33+ args <- SE.getArgs
34+ le <- K.initLogEnv "add_context_loop" "test"
35+ K.runKatipContextT le () "loop" $ case args of
36+ [] -> run (Just 1000000)
37+ ns:_ -> let n = read ns
38+ in if n < 0
39+ then run Nothing
40+ else run (Just n)
41+
42+
43+-------------------------------------------------------------------------------
44+-- | Semantically, each loop adds more context on. If we were using
45+-- something like forever, unfoldM, etc, which is probably what we
46+-- actually want, it would add the same context each time,
47+-- non-redundantly and thus no leak.
48+run :: Maybe Int -> K.KatipContextT IO ()
49+run mlim = go 0
50+ where
51+ go n = do
52+ K.katipAddContext (SomeContext 1000) $ do
53+ K.logFM K.InfoS "loop"
54+ unless (done n) $ go (n + 1)
55+ done n = case mlim of
56+ Nothing -> False
57+ Just lim -> n >= lim
58+
59+
60+-------------------------------------------------------------------------------
61+data SomeContext = SomeContext Int
62+
63+
64+instance K.ToObject SomeContext where
65+ toObject (SomeContext sz) = HM.fromList [ SC.toS (show n) A..= () | n <- [0..sz]]
66+
67+
68+instance K.LogItem SomeContext where
69+ payloadKeys _ _ = K.AllKeys
katip/examples/example.hsnew file+128
@@ -0,0 +1,128 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE UndecidableInstances #-}
9+module Main
10+ ( main
11+ ) where
12+
13+
14+-------------------------------------------------------------------------------
15+import qualified Control.Applicative as A
16+import Control.Exception
17+import Control.Monad.Base
18+import Control.Monad.Reader
19+import Control.Monad.Trans.Control
20+import Data.Aeson
21+import Data.Monoid as M
22+import System.IO (stdout)
23+-------------------------------------------------------------------------------
24+import Katip
25+-------------------------------------------------------------------------------
26+
27+
28+-- | An example of advanced katip usage. Be sure to check out
29+-- example_lens.hs for a slightly cleaner and more general pattern.
30+main :: IO ()
31+main = do
32+ -- We'll set up a scribe that logs to stdout and will only log item
33+ -- fields permitted for Verbosity 2 and will throw out Debug
34+ -- messages entirely. Note that katip provides facilities like
35+ -- 'unregisterScribe' and 'registerScribe' to make it possible to
36+ -- hot-swap scribes at runtime if you need to. 'closeScribes' is
37+ -- blocking and flushes all messages out of a scribe and cleans up
38+ -- resources that were allocated at creation.
39+ handleScribe <- mkHandleScribe ColorIfTerminal stdout (permitItem InfoS) V2
40+ let mkLogEnv = registerScribe "stdout" handleScribe defaultScribeSettings =<< initLogEnv "MyApp" "production"
41+ bracket mkLogEnv closeScribes $ \le -> do
42+ let s = MyState M.mempty mempty le
43+ runStack s $ do
44+ $(logTM) InfoS "Started"
45+ -- this will add "confrabulation" to the current namespace, making
46+ -- logs made under this block have the namespace of
47+ -- "main.confrabulation". Further, ConfrabLogCTX's key/value
48+ -- context will also get merged with the context above it. You can
49+ -- use this to stack up various contextual details throughout your
50+ -- code and they will be flattened out and combined in the log
51+ -- output.
52+ katipAddNamespace "confrabulation" $ katipAddContext (ConfrabLogCTX 42) $ do
53+ $(logTM) DebugS "Confrabulating widgets, with extra namespace and context"
54+ confrabulateWidgets
55+ $(logTM) InfoS "Namespace and context are back to normal"
56+ katipNoLogging $
57+ $(logTM) DebugS "You'll never see this log message!"
58+
59+
60+-------------------------------------------------------------------------------
61+newtype ConfrabLogCTX = ConfrabLogCTX Int
62+
63+
64+instance ToJSON ConfrabLogCTX where
65+ toJSON (ConfrabLogCTX factor) = object ["confrab_factor" .= factor]
66+
67+
68+instance ToObject ConfrabLogCTX
69+
70+
71+instance LogItem ConfrabLogCTX where
72+ payloadKeys _verb _a = AllKeys
73+
74+
75+-------------------------------------------------------------------------------
76+confrabulateWidgets :: (Monad m) => m ()
77+confrabulateWidgets = return ()
78+
79+
80+-------------------------------------------------------------------------------
81+data MyState = MyState {
82+ msKNamespace :: Namespace
83+ , msKContext :: LogContexts
84+ , msLogEnv :: LogEnv
85+ }
86+
87+
88+-------------------------------------------------------------------------------
89+newtype MyStack m a = MyStack {
90+ unStack :: ReaderT MyState m a
91+ } deriving (MonadReader MyState, Functor, A.Applicative, Monad, MonadIO, MonadTrans)
92+
93+
94+-- MonadBase, MonadTransControl, and MonadBaseControl aren't strictly
95+-- needed for this example, but they are commonly required and
96+-- MonadTransControl/MonadBaseControl are a pain to implement, so I've
97+-- included them. Note that KatipT and KatipContextT already do this work for you.
98+instance MonadBase b m => MonadBase b (MyStack m) where
99+ liftBase = liftBaseDefault
100+
101+
102+instance MonadTransControl MyStack where
103+ type StT MyStack a = StT (ReaderT Int) a
104+ liftWith = defaultLiftWith MyStack unStack
105+ restoreT = defaultRestoreT MyStack
106+
107+
108+instance MonadBaseControl b m => MonadBaseControl b (MyStack m) where
109+ type StM (MyStack m) a = ComposeSt MyStack m a
110+ liftBaseWith = defaultLiftBaseWith
111+ restoreM = defaultRestoreM
112+
113+
114+instance (MonadIO m) => Katip (MyStack m) where
115+ getLogEnv = asks msLogEnv
116+ localLogEnv f (MyStack m) = MyStack (local (\s -> s { msLogEnv = f (msLogEnv s)}) m)
117+
118+
119+instance (MonadIO m) => KatipContext (MyStack m) where
120+ getKatipContext = asks msKContext
121+ localKatipContext f (MyStack m) = MyStack (local (\s -> s { msKContext = f (msKContext s)}) m)
122+ getKatipNamespace = asks msKNamespace
123+ localKatipNamespace f (MyStack m) = MyStack (local (\s -> s { msKNamespace = f (msKNamespace s)}) m)
124+
125+
126+-------------------------------------------------------------------------------
127+runStack :: MyState -> MyStack m a -> m a
128+runStack s f = runReaderT (unStack f) s
katip/examples/example_lens.hsnew file+134
@@ -0,0 +1,134 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE MultiParamTypeClasses #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE UndecidableInstances #-}
9+module Main
10+ ( main
11+ ) where
12+
13+
14+-------------------------------------------------------------------------------
15+import Control.Applicative as A
16+import Control.Exception
17+import Control.Lens hiding ((.=))
18+import Control.Monad.Base
19+import Control.Monad.Reader
20+import Control.Monad.Trans.Control
21+import Data.Aeson
22+import Data.Monoid as M
23+import System.IO (stdout)
24+-------------------------------------------------------------------------------
25+import Katip
26+-------------------------------------------------------------------------------
27+
28+
29+data MyState = MyState {
30+ _msKNamespace :: Namespace
31+ , _msKContext :: LogContexts
32+ , _msLogEnv :: LogEnv
33+ }
34+
35+
36+-- This gives us HasMyState, which is helpful for complex stacks where
37+-- MyState may be nested somewhere deeper inside a larger data
38+-- structure. You can keep functions that operate on MyState as
39+-- general as possible.
40+makeClassy ''MyState
41+
42+
43+-------------------------------------------------------------------------------
44+-- | An example of advanced katip usage with Lens.
45+main :: IO ()
46+main = do
47+ -- We'll set up a scribe that logs to stdout and will only log item
48+ -- fields permitted for Verbosity 2 and will throw out Debug
49+ -- messages entirely. Note that katip provides facilities like
50+ -- 'unregisterScribe' and 'registerScribe' to make it possible to
51+ -- hot-swap scribes at runtime if you need to. 'closeScribes' is
52+ -- blocking and flushes all messages out of a scribe and cleans up
53+ -- resources that were allocated at creation.
54+ handleScribe <- mkHandleScribe ColorIfTerminal stdout (permitItem InfoS) V2
55+ let mkLogEnv = registerScribe "stdout" handleScribe defaultScribeSettings =<< initLogEnv "MyApp" "production"
56+ bracket mkLogEnv closeScribes $ \le -> do
57+ let s = MyState M.mempty mempty le
58+ runStack s $ do
59+ $(logTM) InfoS "Started"
60+ -- this will add "confrabulation" to the current namespace, making
61+ -- logs made under this block have the namespace of
62+ -- "main.confrabulation". Further, ConfrabLogCTX's key/value
63+ -- context will also get merged with the context above it. You can
64+ -- use this to stack up various contextual details throughout your
65+ -- code and they will be flattened out and combined in the log
66+ -- output.
67+ katipAddNamespace "confrabulation" $ katipAddContext (ConfrabLogCTX 42) $ do
68+ $(logTM) DebugS "Confrabulating widgets, with extra namespace and context"
69+ confrabulateWidgets
70+ $(logTM) InfoS "Namespace and context are back to normal"
71+ katipNoLogging $
72+ $(logTM) DebugS "You'll never see this log message!"
73+
74+
75+-------------------------------------------------------------------------------
76+newtype ConfrabLogCTX = ConfrabLogCTX Int
77+
78+
79+instance ToJSON ConfrabLogCTX where
80+ toJSON (ConfrabLogCTX factor) = object ["confrab_factor" .= factor]
81+
82+
83+instance ToObject ConfrabLogCTX
84+
85+
86+instance LogItem ConfrabLogCTX where
87+ payloadKeys _verb _a = AllKeys
88+
89+
90+-------------------------------------------------------------------------------
91+confrabulateWidgets :: (Monad m) => m ()
92+confrabulateWidgets = return ()
93+
94+
95+-------------------------------------------------------------------------------
96+newtype MyStack m a = MyStack {
97+ unStack :: ReaderT MyState m a
98+ } deriving (MonadReader MyState, Functor, A.Applicative, Monad, MonadIO, MonadTrans)
99+
100+
101+-- MonadBase, MonadTransControl, and MonadBaseControl aren't strictly
102+-- needed for this example, but they are commonly required and
103+-- MonadTransControl/MonadBaseControl are a pain to implement, so I've
104+-- included them. Note that KatipT and KatipContextT already do this work for you.
105+instance MonadBase b m => MonadBase b (MyStack m) where
106+ liftBase = liftBaseDefault
107+
108+
109+instance MonadTransControl MyStack where
110+ type StT MyStack a = StT (ReaderT Int) a
111+ liftWith = defaultLiftWith MyStack unStack
112+ restoreT = defaultRestoreT MyStack
113+
114+
115+instance MonadBaseControl b m => MonadBaseControl b (MyStack m) where
116+ type StM (MyStack m) a = ComposeSt MyStack m a
117+ liftBaseWith = defaultLiftBaseWith
118+ restoreM = defaultRestoreM
119+
120+instance (MonadIO m) => Katip (MyStack m) where
121+ getLogEnv = view msLogEnv
122+ localLogEnv f (MyStack m) = MyStack (local (over msLogEnv f) m)
123+
124+
125+instance (MonadIO m) => KatipContext (MyStack m) where
126+ getKatipContext = view msKContext
127+ localKatipContext f (MyStack m) = MyStack (local (over msKContext f) m)
128+ getKatipNamespace = view msKNamespace
129+ localKatipNamespace f (MyStack m) = MyStack (local (over msKNamespace f) m)
130+
131+
132+-------------------------------------------------------------------------------
133+runStack :: MyState -> MyStack m a -> m a
134+runStack s f = runReaderT (unStack f) s
katip/katip.cabal+108−32
@@ -1,24 +1,48 @@
11name: katip
2−version: 0.1.0.0
3−synopsis: A structured logging framework for tagging each log message with its
4− surrounding context.
2+version: 0.8.5.0
3+synopsis: A structured logging framework.
4+description:
5+ Katip is a structured logging framework. See README.md for more details.
6+
57license: BSD3
68license-file: LICENSE
7−author: Ozgun Ataman
8−maintainer: ozgun.ataman@soostone.com
9−copyright: Soostone Inc, 2015
9+author: Ozgun Ataman, Michael Xavier
10+maintainer: michael.xavier@soostone.com
11+copyright: Soostone Inc, 2015-2017
1012category: Data, Text, Logging
13+homepage: https://github.com/Soostone/katip
14+bug-reports: https://github.com/Soostone/katip/issues
1115build-type: Simple
1216cabal-version: >=1.10
17+extra-source-files:
18+ README.md
19+ changelog.md
20+ examples/example.hs
21+ examples/example_lens.hs
22+ bench/Main.hs
23+ test/Main.hs
24+ test/Katip/Tests.hs
25+ test/Katip/Tests/Scribes/Handle.hs
26+ test/Katip/Tests/Scribes/Handle-text.golden
27+ test/Katip/Tests/Format/Time.hs
28+tested-with: GHC == 8.0.1, GHC == 8.2.2, GHC == 8.4.3
29+
30+source-repository head
31+ type: git
32+ location: https://github.com/Soostone/katip.git
33+
34+flag lib-Werror
35+ default: False
36+ manual: True
1337
1438library
1539 exposed-modules:
1640 Katip
1741 Katip.Core
42+ Katip.Format.Time
1843 Katip.Monadic
1944 Katip.Scribes.Handle
2045
21− -- other-modules:
2246 default-extensions:
2347 DeriveGeneric
2448 FlexibleContexts
@@ -29,32 +53,78 @@ library
2953 TemplateHaskell
3054 OverloadedStrings
3155
32− build-depends: base >=4.5 && <4.7
33− , aeson >=0.6 && <0.9
34− , auto-update >= 0.1 && < 0.2
35− , bytestring >= 0.9 && < 0.11
36− , containers >=0.4 && <0.6
37− , either >= 4 && < 4.4
38− , exceptions >= 0.5 && < 0.7
39− , hostname >=1.0 && <1.1
40− , lens >= 4.4 && <4.8
41− , lens-aeson < 1.1
42− , old-locale >= 1.0 && < 1.1
43− , string-conv >= 0.1 && < 0.2
44− , template-haskell >= 2.8 && < 2.10
45− , text >= 0.11 && <1.3
46− , time >= 1 && < 1.6
47− , transformers >= 0.3 && < 0.5
48− , unix >= 2.5 && < 2.8
49− , unordered-containers >= 0.2 && < 0.3
50− , monad-control >= 1.0 && < 1.1
51− , mtl
52− , transformers-base
53− , resourcet
56+ build-depends: base >=4.9.0.0 && <5
57+ , aeson >=0.6
58+ , async < 3.0.0.0
59+ , auto-update >= 0.1
60+ , bytestring >= 0.9
61+ , containers >=0.4
62+ , either >= 4
63+ , safe-exceptions >= 0.1.0.0
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
71+ , transformers-compat
72+ , unordered-containers >= 0.2
73+ , monad-control >= 1.0
74+ , mtl >= 2.0
75+ , transformers-base >= 0.3
76+ , resourcet >= 1.1
77+ , scientific >= 0.3.3.0
78+ , microlens >= 0.2.0.0
79+ , microlens-th >= 0.1.0.0
80+ , semigroups
81+ , unliftio-core >= 0.1
82+ , stm >= 2.4
5483
5584 hs-source-dirs: src
5685 default-language: Haskell2010
5786 ghc-options: -Wall
87+ if flag(lib-Werror)
88+ ghc-options: -Werror
89+ if os(windows)
90+ build-depends: Win32 >=2.3 && <2.9
91+ exposed-modules: Katip.Compat
92+ else
93+ build-depends: unix >= 2.5 && <2.8
94+
95+
96+test-suite test
97+ type: exitcode-stdio-1.0
98+ main-is: Main.hs
99+ hs-source-dirs: test
100+ other-modules:
101+ Katip.Tests.Format.Time
102+ Katip.Tests.Scribes.Handle
103+ Katip.Tests
104+ default-language: Haskell2010
105+ build-depends: base
106+ , katip
107+ , aeson
108+ , bytestring
109+ , tasty >= 0.10.1.2
110+ , tasty-golden
111+ , tasty-hunit
112+ , tasty-quickcheck
113+ , quickcheck-instances
114+ , template-haskell
115+ , text
116+ , time
117+ , time-locale-compat >= 0.1.0.1
118+ , directory
119+ , regex-tdfa
120+ , unordered-containers
121+ , microlens
122+ , containers
123+ , stm
124+ , safe-exceptions
125+ ghc-options: -Wall
126+ if flag(lib-Werror)
127+ ghc-options: -Werror
58128
59129
60130benchmark bench
@@ -62,15 +132,21 @@ benchmark bench
62132 main-is: Main.hs
63133 hs-source-dirs: bench
64134 default-language: Haskell2010
65− ghc-options: -O2
135+ ghc-options: -O2 -Wall -threaded -rtsopts -with-rtsopts=-N
136+ if flag(lib-Werror)
137+ ghc-options: -Werror
66138 build-depends:
67139 base
68140 , aeson
69141 , blaze-builder
70142 , katip
71− , criterion >= 0.8 && < 1.0
72− , criterion-plus
143+ , criterion >= 1.1.0.0
73144 , unix
74145 , text
75146 , time
76147 , transformers
148+ , deepseq
149+ , async
150+ , filepath
151+ , safe-exceptions
152+ , directory
katip/katip.jpgnew file

Binary file not shown.

katip/src/Katip.hs+253−14
@@ -1,52 +1,203 @@
1+-- | Includes all of the APIs youll need to use Katip. Be sure to
2+-- check out the included @examples@ directory for an example of
3+-- usage.
4+--
5+-- Here's a basic example:
6+--
7+-- @
8+--
9+-- import Control.Exception
10+-- import Katip
11+--
12+-- main :: IO ()
13+-- main = do
14+-- handleScribe <- mkHandleScribe ColorIfTerminal stdout (permitItem InfoS) V2
15+-- let makeLogEnv = registerScribe "stdout" handleScribe defaultScribeSettings =<< initLogEnv \"MyApp\" \"production\"
16+-- -- closeScribes will stop accepting new logs, flush existing ones and clean up resources
17+-- bracket makeLogEnv closeScribes $ \le -> do
18+-- let initialContext = () -- this context will be attached to every log in your app and merged w/ subsequent contexts
19+-- let initialNamespace = "main"
20+-- runKatipContextT le initialContext initialNamespace $ do
21+-- $(logTM) InfoS "Hello Katip"
22+-- -- This adds a namespace to the current namespace and merges a piece of contextual data into your context
23+-- katipAddNamespace "additional_namespace" $ katipAddContext (sl "some_context" True) $ do
24+-- $(logTM) WarningS "Now we're getting fancy"
25+-- katipNoLogging $ do
26+-- $(logTM) DebugS "You will never see this!"
27+--
28+-- @
29+--
30+-- Another common case that you have some sort of App monad that's
31+-- based on ReaderT with some Config state. This is a perfect place to
32+-- insert read-only katip state:
33+--
34+-- @
35+--
36+-- import Katip as K
37+--
38+-- data Config = Config {
39+-- logNamespace :: K.Namespace
40+-- , logContext :: K.LogContexts
41+-- , logEnv :: K.LogEnv
42+-- -- whatever other read-only config you need
43+-- }
44+--
45+-- newtype App m a = App {
46+-- unApp :: ReaderT Config m a
47+-- } deriving (Functor, Applicative, Monad, MonadIO, MonadReader Config) -- these are necessary
48+--
49+--
50+-- -- These instances get even easier with lenses!
51+-- instance (MonadIO m) => K.Katip (App m) where
52+-- getLogEnv = asks logEnv
53+-- -- with lens:
54+-- -- getLogEnv = view logEnv
55+-- localLogEnv f (App m) = App (local (\\s -> s { logEnv = f (logEnv s)}) m)
56+-- -- with lens:
57+-- -- localLogEnv f (App m) = App (local (over logEnv f) m)
58+--
59+--
60+-- instance (MonadIO m) => K.KatipContext (App m) where
61+-- getKatipContext = asks logContext
62+-- -- with lens:
63+-- -- getKatipContext = view logContext
64+-- localKatipContext f (App m) = App (local (\\s -> s { logContext = f (logContext s)}) m)
65+-- -- with lens:
66+-- -- localKatipContext f (App m) = App (local (over logContext f) m)
67+-- getKatipNamespace = asks logNamespace
68+-- -- with lens:
69+-- -- getKatipNamespace = view logNamespace
70+-- localKatipNamespace f (App m) = App (local (\\s -> s { logNamespace = f (logNamespace s)}) m)
71+-- -- with lens:
72+-- -- localKatipNamespace f (App m) = App (local (over logNamespace f) m)
73+--
74+-- @
75+--
76+-- To get up and running, the workflow is generally:
77+--
78+-- * Set up a 'LogEnv' using 'initLogEnv'.
79+--
80+-- * Add 'Scribe's using 'registerScribe'.
81+--
82+-- * Either use 'KatipT' or 'KatipContextT' for a pre-built
83+-- transformer stack or add 'Katip' and 'KatipContext' instances to
84+-- your own transformer stack. If you do the latter, you may want to
85+-- look in the @examples@ dir for some tips on composing contexts and
86+-- namespaces.
87+--
88+-- * Define some structured log data throughout your application and
89+-- implement 'ToObject' and 'LogItem' for them.
90+--
91+-- * Begin logging with 'logT', 'logTM', etc.
92+--
93+-- * Define your own 'Scribe' if you need to output to some as-yet
94+-- unsupported format or service. If you think it would be useful to
95+-- others, consider releasing your own package.
196module Katip
297 (
398
499 -- * Framework Types
5−
6− Katip (..)
7− , Namespace (..)
100+ Namespace (..)
8101 , Environment (..)
9− , Severity (..), renderSeverity
102+ , Severity (..)
103+ , renderSeverity
104+ , textToSeverity
10105 , Verbosity (..)
11106 , ToObject (..)
12107 , LogItem (..)
108+ , Item(..)
109+ , ThreadIdText(..)
13110 , PayloadSelection (..)
14111 , Scribe (..)
15112 , LogEnv (..)
16− , KatipContext(..)
17− , AnyLogContext
18− , LogContexts, liftPayload
113+ , SimpleLogPayload, sl
114+ , defaultScribeSettings
115+ , ScribeSettings
116+ , scribeBufferSize
117+ , _scribeBufferSize
118+
119+ -- ** @lens@-compatible Lenses
120+ , itemApp
121+ , itemEnv
122+ , itemSeverity
123+ , itemThread
124+ , itemHost
125+ , itemProcess
126+ , itemPayload
127+ , itemMessage
128+ , itemTime
129+ , itemNamespace
130+ , itemLoc
131+ , logEnvHost
132+ , logEnvPid
133+ , logEnvApp
134+ , logEnvEnv
135+ , logEnvTimer
136+ , logEnvScribes
19137
20− -- * A Built-in Monad For Logging
138+ -- * A Built-in Monad For Simple Logging
21139 , KatipT (..)
22140 , runKatipT
23141
24142 -- * Initializing Loggers
25143 , initLogEnv
26144 , registerScribe
145+ -- * Dropping scribes temporarily
27146 , unregisterScribe
147+ , clearScribes
148+ -- * Finalizing scribes at shutdown
149+ , closeScribes
150+ , closeScribe
28151
29152 -- * Logging Functions
30153 , LogStr (..)
31154 , logStr, ls, showLS
32155
156+ -- ** 'Katip' Logging Functions
157+ -- $katiplogging
158+ , Katip (..)
33159 , logF
34160 , logMsg
35161 , logT
162+ , logLoc
36163 , logItem
164+ , logKatipItem
165+ , logException
166+ -- ** 'KatipContext': Logging With Context
167+ -- $katipcontextlogging
168+ , KatipContext (..)
37169 , logFM
38170 , logTM
171+ , logLocM
39172 , logItemM
40−
41− -- * Transformer for appending to a log context
42− , LogT
43− , runLogT
44− , BlankLogT
45− , runBlankLogT
173+ , logExceptionM
174+ , AnyLogContext
175+ , LogContexts, liftPayload
176+ -- *** Temporarily Changing Logging Behavior
177+ , katipAddNamespace
178+ , katipAddContext
179+ , katipNoLogging
46180
47181 -- * Included Scribes
48182 , mkHandleScribe
183+ , mkHandleScribeWithFormatter
184+ , mkFileScribe
49185 , ColorStrategy (..)
186+ , ItemFormatter
187+ , bracketFormat
188+ , jsonFormat
189+
190+ -- * Tools for implementing Scribes
191+ , PermitFunc
192+ , permitAND
193+ , permitOR
194+ , permitItem
195+ , payloadObject
196+ , itemJson
197+
198+ -- * KatipContextT - Utility transformer that provides Katip and KatipContext instances
199+ , KatipContextT
200+ , runKatipContextT
50201 ) where
51202
52203-------------------------------------------------------------------------------
@@ -54,3 +205,91 @@ import Katip.Core
54205import Katip.Monadic
55206import Katip.Scribes.Handle
56207-------------------------------------------------------------------------------
208+
209+
210+{- $katiplogging
211+
212+ These logging functions use the basic 'Katip' constraint and thus
213+will require varying degrees of explicit detail such as 'Namespace'
214+and individual log items to be passed in. These can be described as
215+the primitives of Katip logging. If you find yourself making multiple
216+log statements within a logical logging context for your app, you may
217+want to look into the 'KatipContext' family of logging functions like
218+'logFM' and 'logTM'. 'KatipContext' in most applications should be
219+considered the default. Here's an example of the pain point:
220+
221+@
222+doDatabaseThings = do
223+ connId <- getConnectionId
224+ logF (ConnectionIDContext connId) "database" InfoS "Doing database stuff"
225+ \-\- ...
226+ logF (ConnectionIDContext connId) "database" InfoS "Wow, passing in the same context is getting tedious"
227+@
228+
229+Another pain point to look out for is nesting actions that log in
230+each other. Let's say you were writing a web app. You want to capture
231+some detail such as the user's ID in the logs, but you also want that
232+info to show up in doDatabaseThings' logs so you can associate those
233+two pieces of information:
234+
235+
236+@
237+webRequestHandler = do
238+ uid <- getUserId
239+ logF (UserIDContext uid) "web" InfoS "Starting web request"
240+ doDatabaseThings
241+@
242+
243+In the above example, doDatabaseThings would overwrite that
244+UserIDContext with its own context and namespace. Sometimes this is
245+what you want and that's why 'logF' and other functions which only
246+require 'Katip' exist. If you are interested in combining log
247+contexts and namespaces, see 'KatipContext'.
248+-}
249+
250+
251+{- $katipcontextlogging
252+
253+ These logging functions use the 'KatipContext' constraint which is a
254+superclass of 'Katip' that also has a mechanism for keeping track of
255+the current context and namespace. This means a few things:
256+
257+1. Functions that use 'KatipContext' like 'logFM' and 'logTM' do not
258+require you to pass in 'LogItem's or 'Namespaces', they pull them from
259+the monadic environment.
260+
261+2. It becomes easy to add functions which add namespaces and/or
262+contexts to the current stack of them. You can (and should) make that
263+action scoped to a monadic action so that when it finishes, the
264+previous context and namespace will be automatically restored.
265+
266+
267+'KatipContextT' provides a simple, 'ReaderT'-based implementation of
268+the 'KatipContext' typeclass, and provides 'katipAddContext' and
269+'katipAddNamespace' functions to append to the context for the
270+duration of a block:
271+
272+
273+@
274+main = do
275+ le <- initLogEnv "MyApp" "production"
276+ \-\- set up scribes here
277+ runKatipContext le () "main" $ do
278+ katipAddNamespace "nextlevel" $ do
279+ $(logTM) InfoS "Logs here will have namespace MyApp.main.nextlevel"
280+
281+ katipAddContext TrivialContext $ do
282+ $(logTM) InfoS "Logs here will have context from TrivialContext"
283+
284+ katipAddContext AnotherContext $ do
285+ $(logTM) InfoS "Logs here will have context from TrivialContext *merged with* context from AnotherContext!"
286+
287+ $(logTM) InfoS "Log context restored to () and namespace to MyApp.main"
288+@
289+
290+'katipAddNamespace' and 'katipAddContext' are one-liners, implemented
291+in terms of 'local' from 'MonadReader'. If you have a custom monad
292+transformer stack and want to add your own version of these, check out
293+<https://github.com/Soostone/katip/tree/master/katip/examples these
294+examples>.
295+-}
katip/src/Katip/Compat.hsnew file+30
@@ -0,0 +1,30 @@
1+{-# LANGUAGE ForeignFunctionInterface #-}
2+{-# LANGUAGE CPP #-}
3+module Katip.Compat
4+ ( ProcessID
5+ , getProcessID
6+ ) where
7+
8+import System.Win32.Process
9+
10+type ProcessID = ProcessId
11+
12+#if !MIN_VERSION_Win32(2,4,0)
13+
14+#if defined(i386_HOST_ARCH)
15+# define WINDOWS_CCONV stdcall
16+#elif defined(x86_64_HOST_ARCH)
17+# define WINDOWS_CCONV ccall
18+#else
19+# error Unknown mingw32 arch
20+#endif
21+
22+foreign import WINDOWS_CCONV unsafe "windows.h GetCurrentProcessId"
23+ c_GetCurrentProcessId :: IO ProcessId
24+
25+getProcessID :: IO ProcessID
26+getProcessID = c_GetCurrentProcessId
27+#else
28+getProcessID :: IO ProcessID
29+getProcessID = getCurrentProcessId
30+#endif
katip/src/Katip/Core.hs+761−90
@@ -1,54 +1,109 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DefaultSignatures #-}
3+{-# LANGUAGE DeriveFunctor #-}
14{-# LANGUAGE DeriveGeneric #-}
5+{-# LANGUAGE ExistentialQuantification #-}
26{-# LANGUAGE FlexibleContexts #-}
37{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE GADTs #-}
49{-# LANGUAGE GeneralizedNewtypeDeriving #-}
510{-# LANGUAGE MultiParamTypeClasses #-}
611{-# LANGUAGE OverloadedStrings #-}
712{-# LANGUAGE RankNTypes #-}
813{-# LANGUAGE RecordWildCards #-}
914{-# LANGUAGE TemplateHaskell #-}
10−
15+{-# LANGUAGE TypeFamilies #-}
16+{-# LANGUAGE UndecidableInstances #-}
17+#if MIN_VERSION_base(4, 9, 0)
18+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
19+#endif
20+-- | This module is not meant to be imported directly and may contain
21+-- internal mechanisms that will change without notice.
1122module Katip.Core where
1223
1324-------------------------------------------------------------------------------
14−import Control.Applicative
25+import Control.Applicative as A
1526import Control.AutoUpdate
1627import Control.Concurrent
17−import Control.Lens
18−import Control.Monad
19−import Control.Monad.Catch
28+import qualified Control.Concurrent.Async as Async
29+import Control.Concurrent.STM
30+import qualified Control.Concurrent.STM.TBQueue as BQ
31+import Control.Exception.Safe
32+import Control.Monad (unless, void, when)
33+import Control.Monad.Base
34+#if MIN_VERSION_base(4, 9, 0)
35+import qualified Control.Monad.Fail as MF
36+#endif
2037import Control.Monad.IO.Class
38+import Control.Monad.IO.Unlift
2139import Control.Monad.Trans.Class
40+import Control.Monad.Trans.Control
41+#if !MIN_VERSION_either(4, 5, 0)
2242import Control.Monad.Trans.Either
43+#endif
44+import Control.Monad.Trans.Except
2345import Control.Monad.Trans.Maybe
2446import Control.Monad.Trans.Reader
25−import Control.Monad.Trans.Resource
26−import Control.Monad.Trans.State
27−import Control.Monad.Trans.Writer
28−import Data.Aeson (ToJSON (..))
29−import qualified Data.Aeson as A
30−import Data.Foldable (foldMap)
31−import qualified Data.HashMap.Strict as HM
47+import Control.Monad.Trans.Resource (MonadResource, ResourceT,
48+ transResourceT)
49+import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST)
50+import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST, mapRWST)
51+import Control.Monad.Trans.State.Lazy (StateT, mapStateT)
52+import qualified Control.Monad.Trans.State.Strict as Strict (StateT, mapStateT)
53+import Control.Monad.Trans.Writer.Lazy (WriterT, mapWriterT)
54+import qualified Control.Monad.Trans.Writer.Strict as Strict (WriterT,
55+ mapWriterT)
56+import Data.Aeson (FromJSON (..), ToJSON (..),
57+ object)
58+import qualified Data.Aeson as A
59+import Data.Foldable as FT
60+import qualified Data.HashMap.Strict as HM
3261import Data.List
33−import qualified Data.Map.Strict as M
34−import Data.Monoid
62+import qualified Data.Map.Strict as M
63+import Data.Maybe (fromMaybe)
64+import Data.Semigroup as SG
3565import Data.String
3666import Data.String.Conv
37−import Data.Text (Text)
38−import qualified Data.Text as T
39−import qualified Data.Text.Lazy.Builder as B
67+import Data.Text (Text)
68+import qualified Data.Text as T
69+import qualified Data.Text.Lazy.Builder as B
4070import Data.Time
41−import GHC.Generics hiding (to)
71+import GHC.Generics hiding (to)
72+#if MIN_VERSION_base(4, 8, 0)
73+#if !MIN_VERSION_base(4, 9, 0)
74+import GHC.SrcLoc
75+#endif
76+import GHC.Stack
77+#endif
4278import Language.Haskell.TH
43−import qualified Language.Haskell.TH.Syntax as TH
79+import qualified Language.Haskell.TH.Syntax as TH
80+import Lens.Micro
81+import Lens.Micro.TH
4482import Network.HostName
83+#if mingw32_HOST_OS
84+import Katip.Compat
85+#else
4586import System.Posix
87+#endif
88+
4689-------------------------------------------------------------------------------
4790
4891
92+readMay :: Read a => String -> Maybe a
93+readMay s = case [x | (x,t) <- reads s, ("","") <- lex t] of
94+ [x] -> Just x
95+ [] -> Nothing -- no parse
96+ _ -> Nothing -- Ambiguous parse
97+
98+
4999-------------------------------------------------------------------------------
100+-- | Represents a heirarchy of namespaces going from general to
101+-- specific. For instance: ["processname", "subsystem"]. Note that
102+-- single-segment namespaces can be created using
103+-- IsString/OverloadedStrings, so "foo" will result in Namespace
104+-- ["foo"].
50105newtype Namespace = Namespace { unNamespace :: [Text] }
51− deriving (Eq,Show,Read,Ord,Generic,ToJSON,Monoid)
106+ deriving (Eq,Show,Read,Ord,Generic,ToJSON,FromJSON,SG.Semigroup,Monoid)
52107
53108instance IsString Namespace where
54109 fromString s = Namespace [fromString s]
@@ -63,7 +118,7 @@ intercalateNs (Namespace xs) = intersperse "." xs
63118-------------------------------------------------------------------------------
64119-- | Application environment, like @prod@, @devel@, @testing@.
65120newtype Environment = Environment { getEnvironment :: Text }
66− deriving (Eq,Show,Read,Ord,Generic,ToJSON,IsString)
121+ deriving (Eq,Show,Read,Ord,Generic,ToJSON,FromJSON,IsString)
67122
68123
69124-------------------------------------------------------------------------------
@@ -88,65 +143,125 @@ data Severity
88143-- - 'V3' implies the maximum amount of payload information.
89144-- - Anything in between is left to the discretion of the developer.
90145data Verbosity = V0 | V1 | V2 | V3
91− deriving (Eq, Ord, Show, Read, Generic, Enum)
146+ deriving (Eq, Ord, Show, Read, Generic, Enum, Bounded)
92147
93148
94149-------------------------------------------------------------------------------
95150renderSeverity :: Severity -> Text
96151renderSeverity s = case s of
97− DebugS -> "Debug"
98− InfoS -> "Info"
99− NoticeS -> "Notice"
100− WarningS -> "Warning"
101− ErrorS -> "Error"
102− CriticalS -> "Critical"
103− AlertS -> "Alert"
152+ DebugS -> "Debug"
153+ InfoS -> "Info"
154+ NoticeS -> "Notice"
155+ WarningS -> "Warning"
156+ ErrorS -> "Error"
157+ CriticalS -> "Critical"
158+ AlertS -> "Alert"
104159 EmergencyS -> "Emergency"
105160
106161
107162-------------------------------------------------------------------------------
163+textToSeverity :: Text -> Maybe Severity
164+textToSeverity = go . T.toLower
165+ where
166+ go "debug" = Just DebugS
167+ go "info" = Just InfoS
168+ go "notice" = Just NoticeS
169+ go "warning" = Just WarningS
170+ go "error" = Just ErrorS
171+ go "critical" = Just CriticalS
172+ go "alert" = Just AlertS
173+ go "emergency" = Just EmergencyS
174+ go _ = Nothing
175+
176+
108177instance ToJSON Severity where
109178 toJSON s = A.String (renderSeverity s)
110179
180+instance FromJSON Severity where
181+ parseJSON = A.withText "Severity" parseSeverity
182+ where
183+ parseSeverity t = case textToSeverity t of
184+ Just x -> return x
185+ Nothing -> fail $ "Invalid Severity " ++ toS t
186+
187+instance ToJSON Verbosity where
188+ toJSON s = A.String $ case s of
189+ V0 -> "V0"
190+ V1 -> "V1"
191+ V2 -> "V2"
192+ V3 -> "V3"
193+
194+instance FromJSON Verbosity where
195+ parseJSON = A.withText "Verbosity" $ \s -> case s of
196+ "V0" -> return V0
197+ "V1" -> return V1
198+ "V2" -> return V2
199+ "V3" -> return V3
200+ _ -> fail $ "Invalid Verbosity " ++ toS s
201+
111202
112203-------------------------------------------------------------------------------
113−-- | Log message with Builder unerneath; use '<>' to concat in O(1).
204+-- | Log message with Builder underneath; use '<>' to concat in O(1).
114205newtype LogStr = LogStr { unLogStr :: B.Builder }
115− deriving (Generic)
206+ deriving (Generic, Show, Eq)
116207
117208instance IsString LogStr where
118209 fromString = LogStr . B.fromString
119210
211+
212+instance Semigroup LogStr where
213+ (LogStr a) <> (LogStr b) = LogStr (a <> b)
214+
215+
120216instance Monoid LogStr where
121− mappend (LogStr a) (LogStr b) = LogStr (a `mappend` b)
217+ mappend = (<>)
122218 mempty = LogStr mempty
123219
220+
221+instance FromJSON LogStr where
222+ parseJSON = A.withText "LogStr" parseLogStr
223+ where
224+ parseLogStr = return . LogStr . B.fromText
225+
124226-------------------------------------------------------------------------------
125227-- | Pack any string-like thing into a 'LogStr'. This will
126−-- automatically work on 'String', 'ByteString, 'Text' and any of the
228+-- automatically work on 'String', 'ByteString', 'Text' and any of the
127229-- lazy variants.
128230logStr :: StringConv a Text => a -> LogStr
129231logStr t = LogStr (B.fromText $ toS t)
130232
131233
132234-------------------------------------------------------------------------------
133−-- | Shorthand for 'logMsg'
235+-- | Shorthand for 'logStr'
134236ls :: StringConv a Text => a -> LogStr
135237ls = logStr
136238
137239
138240-------------------------------------------------------------------------------
241+-- | Convert any showable type into a 'LogStr'.
139242showLS :: Show a => a -> LogStr
140243showLS = ls . show
141244
142245
246+-------------------------------------------------------------------------------
247+newtype ThreadIdText = ThreadIdText {
248+ getThreadIdText :: Text
249+ } deriving (ToJSON, FromJSON, Show, Eq, Ord)
250+
251+
252+mkThreadIdText :: ThreadId -> ThreadIdText
253+mkThreadIdText = ThreadIdText . stripPrefix' "ThreadId " . T.pack . show
254+ where
255+ stripPrefix' pfx t = fromMaybe t (T.stripPrefix pfx t)
256+
257+
143258-------------------------------------------------------------------------------
144259-- | This has everything each log message will contain.
145260data Item a = Item {
146261 _itemApp :: Namespace
147262 , _itemEnv :: Environment
148263 , _itemSeverity :: Severity
149− , _itemThread :: ThreadId
264+ , _itemThread :: ThreadIdText
150265 , _itemHost :: HostName
151266 , _itemProcess :: ProcessID
152267 , _itemPayload :: a
@@ -154,18 +269,75 @@ data Item a = Item {
154269 , _itemTime :: UTCTime
155270 , _itemNamespace :: Namespace
156271 , _itemLoc :: Maybe Loc
157− } deriving (Generic)
272+ } deriving (Generic, Functor)
158273makeLenses ''Item
159274
275+-- Manual instance because 'Loc' has no 'Eq' and 'Show' instances in old
276+-- versions of template-haskell (< 2.10)
277+instance Eq a => Eq (Item a) where
278+ a == b = FT.and [ _itemApp a == _itemApp b
279+ , _itemEnv a == _itemEnv b
280+ , _itemSeverity a == _itemSeverity b
281+ , _itemThread a == _itemThread b
282+ , _itemHost a == _itemHost b
283+ , _itemProcess a == _itemProcess b
284+ , _itemPayload a == _itemPayload b
285+ , _itemMessage a == _itemMessage b
286+ , _itemTime a == _itemTime b
287+ , _itemNamespace a == _itemNamespace b
288+ , case (_itemLoc a, _itemLoc b) of
289+ (Nothing, Nothing) -> True
290+ (Just l1, Just l2) -> FT.and [ loc_filename l1 == loc_filename l2
291+ , loc_package l1 == loc_package l2
292+ , loc_module l1 == loc_module l2
293+ , loc_start l1 == loc_start l2
294+ , loc_end l1 == loc_end l2
295+ ]
296+ _ -> False
297+ ]
298+
299+instance Show a => Show (Item a) where
300+ showsPrec d Item{..} = showParen (d >= 11) ( showString "Item {"
301+ . field "_itemApp" _itemApp
302+ . field "_itemEnv" _itemEnv
303+ . field "_itemSeverity" _itemSeverity
304+ . field "_itemThread" _itemThread
305+ . field "_itemHost" _itemHost
306+ . field "_itemProcess" _itemProcess
307+ . field "_itemPayload" _itemPayload
308+ . field "_itemMessage" _itemMessage
309+ . field "_itemTime" _itemTime
310+ . field "_itemNamespace" _itemNamespace
311+ . showString "_itemLoc = " . shows (LocShow <$> _itemLoc)
312+ . showChar '}'
313+ )
314+ where
315+ field n v = showString n . showString " = " . shows v . showString ", "
316+
317+newtype LocShow = LocShow Loc
318+
319+
320+instance Show LocShow where
321+ showsPrec d (LocShow Loc{..}) = showParen (d >= 11) ( showString "Loc {"
322+ . field "loc_filename" loc_filename
323+ . field "loc_package" loc_package
324+ . field "loc_module" loc_module
325+ . field "loc_start" loc_start
326+ . showString "loc_end = " . shows loc_end
327+ . showChar '}'
328+ )
329+ where
330+ field n v = showString n . showString " = " . shows v . showString ", "
331+
160332
161333instance ToJSON a => ToJSON (Item a) where
162334 toJSON Item{..} = A.object
163335 [ "app" A..= _itemApp
164336 , "env" A..= _itemEnv
165337 , "sev" A..= _itemSeverity
166− , "thread" A..= show _itemThread
338+ , "thread" A..= getThreadIdText _itemThread
167339 , "host" A..= _itemHost
168− , "pid" A..= A.String (toS (show _itemProcess))
340+ , "pid" A..= ProcessIDJs _itemProcess
169341 , "data" A..= _itemPayload
170342 , "msg" A..= (B.toLazyText $ unLogStr _itemMessage)
171343 , "at" A..= _itemTime
@@ -186,17 +358,76 @@ instance ToJSON LocJs where
186358 ]
187359
188360
361+instance FromJSON LocJs where
362+ parseJSON = A.withObject "LocJs" parseLocJs
363+ where
364+ parseLocJs o = do
365+ fn <- o A..: "loc_fn"
366+ p <- o A..: "loc_pkg"
367+ m <- o A..: "loc_mod"
368+ l <- o A..: "loc_ln"
369+ c <- o A..: "loc_col"
370+ return $ LocJs $ Loc fn p m (l, c) (l, c)
371+
372+
373+instance FromJSON a => FromJSON (Item a) where
374+ parseJSON = A.withObject "Item" parseItem
375+ where
376+ parseItem o = Item
377+ <$> o A..: "app"
378+ <*> o A..: "env"
379+ <*> o A..: "sev"
380+ <*> o A..: "thread"
381+ <*> o A..: "host"
382+ <*> (getProcessIDJs <$> o A..: "pid")
383+ <*> o A..: "data"
384+ <*> o A..: "msg"
385+ <*> o A..: "at"
386+ <*> o A..: "ns"
387+ <*> (fmap getLocJs <$> o A..: "loc")
388+
389+
390+processIDToText :: ProcessID -> Text
391+processIDToText = toS . show
392+
393+
394+textToProcessID :: Text -> Maybe ProcessID
395+textToProcessID = readMay . toS
396+
397+
398+newtype ProcessIDJs = ProcessIDJs {
399+ getProcessIDJs :: ProcessID
400+ }
401+
402+
403+instance ToJSON ProcessIDJs where
404+ toJSON (ProcessIDJs p) = A.String (processIDToText p)
405+
406+
407+instance FromJSON ProcessIDJs where
408+ parseJSON = A.withText "ProcessID" parseProcessID
409+ where
410+ parseProcessID t = case textToProcessID t of
411+ Just p -> return $ ProcessIDJs p
412+ Nothing -> fail $ "Invalid ProcessIDJs " ++ toS t
413+
414+
189415-------------------------------------------------------------------------------
190416-- | Field selector by verbosity within JSON payload.
191417data PayloadSelection
192418 = AllKeys
193419 | SomeKeys [Text]
420+ deriving (Show, Eq)
421+
422+instance Semigroup PayloadSelection where
423+ AllKeys <> _ = AllKeys
424+ _ <> AllKeys = AllKeys
425+ SomeKeys as <> SomeKeys bs = SomeKeys (as <> bs)
426+
194427
195428instance Monoid PayloadSelection where
196429 mempty = SomeKeys []
197− mappend AllKeys _ = AllKeys
198− mappend _ AllKeys = AllKeys
199− mappend (SomeKeys as) (SomeKeys bs) = SomeKeys (as++bs)
430+ mappend = (<>)
200431
201432
202433-------------------------------------------------------------------------------
@@ -209,17 +440,26 @@ instance Monoid PayloadSelection where
209440-- a ToJSON instance like:
210441--
211442-- > instance ToObject Foo
212−class ToJSON a => ToObject a where
443+class ToObject a where
213444 toObject :: a -> A.Object
445+ default toObject :: ToJSON a => a -> A.Object
214446 toObject v = case toJSON v of
215447 A.Object o -> o
216− _ -> mempty
448+ _ -> mempty
217449
218450instance ToObject ()
219451instance ToObject A.Object
220452
221453-------------------------------------------------------------------------------
222−-- | Payload objects need instances of this class.
454+-- | Payload objects need instances of this class. LogItem makes it so
455+-- that you can have very verbose items getting logged with lots of
456+-- extra fields but under normal circumstances, if your scribe is
457+-- configured for a lower verbosity level, it will only log a
458+-- selection of those keys. Furthermore, each 'Scribe' can be
459+-- configured with a different 'Verbosity' level. You could even use
460+-- 'registerScribe', 'unregisterScribe', and 'clearScribes' to at
461+-- runtime swap out your existing scribes for more verbose debugging
462+-- scribes if you wanted to.
223463--
224464-- When defining 'payloadKeys', don't redundantly declare the same
225465-- keys for higher levels of verbosity. Each level of verbosity
@@ -234,13 +474,54 @@ class ToObject a => LogItem a where
234474instance LogItem () where payloadKeys _ _ = SomeKeys []
235475
236476
477+data AnyLogPayload = forall a. ToJSON a => AnyLogPayload a
478+
479+newtype SimpleLogPayload = SimpleLogPayload {
480+ unSimpleLogPayload :: [(Text, AnyLogPayload)]
481+ }
482+
483+-------------------------------------------------------------------------------
484+-- | A built-in convenience log payload that won't log anything on 'V0',
485+-- but will log everything in any other level of verbosity. Intended
486+-- for easy in-line usage without having to define new log types.
487+--
488+-- Construct using 'sl' and combine multiple tuples using '<>' from
489+-- 'Monoid'.
490+instance ToJSON SimpleLogPayload where
491+ toJSON (SimpleLogPayload as) = object $ map go as
492+ where go (k, AnyLogPayload v) = k A..= v
493+
494+
495+instance ToObject SimpleLogPayload
496+
497+
498+instance LogItem SimpleLogPayload where
499+ payloadKeys V0 _ = SomeKeys []
500+ payloadKeys _ _ = AllKeys
501+
502+
503+instance Semigroup SimpleLogPayload where
504+ SimpleLogPayload a <> SimpleLogPayload b = SimpleLogPayload (a <> b)
505+
506+
507+instance Monoid SimpleLogPayload where
508+ mempty = SimpleLogPayload []
509+ mappend = (<>)
510+
511+
512+-------------------------------------------------------------------------------
513+-- | Construct a simple log from any JSON item.
514+sl :: ToJSON a => Text -> a -> SimpleLogPayload
515+sl a b = SimpleLogPayload [(a, AnyLogPayload b)]
516+
517+
237518-------------------------------------------------------------------------------
238519-- | Constrain payload based on verbosity. Backends should use this to
239520-- automatically bubble higher verbosity levels to lower ones.
240521payloadObject :: LogItem a => Verbosity -> a -> A.Object
241−payloadObject verb a = case foldMap (flip payloadKeys a) [(V0)..verb] of
242− AllKeys -> toObject a
243− SomeKeys ks -> HM.filterWithKey (\ k _ -> k `elem` ks) $ toObject a
522+payloadObject verb a = case FT.foldMap (flip payloadKeys a) [(V0)..verb] of
523+ AllKeys -> toObject a
524+ SomeKeys ks -> HM.filterWithKey (\ k _ -> k `FT.elem` ks) $ toObject a
244525
245526
246527-------------------------------------------------------------------------------
@@ -254,30 +535,140 @@ itemJson verb a = toJSON $ a & itemPayload %~ payloadObject verb
254535-------------------------------------------------------------------------------
255536-- | Scribes are handlers of incoming items. Each registered scribe
256537-- knows how to push a log item somewhere.
538+--
539+-- = Guidelines for writing your own 'Scribe'
540+--
541+-- Scribes should always take a 'Severity' and 'Verbosity'.
542+--
543+-- Severity is used to __exclude log messages__ that are lower than
544+-- the provided Severity. For instance, if the user passes InfoS,
545+-- DebugS items should be ignored. Katip provides the 'permitItem'
546+-- utility for this. The user or the scribe may use 'permitAND' and
547+-- 'permitOR' to further customize this filtering, even dynamically if
548+-- they wish to.
549+--
550+-- Verbosity is used to select keys from the log item's payload. Each
551+-- 'LogItem' instance describes what keys should be retained for each
552+-- Verbosity level. Use the 'payloadObject' utility for extracting the keys
553+-- that should be written.
554+--
555+-- Scribes provide a finalizer IO action ('scribeFinalizer') that is
556+-- meant to synchronously flush any remaining writes and clean up any
557+-- resources acquired when the scribe was created. Internally, katip
558+-- keeps a buffer for each scribe's writes. When 'closeScribe' or
559+-- 'closeScribes' is called, that buffer stops accepting new log
560+-- messages and after the last item in its buffer is sent to 'liPush',
561+-- calls the finalizer. Thus, when the finalizer returns, katip can
562+-- assume that all resources are cleaned up and all log messages are
563+-- durably written.
564+--
565+-- While katip internally buffers messages per 'ScribeSettings', it
566+-- sends them one at a time to the scribe. Depending on the scribe
567+-- itself, it may make sense for that scribe to keep its own internal
568+-- buffer to batch-send logs if writing items one at a time is not
569+-- efficient. The scribe implementer must be sure that on
570+-- finalization, all writes are committed synchronously.
571+
572+-- | Signature of a function passed to `Scribe` constructor and
573+-- mkScribe* functions that decides which messages to be
574+-- logged. Typically filters based on 'Severity', but can be
575+-- combined with other, custom logic with 'permitAND' and 'permitOR'
576+type PermitFunc = forall a. Item a -> IO Bool
577+
578+
579+-- | AND together 2 permit functions
580+permitAND :: PermitFunc -> PermitFunc -> PermitFunc
581+permitAND f1 f2 = \a -> liftA2 (&&) (f1 a) (f2 a)
582+
583+-- | OR together 2 permit functions
584+permitOR :: PermitFunc -> PermitFunc -> PermitFunc
585+permitOR f1 f2 = \a -> liftA2 (||) (f1 a) (f2 a)
586+
587+
257588data Scribe = Scribe {
258− lhPush :: forall a. LogItem a => Item a -> IO ()
259− }
589+ liPush :: forall a. LogItem a => Item a -> IO ()
590+ -- ^ How do we write an item to the scribe's output?
591+ , scribeFinalizer :: IO ()
592+ -- ^ Provide a __blocking__ finalizer to call when your scribe is
593+ -- removed. All pending writes should be flushed synchronously. If
594+ -- this is not relevant to your scribe, return () is fine.
595+ , scribePermitItem :: PermitFunc
596+ -- ^ Provide a filtering function to allow the item to be logged,
597+ -- or not. It can check Severity or some string in item's
598+ -- body. The initial value of this is usually created from
599+ -- 'permitItem'. Scribes and users can customize this by ANDing
600+ -- or ORing onto the default with 'permitAND' or 'permitOR'
601+ }
602+
603+
604+whenM :: Monad m => m Bool -> m () -> m ()
605+whenM mbool = (>>=) mbool . flip when
606+
607+
608+-- | Combine two scribes. Publishes to the left scribe if the left
609+-- would permit the item and to the right scribe if the right would
610+-- permit the item. Finalizers are called in sequence from left to
611+-- right.
612+instance Semigroup Scribe where
613+ (Scribe pushA finA permitA) <> (Scribe pushB finB permitB) =
614+ Scribe (\item -> whenM (permitA item) (pushA item)
615+ >> whenM (permitB item) (pushB item)
616+ )
617+ (finA `finally` finB)
618+ (permitOR permitA permitB)
260619
261620
262621instance Monoid Scribe where
263− mempty = Scribe $ const $ return ()
264− mappend (Scribe a) (Scribe b) = Scribe $ \ item -> do
265− a item
266− b item
622+ mempty = Scribe (const (return ())) (return ()) (permitItem DebugS)
623+ mappend = (<>)
624+
625+
626+-------------------------------------------------------------------------------
627+data ScribeHandle = ScribeHandle {
628+ shScribe :: Scribe
629+ , shChan :: BQ.TBQueue WorkerMessage
630+ }
631+
632+
633+-------------------------------------------------------------------------------
634+data WorkerMessage where
635+ NewItem :: LogItem a => Item a -> WorkerMessage
636+ PoisonPill :: WorkerMessage
637+
638+
639+-------------------------------------------------------------------------------
640+-- | Should this item be logged given the user's maximum severity?
641+-- Most new scribes will use this as a base for their 'PermitFunc'
642+permitItem :: Monad m => Severity -> Item a -> m Bool
643+permitItem sev item = return (_itemSeverity item >= sev)
644+
267645
268646-------------------------------------------------------------------------------
269647data LogEnv = LogEnv {
270648 _logEnvHost :: HostName
271649 , _logEnvPid :: ProcessID
272− , _logEnvNs :: Namespace
650+ , _logEnvApp :: Namespace
651+ -- ^ Name of application. This will typically never change. This
652+ -- field gets prepended to the namespace of your individual log
653+ -- messages. For example, if your app is MyApp and you write a log
654+ -- using "logItem" and the namespace "WebServer", the final
655+ -- namespace will be "MyApp.WebServer"
273656 , _logEnvEnv :: Environment
274657 , _logEnvTimer :: IO UTCTime
275− , _logEnvScribes :: M.Map Text Scribe
658+ -- ^ Action to fetch the timestamp. You can use something like
659+ -- 'AutoUpdate' for high volume logs but note that this may cause
660+ -- some output forms to display logs out of order. Alternatively,
661+ -- you could just use 'getCurrentTime'.
662+ , _logEnvScribes :: M.Map Text ScribeHandle
276663 }
277664makeLenses ''LogEnv
278665
279666
280667-------------------------------------------------------------------------------
668+-- | Create a reasonable default InitLogEnv. Uses an 'AutoUpdate' which
669+-- updates the timer every 1ms. If you need even more timestamp
670+-- precision at the cost of performance, consider setting
671+-- '_logEnvTimer' with 'getCurrentTime'.
281672initLogEnv
282673 :: Namespace
283674 -- ^ A base namespace for this application
@@ -289,69 +680,243 @@ initLogEnv an env = LogEnv
289680 <*> getProcessID
290681 <*> pure an
291682 <*> pure env
292− <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
683+ <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime, updateFreq = 1000 }
293684 <*> pure mempty
294685
295686
296687-------------------------------------------------------------------------------
688+-- | Add a scribe to the list. All future log calls will go to this
689+-- scribe in addition to the others. Writes will be buffered per the
690+-- ScribeSettings to prevent slow scribes from slowing down
691+-- logging. Writes will be dropped if the buffer fills.
297692registerScribe
298693 :: Text
299694 -- ^ Name the scribe
300695 -> Scribe
696+ -> ScribeSettings
301697 -> LogEnv
302− -> LogEnv
303−registerScribe nm h = logEnvScribes . at nm .~ Just h
698+ -> IO LogEnv
699+registerScribe nm scribe ScribeSettings {..} le = do
700+ queue <- atomically (BQ.newTBQueue (fromIntegral _scribeBufferSize))
701+ worker <- spawnScribeWorker scribe queue
702+ let fin = do
703+ atomically (BQ.writeTBQueue queue PoisonPill)
704+ -- wait for our worker to finish final write
705+ void (Async.waitCatch worker)
706+ -- wait for scribe to finish final write
707+ void (scribeFinalizer scribe)
708+
709+ let sh = ScribeHandle (scribe { scribeFinalizer = fin }) queue
710+ return (le & logEnvScribes %~ M.insert nm sh)
711+
712+
713+-------------------------------------------------------------------------------
714+spawnScribeWorker :: Scribe -> BQ.TBQueue WorkerMessage -> IO (Async.Async ())
715+spawnScribeWorker (Scribe write _ _) queue = Async.async go
716+ where
717+ go = do
718+ newCmd <- atomically (BQ.readTBQueue queue)
719+ case newCmd of
720+ NewItem a -> do
721+ -- Swallow any direct exceptions from the
722+ -- scribe. safe-exceptions won't catch async exceptions.
723+ void (tryAny (write a))
724+ go
725+ PoisonPill -> return ()
726+
727+
728+-------------------------------------------------------------------------------
729+data ScribeSettings = ScribeSettings {
730+ _scribeBufferSize :: Int
731+ }
732+ deriving (Show, Eq)
733+
734+makeLenses ''ScribeSettings
735+
736+
737+-- | Reasonable defaults for a scribe. Buffer
738+-- size of 4096.
739+defaultScribeSettings :: ScribeSettings
740+defaultScribeSettings = ScribeSettings 4096
304741
305742
306743-------------------------------------------------------------------------------
744+-- | Remove a scribe from the environment. This does __not__ finalize
745+-- the scribe. This mainly only makes sense to use with something like
746+-- MonadReader's @local@ function to temporarily disavow a single
747+-- logger for a block of code.
307748unregisterScribe
308749 :: Text
309750 -- ^ Name of the scribe
310751 -> LogEnv
311752 -> LogEnv
312−unregisterScribe nm = logEnvScribes . at nm .~ Nothing
753+unregisterScribe nm = logEnvScribes %~ M.delete nm
313754
314755
756+-------------------------------------------------------------------------------
757+-- | Unregister __all__ scribes. Note that this is __not__ for closing or
758+-- finalizing scribes, use 'closeScribes' for that. This mainly only
759+-- makes sense to use with something like MonadReader's @local@
760+-- function to temporarily disavow any loggers for a block of code.
761+clearScribes
762+ :: LogEnv
763+ -> LogEnv
764+clearScribes = logEnvScribes .~ mempty
765+
766+
767+-------------------------------------------------------------------------------
768+-- | Finalize a scribe. The scribe is removed from the environment,
769+-- its finalizer is called so that it can never be written to again
770+-- and all pending writes are flushed. Note that this will throw any
771+-- exceptions yoru finalizer will throw, and that LogEnv is immutable,
772+-- so it will not be removed in that case.
773+closeScribe
774+ :: Text
775+ -- ^ Name of the scribe
776+ -> LogEnv
777+ -> IO LogEnv
778+closeScribe nm le = do
779+ maybe (return ()) (scribeFinalizer . shScribe) (M.lookup nm (_logEnvScribes le))
780+ return (le & logEnvScribes %~ M.delete nm)
781+
782+
783+-------------------------------------------------------------------------------
784+-- | Call this at the end of your program. This is a blocking call
785+-- that stop writing to a scribe's queue, waits for the queue to
786+-- empty, finalizes each scribe in the log environment and then
787+-- removes it. Finalizers are all run even if one of them throws, but
788+-- the exception will be re-thrown at the end.
789+closeScribes
790+ :: LogEnv
791+ -> IO LogEnv
792+closeScribes le = do
793+ -- We want to run every finalizer here so we'll not save
794+ -- intermediate logenvs and just clear scribes at the end.
795+ let actions = [void (closeScribe k le) | k <- M.keys (_logEnvScribes le)]
796+ FT.foldr finally (return ()) actions
797+ return (le & logEnvScribes .~ mempty)
798+
315799
316800-------------------------------------------------------------------------------
317−-- | Monads where katip logging actions can be performed
318−class MonadIO m => Katip m where
801+-- | Monads where katip logging actions can be performed. Katip is the
802+-- most basic logging monad. You will typically use this directly if
803+-- you either don't want to use namespaces/contexts heavily or if you
804+-- want to pass in specific contexts and/or namespaces at each log site.
805+--
806+-- For something more powerful, look at the docs for 'KatipContext',
807+-- which keeps a namespace and merged context. You can write simple
808+-- functions that add additional namespacing and merges additional
809+-- context on the fly.
810+--
811+-- 'localLogEnv' was added to allow for lexically-scoped modifications
812+-- of the log env that are reverted when the supplied monad
813+-- completes. 'katipNoLogging', for example, uses this to temporarily
814+-- pause log outputs.
815+class MonadIO m => Katip m where
319816 getLogEnv :: m LogEnv
817+ localLogEnv :: (LogEnv -> LogEnv) -> m a -> m a
320818
321819
322820instance Katip m => Katip (ReaderT s m) where
323821 getLogEnv = lift getLogEnv
822+ localLogEnv = mapReaderT . localLogEnv
324823
325824
825+#if !MIN_VERSION_either(4, 5, 0)
326826instance Katip m => Katip (EitherT s m) where
327827 getLogEnv = lift getLogEnv
828+ localLogEnv = mapEitherT . localLogEnv
829+#endif
830+
831+
832+instance Katip m => Katip (ExceptT s m) where
833+ getLogEnv = lift getLogEnv
834+ localLogEnv = mapExceptT . localLogEnv
328835
329836
330837instance Katip m => Katip (MaybeT m) where
331838 getLogEnv = lift getLogEnv
839+ localLogEnv = mapMaybeT . localLogEnv
332840
333841
334842instance Katip m => Katip (StateT s m) where
335843 getLogEnv = lift getLogEnv
844+ localLogEnv = mapStateT . localLogEnv
845+
846+
847+instance (Katip m, Monoid w) => Katip (RWST r w s m) where
848+ getLogEnv = lift getLogEnv
849+ localLogEnv = mapRWST . localLogEnv
850+
851+
852+instance (Katip m, Monoid w) => Katip (Strict.RWST r w s m) where
853+ getLogEnv = lift getLogEnv
854+ localLogEnv = Strict.mapRWST . localLogEnv
855+
856+
857+instance Katip m => Katip (Strict.StateT s m) where
858+ getLogEnv = lift getLogEnv
859+ localLogEnv = Strict.mapStateT . localLogEnv
336860
337861
338862instance (Katip m, Monoid s) => Katip (WriterT s m) where
339863 getLogEnv = lift getLogEnv
864+ localLogEnv = mapWriterT . localLogEnv
865+
866+
867+instance (Katip m, Monoid s) => Katip (Strict.WriterT s m) where
868+ getLogEnv = lift getLogEnv
869+ localLogEnv = Strict.mapWriterT . localLogEnv
870+
340871
341872instance (Katip m) => Katip (ResourceT m) where
342873 getLogEnv = lift getLogEnv
874+ localLogEnv = transResourceT . localLogEnv
343875
344876
345877-------------------------------------------------------------------------------
346−-- | A concrete monad you can use to run logging actions.
878+-- | A concrete monad you can use to run logging actions. Use this if
879+-- you prefer an explicit monad transformer stack and adding layers as
880+-- opposed to implementing 'Katip' for your monad.
347881newtype KatipT m a = KatipT { unKatipT :: ReaderT LogEnv m a }
348882 deriving ( Functor, Applicative, Monad, MonadIO
349− , MonadMask, MonadCatch, MonadThrow, MonadTrans )
883+ , MonadMask, MonadCatch, MonadThrow, MonadResource, MonadTrans
884+ , MonadBase b)
350885
351886
352887instance MonadIO m => Katip (KatipT m) where
353888 getLogEnv = KatipT ask
354−
889+ localLogEnv f (KatipT m) = KatipT $ local f m
890+
891+
892+instance MonadTransControl KatipT where
893+ type StT (KatipT) a = a
894+ liftWith f = KatipT $ ReaderT $ \le -> f $ \t -> runKatipT le t
895+ restoreT = KatipT . ReaderT . const
896+ {-# INLINABLE liftWith #-}
897+ {-# INLINABLE restoreT #-}
898+
899+
900+instance (MonadBaseControl b m) => MonadBaseControl b (KatipT m) where
901+ type StM ((KatipT) m) a = ComposeSt (KatipT) m a
902+ liftBaseWith = defaultLiftBaseWith
903+ restoreM = defaultRestoreM
904+
905+instance 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
910+ askUnliftIO = KatipT $
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
355920
356921-------------------------------------------------------------------------------
357922-- | Execute 'KatipT' on a log env.
@@ -359,11 +924,22 @@ runKatipT :: LogEnv -> KatipT m a -> m a
359924runKatipT le (KatipT f) = runReaderT f le
360925
361926
927+-------------------------------------------------------------------------------
928+-- | Disable all scribes for the given monadic action, then restore
929+-- them afterwards. Works in any Katip monad.
930+katipNoLogging
931+ :: ( Katip m
932+ )
933+ => m a
934+ -> m a
935+katipNoLogging = localLogEnv (\le -> set logEnvScribes mempty le)
936+
937+
362938-------------------------------------------------------------------------------
363939-- | Log with everything, including a source code location. This is
364940-- very low level and you typically can use 'logT' in its place.
365941logItem
366− :: (Applicative m, LogItem a, Katip m)
942+ :: (A.Applicative m, LogItem a, Katip m)
367943 => a
368944 -> Namespace
369945 -> Maybe Loc
@@ -372,19 +948,43 @@ logItem
372948 -> m ()
373949logItem a ns loc sev msg = do
374950 LogEnv{..} <- getLogEnv
375− item <- Item
376− <$> pure _logEnvNs
377− <*> pure _logEnvEnv
378− <*> pure sev
379− <*> liftIO myThreadId
380− <*> pure _logEnvHost
381− <*> pure _logEnvPid
382− <*> pure a
383− <*> pure msg
384− <*> liftIO _logEnvTimer
385− <*> pure (_logEnvNs <> ns)
386− <*> pure loc
387− liftIO $ forM_ (M.elems _logEnvScribes) $ \ (Scribe h) -> h item
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 $
974+ FT.forM_ (M.elems _logEnvScribes) $ \ ScribeHandle {..} -> do
975+ whenM (scribePermitItem shScribe item) $
976+ void $ atomically (tryWriteTBQueue shChan (NewItem item))
977+
978+-------------------------------------------------------------------------------
979+tryWriteTBQueue
980+ :: TBQueue a
981+ -> a
982+ -> STM Bool
983+ -- ^ Did we write?
984+tryWriteTBQueue q a = do
985+ full <- isFullTBQueue q
986+ unless full (writeTBQueue q a)
987+ return (not full)
388988
389989
390990-------------------------------------------------------------------------------
@@ -403,6 +1003,25 @@ logF
4031003logF a ns sev msg = logItem a ns Nothing sev msg
4041004
4051005
1006+
1007+-------------------------------------------------------------------------------
1008+-- | Perform an action while logging any exceptions that may occur.
1009+-- Inspired by 'onException`.
1010+--
1011+-- >>>> logException () mempty ErrorS (error "foo")
1012+logException
1013+ :: (Katip m, LogItem a, MonadCatch m, Applicative m)
1014+ => a -- ^ Log context
1015+ -> Namespace -- ^ Namespace
1016+ -> Severity -- ^ Severity
1017+ -> m b -- ^ Main action being run
1018+ -> m b
1019+logException a ns sev action = action `catchAny` \e -> f e >> throwM e
1020+ where
1021+ f e = logF a ns sev (msg e)
1022+ msg e = ls (T.pack "An exception has occurred: ") <> showLS e
1023+
1024+
4061025-------------------------------------------------------------------------------
4071026-- | Log a message without any payload/context or code location.
4081027logMsg
@@ -428,14 +1047,14 @@ instance TH.Lift Verbosity where
4281047
4291048
4301049instance TH.Lift Severity where
431− lift DebugS = [| DebugS |]
432− lift InfoS = [| InfoS |]
433− lift NoticeS = [| NoticeS |]
434− lift WarningS = [| WarningS |]
435− lift ErrorS = [| ErrorS |]
1050+ lift DebugS = [| DebugS |]
1051+ lift InfoS = [| InfoS |]
1052+ lift NoticeS = [| NoticeS |]
1053+ lift WarningS = [| WarningS |]
1054+ lift ErrorS = [| ErrorS |]
4361055 lift CriticalS = [| CriticalS |]
437− lift AlertS = [| AlertS |]
438− lift EmergencyS = [| EmergencyS |]
1056+ lift AlertS = [| AlertS |]
1057+ lift EmergencyS = [| EmergencyS |]
4391058
4401059
4411060-- | Lift a location into an Exp.
@@ -451,17 +1070,69 @@ liftLoc (Loc a b c (d1, d2) (e1, e2)) = [|Loc
4511070
4521071-------------------------------------------------------------------------------
4531072-- | For use when you want to include location in your logs. This will
454−-- fill the 'Maybe Loc' gap in 'logF' of this module.
455−getLoc :: Q Exp
456−getLoc = [| $(location >>= liftLoc) |]
1073+-- fill the 'Maybe Loc' gap in 'logF' of this module, and relies on implicit
1074+-- callstacks when available (GHC > 7.8).
1075+#if MIN_VERSION_base(4, 8, 0)
1076+getLoc :: HasCallStack => Maybe Loc
1077+getLoc = case getCallStack callStack of
1078+ [] -> Nothing
1079+ xs -> Just . toLoc . head $ filter filterKatip xs
1080+ where
1081+ filterKatip :: (String, SrcLoc) -> Bool
1082+ filterKatip (_, srcloc) = not $
1083+ "katip-" `isPrefixOf` srcLocPackage srcloc
1084+
1085+ toLoc :: (String, SrcLoc) -> Loc
1086+ toLoc (_, l) = Loc {
1087+ loc_filename = srcLocFile l
1088+ , loc_package = srcLocPackage l
1089+ , loc_module = srcLocModule l
1090+ , loc_start = (srcLocStartLine l, srcLocStartCol l)
1091+ , loc_end = (srcLocEndLine l, srcLocEndCol l)
1092+ }
1093+#else
1094+getLoc :: Maybe Loc
1095+getLoc = Nothing
1096+#endif
4571097
4581098
4591099-------------------------------------------------------------------------------
460−-- | 'Loc'-tagged logging when using template-haskell is OK.
1100+-- Like `getLoc`, but uses template-haskell and works with older versions of
1101+-- the compiler (GHC 7.8 or older).
1102+getLocTH :: ExpQ
1103+getLocTH = [| $(location >>= liftLoc) |]
1104+
1105+
1106+-------------------------------------------------------------------------------
1107+-- | 'Loc'-tagged logging when using template-haskell.
4611108--
4621109-- @$(logT) obj mempty InfoS "Hello world"@
4631110logT :: ExpQ
464−logT = [| \ a ns sev msg -> logItem a ns (Just $(getLoc)) sev msg |]
1111+logT = [| \ a ns sev msg -> logItem a ns (Just $(getLocTH)) sev msg |]
1112+
1113+
1114+-------------------------------------------------------------------------------
1115+-- | 'Loc'-tagged logging using 'GHC.Stack' when available.
1116+--
1117+-- This function does not require template-haskell as it
1118+-- automatically uses <https://hackage.haskell.org/package/base-4.8.2.0/docs/GHC-Stack.html#v:getCallStack implicit-callstacks>
1119+-- when the code is compiled using GHC > 7.8. Using an older version of the
1120+-- compiler will result in the emission of a log line without any location information,
1121+-- so be aware of it. Users using GHC <= 7.8 may want to use the template-haskell function
1122+-- `logT` for maximum compatibility.
1123+--
1124+-- @logLoc obj mempty InfoS "Hello world"@
1125+#if MIN_VERSION_base(4, 8, 0)
1126+logLoc :: (Applicative m, LogItem a, Katip m, HasCallStack)
1127+#else
1128+logLoc :: (Applicative m, LogItem a, Katip m)
1129+#endif
1130+ => a
1131+ -> Namespace
1132+ -> Severity
1133+ -> LogStr
1134+ -> m ()
1135+logLoc a ns = logItem a ns getLoc
4651136
4661137
4671138-- taken from the file-location package
katip/src/Katip/Format/Time.hsnew file+213
@@ -0,0 +1,213 @@
1+-- | Time and memory efficient time encoding helper functions.
2+--
3+module Katip.Format.Time
4+ ( formatAsLogTime
5+ , formatAsIso8601
6+ ) where
7+
8+import Control.Monad.ST (ST)
9+
10+import Data.Int (Int64)
11+import qualified Data.Text.Array as TA
12+import Data.Text (Text)
13+import Data.Text.Internal (Text(..))
14+import Data.Time (UTCTime(..), toGregorian, Day, DiffTime)
15+import Data.Word (Word16)
16+import Unsafe.Coerce (unsafeCoerce)
17+
18+-- Note: All functions here are optimized to never allocate anything
19+-- on heap. At least on ghc 8.0.1 no extra strictness annotations are
20+-- seem to be needed.
21+--
22+-- Exported functions are INLINEABLE
23+
24+
25+-- | Format 'UTCTime' into a short human readable format.
26+--
27+-- >>> formatAsLogTime $ UTCTime (fromGregorian 2016 1 23) 5025.123456789012
28+-- "2016-01-23 01:23:45"
29+--
30+formatAsLogTime :: UTCTime -> Text
31+formatAsLogTime (UTCTime day time) = toText $ TA.run2 $ do
32+ buf <- TA.new 19 -- length "2016-10-20 12:34:56"
33+ _ <- writeDay buf 0 day
34+ TA.unsafeWrite buf 10 0x20 -- space
35+ _ <- writeTimeOfDay False buf 11 (diffTimeOfDay64 time)
36+ return (buf, 19)
37+ where
38+ toText (arr, len) = Text arr 0 len
39+
40+{-# INLINEABLE formatAsLogTime #-}
41+
42+-- | Format 'UTCTime' into a Iso8601 format.
43+--
44+-- Note that this function may overcommit up to 12*2 bytes, depending
45+-- on sub-second precision. If this is an issue, make a copy with a
46+-- 'Data.Text.copy'.
47+--
48+-- >>> formatAsIso8601 $ UTCTime (fromGregorian 2016 1 23) 5025.123456789012
49+-- "2016-11-23T01:23:45.123456789012Z"
50+-- >>> formatAsIso8601 $ UTCTime (fromGregorian 2016 1 23) 5025.123
51+-- "2016-01-23T01:23:45.123Z"
52+-- >>> formatAsIso8601 $ UTCTime (fromGregorian 2016 1 23) 5025
53+-- "2016-01-23T01:23:45Z"
54+
55+--
56+formatAsIso8601 :: UTCTime -> Text
57+formatAsIso8601 (UTCTime day time) = toText $ TA.run2 $ do
58+ buf <- TA.new 33 -- length "2016-10-20 12:34:56.123456789012Z"
59+ _ <- writeDay buf 0 day
60+ TA.unsafeWrite buf 10 0x54 -- T
61+ next <- writeTimeOfDay True buf 11 (diffTimeOfDay64 time)
62+ TA.unsafeWrite buf next 0x5A -- Z
63+ return (buf, next+1)
64+ where
65+ toText (arr, len) = Text arr 0 len
66+
67+{-# INLINEABLE formatAsIso8601 #-}
68+
69+-- | Writes the @YYYY-MM-DD@ part of timestamp
70+writeDay :: TA.MArray s -> Int -> Day -> ST s Int
71+writeDay buf off day =
72+ do
73+ TA.unsafeWrite buf (off + 0) (digit y1)
74+ TA.unsafeWrite buf (off + 1) (digit y2)
75+ TA.unsafeWrite buf (off + 2) (digit y3)
76+ TA.unsafeWrite buf (off + 3) (digit y4)
77+ TA.unsafeWrite buf (off + 4) 0x2d -- dash
78+ TA.unsafeWrite buf (off + 5) m1
79+ TA.unsafeWrite buf (off + 6) m2
80+ TA.unsafeWrite buf (off + 7) 0x2d -- dash
81+ TA.unsafeWrite buf (off + 8) d1
82+ TA.unsafeWrite buf (off + 9) d2
83+ return (off + 10)
84+ where
85+ (yr,m,d) = toGregorian day
86+ (y1, ya) = fromIntegral (abs yr) `quotRem` 1000
87+ (y2, yb) = ya `quotRem` 100
88+ (y3, y4) = yb `quotRem` 10
89+ T m1 m2 = twoDigits m
90+ T d1 d2 = twoDigits d
91+{-# INLINE writeDay #-}
92+
93+-- | Write time of day, optionally with sub seconds
94+writeTimeOfDay :: Bool -> TA.MArray s -> Int -> TimeOfDay64 -> ST s Int
95+writeTimeOfDay doSubSeconds buf off (TOD hh mm ss) =
96+ do
97+
98+ TA.unsafeWrite buf off h1
99+ TA.unsafeWrite buf (off + 1) h2
100+ TA.unsafeWrite buf (off + 2) 0x3A -- colon
101+ TA.unsafeWrite buf (off + 3) m1
102+ TA.unsafeWrite buf (off + 4) m2
103+ TA.unsafeWrite buf (off + 5) 0x3A -- colon
104+ TA.unsafeWrite buf (off + 6) s1
105+ TA.unsafeWrite buf (off + 7) s2
106+ if doSubSeconds && frac /= 0
107+ then writeFracSeconds buf (off + 8) frac
108+ else return (off + 8)
109+ where
110+ T h1 h2 = twoDigits hh
111+ T m1 m2 = twoDigits mm
112+ T s1 s2 = twoDigits (fromIntegral real)
113+ (real,frac) = ss `quotRem` pico
114+ pico = 1000000000000 -- number of picoseconds in 1 second
115+
116+
117+writeFracSeconds :: TA.MArray s -> Int -> Int64 -> ST s Int
118+writeFracSeconds buf off frac =
119+ do
120+ TA.unsafeWrite buf off 0x2e -- period
121+ if mills == 0
122+ then do
123+ writeTrunc6 buf (off + 1) (fromIntegral mics)
124+ else do
125+ writeDigit6 buf (off + 1) (fromIntegral mics)
126+ writeTrunc6 buf (off + 7) (fromIntegral mills)
127+
128+ where
129+ (mics, mills) = frac `quotRem` micro
130+ micro = 1000000 -- number of microseconds in 1 second
131+
132+
133+writeDigit6 :: TA.MArray s -> Int -> Int -> ST s ()
134+writeDigit6 buf off i =
135+ do
136+ writeDigit3 buf off f1
137+ writeDigit3 buf (off+3) f2
138+ where
139+ (f1, f2) = i `quotRem` 1000
140+
141+{-# INLINE writeDigit6 #-}
142+
143+writeDigit3 :: TA.MArray s -> Int -> Int -> ST s ()
144+writeDigit3 buf off i =
145+ do
146+ TA.unsafeWrite buf off (digit d1)
147+ TA.unsafeWrite buf (off+1) (digit d2)
148+ TA.unsafeWrite buf (off+2) (digit d3)
149+ where
150+ (d1, d) = i `quotRem` 100
151+ (d2, d3) = d `quotRem` 10
152+
153+{-# INLINE writeDigit3 #-}
154+
155+writeTrunc6 :: TA.MArray s -> Int -> Int -> ST s Int
156+writeTrunc6 buf off i =
157+ if f2 == 0
158+ then writeTrunc3 buf off f1
159+ else do
160+ writeDigit3 buf off f1
161+ writeTrunc3 buf (off+3) f2
162+ where
163+ (f1, f2) = i `quotRem` 1000
164+
165+{-# INLINE writeTrunc6 #-}
166+
167+
168+writeTrunc3 :: TA.MArray s -> Int -> Int -> ST s Int
169+writeTrunc3 buf off i
170+ | d == 0 = do
171+ TA.unsafeWrite buf off (digit d1)
172+ return (off+1)
173+ | d3 == 0 = do
174+ TA.unsafeWrite buf off (digit d1)
175+ TA.unsafeWrite buf (off+1) (digit d2)
176+ return (off+2)
177+
178+ | otherwise = do
179+ TA.unsafeWrite buf off (digit d1)
180+ TA.unsafeWrite buf (off+1) (digit d2)
181+ TA.unsafeWrite buf (off+2) (digit d3)
182+ return (off+3)
183+ where
184+ (d1, d) = i `quotRem` 100
185+ (d2, d3) = d `quotRem` 10
186+
187+{-# INLINE writeTrunc3 #-}
188+
189+
190+-- Following code was adapted from aeson package.
191+--
192+-- Copyright: (c) 2015-2016 Bryan O'Sullivan
193+-- License: BSD3
194+
195+data T = T {-# UNPACK #-} !Word16 {-# UNPACK #-} !Word16
196+
197+twoDigits :: Int -> T
198+twoDigits a = T (digit hi) (digit lo)
199+ where (hi,lo) = a `quotRem` 10
200+
201+digit :: Int -> Word16
202+digit x = fromIntegral (x + 48)
203+
204+
205+data TimeOfDay64 = TOD {-# UNPACK #-} !Int
206+ {-# UNPACK #-} !Int
207+ {-# UNPACK #-} !Int64
208+
209+diffTimeOfDay64 :: DiffTime -> TimeOfDay64
210+diffTimeOfDay64 t = TOD (fromIntegral h) (fromIntegral m) s
211+ where (h,mp) = fromIntegral pico `quotRem` 3600000000000000
212+ (m,s) = mp `quotRem` 60000000000000
213+ pico = unsafeCoerce t :: Integer
katip/src/Katip/Monadic.hs+364−145
@@ -7,20 +7,20 @@
77{-# LANGUAGE TemplateHaskell #-}
88{-# LANGUAGE TypeFamilies #-}
99{-# LANGUAGE UndecidableInstances #-}
10−
10+#if MIN_VERSION_base(4, 9, 0)
11+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
12+#endif
1113-- | Provides support for treating payloads and namespaces as
1214-- composable contexts. The common pattern would be to provide a
13−-- 'KatipContext' instance for your base monad. When your program
14−-- changes to a more detailed context, say to the database, you can
15−-- use 'LogT' to tack on a typed context and a namespace
16−-- which will be merged into the enclosing monad's context and
17−-- namespace.
15+-- 'KatipContext' instance for your base monad.
1816module Katip.Monadic
1917 (
2018 -- * Monadic variants of logging functions from "Katip.Core"
2119 logFM
2220 , logTM
21+ , logLocM
2322 , logItemM
23+ , logExceptionM
2424
2525 -- * Machinery for merging typed log payloads/contexts
2626 , KatipContext(..)
@@ -28,42 +28,56 @@ module Katip.Monadic
2828 , LogContexts
2929 , liftPayload
3030
31− -- * Transformer for appending to a log context
32− , LogT'
33− , LogT
34− , runLogT
35− , BlankLogT
36− , runBlankLogT
31+ -- * KatipContextT - Utility transformer that provides Katip and KatipContext instances
32+ , KatipContextT(..)
33+ , runKatipContextT
34+ , katipAddNamespace
35+ , katipAddContext
36+ , KatipContextTState(..)
37+ , NoLoggingT (..)
38+ , askLoggerIO
3739 ) where
3840
3941
4042-------------------------------------------------------------------------------
4143import Control.Applicative
42−import Control.Monad
44+import Control.Exception.Safe
4345import Control.Monad.Base
44−import Control.Monad.Catch
4546import Control.Monad.Error.Class
46−import Control.Monad.Fix
47+#if MIN_VERSION_base(4, 9, 0)
48+import qualified Control.Monad.Fail as MF
49+#endif
4750import Control.Monad.IO.Class
48−import Control.Monad.RWS.Class
49−import Control.Monad.Trans.Class
51+import Control.Monad.IO.Unlift
52+import Control.Monad.Reader
53+import Control.Monad.State
5054import Control.Monad.Trans.Control
51−import Control.Monad.Trans.Either (EitherT)
52−import Control.Monad.Trans.Error (ErrorT)
53−import Control.Monad.Trans.Identity (IdentityT)
54−import Control.Monad.Trans.List (ListT)
55−import Control.Monad.Trans.Maybe (MaybeT)
56−import Control.Monad.Trans.Reader (ReaderT (..), runReaderT)
57−import Control.Monad.Trans.Resource (ResourceT)
58−import Control.Monad.Trans.RWS (RWST)
59−import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST)
60−import Control.Monad.Trans.State (StateT)
61−import qualified Control.Monad.Trans.State.Strict as Strict (StateT)
62−import Control.Monad.Trans.Writer (WriterT)
63−import qualified Control.Monad.Trans.Writer.Strict as Strict (WriterT)
55+#if !MIN_VERSION_either(4, 5, 0)
56+import Control.Monad.Trans.Either (EitherT, mapEitherT)
57+#endif
58+import Control.Monad.Trans.Except (ExceptT, mapExceptT)
59+import Control.Monad.Trans.Identity (IdentityT, mapIdentityT)
60+import Control.Monad.Trans.Maybe (MaybeT, mapMaybeT)
61+import Control.Monad.Trans.Resource (MonadResource,
62+ ResourceT, transResourceT)
63+import Control.Monad.Trans.RWS (RWST, mapRWST)
64+import qualified Control.Monad.Trans.RWS.Strict as Strict (RWST, mapRWST)
65+import qualified Control.Monad.Trans.State.Strict as Strict (StateT, mapStateT)
66+import qualified Control.Monad.Trans.Writer.Strict as Strict (WriterT,
67+ mapWriterT)
68+import Control.Monad.Writer hiding ((<>))
6469import Data.Aeson
70+import qualified Data.Foldable as FT
6571import qualified Data.HashMap.Strict as HM
66−import Data.Monoid
72+import Data.Semigroup as Semi
73+import Data.Sequence as Seq
74+import Data.Text (Text)
75+#if MIN_VERSION_base(4, 8, 0)
76+#if !MIN_VERSION_base(4, 9, 0)
77+import GHC.SrcLoc
78+#endif
79+import GHC.Stack
80+#endif
6781import Language.Haskell.TH
6882-------------------------------------------------------------------------------
6983import Katip.Core
@@ -77,17 +91,32 @@ data AnyLogContext where
7791
7892-------------------------------------------------------------------------------
7993-- | Heterogeneous list of log contexts that provides a smart
80−-- 'LogContext' instance for combining multiple payload policies.
81−newtype LogContexts = LogContexts [AnyLogContext] deriving (Monoid)
94+-- 'LogContext' instance for combining multiple payload policies. This
95+-- is critical for log contexts deep down in a stack to be able to
96+-- inject their own context without worrying about other context that
97+-- has already been set. Also note that contexts are treated as a
98+-- sequence and '<>' will be appended to the right hand side of the
99+-- sequence. If there are conflicting keys in the contexts, the /right
100+-- side will take precedence/, which is counter to how monoid works
101+-- for 'Map' and 'HashMap', so bear that in mind. The reasoning is
102+-- that if the user is /sequentially/ adding contexts to the right
103+-- side of the sequence, on conflict the intent is to overwrite with
104+-- the newer value (i.e. the rightmost value).
105+--
106+-- Additional note: you should not mappend LogContexts in any sort of
107+-- infinite loop, as it retains all data, so that would be a memory
108+-- leak.
109+newtype LogContexts = LogContexts (Seq AnyLogContext) deriving (Monoid, Semigroup)
82110
83111instance ToJSON LogContexts where
84112 toJSON (LogContexts cs) =
85− Object $ mconcat $ map (\(AnyLogContext v) -> toObject v) cs
113+ -- flip mappend to get right-biased merge
114+ Object $ FT.foldr (flip mappend) mempty $ fmap (\(AnyLogContext v) -> toObject v) cs
86115
87116instance ToObject LogContexts
88117
89118instance LogItem LogContexts where
90− payloadKeys verb (LogContexts vs) = mconcat $ map payloadKeys' vs
119+ payloadKeys verb (LogContexts vs) = FT.foldr (flip mappend) mempty $ fmap payloadKeys' vs
91120 where
92121 -- To ensure AllKeys doesn't leak keys from other values when
93122 -- combined, we resolve AllKeys to its equivalent SomeKeys
@@ -98,52 +127,125 @@ instance LogItem LogContexts where
98127
99128
100129-------------------------------------------------------------------------------
101−-- | Lift a log context into the generic wrapper
130+-- | Lift a log context into the generic wrapper so that it can
131+-- combine with the existing log context.
102132liftPayload :: (LogItem a) => a -> LogContexts
103−liftPayload = LogContexts . (:[]) . AnyLogContext
133+liftPayload = LogContexts . Seq.singleton . AnyLogContext
104134
105135
106136-------------------------------------------------------------------------------
107−-- | A monadic context that has an inherant way to get logging
108−-- context and namespace. Examples include a web application monad or
109−-- database monad. Combine with 'LogT' to nest.
137+-- | A monadic context that has an inherant way to get logging context
138+-- and namespace. Examples include a web application monad or database
139+-- monad. The @local@ variants are just like @local@ from Reader and
140+-- indeed you can easily implement them with @local@ if you happen to
141+-- be using a Reader in your monad. These give us 'katipAddNamespace'
142+-- and 'katipAddContext' that works with *any* 'KatipContext', as
143+-- opposed to making users have to implement these functions on their
144+-- own in each app.
110145class Katip m => KatipContext m where
111146 getKatipContext :: m LogContexts
112− getKatipNamespace :: m Namespace
113−
114−--TODO: is this INLINABLE?
115−#define TRANS(T) \
116− instance (KatipContext m, Katip (T m)) => KatipContext (T m) where \
117− getKatipContext = lift getKatipContext; \
118− getKatipNamespace = lift getKatipNamespace
119−
120−#define TRANS_CTX(CTX, T) \
121− instance (CTX, KatipContext m, Katip (T m)) => KatipContext (T m) where \
122− getKatipContext = lift getKatipContext; \
123− getKatipNamespace = lift getKatipNamespace
124−
125−TRANS(IdentityT)
126−TRANS(MaybeT)
127−TRANS(EitherT e)
128−TRANS(ListT)
129−TRANS(ReaderT r)
130−TRANS(ResourceT)
131−TRANS(Strict.StateT s)
132−TRANS(StateT s)
133−TRANS_CTX(Error e, ErrorT e)
134−TRANS_CTX(Monoid w, Strict.WriterT w)
135−TRANS_CTX(Monoid w, WriterT w)
136−TRANS_CTX(Monoid w, Strict.RWST r w s)
137−TRANS_CTX(Monoid w, RWST r w s)
147+ -- | Temporarily modify the current context for the duration of the
148+ -- supplied monad. Used in 'katipAddContext'
149+ localKatipContext :: (LogContexts -> LogContexts) -> m a -> m a
150+ getKatipNamespace :: m Namespace
151+ -- | Temporarily modify the current namespace for the duration of the
152+ -- supplied monad. Used in 'katipAddNamespace'
153+ localKatipNamespace :: (Namespace -> Namespace) -> m a -> m a
154+
155+instance (KatipContext m, Katip (IdentityT m)) => KatipContext (IdentityT m) where
156+ getKatipContext = lift getKatipContext
157+ localKatipContext = mapIdentityT . localKatipContext
158+ getKatipNamespace = lift getKatipNamespace
159+ localKatipNamespace = mapIdentityT . localKatipNamespace
160+
161+
162+instance (KatipContext m, Katip (MaybeT m)) => KatipContext (MaybeT m) where
163+ getKatipContext = lift getKatipContext
164+ localKatipContext = mapMaybeT . localKatipContext
165+ getKatipNamespace = lift getKatipNamespace
166+ localKatipNamespace = mapMaybeT . localKatipNamespace
167+
168+
169+#if !MIN_VERSION_either(4, 5, 0)
170+instance (KatipContext m, Katip (EitherT e m)) => KatipContext (EitherT e m) where
171+ getKatipContext = lift getKatipContext
172+ localKatipContext = mapEitherT . localKatipContext
173+ getKatipNamespace = lift getKatipNamespace
174+ localKatipNamespace = mapEitherT . localKatipNamespace
175+#endif
176+
177+
178+instance (KatipContext m, Katip (ReaderT r m)) => KatipContext (ReaderT r m) where
179+ getKatipContext = lift getKatipContext
180+ localKatipContext = mapReaderT . localKatipContext
181+ getKatipNamespace = lift getKatipNamespace
182+ localKatipNamespace = mapReaderT . localKatipNamespace
183+
184+
185+instance (KatipContext m, Katip (ResourceT m)) => KatipContext (ResourceT m) where
186+ getKatipContext = lift getKatipContext
187+ localKatipContext = transResourceT . localKatipContext
188+ getKatipNamespace = lift getKatipNamespace
189+ localKatipNamespace = transResourceT . localKatipNamespace
190+
191+
192+instance (KatipContext m, Katip (Strict.StateT s m)) => KatipContext (Strict.StateT s m) where
193+ getKatipContext = lift getKatipContext
194+ localKatipContext = Strict.mapStateT . localKatipContext
195+ getKatipNamespace = lift getKatipNamespace
196+ localKatipNamespace = Strict.mapStateT . localKatipNamespace
197+
198+
199+instance (KatipContext m, Katip (StateT s m)) => KatipContext (StateT s m) where
200+ getKatipContext = lift getKatipContext
201+ localKatipContext = mapStateT . localKatipContext
202+ getKatipNamespace = lift getKatipNamespace
203+ localKatipNamespace = mapStateT . localKatipNamespace
204+
205+
206+instance (KatipContext m, Katip (ExceptT e m)) => KatipContext (ExceptT e m) where
207+ getKatipContext = lift getKatipContext
208+ localKatipContext = mapExceptT . localKatipContext
209+ getKatipNamespace = lift getKatipNamespace
210+ localKatipNamespace = mapExceptT . localKatipNamespace
211+
212+
213+instance (Monoid w, KatipContext m, Katip (Strict.WriterT w m)) => KatipContext (Strict.WriterT w m) where
214+ getKatipContext = lift getKatipContext
215+ localKatipContext = Strict.mapWriterT . localKatipContext
216+ getKatipNamespace = lift getKatipNamespace
217+ localKatipNamespace = Strict.mapWriterT . localKatipNamespace
218+
219+
220+instance (Monoid w, KatipContext m, Katip (WriterT w m)) => KatipContext (WriterT w m) where
221+ getKatipContext = lift getKatipContext
222+ localKatipContext = mapWriterT . localKatipContext
223+ getKatipNamespace = lift getKatipNamespace
224+ localKatipNamespace = mapWriterT . localKatipNamespace
225+
226+
227+instance (Monoid w, KatipContext m, Katip (Strict.RWST r w s m)) => KatipContext (Strict.RWST r w s m) where
228+ getKatipContext = lift getKatipContext
229+ localKatipContext = Strict.mapRWST . localKatipContext
230+ getKatipNamespace = lift getKatipNamespace
231+ localKatipNamespace = Strict.mapRWST . localKatipNamespace
232+
233+
234+instance (Monoid w, KatipContext m, Katip (RWST r w s m)) => KatipContext (RWST r w s m) where
235+ getKatipContext = lift getKatipContext
236+ localKatipContext = mapRWST . localKatipContext
237+ getKatipNamespace = lift getKatipNamespace
238+ localKatipNamespace = mapRWST . localKatipNamespace
239+
138240
139241deriving instance (Monad m, KatipContext m) => KatipContext (KatipT m)
140242
141243-------------------------------------------------------------------------------
142244-- | Log with everything, including a source code location. This is
143245-- very low level and you typically can use 'logTM' in its
144−-- place. Automaticallysupplies payload and namespace.
246+-- place. Automatically supplies payload and namespace.
145247logItemM
146− :: (Applicative m, KatipContext m, Katip m)
248+ :: (Applicative m, KatipContext m, HasCallStack)
147249 => Maybe Loc
148250 -> Severity
149251 -> LogStr
@@ -154,12 +256,11 @@ logItemM loc sev msg = do
154256 logItem ctx ns loc sev msg
155257
156258
157−
158259-------------------------------------------------------------------------------
159260-- | Log with full context, but without any code
160261-- location. Automatically supplies payload and namespace.
161262logFM
162− :: (Applicative m, KatipContext m, Katip m)
263+ :: (Applicative m, KatipContext m)
163264 => Severity
164265 -- ^ Severity of the message
165266 -> LogStr
@@ -172,36 +273,71 @@ logFM sev msg = do
172273
173274
174275-------------------------------------------------------------------------------
175−-- | 'Loc'-tagged logging when using template-haskell is OK. Automatically supplies payload and namespace.
276+-- | 'Loc'-tagged logging when using template-haskell. Automatically
277+-- supplies payload and namespace.
176278--
177−-- @$(logt) InfoS "Hello world"@
279+-- @$(logTM) InfoS "Hello world"@
178280logTM :: ExpQ
179−logTM = [| logItemM (Just $(getLoc)) |]
281+logTM = [| logItemM (Just $(getLocTH)) |]
180282
181283
182284-------------------------------------------------------------------------------
183−-- | Wrapper over 'ReaderT' that provides a passthrough instance for
184−-- 'MonadReader' so your stack will not be affected. Also provides a
185−-- 'KatipContext' instance which combines with the inner monad's
186−-- instance.
285+-- | 'Loc'-tagged logging when using 'GHC.Stack.getCallStack' implicit-callstacks>.
286+-- Automatically supplies payload and namespace.
187287--
188−-- @
189−-- instance KatipContext m WebMonad where
190−-- getKatipContext = ...
191−-- getKatipNamespace = Namespace ["web"]
288+-- Same consideration as `logLoc` applies.
192289--
193−-- dbCallInWeb = runLogT getDBContext (Namespace ["db"]) $ do
194−-- someStuff
195−-- @
290+-- Location will be logged from the module that invokes 'logLocM' so
291+-- be aware that wrapping 'logLocM' will make location reporting useless.
292+--
293+-- This function does not require template-haskell. Using GHC <= 7.8 will result
294+-- in the emission of a log line without any location information.
295+-- Users using GHC <= 7.8 may want to use the template-haskell function
296+-- `logTM` for maximum compatibility.
297+--
298+-- @logLocM InfoS "Hello world"@
299+logLocM :: (Applicative m, KatipContext m, HasCallStack)
300+ => Severity
301+ -> LogStr
302+ -> m ()
303+logLocM = logItemM getLoc
304+
305+
306+-------------------------------------------------------------------------------
307+-- | Perform an action while logging any exceptions that may occur.
308+-- Inspired by 'onException`.
196309--
197−-- In the above example, any logging inside of @dbCallInWeb@ will get
198−-- the (optionally) monadic log context for the db, combine it with
199−-- the web server's and will use the namespace @Namespace ["web","db"]@.
310+-- >>>> error "foo" `logExceptionM` ErrorS
311+logExceptionM
312+ :: (KatipContext m, MonadCatch m, Applicative m)
313+ => m a -- ^ Main action to run
314+ -> Severity -- ^ Severity
315+ -> m a
316+logExceptionM action sev = action `catchAny` \e -> f e >> throwM e
317+ where
318+ f e = logFM sev (msg e)
319+ msg e = ls ("An exception has occurred: " :: Text) Semi.<> showLS e
320+
321+
322+-------------------------------------------------------------------------------
323+-- | Provides a simple transformer that defines a 'KatipContext'
324+-- instance for a fixed namespace and context. Just like 'KatipT', you
325+-- should use this if you prefer an explicit transformer stack and
326+-- don't want to (or cannot) define 'KatipContext' for your monad
327+-- . This is the slightly more powerful version of KatipT in that it
328+-- provides KatipContext instead of just Katip. For instance:
200329--
201−-- 'LogT' is exported which fixes @n ~ m@. The two type
202−-- variables must be present for defining a 'MonadTransControl' instance.
203−newtype LogT' n m a = LogT {
204− unLogT :: ReaderT (n LogContexts, n Namespace) m a
330+-- @
331+-- threadWithLogging = do
332+-- le <- getLogEnv
333+-- ctx <- getKatipContext
334+-- ns <- getKatipNamespace
335+-- forkIO $ runKatipContextT le ctx ns $ do
336+-- $(logTM) InfoS "Look, I can log in IO and retain context!"
337+-- doOtherStuff
338+-- @
339+newtype KatipContextT m a = KatipContextT {
340+ unKatipContextT :: ReaderT KatipContextTState m a
205341 } deriving ( Functor
206342 , Applicative
207343 , Monad
@@ -214,57 +350,111 @@ newtype LogT' n m a = LogT {
214350 , MonadWriter w
215351 , MonadError e
216352 , MonadPlus
353+ , MonadResource
354+ , Alternative
217355 , MonadFix
218− , Katip
356+ , MonadTrans
219357 )
220358
221−type LogT m a = LogT' m m a
222359
223−instance MonadTrans (LogT' n) where
224− lift = LogT . lift
360+data KatipContextTState = KatipContextTState {
361+ ltsLogEnv :: !LogEnv
362+ , ltsContext :: !LogContexts
363+ , ltsNamespace :: !Namespace
364+ }
225365
226−-- Reader is a passthrough. We don't expose our internal reader so as not to conflict
227−instance (MonadReader r m) => MonadReader r (LogT' n m) where
228− ask = lift ask
229− local f (LogT (ReaderT m)) = LogT $ ReaderT $ \r ->
230− local f (m r)
231366
232−instance MonadTransControl (LogT' n) where
233− type StT (LogT' n) a = StT (ReaderT (n LogContexts)) a
234− liftWith = defaultLiftWith LogT unLogT
235− restoreT = defaultRestoreT LogT
367+instance MonadTransControl KatipContextT where
368+ type StT KatipContextT a = StT (ReaderT KatipContextTState) a
369+ liftWith = defaultLiftWith KatipContextT unKatipContextT
370+ restoreT = defaultRestoreT KatipContextT
236371 {-# INLINE liftWith #-}
237372 {-# INLINE restoreT #-}
238373
239−instance (MonadBaseControl b m) => MonadBaseControl b (LogT' n m) where
240− type StM ((LogT' n) m) a = ComposeSt (LogT' n) m a
374+
375+instance (MonadBaseControl b m) => MonadBaseControl b (KatipContextT m) where
376+ type StM (KatipContextT m) a = ComposeSt KatipContextT m a
241377 liftBaseWith = defaultLiftBaseWith
242378 restoreM = defaultRestoreM
243379
244−instance (KatipContext m, Monad m) => KatipContext (LogT' m m) where
245− getKatipContext = LogT $ ReaderT $ \(gctxs, _) -> do
246− ctxs <- gctxs
247− ctxs' <- getKatipContext
248− return $ ctxs <> ctxs'
249− getKatipNamespace = LogT $ ReaderT $ \(_, getNsInner) -> do
250− nsOuter <- getKatipNamespace
251− nsInner <- getNsInner
252− return $ nsOuter <> nsInner
253−
254−runLogT
255− :: (Functor m, LogItem a)
256− => m a
257− -> m Namespace
258− -> LogT m b
259− -> m b
260−runLogT lgtr nsgtr m = runReaderT (unLogT m) (liftPayload <$> lgtr, nsgtr)
380+
381+-- Reader is a passthrough. We don't expose our internal reader so as not to conflict
382+instance (MonadReader r m) => MonadReader r (KatipContextT m) where
383+ ask = lift ask
384+ local f (KatipContextT (ReaderT m)) = KatipContextT $ ReaderT $ \r ->
385+ local f (m r)
386+
387+
388+instance (MonadIO m) => Katip (KatipContextT m) where
389+ getLogEnv = KatipContextT $ ReaderT $ \lts -> return (ltsLogEnv lts)
390+ localLogEnv f (KatipContextT m) = KatipContextT (local (\s -> s { ltsLogEnv = f (ltsLogEnv s)}) m)
391+
392+
393+instance (MonadIO m) => KatipContext (KatipContextT m) where
394+ getKatipContext = KatipContextT $ ReaderT $ \lts -> return (ltsContext lts)
395+ localKatipContext f (KatipContextT m) = KatipContextT $ local (\s -> s { ltsContext = f (ltsContext s)}) m
396+ getKatipNamespace = KatipContextT $ ReaderT $ \lts -> return (ltsNamespace lts)
397+ localKatipNamespace f (KatipContextT m) = KatipContextT $ local (\s -> s { ltsNamespace = f (ltsNamespace s)}) m
398+
399+instance 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
404+ askUnliftIO = KatipContextT $
405+ withUnliftIO $ \u ->
406+ pure (UnliftIO (unliftIO u . unKatipContextT))
407+#endif
408+
409+
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
415+
416+-------------------------------------------------------------------------------
417+runKatipContextT :: (LogItem c) => LogEnv -> c -> Namespace -> KatipContextT m a -> m a
418+runKatipContextT le ctx ns = flip runReaderT lts . unKatipContextT
419+ where
420+ lts = KatipContextTState le (liftPayload ctx) ns
261421
262422
263423-------------------------------------------------------------------------------
264−-- | Type analogous to IdentityT that provides an empty log
265−-- context. Use this if your root monad doesn't have a LogContext instance.
266−newtype BlankLogT m a = BlankLogT {
267− runBlankLogT :: m a
424+-- | Append a namespace segment to the current namespace for the given
425+-- monadic action, then restore the previous state
426+-- afterwards. Works with anything implementing KatipContext.
427+katipAddNamespace
428+ :: (KatipContext m)
429+ => Namespace
430+ -> m a
431+ -> m a
432+katipAddNamespace ns = localKatipNamespace (<> ns)
433+
434+
435+-------------------------------------------------------------------------------
436+-- | Append some context to the current context for the given monadic
437+-- action, then restore the previous state afterwards. Important note:
438+-- be careful using this in a loop. If you're using something like
439+-- 'forever' or 'replicateM_' that does explicit sharing to avoid a
440+-- memory leak, youll be fine as it will *sequence* calls to
441+-- 'katipAddNamespace', so each loop will get the same context
442+-- added. If you instead roll your own recursion and you're recursing
443+-- in the action you provide, you'll instead accumulate tons of
444+-- redundant contexts and even if they all merge on log, they are
445+-- stored in a sequence and will leak memory. Works with anything
446+-- implementing KatipContext.
447+katipAddContext
448+ :: ( LogItem i
449+ , KatipContext m
450+ )
451+ => i
452+ -> m a
453+ -> m a
454+katipAddContext i = localKatipContext (<> (liftPayload i))
455+
456+newtype NoLoggingT m a = NoLoggingT {
457+ runNoLoggingT :: m a
268458 } deriving ( Functor
269459 , Applicative
270460 , Monad
@@ -273,30 +463,59 @@ newtype BlankLogT m a = BlankLogT {
273463 , MonadCatch
274464 , MonadMask
275465 , MonadBase b
276− , MonadReader r
277466 , MonadState s
278467 , MonadWriter w
279468 , MonadError e
280469 , MonadPlus
470+ , Alternative
281471 , MonadFix
282− , Katip
472+ , MonadReader r
283473 )
284474
285−instance Katip m => KatipContext (BlankLogT m) where
286− getKatipContext = return mempty
287− getKatipNamespace = return mempty
288−
289−instance MonadTrans BlankLogT where
290− lift = BlankLogT
475+instance MonadTrans NoLoggingT where
476+ lift = NoLoggingT
291477
292−instance MonadTransControl BlankLogT where
293− type StT BlankLogT a = a
294− liftWith f = BlankLogT $ f runBlankLogT
295− restoreT = BlankLogT
296− {-# INLINABLE liftWith #-}
297− {-# INLINABLE restoreT #-}
478+instance MonadTransControl NoLoggingT where
479+ type StT NoLoggingT a = a
480+ liftWith f = NoLoggingT $ f runNoLoggingT
481+ restoreT = NoLoggingT
482+ {-# INLINE liftWith #-}
483+ {-# INLINE restoreT #-}
298484
299−instance (MonadBaseControl b m) => MonadBaseControl b (BlankLogT m) where
300− type StM (BlankLogT m) a = ComposeSt BlankLogT m a
301− liftBaseWith = defaultLiftBaseWith
302− restoreM = defaultRestoreM
485+instance MonadBaseControl b m => MonadBaseControl b (NoLoggingT m) where
486+ type StM (NoLoggingT m) a = StM m a
487+ liftBaseWith f = NoLoggingT $
488+ liftBaseWith $ \runInBase ->
489+ f $ runInBase . runNoLoggingT
490+ restoreM = NoLoggingT . restoreM
491+
492+instance 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
497+ askUnliftIO = NoLoggingT $
498+ withUnliftIO $ \u ->
499+ pure (UnliftIO (unliftIO u . runNoLoggingT))
500+#endif
501+
502+
503+instance MonadIO m => Katip (NoLoggingT m) where
504+ getLogEnv = liftIO (initLogEnv "NoLoggingT" "no-logging")
505+ localLogEnv = const id
506+
507+instance MonadIO m => KatipContext (NoLoggingT m) where
508+ getKatipContext = pure mempty
509+ localKatipContext = const id
510+ getKatipNamespace = pure mempty
511+ localKatipNamespace = const id
512+
513+
514+-- | Convenience function for when you have to integrate with a third
515+-- party API that takes a generic logging function as an argument.
516+askLoggerIO :: (Applicative m, KatipContext m) => m (Severity -> LogStr -> IO ())
517+askLoggerIO = do
518+ ctx <- getKatipContext
519+ ns <- getKatipNamespace
520+ logEnv <- getLogEnv
521+ pure (\sev msg -> runKatipT logEnv $ logF ctx ns sev msg)
katip/src/Katip/Scribes/Handle.hs+129−54
@@ -1,43 +1,46 @@
1−{-# LANGUAGE RecordWildCards #-}
2−
31module Katip.Scribes.Handle where
42
53-------------------------------------------------------------------------------
6−import Control.Lens
7−import Control.Monad
8−import Data.Aeson.Lens
4+import Control.Applicative as A
5+import Control.Concurrent
6+import Control.Exception (bracket_, finally)
7+import Data.Aeson
8+import Data.Text.Lazy (toStrict)
9+import Data.Text.Lazy.Encoding (decodeUtf8)
910import qualified Data.HashMap.Strict as HM
10−import Data.Monoid
11+import Data.Monoid as M
12+import Data.Scientific as S
13+import Data.Text (Text)
1114import Data.Text.Lazy.Builder
1215import Data.Text.Lazy.IO as T
13−import Data.Time
1416import System.IO
15−import System.IO.Unsafe (unsafePerformIO)
16−import System.Locale
1717-------------------------------------------------------------------------------
1818import Katip.Core
19+import Katip.Format.Time (formatAsLogTime)
1920-------------------------------------------------------------------------------
2021
2122
2223-------------------------------------------------------------------------------
2324brackets :: Builder -> Builder
24−brackets m = fromText "[" <> m <> fromText "]"
25+brackets m = fromText "[" M.<> m <> fromText "]"
2526
2627
2728-------------------------------------------------------------------------------
2829getKeys :: LogItem s => Verbosity -> s -> [Builder]
29−getKeys verb a = payloadObject verb a ^..
30− to HM.toList . traverse . to rendPair
30+getKeys verb a = concat (renderPair A.<$> HM.toList (payloadObject verb a))
3131 where
32− rendPair (k,v) = fromText k <> fromText ":" <> (v ^. _Primitive . to renderPrim)
33−
34−
35−-------------------------------------------------------------------------------
36−renderPrim :: Primitive -> Builder
37−renderPrim (StringPrim t) = fromText t
38−renderPrim (NumberPrim s) = fromString (show s)
39−renderPrim (BoolPrim b) = fromString (show b)
40−renderPrim NullPrim = fromText "null"
32+ renderPair :: (Text, Value) -> [Builder]
33+ renderPair (k,v) =
34+ case v of
35+ Object o -> concat [renderPair (k <> "." <> k', v') | (k', v') <- HM.toList o]
36+ String t -> [fromText (k <> ":" <> t)]
37+ Number n -> [fromText (k <> ":") <> fromString (formatNumber n)]
38+ Bool b -> [fromText (k <> ":") <> fromString (show b)]
39+ Null -> [fromText (k <> ":null")]
40+ _ -> mempty -- Can't think of a sensible way to handle arrays
41+ formatNumber :: Scientific -> String
42+ formatNumber n =
43+ formatScientific Generic (if isFloating n then Nothing else Just 0) n
4144
4245
4346-------------------------------------------------------------------------------
@@ -46,42 +49,119 @@ data ColorStrategy
4649 -- ^ Whether to use color control chars in log output
4750 | ColorIfTerminal
4851 -- ^ Color if output is a terminal
49−
52+ deriving (Show, Eq)
5053
5154-------------------------------------------------------------------------------
52−mkHandleScribe :: ColorStrategy -> Handle -> Severity -> Verbosity -> IO Scribe
53−mkHandleScribe cs h sev verb = do
55+-- | Logs to a file handle such as stdout, stderr, or a file. Contexts
56+-- and other information will be flattened out into bracketed
57+-- fields. For example:
58+--
59+-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:32:7] Started
60+-- > [2016-05-11 21:01:15][MyApp.confrabulation][Debug][myhost.example.com][PID 1724][ThreadId 1154][confrab_factor:42.0][main:Helpers.Logging Helpers/Logging.hs:41:9] Confrabulating widgets, with extra namespace and context
61+-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:43:7] Namespace and context are back to normal
62+--
63+-- Returns the newly-created `Scribe`. The finalizer flushes the
64+-- handle. Handle mode is set to 'LineBuffering' automatically.
65+mkHandleScribe :: ColorStrategy -> Handle -> PermitFunc -> Verbosity -> IO Scribe
66+mkHandleScribe = mkHandleScribeWithFormatter bracketFormat
67+
68+-- | Logs to a file handle such as stdout, stderr, or a file. Takes a custom
69+-- `ItemFormatter` that can be used to format `Item` as needed.
70+--
71+-- Returns the newly-created `Scribe`. The finalizer flushes the
72+-- handle. Handle mode is set to 'LineBuffering' automatically.
73+mkHandleScribeWithFormatter :: (forall a . LogItem a => ItemFormatter a)
74+ -> ColorStrategy
75+ -> Handle
76+ -> PermitFunc
77+ -> Verbosity
78+ -> IO Scribe
79+mkHandleScribeWithFormatter itemFormatter cs h permitF verb = do
5480 hSetBuffering h LineBuffering
5581 colorize <- case cs of
5682 ColorIfTerminal -> hIsTerminalDevice h
57− ColorLog b -> return b
58− return $ Scribe $ \ i@Item{..} -> do
59− when (_itemSeverity >= sev) $
60− T.hPutStrLn h $ toLazyText $ formatItem colorize verb i
83+ ColorLog b -> return b
84+ lock <- newMVar ()
85+ let logger i@Item{..} = do
86+ bracket_ (takeMVar lock) (putMVar lock ()) $
87+ T.hPutStrLn h $ toLazyText $ itemFormatter colorize verb i
88+ return $ Scribe logger (hFlush h) permitF
6189
6290
6391-------------------------------------------------------------------------------
64−formatItem :: LogItem a => Bool -> Verbosity -> Item a -> Builder
65−formatItem withColor verb Item{..} =
92+-- | A specialization of 'mkHandleScribe' that takes a 'FilePath'
93+-- instead of a 'Handle'. It is responsible for opening the file in
94+-- 'AppendMode' and will close the file handle on
95+-- 'closeScribe'/'closeScribes'. Does not do log coloring. Sets handle
96+-- to 'LineBuffering' mode.
97+mkFileScribe :: FilePath -> PermitFunc -> Verbosity -> IO Scribe
98+mkFileScribe f permitF verb = do
99+ h <- openFile f AppendMode
100+ Scribe logger finalizer permit <- mkHandleScribe (ColorLog False) h permitF verb
101+ return (Scribe logger (finalizer `finally` hClose h) permit)
102+
103+
104+-------------------------------------------------------------------------------
105+-- | A custom ItemFormatter for logging `Item`s. Takes a `Bool` indicating
106+-- whether to colorize the output, `Verbosity` of output, and an `Item` to
107+-- format.
108+--
109+-- See `bracketFormat` and `jsonFormat` for examples.
110+type ItemFormatter a = Bool -> Verbosity -> Item a -> Builder
111+
112+
113+formatItem :: LogItem a => ItemFormatter a
114+formatItem = bracketFormat
115+{-# DEPRECATED formatItem "Use bracketFormat instead" #-}
116+
117+-- | A traditional 'bracketed' log format. Contexts and other information will
118+-- be flattened out into bracketed fields. For example:
119+--
120+-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:32:7] Started
121+-- > [2016-05-11 21:01:15][MyApp.confrabulation][Debug][myhost.example.com][PID 1724][ThreadId 1154][confrab_factor:42.0][main:Helpers.Logging Helpers/Logging.hs:41:9] Confrabulating widgets, with extra namespace and context
122+-- > [2016-05-11 21:01:15][MyApp][Info][myhost.example.com][PID 1724][ThreadId 1154][main:Helpers.Logging Helpers/Logging.hs:43:7] Namespace and context are back to normal
123+bracketFormat :: LogItem a => ItemFormatter a
124+bracketFormat withColor verb Item{..} =
66125 brackets nowStr <>
67126 brackets (mconcat $ map fromText $ intercalateNs _itemNamespace) <>
68127 brackets (fromText (renderSeverity' _itemSeverity)) <>
69128 brackets (fromString _itemHost) <>
70− brackets (fromString (show _itemProcess)) <>
71− brackets (fromString (show _itemThread)) <>
129+ brackets ("PID " <> fromString (show _itemProcess)) <>
130+ brackets ("ThreadId " <> fromText (getThreadIdText _itemThread)) <>
72131 mconcat ks <>
73132 maybe mempty (brackets . fromString . locationToString) _itemLoc <>
74133 fromText " " <> (unLogStr _itemMessage)
75134 where
76− nowStr = fromString $ formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" _itemTime
135+ nowStr = fromText (formatAsLogTime _itemTime)
77136 ks = map brackets $ getKeys verb _itemPayload
78− renderSeverity' s = case s of
79− EmergencyS -> red $ renderSeverity s
80− AlertS -> red $ renderSeverity s
81− CriticalS -> red $ renderSeverity s
82− ErrorS -> red $ renderSeverity s
83− WarningS -> yellow $ renderSeverity s
84− _ -> renderSeverity s
137+ renderSeverity' severity =
138+ colorBySeverity withColor severity (renderSeverity severity)
139+
140+-- | Logs items as JSON. This can be useful in circumstances where you already
141+-- have infrastructure that is expecting JSON to be logged to a standard stream
142+-- or file. For example:
143+--
144+-- > {"at":"2018-10-02T21:50:30.4523848Z","env":"production","ns":["MyApp"],"data":{},"app":["MyApp"],"msg":"Started","pid":"10456","loc":{"loc_col":9,"loc_pkg":"main","loc_mod":"Helpers.Logging","loc_fn":"Helpers\\Logging.hs","loc_ln":44},"host":"myhost.example.com","sev":"Info","thread":"ThreadId 139"}
145+-- > {"at":"2018-10-02T21:50:30.4523848Z","env":"production","ns":["MyApp","confrabulation"],"data":{"confrab_factor":42},"app":["MyApp"],"msg":"Confrabulating widgets, with extra namespace and context","pid":"10456","loc":{"loc_col":11,"loc_pkg":"main","loc_mod":"Helpers.Logging","loc_fn":"Helpers\\Logging.hs","loc_ln":53},"host":"myhost.example.com","sev":"Debug","thread":"ThreadId 139"}
146+-- > {"at":"2018-10-02T21:50:30.4523848Z","env":"production","ns":["MyApp"],"data":{},"app":["MyApp"],"msg":"Namespace and context are back to normal","pid":"10456","loc":{"loc_col":9,"loc_pkg":"main","loc_mod":"Helpers.Logging","loc_fn":"Helpers\\Logging.hs","loc_ln":55},"host":"myhost.example.com","sev":"Info","thread":"ThreadId 139"}
147+jsonFormat :: LogItem a => ItemFormatter a
148+jsonFormat withColor verb i =
149+ fromText $
150+ colorBySeverity withColor (_itemSeverity i) $
151+ toStrict $ decodeUtf8 $ encode $ itemJson verb i
152+
153+-- | Color a text message based on `Severity`. `ErrorS` and more severe errors
154+-- are colored red, `WarningS` is colored yellow, and all other messages are
155+-- rendered in the default color.
156+colorBySeverity :: Bool -> Severity -> Text -> Text
157+colorBySeverity withColor severity msg = case severity of
158+ EmergencyS -> red msg
159+ AlertS -> red msg
160+ CriticalS -> red msg
161+ ErrorS -> red msg
162+ WarningS -> yellow msg
163+ _ -> msg
164+ where
85165 red = colorize "31"
86166 yellow = colorize "33"
87167 colorize c s
@@ -89,17 +169,12 @@ formatItem withColor verb Item{..} =
89169 | otherwise = s
90170
91171
92−-------------------------------------------------------------------------------
93−-- | An implicit environment to enable logging directly ouf of the IO monad.
94−_ioLogEnv :: LogEnv
95−_ioLogEnv = unsafePerformIO $ do
96− le <- initLogEnv "io" "io"
97− lh <- mkHandleScribe ColorIfTerminal stdout DebugS V3
98− return $ registerScribe "stdout" lh le
99−{-# NOINLINE _ioLogEnv #-}
100−
101−
102−-- -------------------------------------------------------------------------------
103−-- -- | A default IO instance to make prototype development easy. User
104−-- -- your own 'Monad' for production.
105−-- instance Katip IO where getLogEnv = return _ioLogEnv
172+-- | Provides a simple log environment with 1 scribe going to
173+-- stdout. This is a decent example of how to build a LogEnv and is
174+-- best for scripts that just need a quick, reasonable set up to log
175+-- to stdout.
176+ioLogEnv :: PermitFunc -> Verbosity -> IO LogEnv
177+ioLogEnv permit verb = do
178+ le <- initLogEnv "io" "io"
179+ lh <- mkHandleScribe ColorIfTerminal stdout permit verb
180+ registerScribe "stdout" lh defaultScribeSettings le
katip/test/Katip/Tests.hsnew file+319
@@ -0,0 +1,319 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE RecordWildCards #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE StandaloneDeriving #-}
8+{-# LANGUAGE TemplateHaskell #-}
9+{-# OPTIONS_GHC -fno-warn-orphans #-}
10+module Katip.Tests
11+ ( tests
12+ ) where
13+
14+
15+-------------------------------------------------------------------------------
16+import Control.Applicative as A
17+import Control.Concurrent.STM
18+import Control.Exception.Safe
19+import Data.Aeson
20+import qualified Data.HashMap.Strict as HM
21+import qualified Data.Map.Strict as M
22+import Data.Monoid as Monoid
23+import Data.Text (Text)
24+import qualified Data.Text.Lazy.Builder as B
25+import Data.Time
26+import Data.Time.Clock.POSIX
27+import Language.Haskell.TH
28+import Lens.Micro (ASetter, (&), (.~))
29+import System.Posix.Types
30+import Test.QuickCheck.Instances ()
31+import Test.Tasty
32+import Test.Tasty.HUnit
33+import Test.Tasty.QuickCheck
34+-------------------------------------------------------------------------------
35+import Katip
36+import Katip.Core
37+-------------------------------------------------------------------------------
38+
39+
40+tests :: TestTree
41+tests = testGroup "Katip"
42+ [
43+ testProperty "JSON cycle Item" $ \(i :: Item ()) ->
44+ prop_json_cycle i
45+ , testProperty "JSON cycle verbosity" $ \(v :: Verbosity) ->
46+ prop_json_cycle v
47+ , eqItemTests
48+ , testProperty "renderSeverity/textToSeverity cycle" $ \sev ->
49+ textToSeverity(renderSeverity sev) === Just sev
50+ , testProperty "processIDToText/textToProcessID cycle" $ \pid ->
51+ textToProcessID (processIDToText pid) === Just pid
52+ , testCase "processIDToText is just the number" $ do
53+ processIDToText 123 @?= "123"
54+ , logContextsTests
55+ , closeScribeTests
56+ , closeScribesTests
57+ , loggingTests
58+ ]
59+
60+
61+-------------------------------------------------------------------------------
62+logContextsTests :: TestTree
63+logContextsTests = testGroup "logContexts"
64+ [
65+ testCase "overwrites with the right-hand side (right-bias)" $ do
66+ let l1 = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("a" :: Text))])
67+ l2 = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("b" :: Text))])
68+ l3 = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("c" :: Text))])
69+ both = l1 <> l2 <> l3
70+ toObject both @?= HM.singleton "foo" (String "c")
71+ , testCase "respects payloadKeys for each constituent payload" $ do
72+ let everything = liftPayload (SimpleLogPayload [("foo", AnyLogPayload ("a" :: Text))])
73+ conservative = liftPayload (ConservativePayload "always" "rarely")
74+ both = everything <> conservative
75+ payloadKeys V2 both @?= SomeKeys ["often_shown", "rarely_shown", "foo"]
76+ payloadKeys V1 both @?= SomeKeys ["often_shown", "foo"]
77+ ]
78+
79+
80+-------------------------------------------------------------------------------
81+closeScribeTests :: TestTree
82+closeScribeTests = testGroup "closeScribe"
83+ [ testCase "removes the specified scribe" $ do
84+ (scr, finalizerCalled) <- trivialScribe
85+ le <- registerScribe "trivial" scr defaultScribeSettings =<< initLogEnv "ns" "test"
86+ le' <- closeScribe "trivial" le
87+ closed <- atomically (readTVar finalizerCalled)
88+ assertBool "finalizer called" closed
89+ assertBool "should not have trivial key in scribes" (not (M.member "trivial" (_logEnvScribes le')))
90+ , testCase "does nothing for a missing scribe" $ do
91+ le <- initLogEnv "ns" "test"
92+ le' <- closeScribe "nah" le
93+ assertBool "does not affect scribes" (M.null (_logEnvScribes le'))
94+ , testCase "re-throws finalizer exceptions" $ do
95+ (scr, finalizerCalled) <- brokenScribe 1
96+ le <- registerScribe "broken" scr defaultScribeSettings =<< initLogEnv "ns" "test"
97+ res <- try (closeScribe "broken" le)
98+ closed <- atomically (readTVar finalizerCalled)
99+ assertBool "finalizer called" closed
100+ case res of
101+ Left (ScribeBroken scribeNo) -> scribeNo @?= 1
102+ Right _ -> assertFailure "Expected to throw a ScribeBroken but it did not"
103+ ]
104+
105+
106+-------------------------------------------------------------------------------
107+loggingTests :: TestTree
108+loggingTests = testGroup "logging"
109+ [ testCase "logs in order with contexts, namespaces, etc" $ do
110+ (le, items) <- recordingEnv
111+ runKatipContextT le (sl "base_context" (42 :: Int)) "base_namespace" $ do
112+ $(logTM) InfoS "basic log"
113+ katipNoLogging $ do
114+ $(logTM) InfoS "you cant see this"
115+ katipAddNamespace "added" $ do
116+ katipAddNamespace "namespace" $ do
117+ $(logTM) InfoS "with namespaces"
118+ katipAddContext (sl "additional" True) $ do
119+ $(logTM) InfoS "additional context"
120+ _ <- closeScribes le
121+ summary <- fmap summarizeItem <$> readTVarIO items
122+ let baseCtx = HM.singleton "base_context" (Number 42)
123+ let baseNS = "tests" <> "base_namespace"
124+ summary @?=
125+ [ (baseNS, baseCtx, "basic log")
126+ , (baseNS <> "added" <> "namespace", baseCtx, "with namespaces")
127+ , (baseNS <> "added", HM.insert "additional" (Bool True) baseCtx, "additional context")
128+ ]
129+ , testCase "Katip.Monadic.logLocM" $ do
130+ (le, items) <- recordingEnv
131+ runKatipContextT le (sl "base_context" (42 :: Int)) "base_namespace" $ logLocM InfoS "basic log"
132+ _ <- closeScribes le
133+ loggedItems <- readTVarIO items
134+ let loc = _itemLoc (head loggedItems)
135+ fmap loc_module loc @?= Just "Katip.Tests"
136+ ]
137+ where
138+ recordingEnv :: IO (LogEnv, TVar [Item Object])
139+ recordingEnv = do
140+ items <- newTVarIO Monoid.mempty
141+ let scribe = Scribe
142+ { liPush = \i -> atomically (modifyTVar' items (<> [toObject <$> i]))
143+ , scribeFinalizer = return ()
144+ , scribePermitItem = permitItem DebugS
145+ }
146+ le1 <- initLogEnv "tests" "test"
147+ le2 <- registerScribe "recorder" scribe defaultScribeSettings le1
148+ return (le2, items)
149+ summarizeItem :: Item Object -> (Namespace, Object, LogStr)
150+ summarizeItem Item {..} = (_itemNamespace, _itemPayload, _itemMessage)
151+
152+
153+-------------------------------------------------------------------------------
154+trivialScribe :: IO (Scribe, TVar Bool)
155+trivialScribe = do
156+ finalizerCalled <- newTVarIO False
157+ let finalizer = atomically (writeTVar finalizerCalled True)
158+ return (Scribe (const (return ())) finalizer (permitItem DebugS), finalizerCalled)
159+
160+
161+-------------------------------------------------------------------------------
162+brokenScribe :: Int -> IO (Scribe, TVar Bool)
163+brokenScribe scribeNum = do
164+ finalizerCalled <- newTVarIO False
165+ let finalizer = do
166+ atomically (writeTVar finalizerCalled True)
167+ throw (ScribeBroken scribeNum)
168+ return (Scribe (const (return ())) finalizer (permitItem DebugS), finalizerCalled)
169+
170+
171+-------------------------------------------------------------------------------
172+data BrokenScribeError = ScribeBroken Int deriving (Show, Typeable)
173+
174+
175+instance Exception BrokenScribeError
176+
177+-------------------------------------------------------------------------------
178+closeScribesTests :: TestTree
179+closeScribesTests = testGroup "closeScribes"
180+ [ testCase "returns a log env with no scribes" $ do
181+ (scr, finalizerCalled) <- trivialScribe
182+ le <- registerScribe "trivial" scr defaultScribeSettings =<< initLogEnv "ns" "test"
183+ le' <- closeScribes le
184+ closed <- atomically (readTVar finalizerCalled)
185+ assertBool "finalizer called" closed
186+ assertBool "remvoes all scribes" (M.null (_logEnvScribes le'))
187+ , testCase "throws the first exception encountered after closing all scribes" $ do
188+ (scr1, finalizerCalled1) <- brokenScribe 1
189+ (scr2, finalizerCalled2) <- brokenScribe 2
190+ le <- registerScribe "broken2" scr2 defaultScribeSettings =<< registerScribe "broken1" scr1 defaultScribeSettings =<< initLogEnv "ns" "test"
191+ res <- try (closeScribes le)
192+ closed1 <- atomically (readTVar finalizerCalled1)
193+ assertBool "finalizer 1 called" closed1
194+ closed2 <- atomically (readTVar finalizerCalled2)
195+ assertBool "finalizer 2 called" closed2
196+ case res of
197+ Left (ScribeBroken scribeNo) -> scribeNo @?= 1
198+ Right _ -> assertFailure "Expected to throw a ScribeBroken but it did not"
199+ ]
200+
201+
202+-------------------------------------------------------------------------------
203+data ConservativePayload = ConservativePayload {
204+ oftenShown :: Text
205+ , rarelyShown :: Text
206+ }
207+
208+
209+instance ToJSON ConservativePayload where
210+ toJSON ConservativePayload {..} = object ["often_shown" .= oftenShown
211+ ,"rarely_shown" .= rarelyShown]
212+
213+
214+instance ToObject ConservativePayload
215+
216+
217+instance LogItem ConservativePayload where
218+ payloadKeys V1 _ = SomeKeys ["often_shown"]
219+ payloadKeys V0 _ = SomeKeys []
220+ payloadKeys _ _ = AllKeys
221+
222+-------------------------------------------------------------------------------
223+prop_json_cycle :: (ToJSON a, FromJSON a, Eq a, Show a) => a -> Property
224+prop_json_cycle a = eitherDecode (encode a) === Right a
225+
226+
227+-------------------------------------------------------------------------------
228+instance Arbitrary a => Arbitrary (Item a) where
229+ arbitrary = Item
230+ A.<$> arbitrary
231+ <*> arbitrary
232+ <*> arbitrary
233+ <*> arbitrary
234+ <*> arbitrary
235+ <*> arbitrary
236+ <*> arbitrary
237+ <*> arbitrary
238+ <*> (getCleanUTCTime <$> arbitrary)
239+ <*> arbitrary
240+ <*> arbitrary
241+
242+
243+-------------------------------------------------------------------------------
244+newtype CleanUTCTime = CleanUTCTime {
245+ getCleanUTCTime :: UTCTime
246+ }
247+
248+
249+-------------------------------------------------------------------------------
250+-- Work around time parsing precision issues in aeson
251+instance Arbitrary CleanUTCTime where
252+ arbitrary = CleanUTCTime . posixSecondsToUTCTime . fromInteger <$> arbitrary
253+
254+
255+-------------------------------------------------------------------------------
256+deriving instance Arbitrary Namespace
257+deriving instance Arbitrary Environment
258+deriving instance Arbitrary ThreadIdText
259+deriving instance Arbitrary CPid
260+
261+
262+-------------------------------------------------------------------------------
263+instance Arbitrary Loc where
264+ arbitrary = do
265+ f <- arbitrary
266+ p <- arbitrary
267+ m <- arbitrary
268+ s <- arbitrary
269+ return $ Loc f p m s s
270+
271+
272+-------------------------------------------------------------------------------
273+instance Arbitrary Verbosity where
274+ arbitrary = oneof $ map pure [V0, V1, V2, V3]
275+
276+-------------------------------------------------------------------------------
277+instance Arbitrary Severity where
278+ arbitrary = oneof $ map pure [ DebugS
279+ , InfoS
280+ , NoticeS
281+ , WarningS
282+ , ErrorS
283+ , CriticalS
284+ , AlertS
285+ , EmergencyS
286+ ]
287+
288+
289+-------------------------------------------------------------------------------
290+instance Arbitrary LogStr where
291+ arbitrary = LogStr . B.fromText <$> arbitrary
292+
293+-------------------------------------------------------------------------------
294+-- Somewhat test whether all fields are taken into account in `==`
295+
296+#if !MIN_VERSION_template_haskell(2, 10, 0)
297+-- `testProperty` requires these instances
298+deriving instance Eq Loc
299+deriving instance Show Loc
300+#endif
301+
302+eqItemTests :: TestTree
303+eqItemTests = testGroup "Eq Item"
304+ [ testProperty "itemApp" $ prop_field itemApp
305+ , testProperty "itemEnv" $ prop_field itemEnv
306+ , testProperty "itemSeverity" $ prop_field itemSeverity
307+ , testProperty "itemThread" $ prop_field itemThread
308+ , testProperty "itemHost" $ prop_field itemHost
309+ , testProperty "itemProcess" $ prop_field itemProcess
310+ , testProperty "itemPayload" $ prop_field itemPayload
311+ , testProperty "itemMessage" $ prop_field itemMessage
312+ , testProperty "itemTime" $ prop_field itemTime
313+ , testProperty "itemNamespace" $ prop_field itemNamespace
314+ , testProperty "itemLoc" $ prop_field itemLoc
315+ ]
316+ where
317+ prop_field :: Eq a => ASetter (Item ()) (Item ()) a a -> Item () -> a -> a -> Bool
318+ prop_field field item f1 f2 =
319+ ((item & field .~ f1) == (item & field .~ f2)) == (f1 == f2)
katip/test/Katip/Tests/Format/Time.hsnew file+81
@@ -0,0 +1,81 @@
1+module Katip.Tests.Format.Time
2+ ( tests
3+ ) where
4+
5+-------------------------------------------------------------------------------
6+import Data.Aeson (toJSON)
7+import qualified Data.Text as Text
8+import Data.Time
9+import Data.Time.Clock.POSIX
10+import qualified Data.Time.Locale.Compat as LC
11+import Test.Tasty
12+import Test.Tasty.HUnit
13+import Test.Tasty.QuickCheck
14+-------------------------------------------------------------------------------
15+import Katip.Format.Time
16+-------------------------------------------------------------------------------
17+
18+
19+tests :: TestTree
20+tests = testGroup "Katip.Format.Time"
21+ [ testCase "formatAsLogTime" $ do
22+ assertEqual "time 1" (formatDT t1) (formatAsLogTime t1)
23+ assertEqual "time 2" (formatDT t2) (formatAsLogTime t2)
24+ assertEqual "time 3" (formatDT t3) (formatAsLogTime t3)
25+ assertEqual "time 4" (formatDT t4) (formatAsLogTime t4)
26+ assertEqual "time 5" (formatDT t5) (formatAsLogTime t5)
27+
28+ , testCase "formatAsIso8601" $ do
29+ assertEqual "time 1" (formatDTISO t1) (formatAsIso8601 t1)
30+ assertEqual "time 2" (formatDTISO t2) (formatAsIso8601 t2)
31+ assertEqual "time 3" (formatDTISO t3) (formatAsIso8601 t3)
32+ assertEqual "time 4" (formatDTISO t4) (formatAsIso8601 t4)
33+ assertEqual "time 5" (formatDTISO t5) (formatAsIso8601 t5)
34+
35+ , testCase "formatAsIso8601 Aeson" $ do
36+ assertEqual "time 1" (toJSON t1) (toJSON $ formatAsIso8601 t1)
37+ assertEqual "time 2" (toJSON t2) (toJSON $ formatAsIso8601 t2)
38+ assertEqual "time 3" (toJSON t3) (toJSON $ formatAsIso8601 t3)
39+ assertEqual "time 4" (toJSON t4) (toJSON $ formatAsIso8601 t4)
40+ assertEqual "time 5" (toJSON t5) (toJSON $ formatAsIso8601 t5)
41+
42+
43+ , testProperty "LogTime same as Date.Time" $ \(ArbUTCTime t) ->
44+ prop_format_log t
45+
46+ , testProperty "ISO 8601 same as Date.Time" $ \(ArbUTCTime t) ->
47+ prop_format_iso t
48+
49+ , testProperty "ISO 8601 same as Aeson" $ \(ArbUTCTime t) ->
50+ prop_format_aeson t
51+
52+ ]
53+ where
54+ t1 = UTCTime (fromGregorian 2016 12 34) 5025.123456789012
55+ t2 = UTCTime (fromGregorian 2016 1 23) 5025.123
56+ t3 = UTCTime (fromGregorian 16 12 1) 025
57+ t4 = UTCTime (fromGregorian 2000 1 1) 0.000000000001
58+ t5 = UTCTime (fromGregorian 2000 12 31) 0.100000000001
59+
60+formatDT :: UTCTime -> Text.Text
61+formatDT = Text.pack . formatTime LC.defaultTimeLocale "%0Y-%m-%d %H:%M:%S"
62+
63+formatDTISO :: UTCTime -> Text.Text
64+formatDTISO = Text.pack . formatTime LC.defaultTimeLocale "%0Y-%m-%dT%H:%M:%S%QZ"
65+
66+prop_format_log :: UTCTime -> Property
67+prop_format_log a = formatDT a === formatAsLogTime a
68+
69+prop_format_iso :: UTCTime -> Property
70+prop_format_iso a = formatDTISO a === formatAsIso8601 a
71+
72+prop_format_aeson :: UTCTime -> Property
73+prop_format_aeson a = toJSON a === toJSON (formatAsIso8601 a)
74+
75+
76+newtype ArbUTCTime = ArbUTCTime UTCTime
77+ deriving (Show)
78+
79+instance Arbitrary ArbUTCTime where
80+ arbitrary = fmap (ArbUTCTime . posixSecondsToUTCTime . fromInteger)
81+ arbitrary
katip/test/Katip/Tests/Scribes/Handle-json.goldennew file+38
@@ -0,0 +1,38 @@
1+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Debug","thread":"1337"}
2+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
3+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Notice","thread":"1337"}
4+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Warning","thread":"1337"}
5+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Error","thread":"1337"}
6+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Critical","thread":"1337"}
7+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Alert","thread":"1337"}
8+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Emergency","thread":"1337"}
9+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"0"}
10+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1"}
11+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
12+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"2147483647"}
13+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
14+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"www.example.com","sev":"Info","thread":"1337"}
15+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"127.0.0.1","sev":"Info","thread":"1337"}
16+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"0","loc":null,"host":"example","sev":"Info","thread":"1337"}
17+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"1","loc":null,"host":"example","sev":"Info","thread":"1337"}
18+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"1337","loc":null,"host":"example","sev":"Info","thread":"1337"}
19+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"2147483647","loc":null,"host":"example","sev":"Info","thread":"1337"}
20+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
21+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message\nwith newline","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
22+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":" a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
23+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"сообщение","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
24+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"哈囉世界","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
25+{"at":"2000-01-01T00:00:00Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
26+{"at":"2123-12-31T23:59:59.999999999999Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
27+{"at":"2016-10-10T01:01:05Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
28+{"at":"2100-12-31T12:59:10.1Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
29+{"at":"1982-01-01T12:30:00.000000000001Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
30+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
31+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo","bar"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
32+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["фу","бар"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
33+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["with\nnewline"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
34+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
35+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":{"loc_col":1,"loc_pkg":"main","loc_mod":"Some.Module","loc_fn":"path/Some/Module.hs","loc_ln":30},"host":"example","sev":"Info","thread":"1337"}
36+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"note":{"deep":"some note"}},"app":["app"],"msg":"message","pid":"7331","loc":{"loc_col":9000,"loc_pkg":"main","loc_mod":"Some.Module","loc_fn":"путь/Some/Module.hs","loc_ln":3000},"host":"example","sev":"Info","thread":"1337"}
37+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"sub":null,"list":[],"text":"","num":0,"float":0},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
38+{"at":"2016-06-12T12:34:56Z","env":"production","ns":["foo"],"data":{"sub":{"note":{"deep":"some note"}},"list":["one","two","three"],"text":"note","num":10,"float":5.5},"app":["app"],"msg":"message","pid":"7331","loc":null,"host":"example","sev":"Info","thread":"1337"}
katip/test/Katip/Tests/Scribes/Handle-text.goldennew file+40
@@ -0,0 +1,40 @@
1+[2016-06-12 12:34:56][foo][Debug][example][PID 7331][ThreadId 1337][note.deep:some note] message
2+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
3+[2016-06-12 12:34:56][foo][Notice][example][PID 7331][ThreadId 1337][note.deep:some note] message
4+[2016-06-12 12:34:56][foo][Warning][example][PID 7331][ThreadId 1337][note.deep:some note] message
5+[2016-06-12 12:34:56][foo][Error][example][PID 7331][ThreadId 1337][note.deep:some note] message
6+[2016-06-12 12:34:56][foo][Critical][example][PID 7331][ThreadId 1337][note.deep:some note] message
7+[2016-06-12 12:34:56][foo][Alert][example][PID 7331][ThreadId 1337][note.deep:some note] message
8+[2016-06-12 12:34:56][foo][Emergency][example][PID 7331][ThreadId 1337][note.deep:some note] message
9+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 0][note.deep:some note] message
10+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1][note.deep:some note] message
11+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
12+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 2147483647][note.deep:some note] message
13+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
14+[2016-06-12 12:34:56][foo][Info][www.example.com][PID 7331][ThreadId 1337][note.deep:some note] message
15+[2016-06-12 12:34:56][foo][Info][127.0.0.1][PID 7331][ThreadId 1337][note.deep:some note] message
16+[2016-06-12 12:34:56][foo][Info][example][PID 0][ThreadId 1337][note.deep:some note] message
17+[2016-06-12 12:34:56][foo][Info][example][PID 1][ThreadId 1337][note.deep:some note] message
18+[2016-06-12 12:34:56][foo][Info][example][PID 1337][ThreadId 1337][note.deep:some note] message
19+[2016-06-12 12:34:56][foo][Info][example][PID 2147483647][ThreadId 1337][note.deep:some note] message
20+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
21+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
22+with newline
23+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message a really long message
24+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] сообщение
25+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] 哈囉世界
26+[2000-01-01 00:00:00][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
27+[2123-12-31 23:59:59][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
28+[2016-10-10 01:01:05][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
29+[2100-12-31 12:59:10][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
30+[1982-01-01 12:30:00][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
31+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
32+[2016-06-12 12:34:56][foo.bar][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
33+[2016-06-12 12:34:56][фу.бар][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
34+[2016-06-12 12:34:56][with
35+newline][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
36+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note] message
37+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note][main:Some.Module path/Some/Module.hs:30:1] message
38+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][note.deep:some note][main:Some.Module путь/Some/Module.hs:3000:9000] message
39+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][sub:null][text:][num:0][float:0] message
40+[2016-06-12 12:34:56][foo][Info][example][PID 7331][ThreadId 1337][sub.note.deep:some note][text:note][num:10][float:5.5] message
katip/test/Katip/Tests/Scribes/Handle.hsnew file+266
@@ -0,0 +1,266 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE RankNTypes #-}
3+module Katip.Tests.Scribes.Handle
4+ ( tests
5+ ) where
6+
7+-------------------------------------------------------------------------------
8+import Control.Monad
9+import Data.Aeson
10+import qualified Data.ByteString.Char8 as B
11+import qualified Data.ByteString.Lazy as BL
12+import Data.Monoid as M
13+import Data.Text (Text)
14+import qualified Data.Text as T
15+import qualified Data.Text.Encoding as T
16+import qualified Data.Text.Lazy as LT
17+import qualified Data.Text.Lazy.Builder as LT
18+import Data.Time
19+import Language.Haskell.TH.Syntax (Loc (..))
20+import Lens.Micro ((.~))
21+import System.Directory
22+import System.IO
23+import Test.Tasty
24+import Test.Tasty.Golden
25+import Test.Tasty.HUnit
26+import Text.Regex.TDFA
27+-------------------------------------------------------------------------------
28+import Katip
29+-------------------------------------------------------------------------------
30+
31+
32+tests :: TestTree
33+tests = testGroup "Katip.Scribes.Handle"
34+ [
35+ withResource setup teardown $ \setupScribe -> testCase "logs the correct data" $ do
36+ (path, h, fin, le) <- setupScribe
37+ runKatipT le $ logItem dummyLogItem "test" Nothing InfoS "test message"
38+ fin
39+ runKatipT le $ logItem dummyLogItem "test" Nothing InfoS "wont make it in"
40+ hClose h
41+ res <- readFile path
42+ let pat = "\\[[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\\]\\[katip-test.test\\]\\[Info\\]\\[.+\\]\\[[PID [:digit:]]+\\]\\[ThreadId [[:digit:]]+\\]\\[note.deep:some note\\] test message" :: String
43+ let matches = res =~ pat
44+ assertBool (show res M.<> " did not match") matches
45+ , withResource setupFile (const (return ())) $ \setupScribe -> testCase "logs correct data to a file" $ do
46+ (path, fin, le) <- setupScribe
47+ runKatipT le $ logItem dummyLogItem "test" Nothing InfoS "test message"
48+ fin
49+ runKatipT le $ logItem dummyLogItem "test" Nothing InfoS "wont make it in"
50+ res <- readFile path
51+ let pat = "\\[[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2} [[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\\]\\[katip-test.test\\]\\[Info\\]\\[.+\\]\\[[PID [:digit:]]+\\]\\[ThreadId [[:digit:]]+\\]\\[note.deep:some note\\] test message" :: String
52+ let matches = res =~ pat
53+ assertBool (show res <> " did not match") matches
54+ , withResource setupTempFile teardownTempFile $ \setupFn ->
55+ goldenVsString "Text-golden"
56+ "test/Katip/Tests/Scribes/Handle-text.golden"
57+ (setupFn >>= writeTextLog)
58+ , withResource setupTempFile teardownTempFile $ \setupFn ->
59+ goldenVsString "Json-golden"
60+ "test/Katip/Tests/Scribes/Handle-json.golden"
61+ (setupFn >>= writeJsonLog)
62+ ]
63+
64+
65+-------------------------------------------------------------------------------
66+data DummyLogItem = DummyLogItem {
67+ dliNote :: Text
68+ }
69+
70+
71+instance ToJSON DummyLogItem where
72+ toJSON dli = object
73+ [ "note" .= object [ "deep" .= dliNote dli
74+ ]
75+ ]
76+
77+
78+instance ToObject DummyLogItem
79+
80+
81+instance LogItem DummyLogItem where
82+ payloadKeys _ _ = AllKeys
83+
84+
85+-------------------------------------------------------------------------------
86+dummyLogItem :: DummyLogItem
87+dummyLogItem = DummyLogItem "some note"
88+
89+
90+-------------------------------------------------------------------------------
91+setup :: IO (FilePath, Handle, IO (), LogEnv)
92+setup = do
93+ tempDir <- getTemporaryDirectory
94+ (fp, h) <- openTempFile tempDir "katip.log"
95+ s <- mkHandleScribe (ColorLog False) h (permitItem DebugS) V3
96+ le <- initLogEnv "katip-test" "test"
97+ le' <- registerScribe "handle" s defaultScribeSettings le
98+ return (fp, h, void (closeScribes le'), le')
99+
100+
101+-------------------------------------------------------------------------------
102+teardown :: (a, Handle, b, c) -> IO ()
103+teardown (_, h, _, _) = do
104+ chk <- hIsOpen h
105+ when chk $ hClose h
106+
107+
108+-------------------------------------------------------------------------------
109+setupFile :: IO (FilePath, IO (), LogEnv)
110+setupFile = do
111+ tempDir <- getTemporaryDirectory
112+ (fp, h) <- openTempFile tempDir "katip.log"
113+ hClose h
114+ s <- mkFileScribe fp (permitItem DebugS) V3
115+ le <- initLogEnv "katip-test" "test"
116+ le' <- registerScribe "handle" s defaultScribeSettings le
117+ return (fp, void (closeScribes le'), le')
118+
119+
120+-- Following code tests Handle scribe output against a golden file.
121+-- This test will fail on non utf8 locales because golden file is in utf-8.
122+-- It generates all meaningfull variations of Item, and also tests
123+-- writing of payload of different Aeson constructors
124+--
125+-- Note: currently Handle scribe does not write Array items at all
126+-------------------------------------------------------------------------------
127+data AllTypesLogItem = AllTypesLogItem
128+ { atlText :: Text
129+ , atlNum :: Int
130+ , atlFloat :: Float
131+ , atlList :: [Text]
132+ , atlSub :: Maybe DummyLogItem
133+ }
134+
135+
136+instance ToJSON AllTypesLogItem where
137+ toJSON it = object
138+ [ "text" .= atlText it
139+ , "num" .= atlNum it
140+ , "float" .= atlFloat it
141+ , "list" .= atlList it
142+ , "sub" .= atlSub it
143+ ]
144+
145+
146+instance ToObject AllTypesLogItem
147+
148+
149+instance LogItem AllTypesLogItem where
150+ payloadKeys _ _ = AllKeys
151+
152+
153+-------------------------------------------------------------------------------
154+theItem :: Item DummyLogItem
155+theItem = Item (Namespace ["app"])
156+ (Environment "production")
157+ (InfoS)
158+ (ThreadIdText "1337")
159+ "example"
160+ 7331
161+ dummyLogItem
162+ "message"
163+ (mkUTCTime 2016 6 12 12 34 56)
164+ (Namespace ["foo"])
165+ Nothing
166+
167+genItems :: [Item DummyLogItem]
168+genItems = concat $
169+ [ [ itemSeverity .~ s $ theItem
170+ | s <- [minBound .. maxBound]
171+ ]
172+ , [ itemThread .~ (ThreadIdText . T.pack $ show t) $ theItem
173+ | t <- [0 :: Int, 1, 1337, 2147483647]
174+ ]
175+ , [ itemHost .~ h $ theItem
176+ | h <- ["example", "www.example.com", "127.0.0.1"]
177+ ]
178+ , [ itemProcess .~ p $ theItem
179+ | p <- [0, 1, 1337, 2147483647]
180+ ]
181+ , [ itemMessage .~ LogStr m $ theItem
182+ | m <- [ "message"
183+ , "message\nwith newline"
184+ , LT.fromLazyText (LT.replicate 40 " a really long message")
185+ , "сообщение"
186+ , "哈囉世界"
187+ ]
188+ ]
189+ , [ itemTime .~ t $ theItem
190+ | t <- genDates
191+ ]
192+ , [ itemNamespace .~ Namespace ns $ theItem
193+ | ns <- [ ["foo"]
194+ , ["foo", "bar"]
195+ , ["фу", "бар"]
196+ , ["with\nnewline"] ]
197+ ]
198+ , [ itemLoc .~ l $ theItem | l <- genLocs
199+ ]
200+ ]
201+
202+genDates :: [UTCTime]
203+genDates =
204+ [ mkUTCTime 2000 1 1 0 0 0.0
205+ , mkUTCTime 2123 12 31 23 59 59.999999999999
206+ , mkUTCTime 2016 10 10 1 1 5.0
207+ , mkUTCTime 2100 12 31 12 59 10.1
208+ , mkUTCTime 1982 1 1 12 30 0.000000000001
209+ ]
210+
211+genLocs :: [Maybe Loc]
212+genLocs =
213+ [ Nothing
214+ , Just $ Loc "path/Some/Module.hs"
215+ "main"
216+ "Some.Module"
217+ (30,1) (30,14)
218+ , Just $ Loc "путь/Some/Module.hs"
219+ "main"
220+ "Some.Module"
221+ (3000,9000) (4000,1)
222+ ]
223+
224+genTypedItems :: [Item AllTypesLogItem]
225+genTypedItems =
226+ [ itemPayload .~ p $ theItem
227+ | p <- [ AllTypesLogItem "" 0 0.0 [] Nothing
228+ , AllTypesLogItem "note" 10 5.5 ["one", "two", "three"]
229+ (Just dummyLogItem)
230+ ]
231+ ]
232+
233+mkUTCTime :: Integer -> Int -> Int -> DiffTime -> DiffTime -> DiffTime -> UTCTime
234+mkUTCTime y mt d h mn s = UTCTime day dt
235+ where
236+ day = fromGregorian y mt d
237+ dt = h * 60 * 60 + mn * 60 + s
238+
239+-------------------------------------------------------------------------------
240+writeTextLog :: (FilePath, Handle) -> IO (BL.ByteString)
241+writeTextLog = writeFormattedLog bracketFormat
242+
243+writeJsonLog :: (FilePath, Handle) -> IO (BL.ByteString)
244+writeJsonLog = writeFormattedLog jsonFormat
245+
246+writeFormattedLog :: (forall a . LogItem a => ItemFormatter a) -> (FilePath, Handle) -> IO (BL.ByteString)
247+writeFormattedLog format (path, h) = do
248+ mapM_ (put . formatOne) genItems
249+ mapM_ (put . formatOne) genTypedItems
250+ hClose h
251+ BL.readFile path
252+ where
253+ formatOne :: LogItem a => Item a -> Text
254+ formatOne = LT.toStrict . LT.toLazyText . format False V3
255+ put = B.hPutStrLn h . T.encodeUtf8
256+
257+setupTempFile :: IO (FilePath, Handle)
258+setupTempFile = do
259+ tempDir <- getTemporaryDirectory
260+ (fp, h) <- openBinaryTempFile tempDir "katip.log"
261+ return (fp, h)
262+
263+teardownTempFile :: (FilePath, Handle) -> IO ()
264+teardownTempFile (_, h) = do
265+ chk <- hIsOpen h
266+ when chk $ hClose h
katip/test/Main.hsnew file+21
@@ -0,0 +1,21 @@
1+module Main (main) where
2+
3+-------------------------------------------------------------------------------
4+import Test.Tasty
5+-------------------------------------------------------------------------------
6+import qualified Katip.Tests
7+import qualified Katip.Tests.Format.Time
8+import qualified Katip.Tests.Scribes.Handle
9+-------------------------------------------------------------------------------
10+
11+
12+main :: IO ()
13+main = defaultMain testSuite
14+
15+testSuite :: TestTree
16+testSuite = testGroup "katip"
17+ [
18+ Katip.Tests.tests
19+ , Katip.Tests.Format.Time.tests
20+ , Katip.Tests.Scribes.Handle.tests
21+ ]
stack.yamlnew file+20
@@ -0,0 +1,20 @@
1+resolver: lts-16.18
2+packages:
3+ - katip
4+ - katip-elasticsearch
5+ - katip-datadog
6+ - katip-logzio
7+extra-deps:
8+ # ghc 8.8 support
9+ - github: wireapp/bloodhound
10+ commit: b409f52b518f20a4c20651b5893c31b7b85cb9b1
11+allow-newer: true
12+flags:
13+ katip:
14+ lib-Werror: true
15+ katip-elasticsearch:
16+ lib-Werror: true
17+ katip-datadog:
18+ lib-Werror: true
19+ katip-logzio:
20+ lib-Werror: true