OpenTelemetry Collectorを使ってDatadogにカスタムメトリクスを送信する
はじめに
株式会社CastingONEでソフトウェアエンジニアをしている @takashabe です。普段はHR領域のSaaSをGoで書いています。
弊社ではモニタリング基盤としてDatadogを採用しており、インフラやアプリケーションのメトリクスを収集しています。
また特定のSLIを設定するためにカスタムメトリクスをDatadogに送信することがあります。
従来はアプリケーション内で直接Datadog APIを叩いてカスタムメトリクスを送信していましたが、それをOpenTelemetry Collectorを使って送信するように変更しようとしています。
このエントリでは、OpenTelemetry Collectorを使ってDatadogにカスタムメトリクスを送信する方法について紹介します。
またサンプルコードは以下にあります。
OpenTelemetry Collectorとは
OpenTelemetry Collectorは、OpenTelemetryの一部として提供されており、複数のデータソースからテレメトリデータを収集、加工、そしてDatadogなどのバックエンドサービスにエクスポートするためのサービスです。
https://opentelemetry.io/docs/collector/ より引用
OpenTelemetry Collectorを使うことで、アプリケーションから直接特定のバックエンドサービスに依存することなく、テレメトリデータを収集することができます。
トレースの話になりますが、我々のシステムでもGoogle Cloud上の実行環境ではCloud Trace、ローカルではJaegerにトレースデータを送信するようにしています。
これをアプリケーションで処理していたものをOpenTelemetry Collectorに集約することで、アプリケーションでは常にOpenTelemetry Collectorに送信するだけで良くなり、コードをシンプルに保つことが期待できます。
カスタムメトリクスを送信する
ひとくちにカスタムメトリクスと言っても、Datadogにはいくつかのメトリクスタイプがあります。
OpenTelemetryとDatadogのメトリクスタイプの対応は以下を参照してください。
ここでは、我々がSLIを設定するために利用している gauge
メトリクスを送信する方法について紹介します。
またアプリケーションからメトリクスを送るのはotel-goを使うことを想定します。
OpenTelemetry Collectorの設定
まずはアプリケーションから送信されたメトリクスを受け取るOpenTelemetry Collectorを起動しておきましょう。
いつものcompose.yamlと、OpenTelemetry Collector用のotel-collector-config.yamlを用意します。
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.102.0
restart: always
command: ["--config=/etc/otel-collector-config.yaml", ""]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- 4317:4317 # OTLP gRPC receiver
- 4318:4318 # OTLP http receiver
otel-collector-config.yamlは以下のようなイメージですが、datadogの場合はsiteによってエンドポイントが異なるので注意してください。
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
exporters:
datadog:
api:
key: "YOUR_API_KEY"
site: "ap1.datadoghq.com"
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [datadog]
見ての通り、pipelineは複数設定できるようになっており、それぞれ柔軟にreceiversやprocessors、exportersを組み合わせることができます。
特に file exporter はデバッグにも便利なので、開発中はよく /dev/stdout
に向けて使っています。
otel-goからgaugeメトリクスを送信する
次にアプリケーションからotel-goを使ってgaugeメトリクスをOpenTelemetry Collectorに送信していきましょう。
まずは定型的ですがproviderの初期化を行います。ここでotelで定義されているサービス名やバージョンなどのリソースを設定しています。
package main
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
)
...
type expoter struct {
provider *sdkmetric.MeterProvider
}
func initProvider() (*expoter, error) {
ctx := context.Background()
resource, err := resource.Merge(resource.Default(),
resource.NewWithAttributes(semconv.SchemaURL,
semconv.ServiceName("my-service"),
semconv.ServiceVersion("0.1.0"),
))
if err != nil {
return nil, err
}
exp, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithInsecure(),
otlpmetricgrpc.WithEndpoint("localhost:4317"),
)
if err != nil {
return nil, err
}
meterProvider := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(
sdkmetric.NewPeriodicReader(exp, sdkmetric.WithInterval(time.Hour)),
),
sdkmetric.WithResource(resource),
)
otel.SetMeterProvider(meterProvider)
return &expoter{
provider: meterProvider,
}, nil
}
次に初期化したproviderを使って、実際にメトリクスを送信する部分です。サンプルとして何らかの配信処理の遅延時間を送信する例を示します。
テレメトリを受信するバックエンドサービス側(datadogなど)で適切にハンドリングできるように、タグを付与しています。
ここでは delivery_id
と env
の2つのタグを使っています。
func main() {
provider, err := initProvider()
if err != nil {
panic(err)
}
metrics := []delayMetric{
{
Name: "delivery.delay",
Value: float64(5 * time.Second),
Tags: []Tag{
{
Name: "delivery_id",
Value: "1",
},
{
Name: "env",
Value: "prod",
},
},
},
{
Name: "delivery.delay",
Value: float64(10 * time.Second),
Tags: []Tag{
{
Name: "delivery_id",
Value: "2",
},
{
Name: "env",
Value: "prod",
},
},
},
}
if err := provider.Gauge(context.Background(), metrics); err != nil {
panic(err)
}
}
type delayMetric struct {
Name string
Value float64
Tags []Tag
}
type Tag struct {
Name string
Value string
}
func (r *expoter) Gauge(ctx context.Context, metrics []delayMetric) error {
for _, m := range metrics {
var attrs []attribute.KeyValue
for _, t := range m.Tags {
attrs = append(attrs, attribute.String(t.Name, t.Value))
}
meter := r.provider.Meter(m.Name)
gauge, err := meter.Float64Gauge(m.Name)
if err != nil {
return err
}
gauge.Record(ctx, m.Value, metric.WithAttributes(attrs...))
}
if err := r.provider.ForceFlush(ctx); err != nil {
return err
}
return nil
}
ここまでやると、OpenTelemetry Collectorにメトリクスが送信され、Datadogにも反映されるはずです。
Datadog上でこんな感じのメトリクスが見えるはずです。
otel、特にメトリクスはトレースに比べて関連するコンポーネントが多いので、全体像を把握するまでは少しややこしい印象ですが、シンプルな値を送信するだけであればシュッと書けると思います。
従来はサンプルコードに載せたような同期的なgaugeメトリクスはサポートされておらず、コールバック的に書く非同期版で送信する必要がありましたが、最近otel-goでも同期的なgaugeメトリクスがサポートされ、Datadogのgaugeメトリクスとの親和性も上がりました。
またCloud Runではゼロスケールしてくれるため、定期的に起動してgaugeメトリクスを取得するようなアプリケーションとの相性も良いです。
もしotel-goでのメトリクス実装に移行しようとして困っていた場合は、最新動向を確認してみると良いかもしれません。
まとめ
OpenTelemetry Collectorを使ってDatadogにgaugeのカスタムメトリクスを送信する方法について紹介しました。
Collector自身も活発に開発が進んでおり、ProcessorやConnectorなどデータをこねくり回すことも柔軟にできるので、導入しておくと便利なことが多いと思います。
Discussion