├── numbersd ├── Setup.hs ├── assets ├── images │ ├── spin.gif │ ├── spin-night.gif │ ├── numbersd-graphites.png │ ├── statsd-monitoring.png │ ├── numbersd-broadcasters.png │ ├── numbersd-downstreams.png │ └── numbersd-monitoring.png ├── stylesheets │ ├── dashboard-min.css │ └── dashboard.css ├── javascripts │ ├── bootstrap-min.js │ ├── dashboard-min.js │ ├── dashboard.js │ ├── tasseo.js │ └── rickshaw-min.js └── numbersd.html ├── .travis.yml ├── script ├── assets.rb ├── poll.sh └── generate.rb ├── .gitignore ├── test ├── Properties.hs ├── Benchmarks.hs └── Properties │ ├── Map.hs │ ├── Generators.hs │ ├── Conduit.hs │ ├── Types.hs │ └── Series.hs ├── src ├── Numbers │ ├── Log.hs │ ├── Store.hs │ ├── Whisper.hs │ ├── Map.hs │ ├── Map │ │ └── Internal.hs │ ├── Http.hs │ ├── Whisper │ │ └── Series.hs │ ├── Conduit.hs │ ├── Config.hs │ └── Types.hs └── Main.hs ├── Makefile ├── numbersd.cabal ├── README.md └── LICENSE /numbersd: -------------------------------------------------------------------------------- 1 | dist/build/numbersd/numbersd -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /assets/images/spin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/spin.gif -------------------------------------------------------------------------------- /assets/images/spin-night.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/spin-night.gif -------------------------------------------------------------------------------- /assets/images/numbersd-graphites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/numbersd-graphites.png -------------------------------------------------------------------------------- /assets/images/statsd-monitoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/statsd-monitoring.png -------------------------------------------------------------------------------- /assets/images/numbersd-broadcasters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/numbersd-broadcasters.png -------------------------------------------------------------------------------- /assets/images/numbersd-downstreams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/numbersd-downstreams.png -------------------------------------------------------------------------------- /assets/images/numbersd-monitoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brendanhay/numbersd/HEAD/assets/images/numbersd-monitoring.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: haskell 2 | before_install: 3 | - cabal install --only-dependencies --enable-tests --force-reinstall 4 | install: 5 | - cabal configure --enable-tests 6 | - cabal build 7 | script: 8 | - cabal test --show-details=always 9 | -------------------------------------------------------------------------------- /script/assets.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "listen" 5 | 6 | callback = Proc.new { puts `make assets` } 7 | 8 | Listen.to("assets/stylesheets", "assets/javascripts") 9 | .ignore(/-min\.(css|js)$/) 10 | .change(&callback) 11 | .start 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | .*.sw[a-z] 4 | */#*# 5 | #*# 6 | *.org 7 | dist 8 | cabal-dev 9 | Procfile 10 | .env 11 | *.o 12 | *.hi 13 | *.chi 14 | *.chs.h 15 | *.org 16 | *.prof* 17 | *.aux* 18 | *.ps* 19 | *.hp* 20 | *.tar 21 | *.gz 22 | *.tmp 23 | TAGS 24 | tags 25 | yuicompressor-* 26 | numbersd.js 27 | numbersd.css 28 | -------------------------------------------------------------------------------- /script/poll.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RANDOM=$$; 4 | 5 | usage() { 6 | echo "Usage: -c [port]" 7 | exit 1 8 | } 9 | 10 | while getopts ":c:" opt 11 | do 12 | case $opt in 13 | c) CONNECT=$OPTARG;; 14 | *) usage;; 15 | esac 16 | done 17 | 18 | echo "poll.sh -c $CONNECT" 19 | 20 | while true 21 | do 22 | url="http://127.0.0.1:${CONNECT-8126}/numbersd.whisper" 23 | echo $url 24 | curl -f -m 1 $url > /dev/null 25 | sleep "5.${RANDOM:0:1}" 26 | done 27 | -------------------------------------------------------------------------------- /test/Properties.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Properties 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Properties (main) where 14 | 15 | import Properties.Conduit 16 | import Properties.Map 17 | import Properties.Series 18 | import Properties.Types 19 | import Test.Framework 20 | 21 | main :: IO () 22 | main = defaultMain tests 23 | 24 | tests :: [Test] 25 | tests = 26 | [ typeProperties 27 | , conduitProperties 28 | , mapProperties 29 | , seriesProperties 30 | ] 31 | -------------------------------------------------------------------------------- /script/generate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "socket" 4 | 5 | WORDS = %w{bucolic bungalow conflate cynosure denouement desuetude ephemeral epiphany} 6 | TYPES = %w{c g ms s} 7 | HOST = "127.0.0.1" 8 | PORT = 8125 9 | 10 | module Math 11 | def self.max(a, b) 12 | a > b ? a : b 13 | end 14 | 15 | def self.min(a, b) 16 | a < b ? a : b 17 | end 18 | end 19 | 20 | def send(sock, val) 21 | key = WORDS.sample 22 | type = TYPES.sample 23 | samp = [0.1, 0.5, nil].sample 24 | rate = "|@#{samp}" if samp 25 | msg = "#{key}:#{val}|#{type}#{rate}" 26 | puts msg 27 | sock.send(msg, 0, HOST, PORT) 28 | end 29 | 30 | numbers = Enumerator.new do |gen| 31 | val = 1 32 | i = 0 33 | loop do 34 | i = i + 0.2 35 | val = Math.max(0, Math.min(1000, val + 0.8 * rand - 0.4 + 0.2 * Math.cos(i))) 36 | gen.yield val 37 | end 38 | end 39 | 40 | @sock = UDPSocket.new 41 | 42 | SAMPLE = [] 43 | 44 | while true do 45 | (1..10).to_a.sample.times do 46 | send(@sock, numbers.next()) 47 | end 48 | 49 | sleep 1 50 | end 51 | -------------------------------------------------------------------------------- /src/Numbers/Log.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Log 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Log where 14 | 15 | import Control.Monad 16 | import Numbers.Types 17 | import System.Log.FastLogger 18 | import System.IO 19 | import System.IO.Unsafe 20 | 21 | infoL :: Loggable a => a -> IO () 22 | infoL = errorL 23 | 24 | errorL :: Loggable a => a -> IO () 25 | errorL = logL defaultLogger 26 | 27 | logL :: Loggable a => Logger -> a -> IO () 28 | logL logger s = loggerPutBuilder logger $ s &&> "\n" 29 | 30 | defaultLogger :: Logger 31 | defaultLogger = unsafePerformIO $ mkLogger True stdout 32 | {-# NOINLINE defaultLogger #-} 33 | 34 | newLogger :: Loggable a => FilePath -> IO (a -> IO ()) 35 | newLogger path = do 36 | h <- case path of 37 | "stdout" -> return stdout 38 | "stderr" -> return stderr 39 | _ -> openFile path AppendMode 40 | logL `liftM` mkLogger True h 41 | -------------------------------------------------------------------------------- /test/Benchmarks.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Benchmarks 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Benchmarks where 14 | 15 | import Criterion.Main 16 | import Numbers.Types 17 | 18 | main :: IO () 19 | main = return () 20 | 21 | -- let a = .. 22 | -- b = .. 23 | -- c = .. 24 | -- evaluate (a `seq` a' `seq` b `seq` b' `seq` ()) 25 | -- defaultMain 26 | -- [ bgroup "listArray" 27 | -- [ bench "UArray" $ whnf aListArray l 28 | -- , bench "BitArray" $ whnf bListArray l 29 | -- ] 30 | -- , bgroup "elems" 31 | -- [ bench "UArray" $ whnf aElems a 32 | -- , bench "BitArray" $ whnf bElems b 33 | -- ] 34 | -- ] 35 | 36 | 37 | -- sCreate :: 38 | 39 | -- sInsert :: Series 40 | 41 | times :: IO [Time] 42 | times = do 43 | ts <- currentTime 44 | return $! map ((ts +) . Time) steps 45 | 46 | steps :: [Int] 47 | steps = take 100 . scanl1 (+) $ repeat 10 48 | 49 | -------------------------------------------------------------------------------- /assets/stylesheets/dashboard-min.css: -------------------------------------------------------------------------------- 1 | body{background:#f2f2f2}.navbar-static-top{margin-bottom:20px}table{background:#fff}.table-bordered{border:solid 1px #e2e2e2;border-left:0}.table-bordered th{background:#f9f9f9}.table-bordered th,.table-bordered td{border-left:solid 1px #e2e2e2}.graph svg{padding-top:3px;background-color:#fff;border:1px solid #e2e2e2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.graph{position:relative;float:left;margin:5px 10px 0 0}.false{width:350px;height:104px}.overlay-name{position:absolute;font-size:16px;text-align:left;width:150px;left:16px;bottom:20px;z-index:200}.overlay-number{position:absolute;font-size:36px;font-weight:bold;right:16px;bottom:20px;z-index:200;opacity:.7;color:#036;text-shadow:-1px -1px 0 rgba(255,255,255,0.3),1px -1px 0 rgba(255,255,255,0.3),-1px 1px 0 rgba(255,255,255,0.3),1px 1px 0 rgba(255,255,255,0.3)}.graph svg{padding-top:3px;background-color:#fff;border:1px solid #e2e2e2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.graph,.false{position:relative;float:left;margin:5px 10px 0 0}.false{width:350px;height:104px}.overlay-name{position:absolute;font-size:16px;text-align:left;width:150px;left:16px;bottom:20px;z-index:200}.overlay-number{position:absolute;font-size:36px;font-weight:bold;right:16px;bottom:20px;z-index:200;opacity:.7;color:#036;text-shadow:-1px -1px 0 rgba(255,255,255,0.3),1px -1px 0 rgba(255,255,255,0.3),-1px 1px 0 rgba(255,255,255,0.3),1px 1px 0 rgba(255,255,255,0.3)} -------------------------------------------------------------------------------- /assets/javascripts/bootstrap-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-tab.js 4 | * Copyright 2012 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f,g;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active:last a")[0],g=a.Event("show",{relatedTarget:e}),b.trigger(g);if(g.isDefaultPrevented())return;f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery) -------------------------------------------------------------------------------- /src/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoOverloadedStrings #-} 2 | 3 | -- | 4 | -- Module : Main 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Main where 16 | 17 | import Control.Concurrent.Async 18 | import Control.Concurrent.STM 19 | import Control.Monad 20 | import Data.Maybe (catMaybes) 21 | import Numbers.Conduit 22 | import Numbers.Config 23 | import Numbers.Http 24 | import Numbers.Log 25 | import Numbers.Store 26 | 27 | main :: IO () 28 | main = withSocketsDo $ do 29 | conf@Config{..} <- parseConfig 30 | 31 | buf <- atomically $ newTBQueue _buffer 32 | infoL "Buffering..." 33 | 34 | ls <- mapM (asyncLink . (`sourceUri` buf)) _listeners 35 | infoL "Listeners started..." 36 | 37 | ss <- sequence $ 38 | catMaybes [sinkHttp conf, sinkLog _logEvents] 39 | ++ map (newSink (graphite _prefix)) _graphites 40 | ++ map (newSink broadcast) _broadcasts 41 | ++ map (newSink downstream) _downstreams 42 | infoL "Sinks started..." 43 | 44 | sto <- asyncLink $ runStore _percentiles _interval ss buf 45 | infoL "Store started..." 46 | 47 | void . waitAnyCancel $ sto:ls 48 | 49 | asyncLink :: IO a -> IO (Async a) 50 | asyncLink io = do 51 | a <- async io 52 | link a 53 | return a -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | CABAL := `which cabal-dev` 3 | 4 | # 5 | # Build 6 | # 7 | 8 | .PHONY: install build install clean dist test conf prof 9 | 10 | all: build 11 | 12 | build: 13 | $(CABAL) build 14 | 15 | install: 16 | $(CABAL) install 17 | 18 | clean: 19 | $(CABAL) clean 20 | 21 | # 22 | # Configure 23 | # 24 | 25 | conf: 26 | $(CABAL) configure 27 | $(MAKE) build 28 | 29 | bench: 30 | $(CABAL) configure --enable-benchmarks 31 | $(MAKE) build 32 | 33 | test: 34 | $(CABAL) configure --enable-tests 35 | $(MAKE) build 36 | 37 | prof: 38 | $(CABAL) configure --enable-executable-profiling 39 | $(MAKE) build 40 | 41 | # 42 | # Interactive 43 | # 44 | 45 | ghci: 46 | $(CABAL) ghci 47 | 48 | # 49 | # Release 50 | # 51 | 52 | .PHONY: dist assets wipe 53 | 54 | # Where numersd serves assets from 55 | RES := assets 56 | 57 | copy: 58 | -@mkdir -p dist/tar/$(RES) 59 | cp -rf $(RES)/numbersd.* dist/tar/$(RES)/ 60 | cp -f README.md LICENSE dist/build/numbersd/numbersd dist/tar/ 61 | 62 | dist: copy 63 | $(eval VER = $(shell dist/tar/numbersd -V | awk '{sub(/-.*/,"",$$3);print $$3}')) 64 | cd dist/tar && tar -czf ../numbersd-$(VER).tar.gz * 65 | -@rm -rf dist/tar 66 | 67 | # 68 | # Assets 69 | # 70 | 71 | YUI=java -jar yuicompressor-2.4.7.jar 72 | FLAGS=--charset utf-8 73 | 74 | assets: wipe numbersd.css numbersd.js 75 | 76 | numbersd.css: 77 | $(MAKE) $(RES)/stylesheets/dashboard-min.css -B 78 | cd $(RES)/stylesheets && cat \ 79 | bootstrap-min.css \ 80 | dashboard-min.css \ 81 | > ../numbersd.css 82 | 83 | numbersd.js: 84 | $(MAKE) $(RES)/javascripts/dashboard-min.js -B 85 | cd $(RES)/javascripts && cat \ 86 | jquery-min.js \ 87 | d3.v2-min.js \ 88 | rickshaw-min.js \ 89 | bootstrap-min.js \ 90 | > ../numbersd.js 91 | 92 | wipe: 93 | -@rm $(RES)/numbersd.css $(RES)/numbersd.js 94 | 95 | %-min.css: %.css 96 | $(YUI) $(FLAGS) --type css -o $@ $< 97 | 98 | %-min.js: %.js 99 | $(YUI) $(FLAGS) --type js -o $@ $< 100 | -------------------------------------------------------------------------------- /src/Numbers/Store.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Store 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Store ( 14 | runStore 15 | ) where 16 | 17 | import Control.Monad 18 | import Control.Monad.IO.Class 19 | import Control.Concurrent.STM 20 | import Numbers.Conduit 21 | import Numbers.Types 22 | 23 | import qualified Data.ByteString.Char8 as BS 24 | import qualified Numbers.Map as M 25 | 26 | runStore :: [Int] 27 | -> Int 28 | -> [EventSink] 29 | -> TBQueue BS.ByteString 30 | -> IO () 31 | runStore qs n sinks q = do 32 | m <- liftIO . M.empty $ M.Continue n f 33 | forever $ atomically (readTBQueue q) >>= liftIO . parse sinks m 34 | where 35 | f k m ts = mapM_ (\p -> pushEvents sinks [Flush k m ts, Aggregate p ts]) 36 | $! calculate qs n k m 37 | 38 | parse :: [EventSink] -> M.Map Key Metric -> BS.ByteString -> IO () 39 | parse sinks m bstr = forM_ (filter (not . BS.null) $ BS.lines bstr) f 40 | where 41 | f b = do 42 | pushEvents sinks [Receive b] 43 | measure "packets_received" m 44 | case decode lineParser b of 45 | Just (k, v) -> do 46 | measure "num_stats" m 47 | pushEvents sinks [Parse k v] 48 | insert k v m 49 | Nothing -> do 50 | measure "bad_lines_seen" m 51 | pushEvents sinks [Invalid b] 52 | 53 | measure :: Key -> M.Map Key Metric -> IO () 54 | measure = flip insert (Counter 1) 55 | 56 | insert :: Key -> Metric -> M.Map Key Metric -> IO () 57 | insert key val = M.update key (aggregate val) 58 | -------------------------------------------------------------------------------- /src/Numbers/Whisper.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Whisper 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Whisper ( 14 | -- * Opaque 15 | Whisper 16 | , newWhisper 17 | 18 | -- * Operations 19 | , insert 20 | , fetch 21 | , keys 22 | ) where 23 | 24 | import Control.Applicative ((<$>)) 25 | import Control.Arrow (second) 26 | import Control.Monad (liftM) 27 | import Data.Maybe 28 | import Numbers.Types 29 | import Numbers.Whisper.Series (Resolution, Series, Step) 30 | 31 | import qualified Numbers.Map as M 32 | import qualified Numbers.Whisper.Series as S 33 | 34 | data Whisper = Whisper 35 | { _res :: Resolution 36 | , _step :: Step 37 | , _db :: M.Map Key Series 38 | } 39 | 40 | newWhisper :: Resolution -> Step -> IO Whisper 41 | newWhisper res step = do 42 | db <- M.empty $ M.Reset res (\_ _ _ -> return ()) 43 | return $! Whisper (res `div` step) step db 44 | -- ^ Investigate implications of div absolute rounding torwards zero 45 | 46 | insert :: Time -> Point -> Whisper -> IO () 47 | insert ts (P k v) Whisper{..} = 48 | M.update k (maybe (S.create _res _step ts v) (S.update ts v)) _db 49 | 50 | fetch :: Time -> Time -> Whisper -> Maybe [Key] -> IO [(Key, Series)] 51 | fetch from to Whisper{..} mks = map (second (S.fetch from to)) `liftM` 52 | case mks of 53 | Nothing -> M.toList _db 54 | Just ks -> catMaybes <$> mapM f ks 55 | where 56 | f :: Key -> IO (Maybe (Key, Series)) 57 | f k = do 58 | mv <- M.lookup k _db 59 | return $ (\v -> (k, v)) <$> mv 60 | 61 | keys :: Whisper -> IO [Key] 62 | keys = M.keys . _db -------------------------------------------------------------------------------- /assets/stylesheets/dashboard.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f2f2f2; 3 | } 4 | 5 | .navbar-static-top { 6 | margin-bottom: 20px; 7 | } 8 | 9 | table { 10 | background: #fff; 11 | } 12 | 13 | .table-bordered { 14 | border: solid 1px #e2e2e2; 15 | border-left: 0; 16 | } 17 | 18 | .table-bordered th { 19 | background: #f9f9f9; 20 | } 21 | 22 | .table-bordered th, 23 | .table-bordered td { 24 | border-left: solid 1px #e2e2e2; 25 | } 26 | 27 | .graph svg { 28 | padding-top: 3px; 29 | background-color: #fff; 30 | border: 1px solid #e2e2e2; 31 | -webkit-border-radius: 4px; 32 | -moz-border-radius: 4px; 33 | border-radius: 4px; 34 | } 35 | 36 | .graph { 37 | position: relative; 38 | float: left; 39 | margin: 5px 10px 0 0; 40 | } 41 | 42 | .false { 43 | width: 350px; 44 | height: 104px; 45 | } 46 | 47 | .overlay-name { 48 | position: absolute; 49 | font-size: 16px; 50 | text-align: left; 51 | width: 150px; 52 | left: 16px; 53 | bottom: 20px; 54 | z-index: 200; 55 | } 56 | 57 | .overlay-number { 58 | position: absolute; 59 | font-size: 36px; 60 | font-weight: bold; 61 | right: 16px; 62 | bottom: 20px; 63 | z-index: 200; 64 | opacity: 0.7; 65 | color: #003366; 66 | text-shadow: 67 | -1px -1px 0 rgba(255,255,255,0.3), 68 | 1px -1px 0 rgba(255,255,255,0.3), 69 | -1px 1px 0 rgba(255,255,255,0.3), 70 | 1px 1px 0 rgba(255,255,255,0.3); 71 | } 72 | 73 | .graph svg { 74 | padding-top: 3px; 75 | background-color: #fff; 76 | border: 1px solid #e2e2e2; 77 | -webkit-border-radius: 4px; 78 | -moz-border-radius: 4px; 79 | border-radius: 4px; 80 | } 81 | 82 | .graph, .false { 83 | position: relative; 84 | float: left; 85 | margin: 5px 10px 0 0; 86 | } 87 | 88 | .false { 89 | width: 350px; 90 | height: 104px; 91 | } 92 | 93 | .overlay-name { 94 | position: absolute; 95 | font-size: 16px; 96 | text-align: left; 97 | width: 150px; 98 | left: 16px; 99 | bottom: 20px; 100 | z-index: 200; 101 | } 102 | 103 | .overlay-number { 104 | position: absolute; 105 | font-size: 36px; 106 | font-weight: bold; 107 | right: 16px; 108 | bottom: 20px; 109 | z-index: 200; 110 | opacity: 0.7; 111 | color: #003366; 112 | text-shadow: 113 | -1px -1px 0 rgba(255,255,255,0.3), 114 | 1px -1px 0 rgba(255,255,255,0.3), 115 | -1px 1px 0 rgba(255,255,255,0.3), 116 | 1px 1px 0 rgba(255,255,255,0.3); 117 | } 118 | -------------------------------------------------------------------------------- /src/Numbers/Map.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Map 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Map ( 14 | -- * Exported Types 15 | Policy(..) 16 | 17 | -- * Opaque 18 | , Map 19 | , empty 20 | 21 | -- * Functions 22 | , toList 23 | , keys 24 | , lookup 25 | , update 26 | ) where 27 | 28 | import Prelude hiding (lookup) 29 | import Control.Monad 30 | import Control.Monad.IO.Class 31 | import Control.Concurrent 32 | import Control.Concurrent.Async hiding (wait) 33 | import Numbers.Types hiding (P) 34 | 35 | import qualified Numbers.Map.Internal as I 36 | 37 | type Handler k v = k -> v -> Time -> IO () 38 | 39 | data Policy k v = Reset Int (Handler k v) 40 | | Continue Int (Handler k v) 41 | | NoPolicy 42 | 43 | data Map k v = Map 44 | { _policy :: Policy k v 45 | , _imap :: I.Map k v 46 | } 47 | 48 | empty :: MonadIO m => Policy k v -> m (Map k v) 49 | empty p = Map p `liftM` I.empty 50 | 51 | toList :: MonadIO m => Map k v -> m [(k, v)] 52 | toList = I.toList . _imap 53 | 54 | keys :: MonadIO m => Map k v -> m [k] 55 | keys = I.keys . _imap 56 | 57 | lookup :: (MonadIO m, Ord k) => k -> Map k v -> m (Maybe v) 58 | lookup key = I.lookup key . _imap 59 | 60 | update :: (MonadIO m, Ord k) => k -> (Maybe v -> v) -> Map k v -> m () 61 | update key f Map{..} = do 62 | u <- I.update key f _imap 63 | case u of 64 | I.New -> scheduleSweep key _policy _imap 65 | I.Existing -> return () 66 | 67 | scheduleSweep :: (MonadIO m, Ord k) => k -> Policy k v -> I.Map k v -> m () 68 | scheduleSweep _ NoPolicy _ = return () 69 | scheduleSweep key (Reset d h) imap = sweep d h I.modify key imap 70 | scheduleSweep key (Continue d h) imap = sweep d h I.create key imap 71 | 72 | sweep :: (MonadIO m, Ord k) => Int -> Handler k v -> (I.Entry v -> Time) -> k -> I.Map k v -> m () 73 | sweep delay handle lastTime key imap = do 74 | liftIO $ async waitAndCheck >>= link 75 | where 76 | waitAndCheck = do 77 | _ <- liftIO . threadDelay $ delay * 1000000 78 | now <- liftIO currentTime 79 | me <- I.deleteIf (\e -> lastTime e + fromIntegral delay <= now) key imap 80 | maybe waitAndCheck (\e -> handle key (I.value e) now) me 81 | -------------------------------------------------------------------------------- /src/Numbers/Map/Internal.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Map 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Map.Internal ( 14 | -- * Exported Types 15 | Entry(..) 16 | , Update(..) 17 | 18 | -- * Opaque 19 | , Map 20 | , empty 21 | 22 | -- * Functions 23 | , toList 24 | , keys 25 | , lookup 26 | , update 27 | , deleteIf 28 | ) where 29 | 30 | import Prelude hiding (lookup) 31 | import Control.Applicative ((<$>)) 32 | import Control.Arrow (second) 33 | import Control.Monad 34 | import Control.Monad.IO.Class 35 | import Control.Concurrent.STM 36 | import Numbers.Types hiding (P) 37 | 38 | import qualified Data.Map as M 39 | 40 | data Entry v = Entry { 41 | create :: Time 42 | , modify :: Time 43 | , value :: v 44 | } 45 | 46 | type Map k v = TVar (M.Map k (Entry v)) 47 | 48 | data Update = New | Existing 49 | deriving (Eq, Show) 50 | 51 | empty :: MonadIO m => m (Map k v) 52 | empty = atomic $ newTVar M.empty 53 | 54 | toList :: MonadIO m => Map k v -> m [(k, v)] 55 | toList tm = map (second value) `liftM` (M.toList `atomicRead` tm) 56 | 57 | keys :: MonadIO m => Map k v -> m [k] 58 | keys tm = M.keys `atomicRead` tm 59 | 60 | lookup :: (MonadIO m, Ord k) => k -> Map k v -> m (Maybe v) 61 | lookup key tm = do 62 | m <- M.lookup key `atomicRead` tm 63 | return $ value <$> m 64 | 65 | update :: (MonadIO m, Ord k) => k -> (Maybe v -> v) -> Map k v -> m Update 66 | update key f tm = do 67 | now <- liftIO currentTime 68 | atomic $! do 69 | me <- M.lookup key <$> readTVar tm 70 | let val = f $ fmap value me 71 | (u, e') = maybe (New, Entry now now val) (\e -> (Existing, e{modify = now, value = val})) me 72 | modifyTVar' tm $ M.insert key e' 73 | return u 74 | 75 | deleteIf :: (MonadIO m, Ord k) => (Entry v -> Bool) -> k -> Map k v -> m (Maybe (Entry v)) 76 | deleteIf f key tm = atomic $! do 77 | me <- M.lookup key <$> readTVar tm 78 | case f <$> me of 79 | Just True -> do 80 | modifyTVar' tm $ M.delete key 81 | return me 82 | _ -> return Nothing 83 | 84 | atomicRead :: MonadIO m => (a -> b) -> TVar a -> m b 85 | atomicRead f tm = f `liftM` atomic (readTVar tm) 86 | 87 | atomic :: MonadIO m => STM a -> m a 88 | atomic = liftIO . atomically 89 | -------------------------------------------------------------------------------- /assets/javascripts/dashboard-min.js: -------------------------------------------------------------------------------- 1 | var graphs=[];var datum=[];var aliases=[];var descriptions=[];var realMetrics=[];var period=(typeof period=="undefined")?5:period;var originalPeriod=period;function gatherRealMetrics(){var b=0;for(var a=0;ag){if(d>=h){graphs[f].series[0].color="#d59295"}else{if(d>=g){graphs[f].series[0].color="#f5cb56"}else{graphs[f].series[0].color="#afdab1"}}}else{if(d<=h){graphs[f].series[0].color="#d59295"}else{if(d<=g){graphs[f].series[0].color="#f5cb56"}else{graphs[f].series[0].color="#b5d9ff"}}}if(a){updateGraphs(f)}}c=null});if(!a){for(var b=0;b0){for(var e=0;eNF')}}function buildContainers(){var c=0;for(var b=0;b');c++}else{if(metrics[b].target.match(/^'+metrics[b].target+"");c++}else{var a=b-c;$(".main").append('
')}}}}gatherRealMetrics();buildContainers();constructGraphs();constructUrl(period);var toolbar=(typeof toolbar=="undefined")?true:toolbar;if(!toolbar){$("div.toolbar").css("display","none")}for(var i=0;i')}}refreshData("now");var refreshInterval=(typeof refresh=="undefined")?2000:refresh;var refreshId=setInterval(refreshData,refreshInterval); -------------------------------------------------------------------------------- /test/Properties/Map.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | 3 | -- | 4 | -- Module : Properties.Map 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Properties.Map ( 16 | mapProperties 17 | ) where 18 | 19 | import Control.Applicative ((<$>)) 20 | import Control.Concurrent.Async 21 | import Control.Monad 22 | import Data.List 23 | import Data.Ord 24 | import Test.Framework 25 | import Test.Framework.Providers.QuickCheck2 26 | import Test.QuickCheck 27 | import Test.QuickCheck.Monadic 28 | import qualified Numbers.Map as M 29 | 30 | 31 | mapProperties :: Test 32 | mapProperties = testGroup "map" 33 | [ testProperty "no values in an empty map" prop_no_vals_in_empty_map 34 | , testProperty "no policy map is just a map" prop_no_policy_map_is_just_map 35 | ] 36 | 37 | prop_no_vals_in_empty_map :: Property 38 | prop_no_vals_in_empty_map = monadicIO $ do 39 | m <- emptyNoPolicyMap 40 | assertMapContains (TestData []) m 41 | 42 | prop_no_policy_map_is_just_map :: TestData -> Property 43 | prop_no_policy_map_is_just_map xs = monadicIO $ do 44 | m <- emptyNoPolicyMap 45 | _ <- addAll xs m 46 | assertMapContains (sumUp xs) m 47 | 48 | sumUp :: TestData -> TestData 49 | sumUp (TestData xs) = TestData . map (\kvs -> (fst $ head kvs, TestValue . sum $ map (val . snd) kvs)) . groupBy (\a b -> fst a == fst b) $ sortBy (comparing fst) xs 50 | 51 | addAll :: TestData -> M.Map TestKey TestValue -> PropertyM IO () 52 | addAll (TestData xs) m = run . void $ mapConcurrently (flip add m) xs 53 | where 54 | add :: (TestKey, TestValue) -> M.Map TestKey TestValue -> IO () 55 | add (k,TestValue v) = M.update k (maybe (TestValue v) (\(TestValue v') -> TestValue (v + v'))) 56 | 57 | emptyNoPolicyMap :: PropertyM IO (M.Map TestKey TestValue) 58 | emptyNoPolicyMap = run $ M.empty M.NoPolicy 59 | 60 | assertMapContains :: TestData -> M.Map TestKey TestValue -> PropertyM IO () 61 | assertMapContains (TestData expected) m = do 62 | actual <- run $ M.toList m 63 | actualkeys <- run $ M.keys m 64 | assertEqual "keys" (map fst expected) actualkeys 65 | mapM_ (\(k,v) -> do v' <- run $ M.lookup k m; assertEqual ("lookup " ++ show k) (Just v) v') expected 66 | assertEqual "toList" expected actual 67 | 68 | val :: TestValue -> Int 69 | val (TestValue v) = v 70 | 71 | data TestData = TestData [(TestKey, TestValue)] 72 | deriving Show 73 | 74 | data TestKey = TestKey Char 75 | deriving (Eq, Ord, Show) 76 | 77 | data TestValue = TestValue Int 78 | deriving (Eq, Ord, Show) 79 | 80 | instance Arbitrary TestData where 81 | arbitrary = do 82 | n <- oneof $ map return [50..100] 83 | TestData <$> replicateM n arbitrary 84 | 85 | instance Arbitrary TestKey where 86 | arbitrary = TestKey <$> choose ('A', 'Z') 87 | 88 | instance Arbitrary TestValue where 89 | arbitrary = TestValue <$> choose (1, 30) 90 | 91 | assertEqual :: (Eq a, Show a) => String -> a -> a -> PropertyM IO () 92 | assertEqual _ a b | a == b = assert True 93 | assertEqual msg a b = do 94 | run . putStrLn $ msg ++ " assertEqual failed.." 95 | run . putStrLn $ "Expected: " ++ show a 96 | run . putStrLn $ "Actual : " ++ show b 97 | assert False -------------------------------------------------------------------------------- /src/Numbers/Http.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | -- | 4 | -- Module : Numbers.Http 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Numbers.Http ( 16 | sinkHttp 17 | ) where 18 | 19 | import Blaze.ByteString.Builder hiding (flush) 20 | import Control.Monad.IO.Class 21 | import Control.Concurrent.Async 22 | import Data.Aeson 23 | import Data.Maybe 24 | import Network.Wai 25 | import Network.Wai.Application.Static 26 | import Network.Wai.Handler.Warp 27 | import Network.HTTP.Types 28 | import Numbers.Log 29 | import Numbers.Types 30 | import Numbers.Conduit 31 | import Numbers.Config 32 | 33 | import qualified Data.HashMap.Strict as H 34 | import qualified Numbers.Whisper as W 35 | 36 | data Format = Raw | Json 37 | 38 | sinkHttp :: Config -> Maybe (IO EventSink) 39 | sinkHttp conf = (flip fmap) (_httpPort conf) $ \port -> do 40 | w <- W.newWhisper (_resolution conf) (_interval conf) 41 | async (run port $ serve conf w) >>= link 42 | infoL $ "Serving /numbersd and /numbersd/render/ on http://0.0.0.0:" <&& port 43 | runSink . awaitForever $ \e -> case e of 44 | Aggregate p ts -> liftIO $ W.insert ts p w 45 | _ -> return () 46 | 47 | -- | Serves whispers as if served by graphite http://graphite.wikidot.com/url-api-reference 48 | serve :: Config -> W.Whisper -> Application 49 | serve conf whis req = case pathInfo req of 50 | ["numbersd", "config"] -> 51 | liftIO $ renderConfig fmt conf whis 52 | ["numbersd", "render"] -> liftIO $ do 53 | t <- currentTime 54 | renderSeries t t mts fmt whis 55 | ["numbersd"] -> 56 | static $ req { pathInfo = ["numbersd.html"] } 57 | _ -> 58 | static req 59 | where 60 | static = staticApp $ defaultWebAppSettings "assets" 61 | fmt = getFormat req 62 | mts = getTargets req 63 | 64 | getFormat :: Request -> Format 65 | getFormat req = case maybeParams format req of 66 | Just (x:_) -> x 67 | _ -> Raw 68 | where 69 | format ("format", Just "raw") = Just Raw 70 | format ("format", Just "json") = Just Json 71 | format _ = Nothing 72 | 73 | getTargets :: Request -> Maybe [Key] 74 | getTargets = maybeParams target 75 | where 76 | target ("target", Just x) = Just $ Key x 77 | target _ = Nothing 78 | 79 | maybeParams :: (QueryItem -> Maybe a) -> Request -> Maybe [a] 80 | maybeParams f = g . catMaybes . map f . queryString 81 | where 82 | g [] = Nothing 83 | g xs = Just xs 84 | 85 | success :: Builder -> Response 86 | success = ResponseBuilder status200 87 | [ ("Content-Type", "text/plain") 88 | , ("Access-Control-Allow-Origin", "*") 89 | ] 90 | 91 | renderSeries :: Time -> Time -> Maybe [Key] -> Format -> W.Whisper -> IO Response 92 | renderSeries from to mks fmt whis = do 93 | ss <- W.fetch from to whis mks 94 | return . success $ f ss 95 | where 96 | f = case fmt of 97 | Raw -> build . map (\(Key k, s) -> k &&> "," &&& s &&> "\n") 98 | Json -> copyLazyByteString . encode 99 | 100 | renderConfig :: Format -> Config -> W.Whisper -> IO Response 101 | renderConfig Raw _ _ = return . success $ copyByteString "" 102 | renderConfig Json conf whis = do 103 | ks <- W.keys whis 104 | return . success . copyLazyByteString . encode 105 | . Object $ H.insert "metrics" (toJSON ks) m 106 | where 107 | (Object m) = toJSON conf 108 | -------------------------------------------------------------------------------- /test/Properties/Generators.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | 3 | -- | 4 | -- Module : Properties.Generators 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Properties.Generators where 16 | 17 | import Control.Applicative ((<$>)) 18 | import Data.Conduit hiding (Flush) 19 | import Data.Foldable (toList) 20 | import Data.Vector (fromList) 21 | import Numbers.Conduit 22 | import Numbers.Types 23 | import Numeric (showFFloat) 24 | import Test.QuickCheck 25 | 26 | import qualified Data.ByteString.Char8 as BS 27 | import qualified Data.Conduit.List as CL 28 | import qualified Data.Set as S 29 | 30 | instance Arbitrary BS.ByteString where 31 | arbitrary = BS.pack <$> arbitrary 32 | 33 | newtype SafeStr = SafeStr String 34 | deriving (Show) 35 | 36 | instance Arbitrary SafeStr where 37 | arbitrary = SafeStr <$> (listOf1 $ elements xs) 38 | where 39 | xs = ['_', '-'] ++ ['a'..'z'] ++ ['A'..'Z'] 40 | 41 | newtype UnsafeStr = UnsafeStr String 42 | deriving (Show) 43 | 44 | instance Arbitrary UnsafeStr where 45 | arbitrary = UnsafeStr <$> (listOf1 $ elements [' '..'~']) 46 | 47 | instance Arbitrary Key where 48 | arbitrary = do 49 | SafeStr s <- arbitrary 50 | return . Key $ BS.pack s 51 | 52 | instance Arbitrary Metric where 53 | arbitrary = oneof 54 | [ Counter <$> arbitrary 55 | , arbitrary >>= \(NonEmpty xs) -> return . Timer $ fromList xs 56 | , Gauge <$> arbitrary 57 | , Set <$> arbitrary 58 | ] 59 | 60 | instance Arbitrary Time where 61 | arbitrary = do 62 | NonNegative n <- arbitrary 63 | return $ Time n 64 | 65 | instance Arbitrary Point where 66 | arbitrary = do 67 | k <- arbitrary 68 | NonNegative v <- arbitrary 69 | return $ P k v 70 | 71 | instance Arbitrary (S.Set Double) where 72 | arbitrary = do 73 | NonEmpty xs <- arbitrary 74 | return . S.fromList $ map f xs 75 | where 76 | f x = read $ showFFloat (Just 1) (x :: Double) "" 77 | 78 | instance Arbitrary Event where 79 | arbitrary = do 80 | s <- BS.pack <$> arbitrary 81 | k <- arbitrary 82 | m <- arbitrary 83 | t <- arbitrary 84 | p <- arbitrary 85 | elements 86 | [ Receive s 87 | , Invalid s 88 | , Parse k m 89 | , Flush k m t 90 | , Aggregate p t 91 | ] 92 | 93 | kindaClose :: (Num a, Fractional a, Ord a) => a -> a -> Bool 94 | kindaClose = thisClose 0.1 95 | 96 | prettyClose :: (Num a, Fractional a, Ord a) => a -> a -> Bool 97 | prettyClose = thisClose 0.0001 98 | 99 | thisClose :: (Num a, Fractional a, Ord a) => a -> a -> a -> Bool 100 | thisClose diff a b 101 | | a > (b - diff) && (a < b + diff) = True 102 | | otherwise = False 103 | 104 | conduitResult :: Monad m => [Event] -> EventConduit m a -> m [a] 105 | conduitResult es con = CL.sourceList es $= con $$ CL.consume 106 | 107 | -- | The very pinnacle of scientific engineering 108 | kindaCloseM :: Metric -> Metric -> Bool 109 | kindaCloseM a b = case (a, b) of 110 | (Counter x, Counter y) -> kindaClose x y 111 | (Gauge x, Gauge y) -> kindaClose x y 112 | (Timer xs, Timer ys) -> f xs ys 113 | (Set xs, Set ys) -> f xs ys 114 | _ -> False 115 | where 116 | f x y | length i /= length n = error "Not equal lengths" 117 | | otherwise = and . map (uncurry kindaClose) $ zip i n 118 | where 119 | i = toList x 120 | n = toList y 121 | -------------------------------------------------------------------------------- /numbersd.cabal: -------------------------------------------------------------------------------- 1 | name: numbersd 2 | version: 0.1.0 3 | synopsis: Port of statsd to Haskell 4 | description: Port of statsd to Haskell 5 | license: OtherLicense 6 | license-file: LICENSE 7 | category: Metrics, Monitoring 8 | stability: Experimental 9 | build-type: Simple 10 | cabal-version: >= 1.10 11 | 12 | author: Brendan Hay 13 | maintainer: Brendan Hay 14 | homepage: http://github.com/brendanhay/numbersd 15 | bug-reports: http://github.com/brendanhay/numbersd/issues 16 | 17 | extra-source-files: README.md 18 | 19 | source-repository head 20 | type: git 21 | location: git://github.com/brendanhay/numbersd.git 22 | 23 | executable numbersd 24 | main-is: Main.hs 25 | hs-source-dirs: src 26 | 27 | default-language: Haskell2010 28 | default-extensions: FlexibleInstances 29 | , GeneralizedNewtypeDeriving 30 | , OverloadedStrings 31 | , RecordWildCards 32 | , TypeSynonymInstances 33 | 34 | ghc-options: -Wall -O2 -rtsopts -threaded 35 | ghc-prof-options: -Wall -prof -fprof-auto -auto-all -with-rtsopts=-hc 36 | 37 | build-depends: base > 4 && < 5 38 | , aeson 39 | , async 40 | , attoparsec 41 | , blaze-builder 42 | , bytestring 43 | , cmdargs 44 | , conduit 45 | , containers 46 | , data-lens 47 | , data-lens-template 48 | , fast-logger 49 | , http-types 50 | , monad-control 51 | , network 52 | , network-conduit < 0.6.2 || > 0.6.2 53 | , regex-pcre 54 | , simple-sendfile < 0.2.9 55 | , split 56 | , statistics 57 | , stm 58 | , stm-conduit 59 | , text 60 | , time 61 | , transformers 62 | , unordered-containers 63 | , vector 64 | , wai 65 | , wai-app-static 66 | , warp 67 | 68 | test-suite numbersd-properties 69 | type: exitcode-stdio-1.0 70 | main-is: Properties.hs 71 | hs-source-dirs: src, test 72 | 73 | default-language: Haskell2010 74 | default-extensions: FlexibleInstances 75 | , GeneralizedNewtypeDeriving 76 | , OverloadedStrings 77 | , RecordWildCards 78 | , TypeSynonymInstances 79 | ghc-options: -main-is Properties -Wall -threaded 80 | 81 | build-depends: base > 4 && < 5 82 | , aeson 83 | , async 84 | , attoparsec 85 | , blaze-builder 86 | , bytestring 87 | , cmdargs 88 | , conduit 89 | , containers 90 | , data-lens 91 | , data-lens-template 92 | , fast-logger 93 | , http-types 94 | , monad-control 95 | , network 96 | , network-conduit < 0.6.2 || > 0.6.2 97 | , regex-pcre 98 | , simple-sendfile < 0.2.9 99 | , split 100 | , statistics 101 | , stm 102 | , stm-conduit 103 | , text 104 | , time 105 | , transformers 106 | , vector 107 | , wai 108 | , warp 109 | , test-framework 110 | , test-framework-quickcheck2 111 | , test-framework-hunit 112 | , HUnit 113 | , QuickCheck 114 | -------------------------------------------------------------------------------- /src/Numbers/Whisper/Series.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Whisper.Series 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Whisper.Series ( 14 | -- * Types 15 | Resolution 16 | , Step 17 | , Time(..) 18 | , Interval(..) 19 | 20 | , Series 21 | , resolution 22 | , start 23 | , end 24 | , step 25 | , values 26 | , datapoints 27 | 28 | -- * Constants 29 | , maxResolution 30 | 31 | -- * Series operations 32 | , create 33 | , fetch 34 | , update 35 | ) where 36 | 37 | import Data.Aeson 38 | import Data.List 39 | import Data.Maybe 40 | import Data.Tuple (swap) 41 | import Numbers.Types 42 | 43 | type Resolution = Int 44 | type Step = Int 45 | 46 | newtype Interval = I Int 47 | deriving (Eq, Ord, Show, Enum, Num, Real, Integral) 48 | 49 | toInterval :: Step -> Time -> Interval 50 | toInterval s (Time t) = I $ t - (t `mod` s) 51 | 52 | instance ToJSON Interval where 53 | toJSON (I i) = toJSON i 54 | 55 | instance ToJSON (Interval, Maybe Double) where 56 | toJSON = toJSON . swap 57 | 58 | data Series = SS 59 | { res :: Resolution 60 | , step :: Step 61 | , end :: Interval 62 | , points :: [Maybe Double] 63 | } deriving (Eq, Show) 64 | 65 | instance ToJSON (Key, Series) where 66 | toJSON (k, ss) = object 67 | [ "target" .= k 68 | , "datapoints" .= datapoints ss 69 | ] 70 | 71 | maxResolution :: Resolution 72 | maxResolution = 5 * 60 73 | 74 | resolution :: Series -> Resolution 75 | resolution = res 76 | 77 | values :: Series -> [Maybe Double] 78 | values = reverse . points 79 | 80 | start :: Series -> Interval 81 | start SS{..} = end - fromIntegral (length points * step) 82 | 83 | datapoints :: Series -> [(Interval, Maybe Double)] 84 | datapoints s = zip (timeline (end s) (res s) (step s)) (values s) 85 | 86 | timeline :: Interval -> Resolution -> Step -> [Interval] 87 | timeline t r s = 88 | take r $ iterate (decrementInterval s) t 89 | 90 | decrementInterval :: Step -> Interval -> Interval 91 | decrementInterval s (I t) = I (t - s) 92 | 93 | instance Loggable Interval where 94 | build (I i) = build i 95 | 96 | instance Loggable Series where 97 | build s@SS{..} = start s &&& "," <&& end &&& "," <&& step &&& "|" 98 | <&& intersperse (sbuild ",") (map (maybe (build noneStr) build) $ values s) 99 | 100 | noneStr :: String 101 | noneStr = "None" 102 | 103 | create :: Resolution -> Step -> Time -> Double -> Series 104 | create r s ts val 105 | | r > maxResolution = error $ "Resolution too high: " ++ show r 106 | | otherwise = SS r s (toInterval s ts) (singleton (r - 1) (Just val)) 107 | 108 | fetch :: Time -> Time -> Series -> Series 109 | fetch _ to s@SS{..} = append (toInterval step to) Nothing s 110 | 111 | update :: Time -> Double -> Series -> Series 112 | update ts val s@SS{..} = append (toInterval step ts) (Just val) s 113 | 114 | append :: Interval -> Maybe Double -> Series -> Series 115 | append to val s@SS{..} = s { points = take res p, end = e } 116 | where 117 | d = distance step end to 118 | (e, p) | to <= end = (end, replace d val points) 119 | | d >= res = (to, singleton (res - 1) val) 120 | | otherwise = (to, extend (d - 1) val points) 121 | 122 | distance :: Step -> Interval -> Interval -> Int 123 | distance s from to = abs $ ceiling diff 124 | where 125 | diff = fromIntegral (abs to - abs from) / fromIntegral s :: Double 126 | 127 | replace :: Int -> Maybe Double -> [Maybe Double] -> [Maybe Double] 128 | replace _ _ [] = [] 129 | replace _ Nothing vs = vs 130 | replace n (Just val) (v:vs) 131 | | n == 0 = (Just $ val + fromMaybe 0 v):vs 132 | | otherwise = v:replace (n - 1) (Just val) vs 133 | 134 | singleton :: Int -> Maybe Double -> [Maybe Double] 135 | singleton n = (: replicate n Nothing) 136 | 137 | extend :: Int -> Maybe Double -> [Maybe Double] -> [Maybe Double] 138 | extend n val = (singleton n val ++) 139 | -------------------------------------------------------------------------------- /src/Numbers/Conduit.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts, RankNTypes, ImpredicativeTypes #-} 2 | 3 | -- | 4 | -- Module : Numbers.Conduit 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Numbers.Conduit ( 16 | -- * Exported Types 17 | Event(..) 18 | , EventConduit 19 | 20 | -- * Opaque 21 | , EventSink 22 | , newSink 23 | , runSink 24 | , pushEvents 25 | 26 | -- * Conduits 27 | , graphite 28 | , broadcast 29 | , downstream 30 | 31 | -- * Sources 32 | , sourceUri 33 | 34 | -- * Sinks 35 | , sinkUri 36 | , sinkQueue 37 | , sinkLog 38 | 39 | -- * Re-exports 40 | , awaitForever 41 | , yield 42 | , (=$) 43 | , S.withSocketsDo 44 | ) where 45 | 46 | import Blaze.ByteString.Builder (toByteString) 47 | import Control.Concurrent.Async 48 | import Control.Concurrent.STM 49 | import Control.Exception 50 | import Control.Monad 51 | import Control.Monad.IO.Class 52 | import Control.Monad.Trans.Control (control) 53 | import Data.Conduit hiding (Flush) 54 | import Data.Conduit.Binary 55 | import Data.String 56 | import Numbers.Log 57 | import Numbers.Types 58 | import System.IO 59 | 60 | import qualified Data.ByteString.Char8 as BS 61 | import qualified Network.Socket as S 62 | import qualified Network.Socket.ByteString as SS 63 | import qualified Data.Conduit.List as CL 64 | import qualified Data.Conduit.Network as T 65 | import qualified Data.Conduit.Network.UDP as U 66 | 67 | data Event = Receive BS.ByteString 68 | | Invalid BS.ByteString 69 | | Parse Key Metric 70 | | Flush Key Metric Time 71 | | Aggregate Point Time 72 | deriving (Show) 73 | 74 | type EventConduit m a = Conduit Event m a 75 | 76 | data EventSink = EventSink 77 | { _queue :: TBQueue Event 78 | , _async :: Async () 79 | } 80 | 81 | newSink :: EventConduit IO BS.ByteString -> Uri -> IO EventSink 82 | newSink con uri = runSink $ con =$ transPipe runResourceT (sinkUri uri) 83 | 84 | runSink :: Sink Event IO () -> IO EventSink 85 | runSink sink = do 86 | q <- atomically $ newTBQueue 1024 87 | a <- async $ sourceQueue q $$ sink 88 | link a 89 | return $ EventSink q a 90 | 91 | pushEvents :: [EventSink] -> [Event] -> IO () 92 | pushEvents hs es = sequence_ [f h e | h <- hs, e <- es] 93 | where 94 | f h e = atomically $ writeTBQueue (_queue h) e 95 | 96 | graphite :: Monad m => String -> EventConduit m BS.ByteString 97 | graphite str = awaitForever $ \e -> case e of 98 | Aggregate p ts -> yield . toByteString $ pref &&> "." &&& p &&> " " &&& ts &&> "\n" 99 | _ -> return () 100 | where 101 | pref = BS.pack str 102 | 103 | broadcast :: Monad m => EventConduit m BS.ByteString 104 | broadcast = awaitForever $ \e -> case e of 105 | Receive bs -> yield bs 106 | _ -> return () 107 | 108 | downstream :: Monad m => EventConduit m BS.ByteString 109 | downstream = awaitForever $ \e -> case e of 110 | Flush k m _ -> yield . toByteString $ build (k, m) 111 | _ -> return () 112 | 113 | sourceUri :: Uri -> TBQueue BS.ByteString -> IO () 114 | sourceUri (File f) q = runResourceT $ 115 | either sourceFile sourceIOHandle (uriHandle f) $$ sinkQueue q 116 | sourceUri (Tcp h p) q = runResourceT $ 117 | T.runTCPServer (T.serverSettings p $ host h) app 118 | where 119 | app d = T.appSource d $$ sinkQueue q 120 | sourceUri (Udp h p) q = control $ \run -> 121 | bracket open S.sClose (run . forever . sink) 122 | where 123 | open = U.bindPort p $ host h 124 | sink s = U.sourceSocket s 2048 $$ CL.map U.msgData =$ sinkQueue q 125 | 126 | sinkUri :: MonadResource m => Uri -> Sink BS.ByteString m () 127 | sinkUri (File f) = either sinkFile sinkIOHandle (uriHandle f) 128 | sinkUri uri = bracketP open S.sClose push 129 | where 130 | open = fst `liftM` T.getSocket (_host uri) (_port uri) 131 | push s = awaitForever $ liftIO . SS.sendAll s 132 | 133 | sinkLog :: [String] -> Maybe (IO EventSink) 134 | sinkLog [] = Nothing 135 | sinkLog es = Just $ runSink f 136 | where 137 | f = awaitForever $ \e -> g $ case e of 138 | Receive bs -> ("receive", "Receive: " <&& bs) 139 | Invalid bs -> ("invalid", "Invalid: " <&& bs) 140 | Parse k v -> ("parse" , "Parse: " <&& (k, v)) 141 | Flush k v ts -> ("flush" , "Flush: " <&& (k, v) &&> " " &&& ts) 142 | Aggregate p ts -> ("aggregate", "Aggregate: " <&& p &&> " " &&& ts) 143 | g (k, v) = when (k `elem` es) (liftIO $ infoL v) 144 | 145 | host :: BS.ByteString -> U.HostPreference 146 | host = fromString . BS.unpack 147 | 148 | uriHandle :: BS.ByteString -> Either FilePath (IO Handle) 149 | uriHandle bs = f `fmap` case bs of 150 | "stdin" -> Right stdin 151 | "stderr" -> Right stderr 152 | "stdout" -> Right stdout 153 | path -> Left $ BS.unpack path 154 | where 155 | f hd = hSetBuffering hd LineBuffering >> return hd 156 | 157 | sourceQueue :: MonadIO m => TBQueue a -> Source m a 158 | sourceQueue q = forever $ liftIO (atomically $ readTBQueue q) >>= yield 159 | 160 | sinkQueue :: MonadIO m => TBQueue a -> Sink a m () 161 | sinkQueue q = awaitForever $ liftIO . atomically . writeTBQueue q 162 | -------------------------------------------------------------------------------- /assets/numbersd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | numbersd 6 | 7 | 8 | 9 | 10 | 11 | 25 | 26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
KeyFlagValue
Listeners--listeners
Http Port--http
Buffer Size--buffer
Resolution--resolution
Interval--interval
Percentiles--percentiles
Log Events--events
Prefix--prefix
Graphites--grapihtes
Broadcasters--broadcasts
Downstreams--downstreams
110 | 111 |
112 | 113 |
114 |
115 | 116 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /src/Numbers/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | -- | 4 | -- Module : Numbers.Config 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Numbers.Config ( 16 | -- * Exported Types 17 | Config(..) 18 | 19 | -- * Functions 20 | , parseConfig 21 | ) where 22 | 23 | import Control.Monad 24 | import Data.Aeson 25 | import Data.Lens.Common 26 | import Data.Lens.Template 27 | import Data.List (intersect) 28 | import Data.List.Split (splitOn) 29 | import Data.Monoid (mempty, mconcat) 30 | import Data.Version (showVersion) 31 | import Numbers.Log 32 | import Numbers.Types 33 | import Numbers.Whisper.Series (maxResolution) 34 | import Paths_numbersd (version) 35 | import System.Console.CmdArgs.Explicit 36 | import System.Environment 37 | import System.Exit 38 | 39 | import qualified Data.ByteString.Char8 as BS 40 | 41 | data Config = Help | Version | Config 42 | { _listeners :: [Uri] 43 | , _httpPort :: Maybe Int 44 | , _buffer :: Int 45 | , _resolution :: Int 46 | , _interval :: Int 47 | , _percentiles :: [Int] 48 | , _logEvents :: [String] 49 | , _prefix :: String 50 | , _graphites :: [Uri] 51 | , _broadcasts :: [Uri] 52 | , _downstreams :: [Uri] 53 | } 54 | 55 | $(makeLens ''Config) 56 | 57 | instance Loggable Config where 58 | build Config{..} = mconcat 59 | [ sbuild "Configuration:" 60 | , "\n -> Listeners: " <&& _listeners 61 | , "\n -> HTTP Port: " <&& _httpPort 62 | , "\n -> Buffer Size: " <&& _buffer 63 | , "\n -> Resolution: " <&& _resolution 64 | , "\n -> Flush Interval: " <&& _interval 65 | , "\n -> Percentiles: " <&& _percentiles 66 | , "\n -> Log Events: " <&& _logEvents 67 | , "\n -> Prefix: " <&& _prefix 68 | , "\n -> Graphites: " <&& _graphites 69 | , "\n -> Broadcasts: " <&& _broadcasts 70 | , "\n -> Downstreams: " <&& _downstreams 71 | ] 72 | build _ = mempty 73 | 74 | instance ToJSON Config where 75 | toJSON Config{..} = object 76 | [ "listeners" .= _listeners 77 | , "http_port" .= _httpPort 78 | , "buffer_size" .= _buffer 79 | , "resolution" .= _resolution 80 | , "interval" .= _interval 81 | , "percentiles" .= _percentiles 82 | , "log_events" .= _logEvents 83 | , "prefix" .= _prefix 84 | , "graphites" .= _graphites 85 | , "broadcasts" .= _broadcasts 86 | , "downstreams" .= _downstreams 87 | ] 88 | toJSON _ = Null 89 | 90 | defaultConfig :: Config 91 | defaultConfig = Config 92 | { _listeners = [Udp (BS.pack "0.0.0.0") 8125] 93 | , _httpPort = Nothing 94 | , _buffer = 8 95 | , _interval = 10 96 | , _resolution = 60 97 | , _percentiles = [90] 98 | , _logEvents = [] 99 | , _graphites = [] 100 | , _prefix = "stats" 101 | , _broadcasts = [] 102 | , _downstreams = [] 103 | } 104 | 105 | parseConfig :: IO Config 106 | parseConfig = do 107 | a <- getArgs 108 | n <- getProgName 109 | case processValue (flags n) a of 110 | Help -> print (helpText [] HelpFormatOne $ flags n) >> exitSuccess 111 | Version -> putStrLn (info n) >> exitSuccess 112 | c -> validate c >> infoL c >> return c 113 | 114 | info :: String -> String 115 | info name = concat 116 | [ name 117 | , " version " 118 | , showVersion version 119 | , " (C) Brendan Hay 2012" 120 | ] 121 | 122 | validate :: Config -> IO () 123 | validate Config{..} = do 124 | check (null _listeners) 125 | "--listeners cannot be blank" 126 | check (not . null $ _listeners `intersect` sinks) 127 | "--listeners cannot contain any URI used by --{graphites,broadcasts,downstreams}" 128 | check (not . null $ _listeners `intersect` ["file://stdout", "file://stderr"]) 129 | "--listeners cannot read from stdout or stderr" 130 | check ("file://stdin" `elem` sinks) 131 | "--{graphites,broadcasts,downstreams} cannot cannot write to stdin" 132 | check (_buffer < 1) 133 | "--buffer must be greater than 0" 134 | check (_interval < 1) 135 | "--interval must be greater than 0" 136 | check (_interval >= _resolution) 137 | "--resolution must be greater than --interval" 138 | check (_resolution > maxResolution) $ 139 | "--resolution must be less than " ++ show maxResolution 140 | check (null _percentiles) 141 | "--percentiles cannot be blank" 142 | return () 143 | where 144 | check p m = when p $ putStrLn m >> exitWith (ExitFailure 1) 145 | sinks = _graphites ++ _broadcasts ++ _downstreams 146 | validate _ = return () 147 | 148 | flags :: String -> Mode Config 149 | flags name = mode name defaultConfig "Numbers" 150 | (flagArg (\x _ -> Left $ "Unexpected argument " ++ x) "") 151 | [ flagReq ["listeners"] (many listeners) "[URI]" 152 | "Incoming stats address and port combinations" 153 | 154 | , flagReq ["http"] (parse (setL httpPort . Just . read)) "PORT" 155 | "HTTP port to serve the overview and time series on" 156 | 157 | , flagReq ["buffer"] (one buffer) "INT" 158 | "Number of packets to buffer, from all listeners" 159 | 160 | , flagReq ["resolution"] (one resolution) "INT" 161 | "Resolution in seconds for time series data" 162 | 163 | , flagReq ["interval"] (one interval) "INT" 164 | "Interval in seconds between key flushes to subscribed sinks" 165 | 166 | , flagReq ["percentiles"] (many percentiles) "[INT]" 167 | "Calculate the Nth percentile(s) for timers" 168 | 169 | , flagReq ["events"] (parse (setL logEvents . splitOn ",")) "[EVENT]" 170 | "Log [receive,invalid,parse,flush] events" 171 | 172 | , flagReq ["prefix"] (many prefix) "STR" 173 | "Prepended to keys in the http interfaces and graphite" 174 | 175 | , flagReq ["graphites"] (many graphites) "[URI]" 176 | "Graphite hosts to deliver metrics to" 177 | 178 | , flagReq ["broadcasts"] (many broadcasts) "[URI]" 179 | "Hosts to broadcast raw unaggregated packets to" 180 | 181 | , flagReq ["downstreams"] (many downstreams) "[URI]" 182 | "Hosts to forward aggregated counters to" 183 | 184 | , flagNone ["help", "h"] (\_ -> Help) 185 | "Display this help message" 186 | 187 | , flagVersion $ \_ -> Version 188 | ] 189 | where 190 | one l = parse (setL l . read) 191 | many l = parse (setL l . map read . splitOn ",") 192 | parse f s = Right . f s 193 | -------------------------------------------------------------------------------- /test/Properties/Conduit.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | 4 | -- | 5 | -- Module : Properties.Conduit 6 | -- Copyright : (c) 2012 Brendan Hay 7 | -- License : This Source Code Form is subject to the terms of 8 | -- the Mozilla Public License, v. 2.0. 9 | -- A copy of the MPL can be found in the LICENSE file or 10 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 11 | -- Maintainer : Brendan Hay 12 | -- Stability : experimental 13 | -- Portability : non-portable (GHC extensions) 14 | -- 15 | 16 | module Properties.Conduit ( 17 | conduitProperties 18 | ) where 19 | 20 | import Control.Applicative hiding (empty) 21 | import Data.Maybe 22 | import Numbers.Conduit 23 | import Numbers.Types 24 | import Properties.Generators 25 | import Test.Framework 26 | import Test.Framework.Providers.QuickCheck2 27 | import Test.QuickCheck 28 | 29 | import qualified Data.Attoparsec.Char8 as PC 30 | import qualified Data.ByteString.Char8 as BS 31 | import qualified Data.Set as S 32 | import qualified Data.Vector as V 33 | 34 | conduitProperties :: Test 35 | conduitProperties = testGroup "sinks" 36 | [ testGroup "graphite" 37 | [ testGroup "aggregate event" 38 | [ testProperty "encodes prefix" prop_graphite_aggr_event_encodes_prefix 39 | , testProperty "encodes key" prop_graphite_aggr_event_encodes_key 40 | , testProperty "encodes value" prop_graphite_aggr_event_encodes_value 41 | ] 42 | , testProperty "ignores non aggregate events" prop_graphite_ignores_non_aggr_events 43 | ] 44 | , testGroup "broadcast" 45 | [ testProperty "doesn't modify received packets" prop_broadcast_doesnt_modify_received_packets 46 | , testProperty "ignores non receive events" prop_broadcast_ignores_non_receive_events 47 | ] 48 | , testGroup "downstream" 49 | [ testGroup "flush event" 50 | [ testProperty "encodes key" prop_downstream_flush_event_encodes_key 51 | , testProperty "aggregates values" prop_downstream_flush_event_aggregates_values 52 | , testProperty "encodes multiple values per line" prop_downstream_flush_event_encodes_mvalues_per_line 53 | ] 54 | , testProperty "ignores non flush events" prop_downstream_ignores_non_flush_events 55 | ] 56 | ] 57 | 58 | prop_graphite_aggr_event_encodes_prefix :: Graphite -> Bool 59 | prop_graphite_aggr_event_encodes_prefix g = 60 | inputGPrefix g == outputGPrefix g 61 | 62 | prop_graphite_aggr_event_encodes_key :: Graphite -> Bool 63 | prop_graphite_aggr_event_encodes_key g = 64 | inputGKey g == outputGKey g 65 | 66 | prop_graphite_aggr_event_encodes_value :: Graphite -> Bool 67 | prop_graphite_aggr_event_encodes_value g = 68 | inputGValue g `kindaClose` outputGValue g 69 | 70 | prop_graphite_ignores_non_aggr_events :: Property 71 | prop_graphite_ignores_non_aggr_events = 72 | forAll (graphite "" `conduitP` p) null 73 | where 74 | p Aggregate{} = False 75 | p _ = True 76 | 77 | prop_broadcast_doesnt_modify_received_packets :: Broadcast -> Bool 78 | prop_broadcast_doesnt_modify_received_packets (Broadcast s bs) = 79 | [s] == bs 80 | 81 | prop_broadcast_ignores_non_receive_events :: Property 82 | prop_broadcast_ignores_non_receive_events = 83 | forAll (broadcast `conduitP` p) null 84 | where 85 | p Receive{} = False 86 | p _ = True 87 | 88 | prop_downstream_flush_event_encodes_key :: Downstream -> Bool 89 | prop_downstream_flush_event_encodes_key d = 90 | inputDKey d == outputDKey d 91 | 92 | prop_downstream_flush_event_aggregates_values :: Downstream -> Bool 93 | prop_downstream_flush_event_aggregates_values d = 94 | inputDMetric d `kindaCloseM` outputDMetric d 95 | 96 | prop_downstream_flush_event_encodes_mvalues_per_line :: Downstream -> Bool 97 | prop_downstream_flush_event_encodes_mvalues_per_line d = 98 | len (inputDMetric d) == ':' `BS.count` inputDEncoded d 99 | where 100 | len (Timer vs) = V.length vs 101 | len (Set vs) = S.size vs 102 | len _ = 1 103 | 104 | prop_downstream_ignores_non_flush_events :: Property 105 | prop_downstream_ignores_non_flush_events = 106 | forAll (downstream `conduitP` p) null 107 | where 108 | p Flush{} = False 109 | p _ = True 110 | 111 | data Downstream = Downstream 112 | { inputDKey :: Key 113 | , inputDMetric :: Metric 114 | , inputDEncoded :: BS.ByteString 115 | , outputDKey :: Key 116 | , outputDMetric :: Metric 117 | } deriving (Show) 118 | 119 | downstreamP :: (Metric -> Bool) -> Gen Downstream 120 | downstreamP p = do 121 | ik <- arbitrary 122 | im <- arbitrary `suchThat` p 123 | it <- arbitrary 124 | r <- BS.intercalate "\n" <$> conduitResult [Flush ik im it] downstream 125 | let (ok, om) = fromMaybe ("failed", Counter 0) $ decode lineParser r 126 | return Downstream 127 | { inputDKey = ik 128 | , inputDMetric = im 129 | , inputDEncoded = r 130 | , outputDKey = ok 131 | , outputDMetric = om 132 | } 133 | 134 | instance Arbitrary Downstream where 135 | arbitrary = downstreamP $ const True 136 | 137 | conduitP :: EventConduit Gen BS.ByteString -> (Event -> Bool) -> Gen [BS.ByteString] 138 | conduitP con p = do 139 | e <- arbitrary `suchThat` p 140 | conduitResult [e] con 141 | 142 | data Graphite = Graphite 143 | { inputGPrefix :: String 144 | , inputGKey :: Key 145 | , inputGTime :: Time 146 | , inputGValue :: Double 147 | , outputGPrefix :: String 148 | , outputGKey :: Key 149 | , outputGTime :: Time 150 | , outputGValue :: Double 151 | } deriving (Show) 152 | 153 | instance Arbitrary Graphite where 154 | arbitrary = do 155 | SafeStr ip <- arbitrary 156 | it <- arbitrary 157 | p@(P ik iv) <- arbitrary 158 | bs <- conduitResult [Aggregate p it] (graphite ip) 159 | let (op, ok, ot, ov) = parseGraphite bs 160 | return Graphite 161 | { inputGPrefix = ip 162 | , inputGKey = ik 163 | , inputGTime = it 164 | , inputGValue = iv 165 | , outputGPrefix = op 166 | , outputGKey = ok 167 | , outputGTime = ot 168 | , outputGValue = ov 169 | } 170 | 171 | parseGraphite :: [BS.ByteString] -> (String, Key, Time, Double) 172 | parseGraphite = fromJust . decode format . BS.concat 173 | where 174 | format = do 175 | p <- PC.takeTill (== '.') <* PC.char '.' 176 | k <- PC.takeTill (== ' ') <* PC.char ' ' 177 | v <- PC.double <* PC.char ' ' 178 | t <- PC.decimal 179 | return (BS.unpack p, Key k, Time t, v) 180 | 181 | data Broadcast = Broadcast BS.ByteString [BS.ByteString] 182 | deriving (Show) 183 | 184 | instance Arbitrary Broadcast where 185 | arbitrary = do 186 | s <- arbitrary 187 | bs <- conduitResult [Receive s] broadcast 188 | return $ Broadcast s bs -------------------------------------------------------------------------------- /assets/javascripts/dashboard.js: -------------------------------------------------------------------------------- 1 | 2 | var graphs = []; // rickshaw objects 3 | var datum = []; // metric data 4 | var aliases = []; // alias strings 5 | var descriptions = []; // description strings 6 | var realMetrics = []; // non-false targets 7 | 8 | // minutes of data in the live feed 9 | var period = (typeof period == 'undefined') ? 5 : period; 10 | var originalPeriod = period; 11 | 12 | // gather our non-false targets 13 | function gatherRealMetrics() { 14 | var falseTargets = 0; 15 | for (var i=0; i warning) { 81 | if (lastValue >= critical) { 82 | graphs[i].series[0].color = '#d59295'; 83 | } else if (lastValue >= warning) { 84 | graphs[i].series[0].color = '#f5cb56'; 85 | } else { 86 | graphs[i].series[0].color = '#afdab1'; 87 | } 88 | } else { 89 | if (lastValue <= critical) { 90 | graphs[i].series[0].color = '#d59295'; 91 | } else if (lastValue <= warning) { 92 | graphs[i].series[0].color = '#f5cb56'; 93 | } else { 94 | graphs[i].series[0].color = '#b5d9ff'; 95 | } 96 | } 97 | // we want to render immediately, i.e. 98 | // as soon as ajax completes 99 | // used for time period / pause view 100 | if (immediately) { 101 | updateGraphs(i); 102 | } 103 | } 104 | values = null; 105 | }); 106 | 107 | // we can wait until all data is gathered, i.e. 108 | // the live refresh should happen synchronously 109 | if (!immediately) { 110 | for (var i=0; i 0) { 136 | for (var i=0; iNF'); 170 | } 171 | } 172 | 173 | // add our containers 174 | function buildContainers() { 175 | var falseTargets = 0; 176 | for (var i=0; i'); 179 | falseTargets++; 180 | } else if (metrics[i].target.match(/^' + metrics[i].target + '') 182 | falseTargets++; 183 | } else { 184 | var j = i - falseTargets; 185 | $('.main').append( 186 | '
' + 187 | '
' + 188 | '
' + 189 | '
' 190 | ); 191 | } 192 | } 193 | } 194 | 195 | // filter out false targets 196 | gatherRealMetrics(); 197 | 198 | // build our div containers 199 | buildContainers(); 200 | 201 | // build our graph objects 202 | constructGraphs(); 203 | 204 | // build our url 205 | constructUrl(period); 206 | 207 | // hide our toolbar if necessary 208 | var toolbar = (typeof toolbar == 'undefined') ? true : toolbar; 209 | if (!toolbar) { $('div.toolbar').css('display', 'none'); } 210 | 211 | // initial load screen 212 | for (var i=0; i'); 216 | } 217 | } 218 | 219 | refreshData('now'); 220 | 221 | // define our refresh and start interval 222 | var refreshInterval = (typeof refresh == 'undefined') ? 2000 : refresh; 223 | var refreshId = setInterval(refreshData, refreshInterval); 224 | -------------------------------------------------------------------------------- /assets/javascripts/tasseo.js: -------------------------------------------------------------------------------- 1 | 2 | var graphs = []; // rickshaw objects 3 | var datum = []; // metric data 4 | var aliases = []; // alias strings 5 | var descriptions = []; // description strings 6 | var realMetrics = []; // non-false targets 7 | 8 | // minutes of data in the live feed 9 | var period = (typeof period == 'undefined') ? 5 : period; 10 | var originalPeriod = period; 11 | 12 | // gather our non-false targets 13 | function gatherRealMetrics() { 14 | var falseTargets = 0; 15 | for (var i=0; i warning) { 81 | if (lastValue >= critical) { 82 | graphs[i].series[0].color = '#d59295'; 83 | } else if (lastValue >= warning) { 84 | graphs[i].series[0].color = '#f5cb56'; 85 | } else { 86 | graphs[i].series[0].color = '#afdab1'; 87 | } 88 | } else { 89 | if (lastValue <= critical) { 90 | graphs[i].series[0].color = '#d59295'; 91 | } else if (lastValue <= warning) { 92 | graphs[i].series[0].color = '#f5cb56'; 93 | } else { 94 | graphs[i].series[0].color = '#b5d9ff'; 95 | } 96 | } 97 | // we want to render immediately, i.e. 98 | // as soon as ajax completes 99 | // used for time period / pause view 100 | if (immediately) { 101 | updateGraphs(i); 102 | } 103 | } 104 | values = null; 105 | }); 106 | 107 | // we can wait until all data is gathered, i.e. 108 | // the live refresh should happen synchronously 109 | if (!immediately) { 110 | for (var i=0; i 0) { 136 | for (var i=0; iNF'); 170 | } 171 | } 172 | 173 | // add our containers 174 | function buildContainers() { 175 | var falseTargets = 0; 176 | for (var i=0; i'); 180 | falseTargets++; 181 | } else if (metrics[i].target.match(/^' + metrics[i].target + '') 184 | falseTargets++; 185 | } else { 186 | console.log($('.main')); 187 | var j = i - falseTargets; 188 | $('.main').append( 189 | '
' + 190 | '
' + 191 | '
' + 192 | '
' 193 | ); 194 | } 195 | } 196 | } 197 | 198 | // filter out false targets 199 | gatherRealMetrics(); 200 | 201 | // build our div containers 202 | buildContainers(); 203 | 204 | // build our graph objects 205 | constructGraphs(); 206 | 207 | // build our url 208 | constructUrl(period); 209 | 210 | // hide our toolbar if necessary 211 | var toolbar = (typeof toolbar == 'undefined') ? true : toolbar; 212 | if (!toolbar) { $('div.toolbar').css('display', 'none'); } 213 | 214 | // initial load screen 215 | for (var i=0; i'); 219 | } 220 | } 221 | 222 | refreshData('now'); 223 | 224 | // define our refresh and start interval 225 | var refreshInterval = (typeof refresh == 'undefined') ? 2000 : refresh; 226 | var refreshId = setInterval(refreshData, refreshInterval); 227 | -------------------------------------------------------------------------------- /test/Properties/Types.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | 3 | -- | 4 | -- Module : Properties.Types 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Properties.Types ( 16 | typeProperties 17 | ) where 18 | 19 | import Prelude hiding (foldl) 20 | import Blaze.ByteString.Builder (toByteString) 21 | import Data.Maybe 22 | import Data.Monoid 23 | import Data.List (union) 24 | import Numbers.Types 25 | import Properties.Generators 26 | import Test.Framework 27 | import Test.Framework.Providers.QuickCheck2 28 | import Test.QuickCheck 29 | 30 | import qualified Data.ByteString.Char8 as BS 31 | import qualified Data.Set as S 32 | import qualified Data.Vector as V 33 | 34 | typeProperties :: Test 35 | typeProperties = testGroup "types" 36 | [ testGroup "uri" 37 | [ testGroup "encode, then decode" 38 | [ testProperty "uri is equiv" prop_encode_decode_uri_equiv 39 | , testProperty "host is equiv" prop_encode_decode_uri_host_equiv 40 | , testProperty "port is equiv" prop_encode_decode_uri_port_equiv 41 | ] 42 | ] 43 | , testGroup "key" 44 | [ testProperty "encode, then decode is equiv" prop_encode_decode_key_equiv 45 | , testProperty "decode strips unsafe chars" prop_decode_key_strips_unsafe 46 | , testProperty "mappend/mconcat is dot delimited" prop_mconcat_keys_is_dot_delimited 47 | ] 48 | , testGroup "metric" 49 | [ testGroup "encode, then decode" 50 | [ testProperty "key is equiv" prop_encode_decode_metric_key_equiv 51 | , testProperty "metric is close enough" prop_encode_decode_metric_equiv 52 | ] 53 | , testProperty "is not zeroed" prop_metric_is_not_zeroed 54 | , testGroup "aggregate" 55 | [ testProperty "with nothing, keeps original" prop_aggregate_metric_with_nothing 56 | , testProperty "with different ctor, keeps rvalue" prop_aggregate_disimilar_metrics_keep_rvalue 57 | , testProperty "counters are summed" prop_aggregate_counters_are_summed 58 | , testProperty "gauges keep rvalue" prop_aggregate_gauges_keep_rvalue 59 | , testProperty "timers are prepended" prop_aggregate_timers_are_prepended 60 | , testProperty "sets are unioned" prop_aggregate_sets_are_unioned 61 | ] 62 | ] 63 | ] 64 | 65 | prop_encode_decode_uri_equiv :: EncodeUri -> Bool 66 | prop_encode_decode_uri_equiv u = 67 | inputUUri u == outputUUri u 68 | 69 | prop_encode_decode_uri_host_equiv :: EncodeUri -> Bool 70 | prop_encode_decode_uri_host_equiv u = 71 | inputUHost u == outputUHost u 72 | 73 | prop_encode_decode_uri_port_equiv :: EncodeUri -> Bool 74 | prop_encode_decode_uri_port_equiv u = 75 | inputUPort u == outputUPort u 76 | 77 | prop_encode_decode_key_equiv :: EncodeKey -> Bool 78 | prop_encode_decode_key_equiv k = 79 | inputKKey k == outputKKey k 80 | 81 | prop_encode_decode_metric_key_equiv :: EncodeMetric -> Bool 82 | prop_encode_decode_metric_key_equiv e = 83 | inputMKey e == outputMKey e 84 | 85 | prop_decode_key_strips_unsafe :: UnsafeStr -> Bool 86 | prop_decode_key_strips_unsafe (UnsafeStr s) = 87 | BS.all (\c -> c `elem` valid) k 88 | where 89 | (Key k) = fromMaybe "failed!" . decode keyParser . BS.pack $ map f s 90 | f ':' = '_' 91 | f c = c 92 | valid = ['a'..'z'] ++ ['0'..'9'] ++ ['A'..'Z'] ++ ['_', '-', '.'] 93 | 94 | prop_mconcat_keys_is_dot_delimited :: [Key] -> Bool 95 | prop_mconcat_keys_is_dot_delimited ks = 96 | length ks == BS.count '.' s 97 | where 98 | (Key s) = mconcat ks 99 | 100 | prop_encode_decode_metric_equiv :: EncodeMetric -> Bool 101 | prop_encode_decode_metric_equiv e = 102 | inputMMetric e `kindaCloseM` outputMMetric e 103 | 104 | prop_metric_is_not_zeroed :: Property 105 | prop_metric_is_not_zeroed = 106 | forAll f (not . zero) 107 | where 108 | f = do 109 | Positive v <- arbitrary 110 | NonEmpty xs <- arbitrary 111 | elements [ Counter v 112 | , Gauge v 113 | , Timer $ V.fromList xs 114 | , Set $ S.fromList xs 115 | ] 116 | 117 | prop_aggregate_metric_with_nothing :: Metric -> Bool 118 | prop_aggregate_metric_with_nothing m = 119 | m `aggregate` Nothing == m 120 | 121 | prop_aggregate_disimilar_metrics_keep_rvalue :: Property 122 | prop_aggregate_disimilar_metrics_keep_rvalue = 123 | forAll f $ \(a, b) -> a `aggregate` (Just b) == b 124 | where 125 | f = suchThat arbitrary (not . uncurry similarM) 126 | 127 | prop_aggregate_counters_are_summed :: Double -> Double -> Bool 128 | prop_aggregate_counters_are_summed x y = 129 | aggregate (Counter x) (Just $ Counter y) == Counter (x + y) 130 | 131 | prop_aggregate_gauges_keep_rvalue :: Double -> Double -> Bool 132 | prop_aggregate_gauges_keep_rvalue x y = 133 | aggregate (Gauge x) (Just $ Gauge y) == Gauge y 134 | 135 | prop_aggregate_timers_are_prepended :: [Double] -> [Double] -> Bool 136 | prop_aggregate_timers_are_prepended xs ys = 137 | aggregate (f xs) (Just $ f ys) == f (ys ++ xs) 138 | where 139 | f = Timer . V.fromList 140 | 141 | prop_aggregate_sets_are_unioned :: [Double] -> [Double] -> Bool 142 | prop_aggregate_sets_are_unioned xs ys = 143 | aggregate (f xs) (Just $ f ys) == f (xs `union` ys) 144 | where 145 | f = Set . S.fromList 146 | 147 | similarM :: Metric -> Metric -> Bool 148 | similarM a b = case (a, b) of 149 | (Counter{}, Counter{}) -> True 150 | (Gauge{}, Gauge{}) -> True 151 | (Timer{}, Timer{}) -> True 152 | (Set{}, Set{}) -> True 153 | _ -> False 154 | 155 | data EncodeUri = EncodeUri 156 | { inputUUri :: Uri 157 | , inputUHost :: BS.ByteString 158 | , inputUPort :: Int 159 | , inputUEncoded :: BS.ByteString 160 | , outputUUri :: Uri 161 | , outputUHost :: BS.ByteString 162 | , outputUPort :: Int 163 | } deriving (Show) 164 | 165 | instance Arbitrary EncodeUri where 166 | arbitrary = do 167 | SafeStr ih <- arbitrary 168 | Positive ip <- arbitrary 169 | iu <- elements [ File $ BS.pack ih 170 | , Tcp (BS.pack ih) ip 171 | , Udp (BS.pack ih) ip 172 | ] 173 | let r = toByteString $ build iu 174 | ou = fromMaybe (File "failed!") $ decode uriParser r 175 | return EncodeUri 176 | { inputUUri = iu 177 | , inputUHost = BS.pack ih 178 | , inputUPort = ip 179 | , inputUEncoded = r 180 | , outputUUri = ou 181 | , outputUHost = host ou 182 | , outputUPort = port ou ip 183 | } 184 | where 185 | host (File f) = f 186 | host u = _host u 187 | port File{} p = p 188 | port u _ = _port u 189 | 190 | data EncodeKey = EncodeKey 191 | { inputKKey :: Key 192 | , inputKEncoded :: BS.ByteString 193 | , outputKKey :: Key 194 | } deriving (Show) 195 | 196 | instance Arbitrary EncodeKey where 197 | arbitrary = do 198 | ik <- arbitrary 199 | let r = toByteString $ build ik 200 | ok = fromMaybe "failed!" $ decode keyParser r 201 | return EncodeKey 202 | { inputKKey = ik 203 | , inputKEncoded = r 204 | , outputKKey = ok 205 | } 206 | 207 | data EncodeMetric = EncodeMetric 208 | { inputMKey :: Key 209 | , inputMMetric :: Metric 210 | , outputMStr :: BS.ByteString 211 | , outputMKey :: Key 212 | , outputMMetric :: Metric 213 | } deriving (Show) 214 | 215 | instance Arbitrary EncodeMetric where 216 | arbitrary = do 217 | ik <- arbitrary 218 | im <- arbitrary 219 | let s = toByteString $ build (ik, im) 220 | (ok, om) = fromMaybe ("failed!", Counter 0) $ decode lineParser s 221 | return EncodeMetric 222 | { inputMKey = ik 223 | , inputMMetric = im 224 | , outputMStr = s 225 | , outputMKey = ok 226 | , outputMMetric = om 227 | } 228 | -------------------------------------------------------------------------------- /src/Numbers/Types.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Numbers.Types 3 | -- Copyright : (c) 2012 Brendan Hay 4 | -- License : This Source Code Form is subject to the terms of 5 | -- the Mozilla Public License, v. 2.0. 6 | -- A copy of the MPL can be found in the LICENSE file or 7 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 8 | -- Maintainer : Brendan Hay 9 | -- Stability : experimental 10 | -- Portability : non-portable (GHC extensions) 11 | -- 12 | 13 | module Numbers.Types ( 14 | -- * Type Classes 15 | Loggable(..) 16 | , sbuild 17 | 18 | -- * Exported Types 19 | , Time(..) 20 | , Uri(..) 21 | , Key(..) 22 | , Metric(..) 23 | , Point(..) 24 | 25 | -- * Functions 26 | , lineParser 27 | , keyParser 28 | , uriParser 29 | , decode 30 | , currentTime 31 | , zero 32 | , aggregate 33 | , calculate 34 | ) where 35 | 36 | import Blaze.ByteString.Builder 37 | import Control.Arrow ((***), first) 38 | import Control.Applicative hiding (empty) 39 | import Control.Monad 40 | import Data.Aeson (ToJSON(..)) 41 | import Data.Attoparsec.ByteString 42 | import Data.List hiding (sort) 43 | import Data.Maybe 44 | import Data.Monoid 45 | import Data.String 46 | import Data.Time.Clock.POSIX 47 | import Numeric (showFFloat) 48 | import Statistics.Function (sort) 49 | import Statistics.Sample 50 | import Text.Regex.PCRE hiding (match) 51 | 52 | import qualified Data.Attoparsec.Char8 as PC 53 | import qualified Data.ByteString.Char8 as BS 54 | import qualified Data.Set as S 55 | import qualified Data.Text.Encoding as TE 56 | import qualified Data.Vector as V 57 | 58 | class Loggable a where 59 | build :: Loggable a => a -> Builder 60 | (&&&) :: (Loggable a, Loggable b) => a -> b -> Builder 61 | (<&&) :: Loggable a => String -> a -> Builder 62 | (&&>) :: Loggable a => a -> String -> Builder 63 | 64 | infixr 7 &&& 65 | infixr 9 <&& 66 | infixr 8 &&> 67 | 68 | a &&& b = build a <> build b 69 | a <&& b = build a &&& b 70 | a &&> b = a &&& build b 71 | 72 | sbuild :: String -> Builder 73 | sbuild = build . BS.pack 74 | 75 | instance Loggable Builder where 76 | build = id 77 | 78 | instance Loggable [Builder] where 79 | build = mconcat 80 | 81 | instance Loggable BS.ByteString where 82 | build = copyByteString 83 | 84 | instance Loggable Int where 85 | build = build . show 86 | 87 | instance Loggable Double where 88 | build n = build $ showFFloat (Just 1) n "" 89 | 90 | instance Loggable String where 91 | build = build . BS.pack 92 | 93 | instance Loggable [String] where 94 | build = build . intercalate ", " 95 | 96 | instance Loggable a => Loggable (Maybe a) where 97 | build (Just x) = build x 98 | build Nothing = mempty 99 | 100 | -- Investigate how to avoid overlapping instances for 101 | -- instance Loggable a => Loggable [a] delcaration 102 | 103 | instance Loggable [Int] where 104 | build = build . show 105 | 106 | instance Loggable [Double] where 107 | build = build . show 108 | 109 | -- ^ 110 | 111 | instance Loggable (V.Vector Double) where 112 | build = build . V.toList 113 | 114 | newtype Time = Time Int 115 | deriving (Eq, Ord, Show, Enum, Num, Real, Integral) 116 | 117 | instance Loggable Time where 118 | build (Time n) = build $ show n 119 | 120 | currentTime :: IO Time 121 | currentTime = (Time . truncate) `liftM` getPOSIXTime 122 | 123 | data Uri = File { _path :: BS.ByteString } 124 | | Tcp { _host :: BS.ByteString, _port :: Int } 125 | | Udp { _host :: BS.ByteString, _port :: Int } 126 | deriving (Eq, Show) 127 | 128 | instance Read Uri where 129 | readsPrec _ a = return (fromJust . decode uriParser $ BS.pack a, "") 130 | 131 | instance IsString Uri where 132 | fromString = fromJust . decode uriParser . BS.pack 133 | 134 | instance ToJSON Uri where 135 | toJSON = toJSON . TE.decodeUtf8 . toByteString . build 136 | 137 | decode :: Parser a -> BS.ByteString -> Maybe a 138 | decode p bstr = maybeResult $ feed (parse p bstr) BS.empty 139 | 140 | instance Loggable Uri where 141 | build (File f) = "file://" <&& f 142 | build (Tcp h p) = "tcp://" <&& h &&& ":" <&& p 143 | build (Udp h p) = "udp://" <&& h &&& ":" <&& p 144 | 145 | instance Loggable [Uri] where 146 | build = mconcat . intersperse (sbuild ", ") . map build 147 | 148 | uriParser :: Parser Uri 149 | uriParser = do 150 | s <- PC.takeTill (== ':') <* string "://" 151 | case BS.unpack s of 152 | "file" -> File <$> PC.takeByteString 153 | "tcp" -> Tcp <$> host <*> port 154 | "udp" -> Udp <$> host <*> port 155 | _ -> error "Unrecognized uri scheme" 156 | where 157 | host = PC.takeTill (== ':') <* PC.char ':' 158 | port = PC.decimal :: Parser Int 159 | 160 | newtype Key = Key BS.ByteString 161 | deriving (Eq, Ord, Show) 162 | 163 | instance IsString Key where 164 | fromString = Key . BS.pack 165 | 166 | instance Monoid Key where 167 | (Key a) `mappend` (Key b) = Key $ BS.concat [a, ".", b] 168 | mempty = Key mempty 169 | 170 | instance Loggable Key where 171 | build (Key k) = build k 172 | 173 | instance Loggable [Key] where 174 | build = mconcat . intersperse s . map build 175 | where 176 | s = sbuild "," 177 | 178 | instance ToJSON Key where 179 | toJSON (Key k) = toJSON k 180 | 181 | keyParser :: Parser Key 182 | keyParser = do 183 | k <- PC.takeTill (== ':') 184 | return $! Key (strip k) 185 | 186 | strip :: BS.ByteString -> BS.ByteString 187 | strip s = foldl (flip $ uncurry replace) s unsafe 188 | 189 | unsafe :: [(Regex, BS.ByteString)] 190 | unsafe = map (first makeRegex . join (***) BS.pack) rs 191 | where 192 | rs = [ ("\\s+", "_") 193 | , ("\\/", "-") 194 | , ("[^a-zA-Z_\\-0-9\\.]", "") 195 | ] 196 | 197 | replace :: Regex -> BS.ByteString -> BS.ByteString -> BS.ByteString 198 | replace regex rep = f 199 | where 200 | f s = case match regex s of 201 | Just (a, _, c) -> a `BS.append` rep `BS.append` f c 202 | _ -> s 203 | 204 | match :: Regex 205 | -> BS.ByteString 206 | -> Maybe (BS.ByteString, BS.ByteString, BS.ByteString) 207 | match = matchM 208 | 209 | data Metric = Counter !Double 210 | | Gauge !Double 211 | | Timer !(V.Vector Double) 212 | | Set !(S.Set Double) 213 | deriving (Eq, Ord, Show) 214 | 215 | instance Loggable (Key, Metric) where 216 | build (k, m) = k &&& s &&& f 217 | where 218 | s = sbuild ":" 219 | f = case m of 220 | Counter v -> v &&> "|c" 221 | Gauge v -> v &&> "|g" 222 | Timer vs -> g (&&> "|ms") $ V.toList vs 223 | Set ss -> g (&&> "|s") $ S.toAscList ss 224 | g h = mconcat . intersperse s . map h 225 | 226 | lineParser :: Parser (Key, Metric) 227 | lineParser = do 228 | k <- keyParser 229 | ms <- metricsParser 230 | return $! (k, foldl1 (flip aggregate . Just) ms) 231 | 232 | metricsParser :: Parser [Metric] 233 | metricsParser = many1 $ do 234 | _ <- optional $ PC.char ':' 235 | v <- value 236 | t <- type' 237 | r <- optional sample 238 | return $! case t of 239 | 'g' -> Gauge v 240 | 'm' -> Timer $ V.singleton v 241 | 's' -> Set $ S.singleton v 242 | _ -> Counter $ maybe v (\n -> v * (1 / n)) r -- ^ Div by zero 243 | where 244 | value = PC.double <* PC.char '|' 245 | sample = PC.char '|' *> PC.char '@' *> PC.double 246 | type' = PC.char 'c' 247 | <|> PC.char 'g' 248 | <|> PC.char 'm' <* PC.char 's' 249 | <|> PC.char 's' 250 | 251 | zero :: Metric -> Bool 252 | zero (Counter 0) = True 253 | zero (Gauge 0) = True 254 | zero (Timer ns) = V.null ns 255 | zero (Set ss) = S.null ss 256 | zero _ = False 257 | 258 | aggregate :: Metric -> Maybe Metric -> Metric 259 | aggregate a Nothing = a 260 | aggregate a (Just b) = b `f` a 261 | where 262 | f (Counter x) (Counter y) = Counter $ x + y 263 | f (Timer x) (Timer y) = Timer $ x V.++ y 264 | f (Set x) (Set y) = Set $ x `S.union` y 265 | f _ _ = b 266 | 267 | data Point = P !Key !Double 268 | deriving (Show) 269 | 270 | instance Loggable Point where 271 | build (P k v) = k &&> " " &&& v 272 | 273 | calculate :: [Int] -> Int -> Key -> Metric -> [Point] 274 | calculate _ n k (Counter v) = 275 | [ P ("counters" <> k) (v / (fromIntegral n / 1000)) 276 | , P ("counters" <> k <> "count") v 277 | ] 278 | calculate _ _ k (Gauge v) = 279 | [ P ("gauges" <> k) v ] 280 | calculate _ _ k (Set ss) = 281 | [ P ("sets" <> k <> "count") (fromIntegral $ S.size ss) ] 282 | calculate qs _ k (Timer vs) = concatMap (quantile k xs) qs <> 283 | [ P ("timers" <> k <> "std") $ stdDev xs 284 | , P ("timers" <> k <> "upper") $ V.last xs 285 | , P ("timers" <> k <> "lower") $ V.head xs 286 | , P ("timers" <> k <> "count") . fromIntegral $ V.length xs 287 | , P ("timers" <> k <> "sum") $ V.sum xs 288 | , P ("timers" <> k <> "mean") $ mean xs 289 | ] 290 | where 291 | xs = sort vs 292 | 293 | quantile :: Key -> V.Vector Double -> Int -> [Point] 294 | quantile k xs q = 295 | [ P ("timers" <> k <> a "mean_") $ mean ys 296 | , P ("timers" <> k <> a "upper_") $ V.last ys 297 | , P ("timers" <> k <> a "sum_") $ V.sum ys 298 | ] 299 | where 300 | a = Key . (`BS.append` BS.pack (show q)) 301 | n = round $ fromIntegral q / 100 * (fromIntegral $ V.length xs :: Double) 302 | ys = V.take n xs 303 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # numbersd 2 | 3 | [![Build Status](https://secure.travis-ci.org/brendanhay/numbersd.png)](http://travis-ci.org/brendanhay/numbersd) 4 | 5 | Table of Contents 6 | ----------------- 7 | 8 | * [Introduction](#introduction) 9 | * [Compatibility](#compatibility) 10 | * [Functionality](#functionality) 11 | - [Listeners](#listeners) 12 | - [Overview and Time Series](#overview-and-time-series) 13 | - [Graphites](#graphites) 14 | - [Broadcasters](#broadcasters) 15 | - [Downstreams](#downstreams) 16 | * [Scenarios](#scenarios) 17 | - [Monitoring](#monitoring) 18 | - [Redundancy](#redundancy) 19 | - [Federation](#federation) 20 | - [Forwarding](#forwarding) 21 | * [Install](#install) 22 | * [Configuration](#configuration) 23 | - [Available Flags](#available-flags) 24 | - [Flag Types](#flag-types) 25 | * [Running](#running) 26 | * [Contribute](#contribute) 27 | * [Licence](#licence) 28 | 29 | 30 | ## Introduction 31 | 32 | > TODO 33 | 34 | 35 | ## Compatibility 36 | 37 | > TODO 38 | 39 | 40 | ## Functionality 41 | 42 | numbersd has identical aggregation characteristics to StatsD. It differs significantly in terms of 43 | philosophy and intended usage. Below are some of the behaviours available. 44 | 45 | ### Listeners 46 | 47 | A listener is a scheme, host, and port specification for a listen socket which will accept and parse 48 | metrics from incoming connections. They are specified with either a `tcp://` or `udp://` scheme to 49 | control the type of listening socket. 50 | 51 | Multiple listeners can be passed as a comma seperated list to the `--listeners` flag 52 | to listen upon multiple ports and protocols simultaneously. 53 | 54 | 55 | ### Overview and Time Series 56 | 57 | If an HTTP port is specified, numbersd will start an embedded HTTP server. GET requests to 58 | the following request paths will be responsed with an appropriate content type: 59 | 60 | * `/overview.json` Internal counters and runtime information. 61 | * `/numbersd.whisper` Low resolution time series in Graphite compatible format. (Identical to `&rawData=true`) 62 | * `/numbersd.json` JSON representation of the `.whisper` format above 63 | 64 | The `.whisper` response type is intended to be used from Nagios or other monitoring tools 65 | to connect directly to a `numbersd` instance running alongside an application. 66 | 67 | There are a number of `check_graphite` Nagios NPRE plugins available which should work identically 68 | to pointing directly at an instance of Graphite. 69 | 70 | 71 | ### Graphites 72 | 73 | (Yes, plural) 74 | 75 | As with all list styled command flags a list of tcp schemed URIs can be specified to 76 | simultaneously connect to multiple backend Graphite instnaces. 77 | 78 | 79 | ### Broadcasters 80 | 81 | Broadcasters perform identically to StatsD's `repeater` backend. They simply forward on received metrics 82 | to a list of tcp and udp schemed URIs. 83 | 84 | The intent being, you can listen on TCP and then broadcast over a UDP connection, or vice versa. 85 | 86 | 87 | ### Downstreams 88 | 89 | Downstreams again take a list of tcp and udp schemed URIs, with the closest simalarity being StatsD's 90 | `statsd-backend` plugin. 91 | 92 | The metrics that can be safely aggregated without losing precision or causing 'slopes' (such as counters) 93 | are forwarded upon `flush`, all the others are forwarded unmodified. 94 | 95 | 96 | ## Scenarios 97 | 98 | The intent of many of the behaviours above, was to provide more granular mechanisms for scaling and organising 99 | a herirachy of metric aggregators. Here are some scenarios that prompted the development of numbersd. 100 | 101 | ### Monitoring 102 | 103 | Using UDP for stats delivery is great, it makes it very easy to write a client and emit metrics but due to the lack 104 | of reliable transmission it makes unsuitable for more critical tasks, even on a network under your control. 105 | 106 | An example monitoring workflow I've observed in production, looks something like: 107 | 108 | 109 |

110 | 111 |

112 | **Figure 1** 113 | 114 | 1. Application emits unreliable UDP packets that are (hopefully) delivered to a monolithic aggregator instance. 115 | 3. The aggregator sends packets over a TCP connection to Graphite. 116 | 4. Nagios invokes an NPRE check on the application host. 117 | 5. The NPRE check reaches out across the network to the Graphite API to quantify application health. 118 | 119 | There are 4 actors involved in [Figure 1](#figure-1): the Application, Network, Aggregator, Graphite, and Nagios. 120 | 121 | For monitoring to be (remotely) reliable we have to make some assumptions .. so in this case lets' remove 122 | the Network (assume reliable UDP transmission) and Nagios (13 year old software always works) from the equation. 123 | 124 | If either the aggregator, or Graphite is temporarily unavailable the NPRE check local to the application will 125 | fail and potentially raise a warning/critical alert. 126 | 127 | By removing both the aggregator and Graphite from the monitoring workflow, it becomes a romantic dinner date for 128 | two between the application and Nagios: 129 | 130 |
131 |

132 | 133 |

134 | **Figure 2** 135 | 136 | 1. The application emits UDP packets via the loopback interface to a local numbersd daemon. 137 | 2. NumbersD pushes metrics over a TCP connection to Graphite. 138 | 3. Nagios invokes an NPRE check on the application host. 139 | 4. The NPRE check calls the local numbersd daemon's `/numbersd.whisper` time series API. 140 | 141 | This has two primary advantages. Firstly, reliability - by ensuring UDP packets are only transmitted 142 | on the localhost. And secondly, by seperating the concerns of metric durability/storage/visualisation 143 | and monitoring, two separate single point of failures have been removed from the monitoring workflow. 144 | 145 | 146 | ### Redundancy 147 | 148 |
149 |

150 | 151 |

152 | **Figure 3** 153 | 154 | Multiple Graphites 155 | 156 | 157 | ### Federation 158 | 159 |
160 |

161 | 162 |

163 | **Figure 4** 164 | 165 | A conceited federation heirarchy 166 | 167 | 168 | ### Forwarding 169 | 170 |
171 |

172 | 173 |

174 | **Figure 5** 175 | 176 | Broadcast metrics to a single point, where the monitoring check happens 177 | 178 | 179 | ## Install 180 | 181 | At present, it is assumed the user knows some of the Haskell eco system and 182 | in particular wrangling cabal-dev to obtain dependencies. I plan to offer pre-built binaries for x86_64 OSX and Linux in future. 183 | 184 | You will need reasonably new versions of GHC and the Haskell Platform which 185 | you can obtain [here](http://www.haskell.org/platform/), then run `make install` in the root directory to compile numbersd. 186 | 187 | There is also a Chef Cookbook which can be used to install numbersd, if that's how you swing: https://github.com/brendanhay/numbersd-cookbook 188 | 189 | ## Configuration 190 | 191 | Command line flags are used to configure numbersd, a full table of all the flags is available [here](#available-flags). 192 | 193 | 194 | ### Available Flags 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 |
FlagDefaultFormatAboutStatsd Equivalent
--listenersudp://0.0.0.0:8125URI,....Incoming stats UDP address and portaddress, port
--httpPORTHTTP port to serve the overview and time series onmgmt_address, mgmt_port
--resolution60INTResolution in seconds for time series data
--interval10INTInterval in seconds between key flushes to subscribed sinksflushInterval
--percentiles90INT,...Calculate the Nth percentile(s) for timerspercentThreshold
--eventsEVENT,...Combination of receive, invalid, parse, or flush events to logdebug, dumpMessages
--prefixSTRPrepended to keys in the http interfaces and graphitelog
--graphitesURI,...Graphite hosts to deliver metrics tographiteHost, graphitePort
--broadcastsURI,...Hosts to broadcast raw, unaggregated packets torepeater
--downstreamsURI,...Hosts to forward aggregated, statsd formatted counters tostatsd-backend
287 | 288 | 289 | ### Flag Types 290 | 291 | * `URI` Combination of scheme, host, and port. The scheme must be one of `(tcp|udp)`. 292 | * `PORT` Port number. Must be within the valid bindable range for non-root users. 293 | * `INT` A valid Haskell [Int](http://www.haskell.org/ghc/docs/latest/html/libraries/base/Prelude.html#t:Int) type. 294 | * `STR` An ASCII encoded string. 295 | * `EVENT` Internal event types must be one of `(receive|invalid|parse|flush)`. 296 | * `[...]` All list types are specified a comma seperated string containing no spaces. For example: `--listeners udp://0.0.0.0:8125,tcp://0.0.0.0:8126` is a valid `[URI]` list. 297 | 298 | 299 | ## Running 300 | 301 | After a successful compile, the `./numbersd` symlink should be pointing to the built binary. 302 | 303 | 304 | ## Contribute 305 | 306 | For any problems, comments or feedback please create an issue [here on GitHub](github.com/brendanhay/numbersd/issues). 307 | 308 | 309 | ## Licence 310 | 311 | numbersd is released under the [Mozilla Public License Version 2.0](http://www.mozilla.org/MPL/) 312 | -------------------------------------------------------------------------------- /test/Properties/Series.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | 3 | -- | 4 | -- Module : Properties.Series 5 | -- Copyright : (c) 2012 Brendan Hay 6 | -- License : This Source Code Form is subject to the terms of 7 | -- the Mozilla Public License, v. 2.0. 8 | -- A copy of the MPL can be found in the LICENSE file or 9 | -- you can obtain it at http://mozilla.org/MPL/2.0/. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : experimental 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | 15 | module Properties.Series ( 16 | seriesProperties 17 | ) where 18 | 19 | import Blaze.ByteString.Builder 20 | import Control.Applicative ((<$>)) 21 | import Data.Maybe 22 | import Numbers.Types 23 | import Numbers.Whisper.Series 24 | import Properties.Generators 25 | import Test.Framework 26 | import Test.Framework.Providers.HUnit 27 | import Test.Framework.Providers.QuickCheck2 28 | import Test.HUnit 29 | import Test.QuickCheck 30 | 31 | seriesProperties :: Test.Framework.Test 32 | seriesProperties = testGroup "time series" 33 | [ testGroup "create" [ 34 | testProperty "input resolution used by create" prop_input_resolution_used_by_create 35 | , testProperty "input step used by create" prop_input_step_used_by_create 36 | , testProperty "input step used by create" prop_input_step_used_by_create 37 | , testProperty "end is less than or equal to create time" prop_end_less_than_or_equal_to_create_time 38 | , testProperty "end is within step of create time" prop_end_within_step_of_create_time 39 | , testProperty "last value equals create value" prop_last_value_equals_create_value 40 | , testProperty "total values equals create value" prop_total_values_equals_create_value 41 | ] 42 | , testGroup "update" [ 43 | testProperty "resolution preserved by update" prop_resolution_preserved_by_update 44 | , testProperty "step preserved by update" prop_step_preserved_by_update 45 | , testProperty "old values ignored by update" prop_old_values_ignored_by_update 46 | , testProperty "later times move end along" prop_later_times_move_end_along_in_update 47 | , testProperty "new ends are less than or equal to update time" prop_new_end_less_than_or_equal_to_update_time 48 | , testProperty "new ends are within step of create time" prop_new_end_within_step_of_create_time 49 | , testProperty "for new ends last value equals update value" prop_new_end_last_value_equals_update_value 50 | , testProperty "update between start and end adds value" prop_update_between_start_and_end_adds_value 51 | ] 52 | , testGroup "fetch" [ 53 | testProperty "fetching from start to end is series identity" prop_fetch_start_to_end_is_series_identity 54 | , testProperty "fetching preserves the resolution" prop_fetch_preserves_resolution 55 | , testProperty "fetching preserves the step" prop_fetch_preserves_step 56 | , testProperty "fetched values are less than or equal to original" prop_fetch_values_less_than_or_equal_to_original 57 | ] 58 | , testGroup "series" [ 59 | testProperty "end is divisible by step" prop_end_divisible_by_step 60 | , testProperty "start - end diff equals resolution * step" prop_start_end_equals_resolution_times_step 61 | , testProperty "values length equals resolution" prop_values_length_equals_resolution 62 | , testProperty "orders values by their insertion time" prop_ordered_by_insertion_time 63 | ] 64 | , testGroup "examples" [ 65 | testCase "a worked example of a create" test_example_create 66 | , testCase "a worked example of an update" test_example_update 67 | , testCase "a worked example of a fetch" test_example_fetch 68 | ] 69 | ] 70 | 71 | prop_input_resolution_used_by_create :: SeriesCreate -> Bool 72 | prop_input_resolution_used_by_create sc = 73 | createInputRes sc == createOutputRes sc 74 | 75 | prop_input_step_used_by_create :: SeriesCreate -> Bool 76 | prop_input_step_used_by_create SeriesCreate{..} = 77 | createInputStep == createOutputStep 78 | 79 | prop_end_less_than_or_equal_to_create_time :: SeriesCreate -> Bool 80 | prop_end_less_than_or_equal_to_create_time SeriesCreate{..} = 81 | fromIntegral createOutputEnd <= (fromIntegral createInputTime :: Int) 82 | 83 | prop_end_within_step_of_create_time :: SeriesCreate -> Bool 84 | prop_end_within_step_of_create_time SeriesCreate{..} = 85 | fromIntegral createOutputEnd > (fromIntegral createInputTime - createInputStep :: Int) 86 | 87 | prop_last_value_equals_create_value :: SeriesCreate -> Bool 88 | prop_last_value_equals_create_value SeriesCreate{..} = 89 | createInputVal == fromJust (last createOutputValues) 90 | 91 | prop_total_values_equals_create_value :: SeriesCreate -> Bool 92 | prop_total_values_equals_create_value SeriesCreate{..} = 93 | createInputVal == sum (catMaybes createOutputValues) 94 | 95 | prop_resolution_preserved_by_update :: SeriesUpdate -> Bool 96 | prop_resolution_preserved_by_update SeriesUpdate{..} = 97 | updateInputRes == updateOutputRes 98 | 99 | prop_step_preserved_by_update :: SeriesUpdate -> Bool 100 | prop_step_preserved_by_update SeriesUpdate{..} = 101 | updateInputStep == updateOutputStep 102 | 103 | prop_old_values_ignored_by_update :: SeriesUpdate -> Property 104 | prop_old_values_ignored_by_update su@SeriesUpdate{..} = 105 | isUpdateBeforeStart su ==> updateInputSeries == updateOutputSeries 106 | 107 | prop_later_times_move_end_along_in_update :: SeriesUpdate -> Property 108 | prop_later_times_move_end_along_in_update su@SeriesUpdate{..} = 109 | isUpdateAfterEnd su ==> updateInputEnd < updateOutputEnd 110 | 111 | prop_new_end_less_than_or_equal_to_update_time :: SeriesUpdate -> Property 112 | prop_new_end_less_than_or_equal_to_update_time su@SeriesUpdate{..} = 113 | isUpdateAfterEnd su ==> fromIntegral updateOutputEnd <= (fromIntegral updateInputTime :: Int) 114 | 115 | prop_new_end_within_step_of_create_time :: SeriesUpdate -> Property 116 | prop_new_end_within_step_of_create_time su@SeriesUpdate{..} = 117 | isUpdateAfterEnd su ==> fromIntegral updateOutputEnd > (fromIntegral updateInputTime - updateInputStep :: Int) 118 | 119 | prop_new_end_last_value_equals_update_value :: SeriesUpdate -> Property 120 | prop_new_end_last_value_equals_update_value su@SeriesUpdate{..} = 121 | isUpdateAfterEnd su ==> updateInputVal == fromJust (last updateOutputValues) 122 | 123 | prop_update_between_start_and_end_adds_value :: SeriesUpdate -> Property 124 | prop_update_between_start_and_end_adds_value su@SeriesUpdate{..} = 125 | isUpdateBetweenStartAndEnd su ==> prettyClose (sum (catMaybes updateInputValues) + updateInputVal) (sum (catMaybes updateOutputValues)) 126 | 127 | prop_fetch_start_to_end_is_series_identity :: Series -> Bool 128 | prop_fetch_start_to_end_is_series_identity series = 129 | series == fetch (Time . fromIntegral $ start series) (Time . fromIntegral $ end series) series 130 | 131 | prop_fetch_preserves_resolution :: SeriesFetch -> Bool 132 | prop_fetch_preserves_resolution SeriesFetch{..} = 133 | fetchInputRes == fetchOutputRes 134 | 135 | prop_fetch_preserves_step :: SeriesFetch -> Bool 136 | prop_fetch_preserves_step SeriesFetch{..} = 137 | fetchInputStep == fetchOutputStep 138 | 139 | prop_fetch_values_less_than_or_equal_to_original :: SeriesFetch -> Bool 140 | prop_fetch_values_less_than_or_equal_to_original SeriesFetch{..} = 141 | sum (catMaybes fetchOutputValues) <= sum (catMaybes fetchInputValues) 142 | 143 | prop_end_divisible_by_step :: Series -> Bool 144 | prop_end_divisible_by_step series = 145 | 0 == fromIntegral (end series) `mod` step series 146 | 147 | prop_start_end_equals_resolution_times_step :: Series -> Bool 148 | prop_start_end_equals_resolution_times_step series = 149 | fromIntegral (end series - start series) == (resolution series * step series) 150 | 151 | prop_values_length_equals_resolution :: Series -> Bool 152 | prop_values_length_equals_resolution series = 153 | resolution series == length (values series) 154 | 155 | prop_ordered_by_insertion_time :: Series -> Property 156 | prop_ordered_by_insertion_time series = 157 | forAll (vector $ resolution series) $ \xs -> 158 | (map Just xs) == values (foldl upd series xs) 159 | where 160 | upd s v = update (incr s) v s 161 | incr s = fromIntegral (end s) + fromIntegral (step s) 162 | 163 | 164 | test_example_create :: Assertion 165 | test_example_create = do 166 | let series = create 5 10 (Time 50000) 3.4 167 | assertEqual "resolution" 5 (resolution series) 168 | assertEqual "step" 10 (step series) 169 | assertEqual "end" (I 50000) (end series) 170 | assertEqual "start" (I 49950) (start series) 171 | assertEqual "values" 172 | [Nothing, Nothing, Nothing, Nothing, Just 3.4] 173 | (values series) 174 | assertEqual "build" 175 | "49950,50000,10|None,None,None,None,3.4" 176 | (toByteString $ build series) 177 | 178 | test_example_update :: Assertion 179 | test_example_update = do 180 | let series = update 50010 4.5 $ create 5 10 (Time 50000) 3.4 181 | assertEqual "resolution" 5 (resolution series) 182 | assertEqual "step" 10 (step series) 183 | assertEqual "end" (I 50010) (end series) 184 | assertEqual "start" (I 49960) (start series) 185 | assertEqual "values" 186 | [Nothing, Nothing, Nothing, Just 3.4, Just 4.5] 187 | (values series) 188 | assertEqual "build" 189 | "49960,50010,10|None,None,None,3.4,4.5" 190 | (toByteString $ build series) 191 | 192 | test_example_fetch :: Assertion 193 | test_example_fetch = do 194 | let series = fetch (Time 49950) (Time 50020) 195 | . update 50010 4.5 $ create 5 10 (Time 50000) 3.4 196 | assertEqual "resolution" 5 (resolution series) 197 | assertEqual "step" 10 (step series) 198 | assertEqual "end" (I 50020) (end series) 199 | assertEqual "start" (I 49970) (start series) 200 | assertEqual "values" 201 | [Nothing, Nothing, Just 3.4, Just 4.5, Nothing] 202 | (values series) 203 | assertEqual "build" 204 | "49970,50020,10|None,None,3.4,4.5,None" 205 | (toByteString $ build series) 206 | 207 | data SeriesCreate = SeriesCreate { 208 | createInputRes :: Resolution 209 | , createInputStep :: Step 210 | , createInputTime :: Time 211 | , createInputVal :: Double 212 | , createOutputSeries :: Series 213 | , createOutputRes :: Resolution 214 | , createOutputStep :: Step 215 | , createOutputStart :: Interval 216 | , createOutputEnd :: Interval 217 | , createOutputValues :: [Maybe Double] 218 | } deriving Show 219 | 220 | instance Arbitrary SeriesCreate where 221 | arbitrary = do 222 | r <- choose (1, maxResolution) 223 | s <- choose (1, 1000) 224 | t <- arbitrary 225 | NonNegative v <- arbitrary 226 | let series = create r s t v 227 | return SeriesCreate { 228 | createInputRes = r 229 | , createInputStep = s 230 | , createInputTime = t 231 | , createInputVal = v 232 | , createOutputSeries = series 233 | , createOutputRes = resolution series 234 | , createOutputStep = step series 235 | , createOutputStart = start series 236 | , createOutputEnd = end series 237 | , createOutputValues = values series 238 | } 239 | 240 | data SeriesUpdate = SeriesUpdate { 241 | updateInputTime :: Time 242 | , updateInputVal :: Double 243 | , updateInputSeries :: Series 244 | , updateInputRes :: Resolution 245 | , updateInputStep :: Step 246 | , updateInputStart :: Interval 247 | , updateInputEnd :: Interval 248 | , updateInputValues :: [Maybe Double] 249 | , updateOutputSeries :: Series 250 | , updateOutputRes :: Resolution 251 | , updateOutputStep :: Step 252 | , updateOutputStart :: Interval 253 | , updateOutputEnd :: Interval 254 | , updateOutputValues :: [Maybe Double] 255 | } deriving Show 256 | 257 | isUpdateBeforeStart :: SeriesUpdate -> Bool 258 | isUpdateBeforeStart SeriesUpdate{..} = 259 | (fromIntegral updateInputTime :: Int) < fromIntegral updateInputStart + updateInputStep 260 | 261 | isUpdateAfterEnd :: SeriesUpdate -> Bool 262 | isUpdateAfterEnd SeriesUpdate{..} = 263 | (fromIntegral updateInputTime :: Int) >= fromIntegral updateInputEnd + updateInputStep 264 | 265 | isUpdateBetweenStartAndEnd :: SeriesUpdate -> Bool 266 | isUpdateBetweenStartAndEnd su = 267 | not (isUpdateBeforeStart su) && not (isUpdateAfterEnd su) 268 | 269 | instance Arbitrary SeriesUpdate where 270 | arbitrary = do 271 | s <- arbitrary 272 | NonNegative t <- arbitrary 273 | NonNegative v <- arbitrary 274 | let series = update t v s 275 | return SeriesUpdate { 276 | updateInputTime = t 277 | , updateInputVal = v 278 | , updateInputSeries = s 279 | , updateInputRes = resolution s 280 | , updateInputStep = step s 281 | , updateInputStart = start s 282 | , updateInputEnd = end s 283 | , updateInputValues = values s 284 | , updateOutputSeries = series 285 | , updateOutputRes = resolution s 286 | , updateOutputStep = step series 287 | , updateOutputStart = start series 288 | , updateOutputEnd = end series 289 | , updateOutputValues = values series 290 | } 291 | 292 | data SeriesFetch = SeriesFetch { 293 | fetchInputFrom :: Time 294 | , fetchInputTo :: Time 295 | , fetchInputSeries :: Series 296 | , fetchInputRes :: Resolution 297 | , fetchInputStep :: Step 298 | , fetchInputStart :: Interval 299 | , fetchInputEnd :: Interval 300 | , fetchInputValues :: [Maybe Double] 301 | , fetchOutputSeries :: Series 302 | , fetchOutputRes :: Resolution 303 | , fetchOutputStep :: Step 304 | , fetchOutputStart :: Interval 305 | , fetchOutputEnd :: Interval 306 | , fetchOutputValues :: [Maybe Double] 307 | } deriving Show 308 | 309 | instance Arbitrary SeriesFetch where 310 | arbitrary = do 311 | s <- arbitrary 312 | NonNegative f <- arbitrary 313 | NonNegative t <- arbitrary 314 | let series = fetch f t s 315 | return SeriesFetch { 316 | fetchInputFrom = f 317 | , fetchInputTo = t 318 | , fetchInputSeries = s 319 | , fetchInputRes = resolution s 320 | , fetchInputStep = step s 321 | , fetchInputStart = start s 322 | , fetchInputEnd = end s 323 | , fetchInputValues = values s 324 | , fetchOutputSeries = series 325 | , fetchOutputRes = resolution s 326 | , fetchOutputStep = step series 327 | , fetchOutputStart = start series 328 | , fetchOutputEnd = end series 329 | , fetchOutputValues = values series 330 | } 331 | 332 | instance Arbitrary Series where 333 | arbitrary = do 334 | s <- createOutputSeries <$> arbitrary 335 | actions <- arbitrary 336 | return $ foldl applyAction s actions 337 | where 338 | applyAction :: Series -> Either (NonNegative Time, NonNegative Time) (NonNegative Time, NonNegative Double) -> Series 339 | applyAction s (Left (NonNegative f, NonNegative t)) = fetch f t s 340 | applyAction s (Right (NonNegative t, NonNegative v)) = update t v s 341 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /assets/javascripts/rickshaw-min.js: -------------------------------------------------------------------------------- 1 | var Rickshaw={namespace:function(a,b){var c=a.split("."),d=Rickshaw;for(var e=1,f=c.length;ethis.window.xMax&&(b=!1),b}return!0},this.onUpdate=function(a){this.updateCallbacks.push(a)},this.registerRenderer=function(a){this._renderers=this._renderers||{},this._renderers[a.name]=a},this.configure=function(a){(a.width||a.height)&&this.setSize(a),Rickshaw.keys(this.defaults).forEach(function(b){this[b]=b in a?a[b]:b in this?this[b]:this.defaults[b]},this),this.setRenderer(a.renderer||this.renderer.name,a)},this.setRenderer=function(a,b){if(!this._renderers[a])throw"couldn't find renderer "+a;this.renderer=this._renderers[a],typeof b=="object"&&this.renderer.configure(b)},this.setSize=function(a){a=a||{};if(typeof window!==undefined)var b=window.getComputedStyle(this.element,null),c=parseInt(b.getPropertyValue("width")),d=parseInt(b.getPropertyValue("height"));this.width=a.width||c||400,this.height=a.height||d||250,this.vis&&this.vis.attr("width",this.width).attr("height",this.height)},this.initialize(a)},Rickshaw.namespace("Rickshaw.Fixtures.Color"),Rickshaw.Fixtures.Color=function(){this.schemes={},this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse(),this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"],this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"],this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse(),this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"},this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse(),this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"],this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]},Rickshaw.namespace("Rickshaw.Fixtures.RandomData"),Rickshaw.Fixtures.RandomData=function(a){var b;a=a||1;var c=200,d=Math.floor((new Date).getTime()/1e3);this.addData=function(b){var e=Math.random()*100+15+c,f=b[0].length,g=1;b.forEach(function(b){var c=Math.random()*20,h=e/25+g++ +(Math.cos(f*g*11/960)+2)*15+(Math.cos(f/7)+2)*7+(Math.cos(f/17)+2)*1;b.push({x:f*a+d,y:h+c})}),c=e*.85}},Rickshaw.namespace("Rickshaw.Fixtures.Time"),Rickshaw.Fixtures.Time=function(){var a=(new Date).getTimezoneOffset()*60,b=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],this.units=[{name:"decade",seconds:315576e3,formatter:function(a){return parseInt(a.getUTCFullYear()/10)*10}},{name:"year",seconds:31557600,formatter:function(a){return a.getUTCFullYear()}},{name:"month",seconds:2635200,formatter:function(a){return b.months[a.getUTCMonth()]}},{name:"week",seconds:604800,formatter:function(a){return b.formatDate(a)}},{name:"day",seconds:86400,formatter:function(a){return a.getUTCDate()}},{name:"6 hour",seconds:21600,formatter:function(a){return b.formatTime(a)}},{name:"hour",seconds:3600,formatter:function(a){return b.formatTime(a)}},{name:"15 minute",seconds:900,formatter:function(a){return b.formatTime(a)}},{name:"minute",seconds:60,formatter:function(a){return a.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(a){return a.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(a){return a.getUTCSeconds()+"s"}}],this.unit=function(a){return this.units.filter(function(b){return a==b.name}).shift()},this.formatDate=function(a){return a.toUTCString().match(/, (\w+ \w+ \w+)/)[1]},this.formatTime=function(a){return a.toUTCString().match(/(\d+:\d+):/)[1]},this.ceil=function(a,b){if(b.name=="month"){var c=new Date((a+b.seconds-1)*1e3),d=new Date(0);return d.setUTCFullYear(c.getUTCFullYear()),d.setUTCMonth(c.getUTCMonth()),d.setUTCDate(1),d.setUTCHours(0),d.setUTCMinutes(0),d.setUTCSeconds(0),d.setUTCMilliseconds(0),d.getTime()/1e3}if(b.name=="year"){var c=new Date((a+b.seconds-1)*1e3),d=new Date(0);return d.setUTCFullYear(c.getUTCFullYear()),d.setUTCMonth(0),d.setUTCDate(1),d.setUTCHours(0),d.setUTCMinutes(0),d.setUTCSeconds(0),d.setUTCMilliseconds(0),d.getTime()/1e3}return Math.ceil(a/b.seconds)*b.seconds}},Rickshaw.namespace("Rickshaw.Fixtures.Number"),Rickshaw.Fixtures.Number.formatKMBT=function(a){return a>=1e12?a/1e12+"T":a>=1e9?a/1e9+"B":a>=1e6?a/1e6+"M":a>=1e3?a/1e3+"K":a<1&&a>0?a.toFixed(2):a==0?"":a},Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(a){return a>=0x4000000000000?a/0x4000000000000+"P":a>=1099511627776?a/1099511627776+"T":a>=1073741824?a/1073741824+"G":a>=1048576?a/1048576+"M":a>=1024?a/1024+"K":a<1&&a>0?a.toFixed(2):a==0?"":a},Rickshaw.namespace("Rickshaw.Color.Palette"),Rickshaw.Color.Palette=function(a){var b=new Rickshaw.Fixtures.Color;a=a||{},this.schemes={},this.scheme=b.schemes[a.scheme]||a.scheme||b.schemes.colorwheel,this.runningIndex=0,this.generatorIndex=0;if(a.interpolatedStopCount){var c=this.scheme.length-1,d,e,f=[];for(d=0;dc.graph.x.range()[1]){b.element&&(b.line.classList.add("offscreen"),b.element.style.display="none"),b.boxes.forEach(function(a){a.rangeElement&&a.rangeElement.classList.add("offscreen")});return}if(!b.element){var e=b.element=document.createElement("div");e.classList.add("annotation"),this.elements.timeline.appendChild(e),e.addEventListener("click",function(a){e.classList.toggle("active"),b.line.classList.toggle("active"),b.boxes.forEach(function(a){a.rangeElement&&a.rangeElement.classList.toggle("active")})},!1)}b.element.style.left=d+"px",b.element.style.display="block",b.boxes.forEach(function(a){var e=a.element;e||(e=a.element=document.createElement("div"),e.classList.add("content"),e.innerHTML=a.content,b.element.appendChild(e),b.line=document.createElement("div"),b.line.classList.add("annotation_line"),c.graph.element.appendChild(b.line),a.end&&(a.rangeElement=document.createElement("div"),a.rangeElement.classList.add("annotation_range"),c.graph.element.appendChild(a.rangeElement)));if(a.end){var f=d,g=Math.min(c.graph.x(a.end),c.graph.x.range()[1]);f>g&&(g=d,f=Math.max(c.graph.x(a.end),c.graph.x.range()[0]));var h=g-f;a.rangeElement.style.left=f+"px",a.rangeElement.style.width=h+"px",a.rangeElement.classList.remove("offscreen")}b.line.classList.remove("offscreen"),b.line.style.left=d+"px"})},this)},this.graph.onUpdate(function(){c.update()})},Rickshaw.namespace("Rickshaw.Graph.Axis.Time"),Rickshaw.Graph.Axis.Time=function(a){var b=this;this.graph=a.graph,this.elements=[],this.ticksTreatment=a.ticksTreatment||"plain",this.fixedTimeUnit=a.timeUnit;var c=new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var a,b=c.units,d=this.graph.x.domain(),e=d[1]-d[0];return b.forEach(function(b){Math.floor(e/b.seconds)>=2&&(a=a||b)}),a||c.units[c.units.length-1]},this.tickOffsets=function(){var a=this.graph.x.domain(),b=this.fixedTimeUnit||this.appropriateTimeUnit(),d=Math.ceil((a[1]-a[0])/b.seconds),e=a[0],f=[];for(var g=0;gb.graph.x.range()[1])return;var c=document.createElement("div");c.style.left=b.graph.x(a.value)+"px",c.classList.add("x_tick"),c.classList.add(b.ticksTreatment);var d=document.createElement("div");d.classList.add("title"),d.innerHTML=a.unit.formatter(new Date(a.value*1e3)),c.appendChild(d),b.graph.element.appendChild(c),b.elements.push(c)})},this.graph.onUpdate(function(){b.render()})},Rickshaw.namespace("Rickshaw.Graph.Axis.Y"),Rickshaw.Graph.Axis.Y=function(a){var b=this,c=.1;this.initialize=function(a){this.graph=a.graph,this.orientation=a.orientation||"right";var c=a.pixelsPerTick||75;this.ticks=a.ticks||Math.floor(this.graph.height/c),this.tickSize=a.tickSize||4,this.ticksTreatment=a.ticksTreatment||"plain",a.element?(this.element=a.element,this.vis=d3.select(a.element).append("svg:svg").attr("class","rickshaw_graph y_axis"),this.element=this.vis[0][0],this.element.style.position="relative",this.setSize({width:a.width,height:a.height})):this.vis=this.graph.vis,this.graph.onUpdate(function(){b.render()})},this.setSize=function(a){a=a||{};if(!this.element)return;if(typeof window!="undefined"){var b=window.getComputedStyle(this.element.parentNode,null),d=parseInt(b.getPropertyValue("width"));if(!a.auto)var e=parseInt(b.getPropertyValue("height"))}this.width=a.width||d||this.graph.width*c,this.height=a.height||e||this.graph.height,this.vis.attr("width",this.width).attr("height",this.height*(1+c));var f=this.height*c;this.element.style.top=-1*f+"px"},this.render=function(){this.graph.height!==this._renderHeight&&this.setSize({auto:!0});var b=d3.svg.axis().scale(this.graph.y).orient(this.orientation);b.tickFormat(a.tickFormat||function(a){return a});if(this.orientation=="left")var d=this.height*c,e="translate("+this.width+", "+d+")";this.element&&this.vis.selectAll("*").remove(),this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",e).call(b.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var f=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(b.ticks(this.ticks).tickSubdivide(0).tickSize(f)),this._renderHeight=this.graph.height},this.initialize(a)},Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight"),Rickshaw.Graph.Behavior.Series.Highlight=function(a){this.graph=a.graph,this.legend=a.legend;var b=this,c={};this.addHighlightEvents=function(a){a.element.addEventListener("mouseover",function(d){b.legend.lines.forEach(function(b){if(a===b)return;c[b.series.name]=c[b.series.name]||b.series.color,b.series.color=d3.interpolateRgb(b.series.color,d3.rgb("#d8d8d8"))(.8).toString()}),b.graph.update()},!1),a.element.addEventListener("mouseout",function(a){b.legend.lines.forEach(function(a){c[a.series.name]&&(a.series.color=c[a.series.name])}),b.graph.update()},!1)},this.legend&&this.legend.lines.forEach(function(a){b.addHighlightEvents(a)})},Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order"),Rickshaw.Graph.Behavior.Series.Order=function(a){this.graph=a.graph,this.legend=a.legend;var b=this;$(function(){$(b.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(a,c){var d=[];$(b.legend.list).find("li").each(function(a,b){if(!b.series)return;d.push(b.series)});for(var e=b.graph.series.length-1;e>=0;e--)b.graph.series[e]=d.shift();b.graph.update()}}),$(b.legend.list).disableSelection()}),this.graph.onUpdate(function(){var a=window.getComputedStyle(b.legend.element).height;b.legend.element.style.height=a})},Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle"),Rickshaw.Graph.Behavior.Series.Toggle=function(a){this.graph=a.graph,this.legend=a.legend;var b=this;this.addAnchor=function(a){var c=document.createElement("a");c.innerHTML="✔",c.classList.add("action"),a.element.insertBefore(c,a.element.firstChild),c.onclick=function(b){a.series.disabled?(a.series.enable(),a.element.classList.remove("disabled")):(a.series.disable(),a.element.classList.add("disabled"))};var d=a.element.getElementsByTagName("span")[0];d.onclick=function(c){var d=a.series.disabled;if(!d)for(var e=0;ee){j=k;break}f[0][k+1]<=e?k++:k--}var e=f[0][j].x,l=this.xFormatter(e),m=b.x(e),n=0,o=b.series.active().map(function(a){return{order:n++,series:a,name:a.name,value:a.stack[j]}}),p,q=function(a,b){return a.value.y0+a.value.y-(b.value.y0+b.value.y)},r=b.y.magnitude.invert(b.element.offsetHeight-d);o.sort(q).forEach(function(a){a.formattedYValue=this.yFormatter.constructor==Array?this.yFormatter[o.indexOf(a)](a.value.y):this.yFormatter(a.value.y),a.graphX=m,a.graphY=b.y(a.value.y0+a.value.y),r>a.value.y0&&r0?this[0].data.forEach(function(b){a.data.push({x:b.x,y:0})}):a.data.length==0&&a.data.push({x:this.timeBase-(this.timeInterval||0),y:0}),this.push(a),this.legend&&this.legend.addLine(this.itemByName(a.name))},addData:function(a){var b=this.getIndex();Rickshaw.keys(a).forEach(function(a){this.itemByName(a)||this.addItem({name:a})},this),this.forEach(function(c){c.data.push({x:(b*this.timeInterval||1)+this.timeBase,y:a[c.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(a){for(var b=0;b0;d--)this.currentSize+=1,this.currentIndex+=1,this.forEach(function(a){a.data.unshift({x:((d-1)*this.timeInterval||1)+this.timeBase,y:0,i:d})},this)},addData:function($super,a){$super(a),this.currentSize+=1,this.currentIndex+=1;if(this.maxDataPoints!==undefined)while(this.currentSize>this.maxDataPoints)this.dropData()},dropData:function(){this.forEach(function(a){a.data.splice(0,1)}),this.currentSize-=1},getIndex:function(){return this.currentIndex}}); --------------------------------------------------------------------------------