├── .github └── workflows │ └── haskell-ci.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── Setup.hs ├── System └── Remote │ └── Monitoring │ └── Statsd.hs ├── ekg-statsd.cabal └── examples ├── Basic.hs ├── Ekg.hs ├── LICENSE ├── Setup.hs └── ekg-statsd-examples.cabal /.github/workflows/haskell-ci.yml: -------------------------------------------------------------------------------- 1 | # This GitHub workflow config has been generated by a script via 2 | # 3 | # haskell-ci 'github' 'ekg-statsd.cabal' 4 | # 5 | # To regenerate the script (for example after adjusting tested-with) run 6 | # 7 | # haskell-ci regenerate 8 | # 9 | # For more information, see https://github.com/haskell-CI/haskell-ci 10 | # 11 | # version: 0.19.20250330 12 | # 13 | # REGENDATA ("0.19.20250330",["github","ekg-statsd.cabal"]) 14 | # 15 | name: Haskell-CI 16 | on: 17 | - push 18 | - pull_request 19 | jobs: 20 | linux: 21 | name: Haskell-CI - Linux - ${{ matrix.compiler }} 22 | runs-on: ubuntu-24.04 23 | timeout-minutes: 24 | 60 25 | container: 26 | image: buildpack-deps:jammy 27 | continue-on-error: ${{ matrix.allow-failure }} 28 | strategy: 29 | matrix: 30 | include: 31 | - compiler: ghc-9.12.2 32 | compilerKind: ghc 33 | compilerVersion: 9.12.2 34 | setup-method: ghcup 35 | allow-failure: false 36 | - compiler: ghc-9.10.1 37 | compilerKind: ghc 38 | compilerVersion: 9.10.1 39 | setup-method: ghcup 40 | allow-failure: false 41 | - compiler: ghc-9.8.2 42 | compilerKind: ghc 43 | compilerVersion: 9.8.2 44 | setup-method: ghcup 45 | allow-failure: false 46 | - compiler: ghc-9.6.6 47 | compilerKind: ghc 48 | compilerVersion: 9.6.6 49 | setup-method: ghcup 50 | allow-failure: false 51 | - compiler: ghc-9.6.4 52 | compilerKind: ghc 53 | compilerVersion: 9.6.4 54 | setup-method: ghcup 55 | allow-failure: false 56 | - compiler: ghc-9.4.8 57 | compilerKind: ghc 58 | compilerVersion: 9.4.8 59 | setup-method: ghcup 60 | allow-failure: false 61 | - compiler: ghc-9.2.8 62 | compilerKind: ghc 63 | compilerVersion: 9.2.8 64 | setup-method: ghcup 65 | allow-failure: false 66 | - compiler: ghc-9.0.2 67 | compilerKind: ghc 68 | compilerVersion: 9.0.2 69 | setup-method: ghcup 70 | allow-failure: false 71 | - compiler: ghc-8.10.7 72 | compilerKind: ghc 73 | compilerVersion: 8.10.7 74 | setup-method: ghcup 75 | allow-failure: false 76 | - compiler: ghc-8.8.3 77 | compilerKind: ghc 78 | compilerVersion: 8.8.3 79 | setup-method: ghcup 80 | allow-failure: false 81 | - compiler: ghc-8.6.5 82 | compilerKind: ghc 83 | compilerVersion: 8.6.5 84 | setup-method: ghcup 85 | allow-failure: false 86 | - compiler: ghc-8.4.4 87 | compilerKind: ghc 88 | compilerVersion: 8.4.4 89 | setup-method: ghcup 90 | allow-failure: false 91 | - compiler: ghc-8.2.2 92 | compilerKind: ghc 93 | compilerVersion: 8.2.2 94 | setup-method: ghcup 95 | allow-failure: false 96 | - compiler: ghc-8.0.2 97 | compilerKind: ghc 98 | compilerVersion: 8.0.2 99 | setup-method: ghcup 100 | allow-failure: false 101 | fail-fast: false 102 | steps: 103 | - name: apt-get install 104 | run: | 105 | apt-get update 106 | apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 libnuma-dev 107 | - name: Install GHCup 108 | run: | 109 | mkdir -p "$HOME/.ghcup/bin" 110 | curl -sL https://downloads.haskell.org/ghcup/0.1.50.1/x86_64-linux-ghcup-0.1.50.1 > "$HOME/.ghcup/bin/ghcup" 111 | chmod a+x "$HOME/.ghcup/bin/ghcup" 112 | - name: Install cabal-install 113 | run: | 114 | "$HOME/.ghcup/bin/ghcup" install cabal 3.14.1.1-p1 || (cat "$HOME"/.ghcup/logs/*.* && false) 115 | echo "CABAL=$HOME/.ghcup/bin/cabal-3.14.1.1-p1 -vnormal+nowrap" >> "$GITHUB_ENV" 116 | - name: Install GHC (GHCup) 117 | if: matrix.setup-method == 'ghcup' 118 | run: | 119 | "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) 120 | HC=$("$HOME/.ghcup/bin/ghcup" whereis ghc "$HCVER") 121 | HCPKG=$(echo "$HC" | sed 's#ghc$#ghc-pkg#') 122 | HADDOCK=$(echo "$HC" | sed 's#ghc$#haddock#') 123 | echo "HC=$HC" >> "$GITHUB_ENV" 124 | echo "HCPKG=$HCPKG" >> "$GITHUB_ENV" 125 | echo "HADDOCK=$HADDOCK" >> "$GITHUB_ENV" 126 | env: 127 | HCKIND: ${{ matrix.compilerKind }} 128 | HCNAME: ${{ matrix.compiler }} 129 | HCVER: ${{ matrix.compilerVersion }} 130 | - name: Set PATH and environment variables 131 | run: | 132 | echo "$HOME/.cabal/bin" >> $GITHUB_PATH 133 | echo "LANG=C.UTF-8" >> "$GITHUB_ENV" 134 | echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" 135 | echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" 136 | HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') 137 | echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" 138 | echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" 139 | echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" 140 | echo "HEADHACKAGE=false" >> "$GITHUB_ENV" 141 | echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" 142 | env: 143 | HCKIND: ${{ matrix.compilerKind }} 144 | HCNAME: ${{ matrix.compiler }} 145 | HCVER: ${{ matrix.compilerVersion }} 146 | - name: env 147 | run: | 148 | env 149 | - name: write cabal config 150 | run: | 151 | mkdir -p $CABAL_DIR 152 | cat >> $CABAL_CONFIG <> $CABAL_CONFIG < cabal-plan.xz 185 | echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2 cabal-plan.xz' | sha256sum -c - 186 | xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan 187 | rm -f cabal-plan.xz 188 | chmod a+x $HOME/.cabal/bin/cabal-plan 189 | cabal-plan --version 190 | - name: checkout 191 | uses: actions/checkout@v4 192 | with: 193 | path: source 194 | - name: initial cabal.project for sdist 195 | run: | 196 | touch cabal.project 197 | echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project 198 | cat cabal.project 199 | - name: sdist 200 | run: | 201 | mkdir -p sdist 202 | $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist 203 | - name: unpack 204 | run: | 205 | mkdir -p unpacked 206 | find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \; 207 | - name: generate cabal.project 208 | run: | 209 | PKGDIR_ekg_statsd="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/ekg-statsd-[0-9.]*')" 210 | echo "PKGDIR_ekg_statsd=${PKGDIR_ekg_statsd}" >> "$GITHUB_ENV" 211 | rm -f cabal.project cabal.project.local 212 | touch cabal.project 213 | touch cabal.project.local 214 | echo "packages: ${PKGDIR_ekg_statsd}" >> cabal.project 215 | if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo "package ekg-statsd" >> cabal.project ; fi 216 | if [ $((HCNUMVER >= 80200)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi 217 | cat >> cabal.project <> cabal.project.local 220 | cat cabal.project 221 | cat cabal.project.local 222 | - name: dump install plan 223 | run: | 224 | $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all 225 | cabal-plan 226 | - name: restore cache 227 | uses: actions/cache/restore@v4 228 | with: 229 | key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} 230 | path: ~/.cabal/store 231 | restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- 232 | - name: install dependencies 233 | run: | 234 | $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all 235 | $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all 236 | - name: build w/o tests 237 | run: | 238 | $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all 239 | - name: build 240 | run: | 241 | $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always 242 | - name: cabal check 243 | run: | 244 | cd ${PKGDIR_ekg_statsd} || false 245 | ${CABAL} -vnormal check 246 | - name: haddock 247 | run: | 248 | $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all 249 | - name: unconstrained build 250 | run: | 251 | rm -f cabal.project.local 252 | $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all 253 | - name: save cache 254 | if: always() 255 | uses: actions/cache/save@v4 256 | with: 257 | key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} 258 | path: ~/.cabal/store 259 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.hi 2 | *.o 3 | *.p_hi 4 | *.prof 5 | *.tix 6 | .hpc/ 7 | /dist/* 8 | /dist-newstyle/* 9 | /examples/dist/* 10 | .cabal-sandbox/ 11 | .DS_Store 12 | cabal.config 13 | cabal.sandbox.config 14 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.2.6.2 (2025-04-22) 2 | * Support GHC 9.12.2 3 | 4 | ## 0.2.6.1 (2024-10-27) 5 | * Support GHC 9.10 6 | 7 | ## 0.2.6.0 (2024-07-31) 8 | 9 | * Support GHC 9.0 through 9.8. 10 | * Bump network to <3.3 11 | 12 | ## 0.2.5.0 (2020-06-15) 13 | 14 | * Bugfix: when reporting counter values to statsd, send only the 15 | increments ([#23](https://github.com/tibbe/ekg-statsd/pull/23)). 16 | 17 | ## 0.2.4.1 (2020-05-21) 18 | 19 | * Sanitize metric names by replacing `:` with `_` to adhere to the StatsD 20 | protocol ([#26](https://github.com/tibbe/ekg-statsd/pull/26)). 21 | * Add support for GHC 8.10 in base bounds and CI. 22 | 23 | ## 0.2.4.0 (2018-08-01) 24 | 25 | * Don't rethrow `ThreadKilled` exceptions to the thread that invoked 26 | `forkStatsd`, so that the statsd thread can be safely killed 27 | ([#20](https://github.com/tibbe/ekg-statsd/pull/20)). 28 | 29 | ## 0.2.3.0 (2018-04-10) 30 | 31 | * API addition: 'statsdFlush', allows to flush the sample to statsd 32 | server manually 33 | ([#18](https://github.com/tibbe/ekg-statsd/pull/18)). 34 | 35 | ## 0.2.2.0 (2017-09-25) 36 | 37 | * Remove internal `diffSample` optimisation and always report the 38 | full state of the `Store` instead of only what's changed between 39 | iterations ([#17](https://github.com/tibbe/ekg-statsd/pull/17)). 40 | 41 | ## 0.2.1.1 (2017-07-31) 42 | 43 | * Support GHC 8.2.1. 44 | * Suppress errors when sending to a non-existent receiver 45 | ([#6](https://github.com/tibbe/ekg-statsd/pull/6)). 46 | 47 | ## 0.2.1.0 (2016-08-11) 48 | 49 | * Send distributions as gauges and counters. 50 | * Update examples. 51 | 52 | ## 0.2.0.4 (2016-05-28) 53 | 54 | * GHC 8.0 support. 55 | 56 | ## 0.2.0.3 (2015-06-06) 57 | 58 | * Support GHC 7.10. 59 | 60 | ## 0.2.0.2 (2015-04-09) 61 | 62 | * Add support for network-2.6. 63 | 64 | ## 0.2.0.1 (2014-09-30) 65 | 66 | * Add support for text-1.2. 67 | 68 | ## 0.2.0.0 (2014-05-27) 69 | 70 | * Add configurable metric name prefix and suffix. 71 | * Add support for GHC 7.4. 72 | 73 | ## 0.1.0.0 (2014-05-01) 74 | 75 | * Initial release. 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Johan Tibell 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Johan Tibell nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ekg-statsd: statsd backend for ekg [![Hackage version](https://img.shields.io/hackage/v/ekg-statsd.svg?label=Hackage)](https://hackage.haskell.org/package/ekg-statsd) [![Build Status](https://secure.travis-ci.org/tibbe/ekg-statsd.svg?branch=master)](http://travis-ci.org/tibbe/ekg-statsd) 2 | 3 | This library lets you send metrics gathered by the ekg family of 4 | packages (e.g. ekg-core and ekg) to 5 | [statsd](https://github.com/etsy/statsd/). While statsd fulfills a 6 | very similar role to ekg, it supports many more backends/graphing 7 | systems (e.g. Graphite). By sending your metrics to statsd, you can 8 | have your ekg metrics appear in these systems. 9 | 10 | # Getting started 11 | 12 | Exporting metrics to statsd is simple. Either create an empty metric 13 | store and register some metrics 14 | 15 | import System.Metrics 16 | import System.Remote.Monitoring.Statsd 17 | 18 | main = do 19 | store <- newStore 20 | registerGcMetrics store 21 | forkStatsd defaultStatsdOptions store 22 | ... 23 | 24 | or use the default metrics and metric store provided by the ekg 25 | package 26 | 27 | import System.Remote.Monitoring 28 | import System.Remote.Monitoring.Statsd 29 | 30 | main = do 31 | handle <- forkServer "localhost" 8000 32 | forkStatsd defaultStatsdOptions (serverMetricStore handle) 33 | ... 34 | 35 | `forkStatsd` starts a new thread the will periodically send your 36 | metrics to statsd using UDP. 37 | 38 | # Get involved! 39 | 40 | Please report bugs via the 41 | [GitHub issue tracker](https://github.com/tibbe/ekg-statsd/issues). 42 | 43 | Master [git repository](https://github.com/tibbe/ekg-statsd): 44 | 45 | git clone https://github.com/tibbe/ekg-statsd.git 46 | 47 | # Authors 48 | 49 | This library is written and maintained by Johan Tibell, 50 | . 51 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /System/Remote/Monitoring/Statsd.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE CPP #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | -- | This module lets you periodically flush metrics to a statsd 7 | -- backend. Example usage: 8 | -- 9 | -- > main = do 10 | -- > store <- newStore 11 | -- > forkStatsd defaultStatsdOptions store 12 | -- 13 | -- You probably want to include some of the predefined metrics defined 14 | -- in the ekg-core package, by calling e.g. the 'registerGcStats' 15 | -- function defined in that package. 16 | -- 17 | -- Note that the StatsD protocol does not allow @':'@ in metric names, so 18 | -- any occurrences are replaced by @'_'@. 19 | module System.Remote.Monitoring.Statsd 20 | ( 21 | -- * The statsd syncer 22 | Statsd 23 | , statsdFlush 24 | , statsdThreadId 25 | , forkStatsd 26 | , StatsdOptions(..) 27 | , defaultStatsdOptions 28 | ) where 29 | 30 | import Control.Concurrent (ThreadId, myThreadId, threadDelay, throwTo) 31 | import Control.Concurrent.MVar (modifyMVar_, newMVar) 32 | import Control.Exception (IOException, AsyncException(ThreadKilled), catch, fromException) 33 | import Control.Monad (foldM, when) 34 | import qualified Data.ByteString.Char8 as B8 35 | import qualified Data.HashMap.Strict as M 36 | import Data.Int (Int64) 37 | import Data.Maybe (fromMaybe) 38 | import Data.Monoid ((<>)) 39 | import qualified Data.Text as T 40 | import qualified Data.Text.Encoding as T 41 | import qualified Data.Text.IO as T 42 | import Data.Time.Clock.POSIX (getPOSIXTime) 43 | import qualified Network.Socket as Socket 44 | import qualified Network.Socket.ByteString as Socket 45 | import qualified System.Metrics as Metrics 46 | import qualified System.Metrics.Distribution.Internal as Distribution 47 | import System.IO (stderr) 48 | 49 | #if __GLASGOW_HASKELL__ >= 706 50 | import Control.Concurrent (forkFinally) 51 | #else 52 | import Control.Concurrent (forkIO) 53 | import Control.Exception (SomeException, mask, try) 54 | import Prelude hiding (catch) 55 | #endif 56 | 57 | -- | A handle that can be used to control the statsd sync thread. 58 | -- Created by 'forkStatsd'. 59 | data Statsd = Statsd 60 | { threadId :: {-# UNPACK #-} !ThreadId 61 | , flush :: IO () 62 | } 63 | 64 | -- | The thread ID of the statsd sync thread. You can stop the sync by 65 | -- killing this thread (i.e. by throwing it an asynchronous 66 | -- exception.) 67 | statsdThreadId :: Statsd -> ThreadId 68 | statsdThreadId = threadId 69 | 70 | -- | Flush a sample to the statsd server 71 | -- 72 | -- @since 0.2.3.0 73 | statsdFlush :: Statsd -> IO () 74 | statsdFlush = flush 75 | 76 | -- | Options to control how to connect to the statsd server and how 77 | -- often to flush metrics. The flush interval should be shorter than 78 | -- the flush interval statsd itself uses to flush data to its 79 | -- backends. 80 | data StatsdOptions = StatsdOptions 81 | { -- | Server hostname or IP address 82 | host :: !T.Text 83 | 84 | -- | Server port 85 | , port :: !Int 86 | 87 | -- | Data push interval, in ms. 88 | , flushInterval :: !Int 89 | 90 | -- | Print debug output to stderr. 91 | , debug :: !Bool 92 | 93 | -- | Prefix to add to all metric names. 94 | , prefix :: !T.Text 95 | 96 | -- | Suffix to add to all metric names. This is particularly 97 | -- useful for sending per host stats by settings this value to: 98 | -- @takeWhile (/= \'.\') \<$\> getHostName@, using @getHostName@ 99 | -- from the @Network.BSD@ module in the network package. 100 | , suffix :: !T.Text 101 | } 102 | 103 | -- | Defaults: 104 | -- 105 | -- * @host@ = @\"127.0.0.1\"@ 106 | -- 107 | -- * @port@ = @8125@ 108 | -- 109 | -- * @flushInterval@ = @1000@ 110 | -- 111 | -- * @debug@ = @False@ 112 | defaultStatsdOptions :: StatsdOptions 113 | defaultStatsdOptions = StatsdOptions 114 | { host = "127.0.0.1" 115 | , port = 8125 116 | , flushInterval = 1000 117 | , debug = False 118 | , prefix = "" 119 | , suffix = "" 120 | } 121 | 122 | -- | Create a thread that periodically flushes the metrics in the 123 | -- store to statsd. 124 | forkStatsd :: StatsdOptions -- ^ Options 125 | -> Metrics.Store -- ^ Metric store 126 | -> IO Statsd -- ^ Statsd sync handle 127 | forkStatsd opts store = do 128 | addrInfos <- Socket.getAddrInfo Nothing (Just $ T.unpack $ host opts) 129 | (Just $ show $ port opts) 130 | (sendSample, closeSocket) <- case addrInfos of 131 | [] -> unsupportedAddressError 132 | (addrInfo:_) -> do 133 | socket <- Socket.socket (Socket.addrFamily addrInfo) 134 | Socket.Datagram Socket.defaultProtocol 135 | 136 | let socketAddress = Socket.addrAddress addrInfo 137 | 138 | sendSample <- if debug opts 139 | then do 140 | Socket.connect socket socketAddress 141 | return $ \msg -> Socket.sendAll socket msg 142 | 143 | else return $ \msg -> Socket.sendAllTo socket msg socketAddress 144 | 145 | return (sendSample, Socket.close socket) 146 | 147 | priorCountsVar <- newMVar M.empty 148 | let flush = do 149 | sample <- Metrics.sampleAll store 150 | modifyMVar_ priorCountsVar (flushSample sample sendSample opts) 151 | 152 | me <- myThreadId 153 | tid <- forkFinally (loop opts flush) $ \ r -> do 154 | closeSocket 155 | case r of 156 | Left e -> case fromException e of 157 | Just ThreadKilled -> return () 158 | _ -> throwTo me e 159 | Right _ -> return () 160 | 161 | return $ Statsd tid flush 162 | where 163 | unsupportedAddressError = ioError $ userError $ 164 | "unsupported address: " ++ T.unpack (host opts) 165 | 166 | loop :: StatsdOptions -- ^ Options 167 | -> IO () -- ^ Action to flush the sample 168 | -> IO () 169 | loop opts flush = do 170 | start <- time 171 | flush 172 | end <- time 173 | threadDelay (flushInterval opts * 1000 - fromIntegral (end - start)) 174 | loop opts flush 175 | 176 | -- | Microseconds since epoch. 177 | time :: IO Int64 178 | time = (round . (* 1000000.0) . toDouble) `fmap` getPOSIXTime 179 | where toDouble = realToFrac :: Real a => a -> Double 180 | 181 | flushSample :: Metrics.Sample -> (B8.ByteString -> IO ()) -> StatsdOptions -> M.HashMap T.Text Int64 -> IO (M.HashMap T.Text Int64) 182 | flushSample sample sendSample opts priorCounts = 183 | foldM flushOne priorCounts (M.toList sample) 184 | where 185 | flushOne pc (name, val) = 186 | let fullName = dottedPrefix <> sanitizeName name <> dottedSuffix 187 | in flushMetric fullName val pc 188 | 189 | sanitizeName = T.map sanitizeChar 190 | sanitizeChar ':' = '_' 191 | sanitizeChar c = c 192 | 193 | flushMetric name (Metrics.Counter n) pc = sendCounter name n pc 194 | flushMetric name (Metrics.Gauge n) pc = sendGauge name n >> return pc 195 | flushMetric name (Metrics.Distribution d) pc = sendDistribution name d pc 196 | flushMetric _ (Metrics.Label _) pc = return pc 197 | 198 | sendGauge name n = send "|g" name (show n) 199 | 200 | -- The statsd convention is to send only the increment 201 | -- since the last report, not the total count. 202 | sendCounter name n pc = do 203 | let old = fromMaybe 0 (M.lookup name pc) 204 | send "|c" name (show (n - old)) 205 | return (M.insert name n pc) 206 | 207 | sendDistribution name d pc = do 208 | sendGauge (name <> "." <> "mean" ) (Distribution.mean d) 209 | sendGauge (name <> "." <> "variance") (Distribution.variance d) 210 | uc <- sendCounter (name <> "." <> "count" ) (Distribution.count d) pc 211 | sendGauge (name <> "." <> "sum" ) (Distribution.sum d) 212 | sendGauge (name <> "." <> "min" ) (Distribution.min d) 213 | sendGauge (name <> "." <> "max" ) (Distribution.max d) 214 | return uc 215 | 216 | isDebug = debug opts 217 | dottedPrefix = if T.null (prefix opts) then "" else prefix opts <> "." 218 | dottedSuffix = if T.null (suffix opts) then "" else "." <> suffix opts 219 | send ty name val = do 220 | let !msg = B8.concat [T.encodeUtf8 name, ":", B8.pack val, ty] 221 | when isDebug $ B8.hPutStrLn stderr $ B8.concat [ "DEBUG: ", msg] 222 | sendSample msg `catch` \ (e :: IOException) -> do 223 | T.hPutStrLn stderr $ "ERROR: Couldn't send message: " <> 224 | T.pack (show e) 225 | return () 226 | 227 | ------------------------------------------------------------------------ 228 | -- Backwards compatibility shims 229 | 230 | #if __GLASGOW_HASKELL__ < 706 231 | forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId 232 | forkFinally action and_then = 233 | mask $ \restore -> 234 | forkIO $ try (restore action) >>= and_then 235 | #endif 236 | -------------------------------------------------------------------------------- /ekg-statsd.cabal: -------------------------------------------------------------------------------- 1 | name: ekg-statsd 2 | version: 0.2.6.2 3 | synopsis: Push metrics to statsd 4 | description: 5 | This library lets you push system metrics to a statsd server. 6 | 7 | homepage: https://github.com/l0negamer/ekg-statsd 8 | bug-reports: https://github.com/l0negamer/ekg-statsd/issues 9 | license: BSD3 10 | license-file: LICENSE 11 | author: Johan Tibell 12 | maintainer: 13 | Johan Tibell , 14 | Mikhail Glushenkov 15 | 16 | category: System 17 | build-type: Simple 18 | extra-source-files: CHANGES.md 19 | cabal-version: >=1.10 20 | tested-with: 21 | GHC ==8.0.2 22 | || ==8.2.2 23 | || ==8.4.4 24 | || ==8.6.5 25 | || ==8.8.3 26 | || ==8.10.7 27 | || ==9.0.2 28 | || ==9.2.8 29 | || ==9.4.8 30 | || ==9.6.4 31 | || ==9.6.6 32 | || ==9.8.2 33 | || ==9.10.1 34 | || ==9.12.2 35 | 36 | library 37 | exposed-modules: System.Remote.Monitoring.Statsd 38 | build-depends: 39 | base >=4.6 && <4.22 40 | , bytestring <1.0 41 | , ekg-core >=0.1 && <1.0 42 | , network <3.3 43 | , text <2.2 44 | , time <1.15 45 | , unordered-containers <0.3 46 | 47 | default-language: Haskell2010 48 | ghc-options: -Wall 49 | 50 | source-repository head 51 | type: git 52 | location: https://github.com/l0negamer/ekg-statsd.git 53 | -------------------------------------------------------------------------------- /examples/Basic.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Main 3 | ( main 4 | ) where 5 | 6 | import Control.Concurrent (threadDelay) 7 | import Control.Exception (evaluate) 8 | import Control.Monad (forever) 9 | import Data.List (foldl') 10 | import System.Metrics 11 | import qualified System.Metrics.Counter as Counter 12 | import System.Remote.Monitoring.Statsd 13 | 14 | -- 'sum' is using a non-strict lazy fold and will blow the stack. 15 | sum' :: Num a => [a] -> a 16 | sum' = foldl' (+) 0 17 | 18 | mean :: Fractional a => [a] -> a 19 | mean xs = sum' xs / fromIntegral (length xs) 20 | 21 | main :: IO () 22 | main = do 23 | store <- newStore 24 | registerGcMetrics store 25 | iters <- createCounter "iterations" store 26 | _ <- forkStatsd defaultStatsdOptions store 27 | let loop :: Int -> IO () 28 | loop n = forever $ do 29 | let n' = fromIntegral n :: Double 30 | _ <- evaluate $ mean [1..n'] 31 | Counter.inc iters 32 | threadDelay 2000 33 | loop 1000000 34 | -------------------------------------------------------------------------------- /examples/Ekg.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Main 3 | ( main 4 | ) where 5 | 6 | import Control.Concurrent (threadDelay) 7 | import Control.Exception (evaluate) 8 | import Data.List (foldl') 9 | import System.Metrics 10 | import qualified System.Metrics.Counter as Counter 11 | import System.Remote.Monitoring 12 | import System.Remote.Monitoring.Statsd 13 | 14 | -- 'sum' is using a non-strict lazy fold and will blow the stack. 15 | sum' :: Num a => [a] -> a 16 | sum' = foldl' (+) 0 17 | 18 | mean :: Fractional a => [a] -> a 19 | mean xs = sum' xs / fromIntegral (length xs) 20 | 21 | main :: IO () 22 | main = do 23 | handle <- forkServer "localhost" 8000 24 | iters <- getCounter "iterations" handle 25 | forkStatsd defaultStatsdOptions (serverMetricStore handle) 26 | let loop n = do 27 | evaluate $ mean [1..n] 28 | Counter.inc iters 29 | threadDelay 2000 30 | loop n 31 | loop 1000000 32 | -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Johan Tibell 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Johan Tibell nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /examples/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /examples/ekg-statsd-examples.cabal: -------------------------------------------------------------------------------- 1 | name: ekg-statsd-examples 2 | version: 0.1.0.0 3 | license: BSD3 4 | license-file: LICENSE 5 | author: Johan Tibell 6 | maintainer: johan.tibell@gmail.com 7 | category: Testing 8 | build-type: Simple 9 | cabal-version: >=1.10 10 | 11 | executable ekg-statsd-examples 12 | main-is: Basic.hs 13 | build-depends: base >=4.6 && <4.15, 14 | ekg-core, 15 | ekg-statsd 16 | default-language: Haskell2010 17 | --------------------------------------------------------------------------------