OpenTelemetryを使ってGoからDatadogへトレースを送ってみた
業務の中でOpenTelemetryを使ってオブザーバビリティを意識した
メトリクスの環境を構築する機会があったのでやりながら学んだことをまとめていきます。
前提
この記事ではトレースの複雑な設計や効率的、堅牢な実装はしません。
あくまでGoからOpenTelemetryを使ってDatadogへデータを流せるところまでをやります。
コレクターの設計などもざっくりやります。
Datadogのセットアップ周りもここでは触れませんmm
OpenTelemetryとは
OSSであること、様々なメトリクスサービスへ連携ができることや複数の分散サービスを
運用していても細かいトレースができるようなものである認識です。
今回の構成
ざっくりですが、以下のような構成でやります。
Podをどのように置くかなどはありますが、以下の構成で進めます。
- アプリケーションとコレクターはKubernetesで構築する
- アプリケーションとコレクターは別のPodで構築する
- DatadogへはコレクターからAPIキーを使ったAPI通信になる
実装について
Go
まずはGoの実装を進めます。
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func main() {
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
ctx := context.Background()
// OpenTelemetry トレーサーを初期化
tp, err := initTracer(ctx, endpoint)
if err != nil {
log.Fatalf("failed to initialize tracer: %v", err)
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
log.Printf("error shutting down tracer provider: %v", err)
}
}()
// Echo サーバーを起動
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(otelecho.Middleware("sample-app")) // OpenTelemetry Echo Middleware
// ヘルスチェックだけ動かす
e.GET("/health", func(c echo.Context) error {
// 検証用にトレースを追加
tracer := otel.Tracer("sample-app")
_, span := tracer.Start(c.Request().Context(), "HealthCheck Local")
defer span.End()
span.SetAttributes(
attribute.String("custom.tag", "🚀 This is a test trace! 🚀"),
attribute.String("custom.debugTag", "🐛 This is a debug trace! 🐛"),
attribute.String("endpoint", "/health"),
attribute.String("service.name", "vote-app"),
attribute.String("environment", "local"),
attribute.Bool("debug_mode", true),
attribute.Int("random_number", 42),
)
return c.String(http.StatusOK, "OK")
})
go func() {
if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
log.Fatalf("server start error: %v", err)
}
}()
shutdown(ctx, e)
}
// OpenTelemetry トレーサーを初期化
func initTracer(ctx context.Context, endpoint string) (*trace.TracerProvider, error) {
traceExporter, err := otlptrace.New(
ctx,
otlptracegrpc.NewClient(
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithEndpoint(endpoint),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}
tp := trace.NewTracerProvider(
trace.WithBatcher(traceExporter),
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("sample-app"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
// Graceful Shutdown を実装
func shutdown(ctx context.Context, e *echo.Echo) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit // シグナルを受信するまでブロック
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
log.Fatalf("server shutdown failed: %v", err)
}
log.Println("Server exited gracefully")
}
本来は分割して別パッケージにするべきですが、説明の関係上全て一緒に書いています。
ヘルスチェックのAPIが起動したタイミングで固定の文字列をトレースとして送るように設定しています。
middlewareはechoが提供してるライブラリを使って入れています。
適切にDIすればアプリケーションがメトリクス用のツールを意識せずに作れますね。
Kubernetes
API側のPod部分は環境変数を読み込んでるだけなので今回は割愛します。(量も多いので)
今回は最小の構成くらいの想定ですが、以下の要素を組み込みます。
- Pod
- Deployment
- Service(Goから受け付ける必要があるため)
- ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: sample-app
labels:
app: otel-collector
data:
otel-collector-config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch/datadog:
send_batch_size: 10
send_batch_max_size: 100
timeout: 20s
resource:
attributes:
- key: service.name
action: insert
value: vote-app
- key: env
action: insert
value: staging
exporters:
datadog:
api:
key: "CHANGE_API_KEY"
site: "ap1.datadoghq.com"
service:
telemetry:
logs:
level: "debug"
pipelines:
traces:
receivers: [otlp]
processors: [resource, batch/datadog]
exporters: [datadog]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: sample-app
labels:
app: otel-collector
spec:
replicas: 1
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:latest
args: ["--config=/etc/otel-collector-config/otel-collector-config.yaml"]
ports:
- containerPort: 4317
volumeMounts:
- name: otel-collector-config-vol
mountPath: /etc/otel-collector-config
volumes:
- name: otel-collector-config-vol
configMap:
name: otel-collector-config
items:
- key: otel-collector-config.yaml
path: otel-collector-config.yaml
---
apiVersion: v1
kind: Service
metadata:
name: otel-collector
namespace: sample-app
labels:
app: otel-collector
spec:
selector:
app: otel-collector
ports:
- protocol: TCP
port: 4317
targetPort: 4317
少し構成としては冗長みもあるかもしれません。サイドカーの構成にした方が良いなどあるでしょうが、
今回はとりあえず早く繋げてみようぜを最優先に進めたのでこのままで行きます。
Datadogの設定箇所については記載しておきます。
receivers(データの受信)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
受信については推奨されていることもありgRPCで接続します。
通信のプロトコルはotlp(OpenTelemetry Protocol)を使用します。
processors(データの前処理)
processors:
batch/datadog:
send_batch_size: 10
send_batch_max_size: 100
timeout: 20s
resource:
attributes:
- key: service.name
action: insert
value: sample-app
- key: env
action: insert
value: test
- batch/datadog:
- 送信設定です
- send_batch_size:送信する件数を設定。今回は10件ごとで送信するようにしている
- send_batch_max_size:一回で送信する最大のサイズ。今回は100件を設定
- timeout:タイムアウトの設定。今回は20秒で設定
- resource
- service.name:Datadog上の識別するサービス名。
- env:環境を識別するもの
exporters(Datadogへの送信)
exporters:
datadog:
api:
key: "CHANGE_API_KEY"
site: "ap1.datadoghq.com"
送信設定です。
APIキーを使ってDatadogに直接送信します。
service(データの流れを定義)
service:
telemetry:
logs:
level: "debug"
pipelines:
traces:
receivers: [otlp]
processors: [resource, batch/datadog]
exporters: [datadog]
- telemetry
- logs.level:今回は検証を行いながらやっていたのでdebugモードにしています。ノイズになるので運用上は外した方が良さそうです
- pipelines
- receivers:送信設定を先ほど定義したotlpを使用
- processors:ここも同様に上で設定したものを使用
- exporters:同様
Datadogでの確認
実際にどのように送られたのか確認します。
実は今回の実装で初めてDatadogを触ったこともあり、ここの確認でめちゃくちゃ迷いました。。
APM > Traces > Explorer
から確認することができました。
一覧にリクエストされたリストが並んでいます。
詳細のトレースをクリックすると、さらにその中の詳細が確認できました。
その中に今回検証用に設定していたSpanの情報を確認することができました。
終わりに
今回は最小アーキテクチャで検証をしたのでOpenTelemetryの良さを目一杯出すというものではなかったですが、実装自体もベンダーツールに非依存なのも良いなと思います。コレクターの向き先を変えれば
アプリケーションは意識をあまりせずにツールの変更ができます。
本来は複数のマイクロサービスを組み合わせた際にメリットを享受できるはずなので、
ちょっと今度は何個か繋いで試してみようかなと思います。
OpenTelemetryの導入編でした。
Discussion