Fix: rrdtool DS names must be <= 19 chars
sanitizeName now keeps the first 15 chars plus a 3-hex hash suffix for names longer than 19 chars (rrdtool's hard limit on DS name length). The previous replace-dots-only version produced names up to 31 chars, which rrdtool rejected with 'invalid DS format'.
This commit is contained in:
@@ -15,11 +15,13 @@ module HomeAssistant.Runtime.Metrics
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad (forever, unless)
|
||||
import Data.Char (ord)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, sortBy)
|
||||
import Data.Ord (comparing)
|
||||
import Data.Text (Text)
|
||||
import Data.Void (Void)
|
||||
import Numeric (showHex)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified System.Metrics as M (Value (..), Sample, Store, sampleAll)
|
||||
@@ -41,8 +43,16 @@ dsTypeOf (M.Counter _) = Just Derive
|
||||
dsTypeOf (M.Gauge _) = Just Gauge
|
||||
dsTypeOf _ = Nothing
|
||||
|
||||
-- | Maps an ekg metric label to a valid rrd DS name (≤19 chars, [A-Za-z0-9_]).
|
||||
-- Long names keep the first 15 chars plus a 3-hex hash suffix for uniqueness.
|
||||
sanitizeName :: Text -> String
|
||||
sanitizeName = T.unpack . T.replace "." "_"
|
||||
sanitizeName name
|
||||
| length sanitized <= 19 = sanitized
|
||||
| otherwise = take 15 sanitized ++ "_" ++ paddedHash
|
||||
where
|
||||
sanitized = T.unpack (T.replace "." "_" name)
|
||||
paddedHash = let h = showHex (sum (map ord sanitized) `mod` 4096) ""
|
||||
in replicate (3 - length h) '0' ++ h
|
||||
|
||||
buildSchema :: M.Sample -> [DsSpec]
|
||||
buildSchema sample =
|
||||
|
||||
+26
-7
@@ -41,22 +41,41 @@ spec = do
|
||||
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"
|
||||
it "replaces dots with underscores for short names" $
|
||||
sanitizeName ("rts.gc.cpu_ms" :: Text) `shouldBe` "rts_gc_cpu_ms"
|
||||
|
||||
it "shortens names longer than 19 chars to 15 chars + _ + 3 hex" $ do
|
||||
let result = sanitizeName ("rts.gc.par_balanced_bytes_copied" :: Text)
|
||||
length result `shouldBe` 19
|
||||
take 15 result `shouldBe` "rts_gc_par_bala"
|
||||
drop 15 result `shouldBe` "_" ++ drop 16 result
|
||||
|
||||
it "is deterministic (same input -> same output)" $
|
||||
sanitizeName ("rts.gc.peak_megabytes_allocated" :: Text)
|
||||
`shouldBe` sanitizeName ("rts.gc.peak_megabytes_allocated" :: Text)
|
||||
|
||||
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")
|
||||
[ ("x.allocated", M.Counter 1000)
|
||||
, ("a.bytes_used", M.Gauge 500)
|
||||
, ("c.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
|
||||
[ DsSpec "a.bytes_used" "a_bytes_used" Gauge
|
||||
, DsSpec "x.allocated" "x_allocated" Derive
|
||||
]
|
||||
|
||||
it "produces dsName <= 19 chars for long ekg GC metric names" $
|
||||
let sample :: HashMap Text M.Value
|
||||
sample = HM.fromList
|
||||
[ ("rts.gc.par_balanced_bytes_copied", M.Gauge 1)
|
||||
, ("rts.gc.peak_megabytes_allocated", M.Gauge 2)
|
||||
, ("rts.gc.cumulative_bytes_used", M.Counter 3)
|
||||
]
|
||||
in map (length . dsName) (buildSchema sample) `shouldSatisfy` all (<= 19)
|
||||
|
||||
describe "buildCreateArgs" $ do
|
||||
it "builds create argv with mixed DERIVE and GAUGE DSes and RRAs" $
|
||||
buildCreateArgs "test.rrd" 10
|
||||
|
||||
Reference in New Issue
Block a user