packages feed

ekg-log (empty) → 0.1.0.1

raw patch · 4 files changed

+260/−0 lines, 4 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, ekg-core, fast-logger, filepath, text, time, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Johan Tibell++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Johan Tibell nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ System/Local/Monitoring/Log.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE BangPatterns        #-}+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE RecordWildCards     #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | This module lets you periodically flush metrics to a log file+-- that automatically rotates itself.+--+-- > main = do+-- >     store <- newStore+-- >     forkEkgLog defaultLogOptions store+--+-- You probably want to include some of the predefined metrics defined+-- in the ekg-core package, by calling e.g. the 'registerGcStats'+-- function defined in that package.+module System.Local.Monitoring.Log+    (+      -- * The statsd syncer+      Log+    , logThreadId+    , forkEkgLog+    , LogOptions(..)+    , defaultLogOptions+    ) where++import           Control.Concurrent    (ThreadId, myThreadId, threadDelay,+                                        throwTo)+import           Control.Exception     ()+import           Control.Monad         (forM_, when)+import           Data.Aeson            as A+import qualified Data.ByteString.Char8 as B8+import qualified Data.ByteString.Lazy  as L+import qualified Data.HashMap.Strict   as M+import           Data.Int              (Int64)+import           Data.Monoid           ((<>))+import qualified Data.Text             as T+import qualified Data.Text.IO          ()+import           Data.Time+import           Data.Time.Clock.POSIX (getPOSIXTime)+import qualified System.FilePath       as FP+import           System.IO             (stderr)+import           System.Log.FastLogger+import qualified System.Metrics        as Metrics++#if __GLASGOW_HASKELL__ >= 706+import           Control.Concurrent    (forkFinally)+#else+import           Control.Concurrent    (forkIO)+import           Control.Exception     (SomeException, mask, try)+import           Prelude               hiding (catch)+#endif++-- | A handle that can be used to control the log sync thread.+-- Created by 'forkEkgLog'.+data Log = Log+    { threadId :: {-# UNPACK #-} !ThreadId+    }++-- | The thread ID of the statsd sync thread. You can stop the sync by+-- killing this thread (i.e. by throwing it an asynchronous+-- exception.)+logThreadId :: Log -> ThreadId+logThreadId = threadId++-- | Options to control whate log file to append to, when to rotate,+-- and how often to flush metrics. The flush interval should be+-- shorter than the flush interval.+data LogOptions = LogOptions+    { -- | Server hostname or IP address+      logfile       :: !FP.FilePath++      -- | Data push interval, in ms.+    , flushInterval :: !Int++      -- | Print debug output to stderr.+    , debug         :: !Bool++      -- | Prefix to add to all metric names.+    , prefix        :: !T.Text++      -- | Suffix to add to all metric names. This is particularly+      -- useful for sending per application stats.+    , suffix        :: !T.Text+    }++-- | Defaults:+--+-- * @logfile@ = @\"./ekg.log\"@+--+-- * @flushInterval@ = @1000@+--+-- * @debug@ = @False@+defaultLogOptions :: LogOptions+defaultLogOptions = LogOptions+    { logfile       = "./ekg.log"+    , flushInterval = 1000+    , debug         = False+    , prefix        = ""+    , suffix        = ""+    }++-- | Create a thread that periodically flushes the metrics in the+-- store to a log file.+forkEkgLog :: LogOptions    -- ^ Options+           -> Metrics.Store -- ^ Metric store+           -> IO Log        -- ^ Statsd sync handle+forkEkgLog opts store = do+    logset <- newFileLoggerSet defaultBufSize (logfile opts)+    me     <- myThreadId+    tid    <- forkFinally (loop store emptySample logset opts) $ \ r -> do+        rmLoggerSet logset+        case r of+            Left e  -> throwTo me e+            Right _ -> return ()+    return $ Log tid+  where+    emptySample = M.empty++loop :: Metrics.Store   -- ^ Metric store+     -> Metrics.Sample  -- ^ Last sampled metrics+     -> LoggerSet       -- ^ FastLogger set+     -> LogOptions      -- ^ Options+     -> IO ()+loop store lastSample logset opts = do+    start <- time+    sample <- Metrics.sampleAll store+    let !diff = diffSamples lastSample sample+    flushSample diff logset opts+    end <- time+    threadDelay (flushInterval opts * 1000 - fromIntegral (end - start))+    loop store sample logset opts++-- | Microseconds since epoch.+time :: IO Int64+time = (round . (* 1000000.0) . toDouble) `fmap` getPOSIXTime+  where toDouble = realToFrac :: Real a => a -> Double++diffSamples :: Metrics.Sample -> Metrics.Sample -> Metrics.Sample+diffSamples prev curr = M.foldlWithKey' combine M.empty curr+  where+    combine m name new = case M.lookup name prev of+        Just old -> case diffMetric old new of+            Just val -> M.insert name val m+            Nothing  -> m+        _        -> M.insert name new m++    diffMetric :: Metrics.Value -> Metrics.Value -> Maybe Metrics.Value+    diffMetric (Metrics.Counter n1) (Metrics.Counter n2)+        | n1 == n2  = Nothing+        | otherwise = Just $! Metrics.Counter $ n2 - n1+    diffMetric (Metrics.Gauge n1) (Metrics.Gauge n2)+        | n1 == n2  = Nothing+        | otherwise = Just $ Metrics.Gauge n2+    diffMetric (Metrics.Label n1) (Metrics.Label n2)+        | n1 == n2  = Nothing+        | otherwise = Just $ Metrics.Label n2+    -- Distributions are assumed to be non-equal.+    diffMetric _ _  = Nothing++flushSample :: Metrics.Sample -> LoggerSet -> LogOptions -> IO ()+flushSample sample logset opts = do+    forM_ (M.toList sample) $ \(name, val) -> do+        time' <- getCurrentTime+        let newName = dottedPrefix <> name <> dottedSuffix+            newObj  = case val of+                (Metrics.Counter v)      -> object [ "timestamp" .= time', newName .= show v ]+                (Metrics.Gauge   v)      -> object [ "timestamp" .= time', newName .= show v ]+                (Metrics.Label   v)      -> object [ "timestamp" .= time', newName .= show v ]+                (Metrics.Distribution v) -> object [ "timestamp" .= time', newName .= show v ]+        flushMetric newObj+  where+    isDebug = debug opts++    dottedPrefix = if T.null (prefix opts) then "" else prefix opts <> "."+    dottedSuffix = if T.null (suffix opts) then "" else "." <> suffix opts++    flushMetric metricObj = do+        let !msg = L.toStrict $ A.encode metricObj+        when isDebug $ B8.hPutStrLn stderr $ B8.concat [ "DEBUG: ", msg]+        pushLogStr logset . toLogStr $ B8.append "\n" msg++------------------------------------------------------------------------+-- Backwards compatibility shims++#if __GLASGOW_HASKELL__ < 706+forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId+forkFinally action and_then =+  mask $ \restore ->+    forkIO $ try (restore action) >>= and_then+#endif
+ ekg-log.cabal view
@@ -0,0 +1,37 @@+name:                ekg-log+version:             0.1.0.1+synopsis:            Push metrics to a log file.+description:+  This library lets you push system metrics to a log file.+homepage:            https://github.com/ixmatus/ekg-log+bug-reports:         https://github.com/ixmatus/ekg-log/issues+license:             BSD3+license-file:        LICENSE+author:              Parnell Springmeyer+maintainer:          parnell@digitalmentat.com+category:            System+build-type:          Simple+cabal-version:       >=1.10++library+  exposed-modules:     +    System.Local.Monitoring.Log++  build-depends:+    base                      >= 4.4     && < 5,+    aeson                     >= 0.7     && < 0.9,+    bytestring                              < 1.0,+    ekg-core                  >= 0.1     && < 1.0,+    text                                    < 1.3,+    fast-logger               >= 2.0     && < 3.0,+    filepath                  >= 1.3     && < 1.4,+    time                                    < 1.5,+    unordered-containers                    < 0.3++  default-language:    Haskell2010++  ghc-options: -Wall++source-repository head+  type:     git+  location: https://github.com/ixmatus/ekg-log.git