オールアバウトTech Blog

株式会社オールアバウトのエンジニアブログです。

golangでCloud Monitoringのデータを元に稼働率を取得するようにした話

こんにちは!オールアバウト SREG@y_hideshi です。

今回はGCPのモニタリングサービスであるCloud Monitoringのデータを元に稼働率を取得するようにした話をしたいと思います。

背景

オールアバウトでは、Cloud MonitoringのUptimeCheckという機能を利用してURL監視やサービスの稼働率をチェックしています。

f:id:allabout-techblog:20201026114142p:plain
Cloud Monitoring:UptimeCheckの画面

上記の画像にある通り、URL単位で稼働率を画面上から確認することができます。ただ、UptimeCheck数が多くなると、各画面に遷移して稼働率を確認するのは大変になります。

そのため、このチェックを自動化しようというのが今回稼働率を自動取得しようとした背景です。

Cloud Monitoringのデータ取得はCloud Monitoring API経由で取得することができ、Cloud Monitoring APIを扱う場合はgolangで便利なパッケージが提供されているので今回はそちらを使用します。

実装について

それでは早速どのようなコードになっているか紹介したいと思うのですが、その前にUptimeCheckとCloud Monitoringのデータについて簡単に説明します。

UptimeCheckとCloud Monitoringデータについて

UptimeCheckでは、特定のURLに対してステータスチェックやレスポンスタイムをチェックすることができます。 リクエストは様々なロケーションから送ることができ、各ロケーションのリクエストから取得できたデータはCloud Monitoringに指標データ(時系列データ)として蓄積されます。 この時蓄積されるデータは6週間しか保持されません。参考リンク

f:id:allabout-techblog:20201030125532p:plain
UptimeCheckの設定一覧

上述したCloud Monitoringのデータは、特定のタイムスタンプにおけるステータスチェックが通ったか通らなかったかの時系列データを持っています。 そのため、一ヶ月分の稼働率を取得する際は各ロケーションごとの時系列データを取得し、自前で稼働率を求める計算をする必要があります。

稼働率の求め方

稼働率の求め方についてはGoogleのサポートに問い合わせしたところ「稼働率の求め方については詳しくお答えすることができない」とのことだったので、計算方法を自分たちで考えWebコンソールで表示される値(Googleさんが算出したか稼働率)になるべく近づけるようにしました。

私が設定した稼働率の求め方(計算方法)は下記の通りです。

  1. 1分間に取得されたCloud Monitoring時系列データのうち、最後のデータをチェックデータとして集計
    • 1分間に6回チェックが走っていた場合、最後のチェック結果だけをデータとして利用するということです。
  2. チェック回数とチェックが通った回数を集計し、各ロケーションごとの稼働率を求める
    • ロケーションごとの稼働率 = チェックが通った回数 / チェック回数
  3. 稼働率 = 各ロケーションごとの稼働率の総和 / ロケーション数として算出

ソースコード

ではコードと一緒に説明していきます。今回は記事用にサンプルとして「特定のUptimeCheckのデータをもとに稼働率を求める」コードを用意しました。

まず、全体のコードを記載し各要素の説明をしていきます。

ざっくりな流れとしては下記の通りです。

  1. 環境情報の設定
  2. Cloud Monitoring APIを実行するため、必要なリクエストパラメータの設定
  3. 各ロケーションごとの時系列データ取得 & ロケーションごとの稼働率を算出
  4. 全体の稼働率を算出
import (
    "context"
    "log"
    "os"
    "time"

    monitoring "cloud.google.com/go/monitoring/apiv3"
    "github.com/golang/protobuf/ptypes/duration"
    "github.com/golang/protobuf/ptypes/timestamp"
    "google.golang.org/api/iterator"
    monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
)

func main() {
  // 1. 環境情報の設定

  // UptimeCheckがあるプロジェクト
    projectNum := os.Getenv("GCP_PROJECT_NUM")
    // UptimeCheckのID
    uptimeCheckID := os.Getenv("UPTIME_CHECK_ID")

  // 2. Cloud Monitoring APIを実行するため、必要なリクエストパラメータの設定
    ctx := context.Background()
    client, err := monitoring.NewMetricClient(ctx)
    if err != nil {
        log.Fatal("could not generate NewUptimeCheckClient: %v", err)
    }
    defer client.Close()

    // 取得するデータの期間を定義
    endTime := time.Date(2020, 10, 23, 0, 0, 0, 0, time.Local)
    startTime := time.Date(2020, 8, 1, 0, 0, 0, 0, time.Local)

    // どのメトリクスデータを取得するか定義する
    // 今回はURL監視を行っている特定のUptimeCheckのデータを取得するため、下記のように定義する
    filter := `metric.type="monitoring.googleapis.com/uptime_check/check_passed" AND resource.type = "uptime_url" AND metric.labels.check_id = "` + uptimeCheckID + `"`

    req := &monitoringpb.ListTimeSeriesRequest{
        Name:   "projects/" + projectNum,
        Filter: filter,
        Interval: &monitoringpb.TimeInterval{
            StartTime: &timestamp.Timestamp{
                Seconds: startTime.Unix(),
            },
            EndTime: &timestamp.Timestamp{
                Seconds: endTime.Unix(),
            },
        },
        // 基本的にGCPのWebコンソールを参考に設定
        Aggregation: &monitoringpb.Aggregation{
            // 各アライメント区間の最後のデータ・ポイントを当該区間の値として採用
            // MonitoringのWebコンソールでは1ケ月の稼働率取得時、3時間ごとのcheckk passの最後の値を利用して稼働率を計算している
            PerSeriesAligner: monitoringpb.Aggregation_ALIGN_NEXT_OLDER,
            AlignmentPeriod: &duration.Duration{
                Seconds: 60, // 1m
            },
        },
        View: monitoringpb.ListTimeSeriesRequest_FULL,
    }

  // 3. 各ロケーションごとの時系列データ取得 & ロケーションごとの稼働率を算出
    // TimeSeriresデータを取得する
    it := client.ListTimeSeries(ctx, req)

    var PercentUptimeSum, uptimeCheckCount float64
    for {
        resp, err := it.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            log.Fatal("could not read time series value: %v", err)
        }
        log.Println("ロケーション:", resp.Metric.Labels["checker_location"], "のデータチェック開始")
        points := resp.GetPoints()
        // Aggregation_ALIGN_NEXT_OLDERの場合、スライスの長さ=最大チェックパス数である
        maxCheckPassCount := int64(len(points))
        var checkPassCount int64
        for _, point := range points {
            log.Println(point.GetInterval().GetStartTime().AsTime())
            if point.GetValue().GetBoolValue() == true {
                checkPassCount++
            }
        }

        locationPercentUptime := float64(checkPassCount) / float64(maxCheckPassCount)
        PercentUptimeSum += locationPercentUptime
        uptimeCheckCount++
  }

  // 4. 全体の稼働率を算出
    result := PercentUptimeSum / uptimeCheckCount
    log.Println("Percent Uptime: ", result)
}

それでは、各ブロックごとに説明していきます。

環境情報の設定

ここでは、UptimeCheckが存在するGCPのプロジェクトIDやチェック対象のUptimeCheckIDを変数として定義しています。 後々、API実行時のリクエストパラメータを設定する際に必要になってくるため、初期の段階で定義しておきました。

  // 1. 環境情報の設定

  // UptimeCheckがあるプロジェクト
    projectNum := os.Getenv("GCP_PROJECT_NUM")
    // UptimeCheckのID
    uptimeCheckID := os.Getenv("UPTIME_CHECK_ID")

Cloud Monitoring APIを実行するため、必要なリクエストパラメータの設定

今回お話しする中ではここが一番重要です。 新しくCloud Monitoring用のClientを作成しているところは割愛して、そのあとのコードについてお話ししていきます。

ここでは、まず取得するデータの期間を定義しています。リクエストパラメータを設定する際にUnixタイムスタンプで渡す必要があるので、timeパッケージを使ってデータを用意します。

    // 取得するデータの期間を定義
    endTime := time.Date(2020, 10, 1, 0, 0, 0, 0, time.Local)
    startTime := time.Date(2020, 9, 1, 0, 0, 0, 0, time.Local)

取得するデータを絞り込むため、下記の要素で絞り込みを行います。

  • UptimeCheckのデータを取得したいのでUptimeCheck用メトリクスタイプを指定
  • 取得対象UptimeCheckのIDを指定
  • リソースタイプはURLチェック
    // どのメトリクスデータを取得するか定義する
    // 今回はURL監視を行っている特定のUptimeCheckのデータを取得するため、下記のように定義する
    filter := `metric.type="monitoring.googleapis.com/uptime_check/check_passed" AND resource.type = "uptime_url" AND metric.labels.check_id = "` + uptimeCheckID + `"`

次にリクエストを送る際の各パラメータについて、簡単に紹介します。

Key名 設定値の意味
Name リクエストを送るプロジェクトのIDを指定
Interval データの取得期間。データを送る際はUnixタイムスタンプにする必要がある
Aggregation データの取得方法について定義する。Aggregationについて
Aggregation.PerSeriesAligner データ群をどのように取得するか具体的な方法を指定する。今回は特定期間の最後のデータを使いたいので「Aggregation_ALIGN_NEXT_OLDER」を使用する。
Aggregation.AlignmentPeriod PerSeriesAlignerで指定した方法で扱うデータの範囲(期間)。今回は「1分間のデータのうち、最後のデータを取得するようにしたいので、ここでは「60s」を指定する
Aggregation.View 取得するデータの範囲を指定する。FULLの場合、時系列データについて全ての情報を取得する。
    req := &monitoringpb.ListTimeSeriesRequest{
        Name:   "projects/" + projectNum,
        Filter: filter,
        Interval: &monitoringpb.TimeInterval{
            StartTime: &timestamp.Timestamp{
                Seconds: startTime.Unix(),
            },
            EndTime: &timestamp.Timestamp{
                Seconds: endTime.Unix(),
            },
        },
        // 基本的にGCPのWebコンソールを参考に設定
        Aggregation: &monitoringpb.Aggregation{
            // 各アライメント区間の最後のデータ・ポイントを当該区間の値として採用
            // MonitoringのWebコンソールでは1ケ月の稼働率取得時、3時間ごとのcheckk passの最後の値を利用して稼働率を計算している
            PerSeriesAligner: monitoringpb.Aggregation_ALIGN_NEXT_OLDER,
            AlignmentPeriod: &duration.Duration{
                Seconds: 60, // 1m
            },
        },
        View: monitoringpb.ListTimeSeriesRequest_FULL,
    }

ここまで来れば、あとはロケーションごとの時系列データを取得していい感じに加工していくだけです。

各ロケーションごとの時系列データ取得 & ロケーションごとの稼働率を算出

ListTimeSeries を実行すると、チェック対象のUptimeCheckに紐づく各ロケーションごとのデータリソースが返されます。 UptimeCheckごとに紐づくデータリソース(ロケーション)数は異なるので、無限ループを回しつつit.Next()で時系列データを取得します。紐づくデータリソースがなくなった時はforを抜けるように設定しています。it.Next()や関連する他の関数詳細については使用しているパッケージのコードを読んでもらうのが最も良いかと!

  // 3. 各ロケーションごとの時系列データ取得 & ロケーションごとの稼働率を算出
    // TimeSeriresデータを取得する
    it := client.ListTimeSeries(ctx, req)

    var PercentUptimeSum, uptimeCheckCount float64
    for {
        resp, err := it.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            log.Fatal("could not read tiem series value: %v", err)
        }
        log.Println("ロケーション:", resp.Metric.Labels["checker_location"], "のデータチェック開始")
        points := resp.GetPoints()
        // Aggregation_ALIGN_NEXT_OLDERの場合、スライスの長さ=最大チェックパス数である
        maxCheckPassCount := int64(len(points))
        var checkPassCount int64
        for _, point := range points {
            log.Println(point.GetInterval().GetStartTime().AsTime())
            if point.GetValue().GetBoolValue() == true {
                checkPassCount++
            }
        }

        locationPercentUptime := float64(checkPassCount) / float64(maxCheckPassCount)
        PercentUptimeSum += locationPercentUptime
        uptimeCheckCount++
  }

時系列データは全て point という構造体で取り扱うことができ、このpointの値を利用して稼働率を求めます。 下記のコードを見てもらえば分かる通り、pointの値がtrueだったら=チェックが正常に通っていれば、チェックパス回数を追加しています。(これはチェックが通ることを前提の考えとなりますが、データ数が多いほどインクリメント回数が増えるのでfalseを条件にしておいた方がいいかもしれません。)

        points := resp.GetPoints()
        // Aggregation_ALIGN_NEXT_OLDERの場合、スライスの長さ=最大チェックパス数である
        maxCheckPassCount := int64(len(points))
    var checkPassCount int64
        for _, point := range points {
            log.Println(point.GetInterval().GetStartTime().AsTime())
            if point.GetValue().GetBoolValue() == true {
                checkPassCount++
            }
    }
    locationPercentUptime := float64(checkPassCount) / float64(maxCheckPassCount)
        PercentUptimeSum += locationPercentUptime
        uptimeCheckCount++

ここまでくると、あとは足し算と割り算です。

  // 4. 全体の稼働率を算出
    result := PercentUptimeSum / uptimeCheckCount
    log.Println("Percent Uptime: ", result)

これでUptimeCheckに紐づくCloudMonitoringのデータをもとに稼働率を取得することができました。 サンプルとして、実際に弊社のUptimeCheckでチェックしているサービスの稼働率を「GCPWebコンソールで確認できる稼働率」と比較してみましたのでご覧ください。

f:id:allabout-techblog:20201026114300p:plain
稼働率の比較

上記画像にある通り、ほぼ同じ値を求めることができるようになりました。

最後に

今回は、Cloud Monitoringのデータを元に稼働率を取得する方法(golang)を紹介しました。 また余談ですが、弊社では多くのUptimeCheckが存在するため、プロジェクトに紐づくUptimeCheck一覧を取得・各UptimeCheckのIDをもとに稼働率を取得するようにしています。なお、データが6週間で消えることもあり一ヶ月ごとに定期実行・JSONとしてGCSに書き出すようにしています。

今回の知見はCloud Monitoringのデータを扱う上で役に立つものだと思うので、ぜひUptimeCheck以外でも試してみてください。

参考資料