YAMAGUCHI::weblog

海水パンツとゴーグルで、巨万の富を築きました。カリブの怪物、フリーアルバイター瞳です。

OpenCensusでStackdriver Monitoringにメトリクスを送信する

はじめに

こんにちは、Stackdriver担当者です。先日Raspberry Pi Zero WにつけたBMP680から得たデータをStackdriver Monitoring API v3を使って送信してダッシュボードを作るという記事を書来ました。

ymotongpoo.hatenablog.com

しかしBMP680のデータを眺めていると結構精度が怪しいのでキャリブレーション用のデータが必要だなと思い、外気温のデータもStackdriver Monitoringに送ることにしました。それにOpenCensusを使ったのですが、GCPUG SlackでStackdriverに送るまとめをOpenCensus meetup vol.1までに公開すると言っていたのを忘れてたので慌てて記事にしました。

TL;DR

OpenWeatherMapとから外気の気温、湿度、大気圧、その他諸々のデータを取得し、OpenCensusを使ってStackdriver Monitoringにデータを送った。

OpenCensus Stats/Metrics

OpenCensusには大きく分けてTraceとStats/Metricsという2つの機能があり、それぞれStackdriver TraceとStackdriver Monitoringに対応したデータを送信できます。

OpenCensus Stats Exporter for Go

今回は OpenCensus Stats/Metrics の Stackdriver Exporter を使って Stackdriver Monitoring に送信するわけですが、Stackdriver Monitoring API v3を使っている用語と OpenCensus で使っている用語が違うので、公式ドキュメントにもある対応表をまず理解したほうが、いろいろなドキュメントが読みやすいです。

OpenCensus Stackdriver Monitoring 補足
Exporter N/A OpenCensusとモニタリングバックエンドをつなぐためのインターフェース
View MetricsDescriptor 記録するメトリクスのメタ情報
Measure MetricKind メトリクスのデータ型の定義
Aggregation ValueType メトリクス送信時の時系列データとしての扱い
Measurement Point ある一時点でのメトリクスの記録
View Data TimeSeries メトリクスの実データを送信前にバッファしておく入れ物
Tag LabelDescriptor ViewっやMeasureに対するラベル(OpenCensusではResourceとMetricに区別したラベルを付けない)

その上でサンプルコードを読むと大まかな雰囲気がわかります。

サンプルコードはPrometheus用のコードになっているので Exporter の部分だけ、Stackdriver Monitoring 用に置き換えてやる必要があります。といっても必要なのは Exporter の初期化の部分だけです。

type GenericNodeMonitoredResource struct {
    Location    string
    NamespaceId string
    NodeId      string
}

func NewGenericNodeMonitoredResource(location, namespace, node string) *GenericNodeMonitoredResource {
    return &GenericNodeMonitoredResource{
        Location:    location,
        NamespaceId: namespace,
        NodeId:      node,
    }
}

func (mr *GenericNodeMonitoredResource) MonitoredResource() (string, map[string]string) {
    labels := map[string]string{
        "location":  mr.Location,
        "namespace": mr.NamespaceId,
        "node_id":   mr.NodeId,
    }
    return "generic_node", labels
}

func GetMetricType(v *view.View) string {
    return fmt.Sprintf("custom.googleapis.com/%s", v.Name)
}

func InitExporter() *stackdriver.Exporter {
    mr := NewGenericNodeMonitoredResource(ResourceLocation, ResourceNamespace, "public-data")
    labels := &stackdriver.Labels{}
    exporter, err := stackdriver.NewExporter(stackdriver.Options{
        ProjectID:               os.Getenv("GOOGLE_CLOUD_PROJECT"),
        Location:                ResourceLocation,
        MonitoredResource:       mr,
        DefaultMonitoringLabels: labels,
        GetMetricType:           GetMetricType,
    })
    if err != nil {
        log.Fatal("failed to initialize ")
    }
    return exporter
}

Exporterの設定ではStackdriver特有の設定を行うところがポイントで、このGoDocをとりあえずガン見することになると思います。

godoc.org

ここのOptions構造体のドキュメントをよく読んでおけば設定ではまることはあまりないはずです。あるとすれば、 MonitoredResourceDefaultMonitoringLabels あたり。基本的にStackdriver Monitoring側で事前定義されているようなラベルは MonitoredResource で作るわけですが、ちょっとはまりどころとして、これが monitoredresource.Interface 型であるということ。Stackdriver用の事前定義の設定ではGKE、GCE、EC2ぐらいしか使わない前提で構造体がほとんど作られていないので(パッケージ参照)、その他のリソースに関しては上のように構造体を自前実装する必要があります。

DefaultMonitoringLabels はそれ以外で固定でつけるようなラベルを入れておくと良いです。ドキュメントにもありますが、ここを設定しないとデフォルトは opencensus_task というラベルで値にプロセス名が入ったものが勝手に送られてしまうので、そうしたくない場合は空の *stackdriver.Labels を設定すれば大丈夫です。

OpenCensus Stats

Exporterの設定は上記ぐらいなので、次にメトリクスを取得する部分であるViewの設定です。これはベストプラクティスとして、View、Measure、Key、といったものはすべてパッケージグローバルで設定しておくというのがあります。

const (
    // OCReportInterval is the interval for OpenCensus to send stats data to
    // Stackdriver Monitoring via its exporter.
    // NOTE: this value should not be no less than 1 minute. Detailes are in the doc.
    // https://cloud.google.com/monitoring/custom-metrics/creating-metrics#writing-ts
    OCReportInterval = 60 * time.Second

    // Measure namess for respecitive OpenCensus Measure
    MeasureTemperature = "temperature"
    MeasurePressure    = "pressure"
    MeasureHumidity    = "humidity"
    MeasureWindSpeed   = "windspeed"
    MeasureWindDeg     = "winddeg"

    // Units are used to define Measures of OpenCensus.
    TemperatureUnit = "C"
    PressureUnit    = "hPa"
    HumidityUnit    = "%"
    WindSpeedUnit   = "mps"
    WindDegUnit     = "degree"

    // ResouceNamespace is used for the exporter to have resource labels.
    ResourceNamespace = "ymotongpoo"
)

var (
    // Measure variables
    MTemperature = stats.Float64(MeasureTemperature, "air temperature", TemperatureUnit)
    MPressure    = stats.Float64(MeasurePressure, "barometric pressure", PressureUnit)
    MHumidity    = stats.Int64(MeasureHumidity, "air humidity", HumidityUnit)
    MWindSpeed   = stats.Float64(MeasureWindSpeed, "wind speed", WindSpeedUnit)
    MWindDeg     = stats.Float64(MeasureWindDeg, "wind degree from North", WindDegUnit)

    TemperatureView = &view.View{
        Name:        MeasureTemperature,
        Measure:     MTemperature,
        TagKeys:     []tag.Key{KeySource},
        Description: "air temperature",
        Aggregation: view.LastValue(),
    }

    PressureView = &view.View{
        Name:        MeasurePressure,
        Measure:     MPressure,
        TagKeys:     []tag.Key{KeySource},
        Description: "barometric pressure",
        Aggregation: view.LastValue(),
    }

    HumidityView = &view.View{
        Name:        MeasureHumidity,
        Measure:     MHumidity,
        TagKeys:     []tag.Key{KeySource},
        Description: "air humidity",
        Aggregation: view.LastValue(),
    }

    WindSpeedView = &view.View{
        Name:        MeasureWindSpeed,
        Measure:     MWindSpeed,
        TagKeys:     []tag.Key{KeySource},
        Description: "wind speed",
        Aggregation: view.LastValue(),
    }

    WeatherReportViews = []*view.View{
        TemperatureView,
        PressureView,
        HumidityView,
        WindSpeedView,
    }

    // KeySource is the key for label in "generic_node",
    KeySource, _ = tag.NewKey("source")
)

パッケージグローバルにとどまらず、そもそもこうした変数や定数だけを持ったパッケージを公開するのも良いでしょう。実際、アプリケーション内の複数のマイクロサービスで共通で使われるようなメトリクスなどは、そうしておくことでViewの初期化が簡単になります。たとえば gRPC 用の事前定義パッケージでは、よく使われるViewが事前定義されています。

これらと先程作成したExporterを使ってViewを初期化する手続きはたったこれだけです。

func InitOpenCensusStats(exporter *stackdriver.Exporter) {
    view.SetReportingPeriod(OCReportInterval)
    view.RegisterExporter(exporter)
    view.Register(WeatherReportViews...)
}

ここで一つだけ注意したいのがViewがStackdriverにレポートを送る間隔の設定(SetReportPeriod)です。これはOpenCensusのドキュメントやStackdriver MonitoringのAPIドキュメントにもあるように、1分以下に設定してしまうと「間隔が短い」と怒られます。

Note: each exporter makes different promises about what the lowest supported duration is. For example, the Stackdriver exporter recommends a value no lower than 1 minute. Consult each exporter per your needs.

Don't make the calls faster than one time per minute.

値を記録する

あとは値を記録するだけです。 stats.Record で Measurement を記録するだけです。記録さえしておけば、Viewがよしなに設定した間隔でStackdriver Monitoringにデータを送ってくれます。

func RecordMeasurement(id string, w *Weather) error {
    ctx, err := tag.New(context.Background(), tag.Upsert(KeySource, id))
    if err != nil {
        logger.Errorf("failed to insert key: %v", err)
        return err
    }

    stats.Record(ctx,
        MTemperature.M(w.Temperature),
        MPressure.M(w.Pressure),
        MHumidity.M(int64(w.Humidity)),
        MWindSpeed.M(w.WindSpeed),
    )
    return nil
}

コードも貼ったので長く見えますが、たったこれだけの手順で簡単にStackdriver MonitoringにOpenCensusを使ってデータを送れるようになります。

参照

OpenCensus + Stackdriver Monitoring

OpenWeatherMap API

Dark Sky API