Nagios のパフォーマンスデータを Parsec で解析してみた
Nagios にはチェックデータを取得した際に、パフォーマンスデータを一緒に付け加えてくれるという機能があり、以下のページにあるツールはこのパフォーマンスデータを使って CPU 使用率やディスク空き容量をグラフ化したりといったことをしています。
で、このパフォーマンスデータがどういう文字列になっているのかというと、
time=0.166938s;;;0.000000 size=32954B;;;0
だったり、
rta=500.000000ms;100.000000;500.000000;0.000000 pl=100%;20;60;0
だったりです。ここでは、このフォーマットを Parsec で解析することを目標にします*1。
このパフォーマンスデータの構文は各ホストやサービスのチェック用プラグインが付け加えるものなのですが、以下のページにはそのデータの構文をどうするべきかについてのガイドラインが示されています。
簡単に説明すると、
'label'=value[UOM];[warn];[crit];[min];[max]
- それぞれのパフォーマンスデータは label=value という key/value ペアで表現されていて、各パフォーマンスデータはスペースで区切られる
- ラベルは任意の文字が使える
- ラベルに空白やイコールやシングルクォートを使いたい場合はラベルをシングルクォートで囲み、さらにシングルクォートを入力するにはシングルクォートを 2 つ連続で書く
- 例: 'ho''ge' ← ho'ge という文字列になる
- UOM 以降のデータはオプショナルである。その際セミコロンは省略してもいい。
- value と min と max のデータは [-0123456789.] のどれかの文字の連続である
- ↑かなり曖昧なので後で細かく書く
- warn と crit のデータは 2.5. Threshold and ranges に書かれている通りのフォーマットである
- 例を挙げるとこんな感じ
書き方 意味 10 0 <= x <= 10 10: 10 <= x ~:10 x <= 10 10:20 10 <= x <= 20 @10:20 10 < x < 20
見ての通りかなり曖昧な文法*2なので、適当に解釈しつつ BNF(厳密な BNF ではない)を作ることにします。
// クォートされていないときに使える文字 <anychar'> ::= any characters except one of " ='\r\n" // クォートされているときに使える文字 <anychar> ::= any characters except one of "'\r\n" <label> ::= "'" (<anychar>|"''")+ "'" | <anychar'>+ // -10.00 や -.01 といった文法は許可する // でも 10. という書き方は許可しない <number> ::= '-'? (<0-9>* '.' <0-9>+) | <0-9>+ <value> ::= <number> <min> ::= <number> <max> ::= <number> // 数字部分のフォーマットは <number> のフォーマットに従う <range> ::= '@'? (<number> | (<number>|'~') ':' <number>?) <warn> ::= <range> <crit> ::= <range> <uom> ::= 's' | 'us' | 'ms' | '%' | 'B' | 'KB' | 'MB' | 'TB' | 'c' // 後ろを省略できることを示しながら書くとこうなったっていう・・・。 <perfdata> ::= <label>=<value>(<uom>?(;(<warn>?(;(<crit>?(;(<min>?(;<max>?)?)?)?)?)?)?)?)? // それぞれのデータはスペースで区切られるし、 // 前後にスペースが入っていても構わない <perfdatalist> ::= ' '* (<perfdata> ' '+)* <perfdata> ' '*
ということで、これらの BNF を Parsec に落としてみるとこうなりました。
p_label = unquoted <|> quoted where anychar' = noneOf " ='\r\n" anychar = noneOf "'\r\n" unquoted = many1 anychar' quoted = between (char '\'') (char '\'') $ many1 $ anychar <|> try (string "''" >> return '\'')
ラベルはクォートされている場合とされていない場合で分けて、あと '' でシングルクォートになる処理はバックトラックが必要になるので try を使ってます。
-- <number> ::= <n0> -- <n0> ::= '-'? <n1> -- <n1> ::= <0-9>+ <n2>? | <n2> -- <n2> ::= '.' <0-9>+ p_number = n0 >>= return.toRational where toRational str = readPair $ break (=='.') str where readPair (int,(_:frac)) = read (int++frac) % (10^length frac) readPair (int,_) = read int % 1 n0 = liftA2 (++) (option "" (string "-")) n1 n1 = liftA2 (++) (many1 digit) (option "" n2) <|> n2 n2 = liftA2 (:) (char '.') (many1 digit)
data Range = Range { rangeInclusive :: Bool, rangeStart :: Maybe Rational, rangeEnd :: Maybe Rational } deriving (Show,Eq) isInRange x (Range inclusive start end) = range x inclusive start end where range x True start end = comp (>=) x start && comp (<=) x end range x False start end = comp (>) x start && comp (<) x end comp cp x (Just y) = cp x y comp cp x Nothing = True -- <range> ::= '@'? (<r1> | <r2>) -- <r1> ::= <number> <r3>? -- <r2> ::= '~' <r3> -- <r3> ::= ':' <number>? p_range = do inclusive <- option True (char '@' >> return False) (start,end) <- try r1 <|> r2 return $ Range inclusive start end where r1 :: GenParser Char st (Maybe Rational,Maybe Rational) r1 = do v <- p_number option (Just (0%1),Just v) (r3 >>= return.(Just v,)) r2 :: GenParser Char st (Maybe Rational,Maybe Rational) r2 = char '~' >> r3 >>= return.(Nothing,) r3 :: GenParser Char st (Maybe Rational) r3 = do char ':' optionMaybe p_number
p_uom = string "s" <|> try (string "us") <|> try (string "ms") <|> string "%" <|> string "B" <|> try (string "KB") <|> try (string "MB") <|> try (string "TB") <|> string "c"
これは一文字目を見ればどの単位なのか分かるから、ほんとは try を使わずに書くこともできるのだけど、汚くなるのでやってない。
あと今はこれを文字列で返しているのだけれども、もしかしたら新しく data 型を作った方がいいかも。
data PerfData = PerfData { pfLabel :: String, pfValue :: Rational, pfUom :: Maybe String, pfWarning :: Maybe Range, pfCritical :: Maybe Range, pfMin :: Maybe Rational, pfMax :: Maybe Rational } deriving (Show,Eq) p_perfdata = p0 where end = (char ' ' >> return ()) <|> eof endNothings5 = endNothings4 >>= return.(Nothing,) endNothings4 = endNothings3 >>= return.(Nothing,) endNothings3 = endNothings2 >>= return.(Nothing,) endNothings2 = endNothings1 >>= return.(Nothing,) endNothings1 = return Nothing semiOrValue p = (char ';' >> return Nothing) <|> (p >>= (optionMaybe (char ';') >>) . return . Just) p0 = do label <- p_label char '=' value <- p_number (uom,(warn,(crit,(min,max)))) <- p1 return $ PerfData label value uom warn crit min max p1 = (end >> endNothings5) <|> do uom <- semiOrValue p_uom p2 >>= return.(uom,) p2 = (end >> endNothings4) <|> do warn <- semiOrValue p_range p3 >>= return.(warn,) p3 = (end >> endNothings3) <|> do crit <- semiOrValue p_range p4 >>= return.(crit,) p4 = (end >> endNothings2) <|> do min <- semiOrValue p_number p5 >>= return.(min,) p5 = (end >> endNothings1) <|> (p_number >>= return.Just)
ついにパフォーマンスデータの解析ができるようになりました。
大分ごちゃごちゃしてるのですが、値の遷移を書くとこんな感じ。
p_perfdatalist = do many (char ' ') many1 $ do pd <- p_perfdata many (char ' ') return pd
あとはパフォーマンスデータのリストを取得するだけ。パフォーマンスデータを解析し終わった時点で、区切り文字のスペースは食われてるので、BNF とはちょっと違う感じに。
とまあこんなコードを書いて、最初の例の文字列
time=0.166938s;;;0.000000 size=32954B;;;0
をパースして print してやると
[PerfData { pfLabel = "time", pfValue = 83469 % 500000, pfUom = Just "s", pfWarning = Nothing, pfCritical = Nothing, pfMin = Just (0 % 1), pfMax = Nothing }, PerfData { pfLabel = "size", pfValue = 32954 % 1, pfUom = Just "B", pfWarning = Nothing, pfCritical = Nothing, pfMin = Just (0 % 1), pfMax = Nothing }]
と出力されて、
rta=500.000000ms;100.000000;500.000000;0.000000 pl=100%;20;60;0
をパースすると
[PerfData { pfLabel = "rta", pfValue = 500 % 1, pfUom = Just "ms", pfWarning = Just (Range { rangeInclusive = True, rangeStart = Just (0 % 1), rangeEnd = Just (100 % 1) }), pfCritical = Just (Range { rangeInclusive = True, rangeStart = Just (0 % 1), rangeEnd = Just (500 % 1) }), pfMin = Just (0 % 1), pfMax = Nothing }, PerfData { pfLabel = "pl", pfValue = 100 % 1, pfUom = Just "%", pfWarning = Just (Range { rangeInclusive = True, rangeStart = Just (0 % 1), rangeEnd = Just (20 % 1) }), pfCritical = Just (Range { rangeInclusive = True, rangeStart = Just (0 % 1), rangeEnd = Just (60 % 1) }), pfMin = Just (0 % 1), pfMax = Nothing }]
と出力されて(それぞれ手動整形済み)、ちゃんと解析できていることが分かります。