はじめに

平素は大変お世話になっております。
クイックガードのパー子です。

Nagios の CloudWatchプラグインを作ったのでご紹介します。

単純な実メトリクスの取得だけでなく、Metric MathMetrics Insights もサポートしています。

背景

弊社では、メインの監視ツールとして (いまどきですが) Nagios を使っています。

案件によっては Nagios だけでなく SaaS を含む他の監視ツールも併用しますが、MSP (Managed Service Provider) である弊社は監視技術とその運用体制そのものが商品です。
それゆえ、枯れたプロダクトである Nagios が弊社の事業にもっとも適しているのです。

AWS環境の監視には CloudWatch Alarms を使うのが一般的ですが、案件によっては監視設定の分散を避けるため CloudWatch Alarms を使わず、Nagios から CloudWatchメトリクスを参照してアラートを飛ばすケースがあります。

そのためには CloudWatch をサポートするプラグインが必要ですが、既存の CloudWatchプラグインはもはやメンテナンスされておらず、機能的にも不充分なことが多いです。
(例えば Metric Math や Metrics Insights をサポートしていない、など。)

そのような背景により、自社で一から作成することにしました。

プラグイン

https://github.com/quickguard-oss/nagios-cloudwatch-plugin

ライセンスは MIT です。

以下の特徴があります。

導入の容易さ

バイナリをポン置きで導入できるように Go で作りました。

内部的には AWS SDK for Go v2AWS GetMetricData API を叩き、メトリクスを取得します。

AWSクレデンシャルも SDK の作法 にしたがって設定すれば OK です。

クエリ

GetMetricData API がサポートしているクエリを実行可能です。

Metric Math、Metrics Insights も使えます。

ステータス判定機構

他の一般的な Nagiosプラグインとは異なる、特殊なステータス判定機構になっています。

一般的なプラグインは取得した単一の値を評価に用いる一方、このプラグインでは指定した時間区間に含まれるデータポイント群を取得します。
それらデータポイント群について、直近の連続する m個のデータポイントのうち、n個以上が Warn範囲または Critical範囲を逸脱していたら NG と判断する、という機構にしました。

つまり、閾値として以下の 4つが連動して正常性を判定します。

  • Warn範囲
  • Critical範囲
  • 評価するデータポイント数 m
  • 逸脱した場合に NG と判断するデータポイント数 n

具体例を見てみましょう。

Warn範囲、Critical範囲が図の領域を占めており、直近 8個のデータポイント (m = 8) を評価するケースを考えます。

この監視試行のステータスは n の値に応じて以下のように決着します。

nステータス理由
〜 -1UNKNOWN負数となる閾値は受け付けない。
0 〜 2CRITICALCritical範囲を逸脱しているデータポイントが 2つあるので。
3 〜 5WARNINGWarn範囲を逸脱しているデータポイントが 5つあるので。
6 〜 8OKWarn範囲、Critical範囲を逸脱しているデータポイントは 6つ未満なので。
9 〜UNKNOWNm < n となる閾値は受け付けない。

使い方

実際の使い方を見ていきます。

事前準備

まずはメトリクスの取得クエリを作ります。

形式は MetricDataQuery の配列です。

queries.json
[
  {
    "Id": "m1",
    "MetricStat": {
      "Metric": {
        "Namespace": "AWS/EBS",
        "MetricName": "BurstBalance",
        "Dimensions": [
          {
            "Name": "VolumeId",
            "Value": "YOUR_VOLUME_ID"
          }
        ]
      },
      "Period": 60,
      "Stat": "Average"
    },
    "ReturnData": false
  },
  {
    "Id": "e1",
    "Label": "BurstUsage",
    "Expression": "DIFF(m1)"
  }
]

レスポンスとして得られるメトリクスの系列は 1本だけにします。
つまり、ステータス判定に用いる 1本を除く残りのメトリクスには "ReturnData": false を指定します。

実行

作成したクエリを -qオプションに渡します。

また、閾値として「Warn 範囲」「Critical 範囲」「評価するデータポイント数 m と、逸脱した場合に NG と判断するデータポイント数 n」をそれぞれ -w -c -p の各オプションで指定します。

$ check_cloudwatch -q "$(< ./queries.json)" -w '-5.0:5.0' -c '-10.0:10.0' -p '3/5'
{"level":"info","service":"CLOUDWATCH","status":"OK","time":"2022-12-22T14:38:39+09:00","message":"BurstUsage = 0.12562037037 | value=0.12562037037;-5.0:5.0;-10.0:10.0;;"}

Warn/Critical範囲の記法は Nagios Plugins Development Guidelines を参照してください。

mn-p 'n/m' の形式で指定します。

ステータスの表示形式

デフォルトでは、当プラグインは実行結果を JSON形式で出力します。

見慣れた Nagios の標準形式で出力するには -Cオプションを指定します。

$ check_cloudwatch -q "$(< ./queries.json)" -C
CLOUDWATCH OK: BurstUsage = 0.06329629629630062 | value=0.06329629629630062;;;;

デバッグ

-vオプションを重ねて、最大で 3段階まで Verbosity を上げられます。

$ check_cloudwatch -q "$(cat ./queries.json | jq -c)" -d 10 -vvv
{"level":"trace","package":"main","classic_output":false,"verbosity":3,"time":"2022-12-22T16:24:31+09:00","message":"set output options"}
{"level":"trace","package":"alert","time":"2022-12-22T16:24:31+09:00","message":"parsing thresholds"}
{"level":"trace","package":"alert","time":"2022-12-22T16:24:31+09:00","message":"parsing warn threshold"}
{"level":"trace","package":"alert","range_start":"-Inf","range_end":"+Inf","alert_if_inside_range":false,"time":"2022-12-22T16:24:31+09:00","message":"range is not specified; use default value"}
{"level":"trace","package":"alert","time":"2022-12-22T16:24:31+09:00","message":"parsing critical threshold"}
{"level":"trace","package":"alert","range_start":"-Inf","range_end":"+Inf","alert_if_inside_range":false,"time":"2022-12-22T16:24:31+09:00","message":"range is not specified; use default value"}
{"level":"trace","package":"alert","time":"2022-12-22T16:24:31+09:00","message":"parsing datapoints threshold"}
{"level":"trace","package":"alert","evaluation_periods":1,"datapoints_to_alarm":1,"time":"2022-12-22T16:24:31+09:00"}
{"level":"trace","package":"cloudwatch","time":"2022-12-22T16:24:31+09:00","message":"creating CloudWatch API client"}
{"level":"trace","package":"cloudwatch","time":"2022-12-22T16:24:31+09:00","message":"parsing API queries"}
{"level":"trace","package":"cloudwatch","queries":[{"Id":"m1","MetricStat":{"Metric":{"Namespace":"AWS/EBS","MetricName":"BurstBalance","Dimensions":[{"Name":"VolumeId","Value":"YOUR_VOLUME_ID"}]},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"e1","Label":"BurstUsage","Expression":"DIFF(m1)"}],"time":"2022-12-22T16:24:31+09:00"}
{"level":"trace","package":"cloudwatch","time":"2022-12-22T16:24:31+09:00","message":"calling GetMetricData API"}
{"level":"trace","package":"cloudwatch","start_time":"2022-12-22T16:14:31+09:00","end_time":"2022-12-22T16:24:31+09:00","timeout":10,"time":"2022-12-22T16:24:31+09:00","message":"API parameters"}
{"level":"debug","GetMetricDataOutput":{"Messages":[],"MetricDataResults":[{"Id":"e1","Label":"BurstUsage","Messages":null,"StatusCode":"Complete","Timestamps":["2022-12-22T07:22:00Z","2022-12-22T07:21:00Z","2022-12-22T07:20:00Z","2022-12-22T07:19:00Z","2022-12-22T07:18:00Z","2022-12-22T07:17:00Z","2022-12-22T07:16:00Z","2022-12-22T07:15:00Z"],"Values":[0.0004999999999881766,0,-0.024518518518590326,0,-0.0025555555555030196,0,0,0]}],"NextToken":null,"ResultMetadata":{}},"time":"2022-12-22T16:24:31+09:00","message":"API call succeeds"}
{"level":"trace","package":"alert","time":"2022-12-22T16:24:31+09:00","message":"checking if metrics are above thresholds"}
{"level":"trace","package":"alert","status":"ok","out_of_warn_range":0,"out_of_critical_range":0,"datapoints_to_alarm":1,"time":"2022-12-22T16:24:31+09:00","message":"service status is healthy"}
{"level":"info","service":"CLOUDWATCH","status":"OK","time":"2022-12-22T16:24:31+09:00","message":"BurstUsage = 0.0004999999999881766 @ 2022-12-22 07:22:00 +0000 UTC; above thresholds [warn,crit] = 0,0; threshold = 1/1 | value=0.0004999999999881766;;;; datapoints_warn=0;1/1;;; datapoints_crit=0;;1/1;;"}

データポイントの時間区間

取得するデータポイントの時間区間は -dオプションで指定します。

直近5分間のデータポイントを取得する:

$ check_cloudwatch -q "$(< ./queries.json)" -d 5 -vv
{"level":"debug","GetMetricDataOutput":{"Messages":[],"MetricDataResults":[{"Id":"e1","Label":"BurstUsage","Messages":null,"StatusCode":"Complete","Timestamps":["2022-12-22T07:22:00Z","2022-12-22T07:21:00Z","2022-12-22T07:20:00Z"],"Values":[0.0004999999999881766,0,-0.024518518518590326]}],"NextToken":null,"ResultMetadata":{}},"time":"2022-12-22T16:24:53+09:00","message":"API call succeeds"}
{"level":"info","service":"CLOUDWATCH","status":"OK","time":"2022-12-22T16:24:53+09:00","message":"BurstUsage = 0.0004999999999881766 @ 2022-12-22 07:22:00 +0000 UTC; above thresholds [warn,crit] = 0,0; threshold = 1/1 | value=0.0004999999999881766;;;; datapoints_warn=0;1/1;;; datapoints_crit=0;;1/1;;"}

直近10分間のデータポイントを取得する:

$ check_cloudwatch -q "$(< ./queries.json)" -d 10 -vv
{"level":"debug","GetMetricDataOutput":{"Messages":[],"MetricDataResults":[{"Id":"e1","Label":"BurstUsage","Messages":null,"StatusCode":"Complete","Timestamps":["2022-12-22T07:23:00Z","2022-12-22T07:22:00Z","2022-12-22T07:21:00Z","2022-12-22T07:20:00Z","2022-12-22T07:19:00Z","2022-12-22T07:18:00Z","2022-12-22T07:17:00Z","2022-12-22T07:16:00Z"],"Values":[0.02657407407410517,0.0004999999999881766,0,-0.024518518518590326,0,-0.0025555555555030196,0,0]}],"NextToken":null,"ResultMetadata":{}},"time":"2022-12-22T16:25:06+09:00","message":"API call succeeds"}
{"level":"info","service":"CLOUDWATCH","status":"OK","time":"2022-12-22T16:25:06+09:00","message":"BurstUsage = 0.02657407407410517 @ 2022-12-22 07:23:00 +0000 UTC; above thresholds [warn,crit] = 0,0; threshold = 1/1 | value=0.02657407407410517;;;; datapoints_warn=0;1/1;;; datapoints_crit=0;;1/1;;"}

取得したデータポイントの数が評価対象数 m に満たない場合は UNKNOWNステータスとなるので、充分な数のデータポイントが含まれる時間区間を指定しましょう。

$ check_cloudwatch -q "$(< ./queries.json)" -d 3 -p '1/100'
{"level":"info","service":"CLOUDWATCH","status":"UNKNOWN","time":"2022-12-22T16:25:25+09:00","message":"invalid argument \"1/100\" for datapoints: insufficient number of metrics to evaluate: got 1 datapoints"}

欠損データポイントの扱い

Sparse な (= データポイントが時間的に連続しておらず、欠損している時間帯がある) メトリクスを評価する際、当プラグインは欠損しているデータポイントをすべて無視してステータス判定します。

つまり、

#時刻
0tv0
1t + 1v1
2t + 2(欠損)
3t + 3v3
4t + 4(欠損)

例えばこのようなデータポイント群が得られた場合、#0, #1, #3 の 3つのデータポイントのみでステータスが判定されます。

ここで仮に -p '1/4' (= 4つのうち 1つ以上のデータポイントの値が正常範囲を逸脱すると NG) と閾値を指定すると、得られたデータポイント数 (= 3) が評価対象のデータポイント数 (= 4) を下回っているために、前セクションで見たとおり UNKNOWNステータスとなってしまいます。

このように Sparse なメトリクスを扱う場合は、FILL関数 による欠損値の補完をオススメします。

Fills the missing values of a time series. There are several options for the values to use as the filler for missing values:

  • You can specify a value to use as the filler value.
  • You can specify a metric to use as the filler value.
  • You can use the REPEAT keyword to fill missing values with the most recent actual value of the metric before the missing value.
  • You can use the LINEAR keyword to fill the missing values with values that create a linear interpolation between the values at the beginning and the end of the gap.

以下のメトリクスを例として、実際に補完してみましょう。

対象メトリクスは API Gateway の IntegrationLatency (ID: m1) です。
当該API Gateway へのアクセスが散発的なので、メトリクスも Sparse です。

これを固定値 0 で FILL してみます。

e1 = FILL(m1, 0)

欠損している箇所が 0 で補完されました。

続いて、固定値ではなく REPEAT または LINEAR で補完するケースを考えます。
この場合、存在するデータポイントの間のギャップしか埋めてくれないため、時間区間の両端は欠損したままとなってしまいます。

これを解決するには、TIME_SERIES関数 を併用します。

TIME_SERIES関数は、区間のすべてのデータポイントが同一の値である時系列を生成する関数です。

Returns a non-sparse time series where every value is set to a scalar argument.

例えば、REPEAT または LINEAR で FILL した時系列 e2 と、

e2 = FILL(m1, LINEAR)

すべての値が 0 で固定された時系列 e3 を、

e3 = TIME_SERIES(0)

MAX関数で合成する、という使い方をします。

e4 = MAX([e2, e3])
queries.json
[
  {
    "Id": "m1",
    "MetricStat": {
      "Metric": {
        "Namespace": "AWS/ApiGateway",
        "MetricName": "IntegrationLatency",
        "Dimensions": [
          {
            "Name": "ApiName",
            "Value": "YOUR_API_NAME"
          }
        ]
      },
      "Period": 300,
      "Stat": "Average"
    },
    "ReturnData": false
  },
  {
    "Id": "e1",
    "Label": "IntegrationLatency",
    "Expression": "MAX([FILL(m1, LINEAR), TIME_SERIES(0)])"
  }
]

合成する際は、欠損値をどう見なすか (= 正常範囲内である、または、正常範囲を逸脱している) によって MIN関数と使い分けるとよいでしょう。

まとめ

自作の Nagios CloudWatchプラグインをご紹介しました。

  • Go製のため導入が容易
  • Metric Math や Metrics Insights をサポート
  • 複数のデータポイント群を用いてステータスを判定

という点が特徴です。

使用する際のちょっとした工夫として、データポイントに欠損がある Sparse なメトリクスを取り扱う場合は、FILL関数や TIME_SERIES関数を組み合わせて欠損値を補完すると監視試行が安定します。

今後ともよろしくお願い申し上げます。