Add Metrics module: pure rrdtool argv builders for ekg samples

This commit is contained in:
2026-09-07 21:30:42 +03:00
parent 055c02d0f2
commit 88af6a3139
5 changed files with 192 additions and 7 deletions
+9 -6
View File
@@ -1,6 +1,7 @@
{ mkDerivation, aeson, annotated-exception, async, base, bytestring { mkDerivation, aeson, annotated-exception, async, base, bytestring
, containers, hedgehog, hspec, hspec-hedgehog, katip, lens , containers, directory, ekg-core, hedgehog, hspec, hspec-hedgehog
, lens-aeson, lib, network, stm, text, time, uuid, websockets , katip, lens, lens-aeson, lib, network, process, stm, text, time
, unordered-containers, uuid, websockets
}: }:
mkDerivation { mkDerivation {
pname = "home-assistant-controller"; pname = "home-assistant-controller";
@@ -9,13 +10,15 @@ mkDerivation {
isLibrary = true; isLibrary = true;
isExecutable = true; isExecutable = true;
libraryHaskellDepends = [ libraryHaskellDepends = [
aeson annotated-exception async base bytestring containers katip aeson annotated-exception async base bytestring containers
lens lens-aeson network stm text time uuid websockets directory ekg-core katip lens lens-aeson network process stm text
time unordered-containers uuid websockets
]; ];
executableHaskellDepends = [ base ]; executableHaskellDepends = [ base ];
testHaskellDepends = [ testHaskellDepends = [
aeson annotated-exception async base containers hedgehog hspec aeson annotated-exception async base containers directory ekg-core
hspec-hedgehog katip stm text time uuid hedgehog hspec hspec-hedgehog katip process stm text time
unordered-containers uuid
]; ];
license = lib.meta.getLicenseFromSpdxId "BSD-3-Clause"; license = lib.meta.getLicenseFromSpdxId "BSD-3-Clause";
mainProgram = "home-assistant-controller"; mainProgram = "home-assistant-controller";
+11 -1
View File
@@ -67,6 +67,7 @@ library
, HomeAssistant.Runtime , HomeAssistant.Runtime
, HomeAssistant.Runtime.Bus , HomeAssistant.Runtime.Bus
, HomeAssistant.Runtime.Connection , HomeAssistant.Runtime.Connection
, HomeAssistant.Runtime.Metrics
, HomeAssistant.Runtime.Supervisor , HomeAssistant.Runtime.Supervisor
-- Modules included in this library but not exported. -- Modules included in this library but not exported.
@@ -91,6 +92,10 @@ library
, uuid , uuid
, katip , katip
, containers , containers
, ekg-core
, unordered-containers
, process
, directory
-- Directories containing source files. -- Directories containing source files.
hs-source-dirs: src hs-source-dirs: src
@@ -136,6 +141,7 @@ test-suite home-assistant-controller-test
, BedroomSpec , BedroomSpec
, BusSpec , BusSpec
, ConnectionSpec , ConnectionSpec
, MetricsSpec
, RuntimeSpec , RuntimeSpec
, SupervisorSpec , SupervisorSpec
, Support , Support
@@ -167,4 +173,8 @@ test-suite home-assistant-controller-test
time, time,
uuid, uuid,
katip, katip,
containers containers,
ekg-core,
unordered-containers,
process,
directory
+72
View File
@@ -0,0 +1,72 @@
{-# LANGUAGE OverloadedStrings #-}
module HomeAssistant.Runtime.Metrics
( DsType (..)
, DsSpec (..)
, dsTypeOf
, sanitizeName
, buildSchema
, buildCreateArgs
, buildUpdateArgs
) where
import Data.Int (Int64)
import Data.List (intercalate, sortBy)
import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.HashMap.Strict as HM
import qualified System.Metrics as M (Value (..), Sample)
data DsType = Derive | Gauge
deriving (Eq, Show)
data DsSpec = DsSpec
{ dsEkgName :: Text
, dsName :: String
, dsType :: DsType
}
deriving (Eq, Show)
dsTypeOf :: M.Value -> Maybe DsType
dsTypeOf (M.Counter _) = Just Derive
dsTypeOf (M.Gauge _) = Just Gauge
dsTypeOf _ = Nothing
sanitizeName :: Text -> String
sanitizeName = T.unpack . T.replace "." "_"
buildSchema :: M.Sample -> [DsSpec]
buildSchema sample =
sortBy (comparing dsName)
[ DsSpec ekgName (sanitizeName ekgName) dt
| (ekgName, val) <- HM.toList sample
, Just dt <- [dsTypeOf val]
]
buildCreateArgs :: FilePath -> Int -> [(String, DsType)] -> [String]
buildCreateArgs path step specs =
["create", path, "--step", show step]
++ concatMap dsArg specs
++ rras
where
dsArg (name, Derive) = ["DS:" ++ name ++ ":DERIVE:20:0:U"]
dsArg (name, Gauge) = ["DS:" ++ name ++ ":GAUGE:20:0:U"]
rras =
[ "RRA:AVERAGE:0.5:1:6000"
, "RRA:MAX:0.5:1:6000"
, "RRA:AVERAGE:0.5:360:1680"
, "RRA:MAX:0.5:360:1680"
]
buildUpdateArgs :: FilePath -> [String] -> [Maybe Int64] -> [String]
buildUpdateArgs path names values =
[ "update"
, path
, "--template"
, intercalate ":" names
, "N:" ++ intercalate ":" (map renderValue values)
]
where
renderValue Nothing = "U"
renderValue (Just n) = show n
+2
View File
@@ -6,6 +6,7 @@ import qualified BackoffProp
import qualified BedroomSpec import qualified BedroomSpec
import qualified BusSpec import qualified BusSpec
import qualified ConnectionSpec import qualified ConnectionSpec
import qualified MetricsSpec
import qualified RuntimeSpec import qualified RuntimeSpec
import qualified SupervisorSpec import qualified SupervisorSpec
@@ -15,6 +16,7 @@ main = hspec $ do
BedroomSpec.spec BedroomSpec.spec
BusSpec.spec BusSpec.spec
ConnectionSpec.spec ConnectionSpec.spec
MetricsSpec.spec
RuntimeSpec.spec RuntimeSpec.spec
SupervisorSpec.spec SupervisorSpec.spec
BackoffProp.spec BackoffProp.spec
+98
View File
@@ -0,0 +1,98 @@
{-# LANGUAGE OverloadedStrings #-}
module MetricsSpec (spec) where
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.Int (Int64)
import Data.Text (Text)
import HomeAssistant.Runtime.Metrics
( DsType (..)
, DsSpec (..)
, dsTypeOf
, sanitizeName
, buildSchema
, buildCreateArgs
, buildUpdateArgs
)
import qualified System.Metrics as M (Value (..))
import Test.Hspec
spec :: Spec
spec = do
describe "dsTypeOf" $ do
it "maps Counter to Derive" $
dsTypeOf (M.Counter 1000) `shouldBe` Just Derive
it "maps Gauge to Gauge" $
dsTypeOf (M.Gauge 500) `shouldBe` Just Gauge
it "maps Label to Nothing" $
dsTypeOf (M.Label "hello") `shouldBe` Nothing
describe "sanitizeName" $ do
it "replaces dots with underscores" $
sanitizeName ("rts.gc.bytes_allocated" :: Text) `shouldBe` "rts_gc_bytes_allocated"
describe "buildSchema" $ do
it "builds sorted DsSpecs from counters and gauges, skipping labels" $
let sample :: HashMap Text M.Value
sample = HM.fromList
[ ("rts.gc.bytes_allocated", M.Counter 1000)
, ("rts.gc.max_bytes_used", M.Gauge 500)
, ("rts.gc.label_thing", M.Label "irrelevant")
]
in buildSchema sample `shouldBe`
[ DsSpec "rts.gc.bytes_allocated" "rts_gc_bytes_allocated" Derive
, DsSpec "rts.gc.max_bytes_used" "rts_gc_max_bytes_used" Gauge
]
describe "buildCreateArgs" $ do
it "builds create argv with mixed DERIVE and GAUGE DSes and RRAs" $
buildCreateArgs "test.rrd" 10
[ ("ds1", Derive)
, ("ds2", Gauge)
]
`shouldBe`
[ "create"
, "test.rrd"
, "--step"
, "10"
, "DS:ds1:DERIVE:20:0:U"
, "DS:ds2:GAUGE:20:0:U"
, "RRA:AVERAGE:0.5:1:6000"
, "RRA:MAX:0.5:1:6000"
, "RRA:AVERAGE:0.5:360:1680"
, "RRA:MAX:0.5:360:1680"
]
describe "buildUpdateArgs" $ do
it "builds update argv with numeric values" $
buildUpdateArgs "test.rrd" ["ds1", "ds2"] [Just 100, Just 200]
`shouldBe`
[ "update"
, "test.rrd"
, "--template"
, "ds1:ds2"
, "N:100:200"
]
it "renders Nothing as U (unknown)" $
buildUpdateArgs "test.rrd" ["ds1", "ds2"] [Just 100, Nothing]
`shouldBe`
[ "update"
, "test.rrd"
, "--template"
, "ds1:ds2"
, "N:100:U"
]
it "renders all-Nothing as all-U" $
buildUpdateArgs "test.rrd" ["ds1"] [Nothing]
`shouldBe`
[ "update"
, "test.rrd"
, "--template"
, "ds1"
, "N:U"
]