はじめに
こんにちは、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構造体のドキュメントをよく読んでおけば設定ではまることはあまりないはずです。あるとすれば、 MonitoredResource
と DefaultMonitoringLabels
あたり。基本的にStackdriver Monitoring側で事前定義されているようなラベルは MonitoredResource
で作るわけですが、ちょっとはまりどころとして、これが monitoredresource.Interface
型であるということ。Stackdriver用の事前定義の設定ではGKE、GCE、EC2ぐらいしか使わない前提で構造体がほとんど作られていないので(パッケージ参照)、その他のリソースに関しては上のように構造体を自前実装する必要があります。
DefaultMonitoringLabels
はそれ以外で固定でつけるようなラベルを入れておくと良いです。ドキュメントにもありますが、ここを設定しないとデフォルトは opencensus_task
というラベルで値にプロセス名が入ったものが勝手に送られてしまうので、そうしたくない場合は空の *stackdriver.Labels
を設定すれば大丈夫です。
OpenCensus Stats
Exporterの設定は上記ぐらいなので、次にメトリクスを取得する部分であるViewの設定です。これはベストプラクティスとして、View、Measure、Key、といったものはすべてパッケージグローバルで設定しておくというのがあります。
const (
OCReportInterval = 60 * time.Second
MeasureTemperature = "temperature"
MeasurePressure = "pressure"
MeasureHumidity = "humidity"
MeasureWindSpeed = "windspeed"
MeasureWindDeg = "winddeg"
TemperatureUnit = "C"
PressureUnit = "hPa"
HumidityUnit = "%"
WindSpeedUnit = "mps"
WindDegUnit = "degree"
ResourceNamespace = "ymotongpoo"
)
var (
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, _ = 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