opentelemetry-go移行を試みた話

1年ほど前からOpenCensusとStackdriverという構成でシステムを運用しています。OpenTelemetryが発表されたとき、一度検証してみようと思ったのですが、当時(2019年6月くらい)はまだSpecが活発に議論されている段階でスルーしていました。しかし11月のKubeCon NA 2019でも大々的に取り上げられていたので、このタイミングで一度移行出来るか試してみた話です。

_OpenCensus/OpenTelemetryともにトレースとメトリクスを出力することが出来ますが、ここではトレースに絞って書きます_

OpenCensusでどんな感じでやってるかについては過去いくつか資料があるのでもし興味があればそちらをご覧ください :pray:

- https://speakerdeck.com/takashabe/opencensus-with-custom-context-propagation-and-exporter
- https://takashabe.booth.pm/items/1574935

## 現状のシステム構成

実際はより多くのシステム間で使用していますが、最低限検証のためにgRPC propagationとStackdriver exporter、さらにTracingにstatusを付ける状態を再現出来るかどうかを試しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13923/e0b4d07b-444a-bcb3-df1b-86c6198ee4cb.png)

またこの状態でOpenCensus実装のトレーシングはこのようなイメージです。

<img width="1302" alt="Screen Shot 2019-12-20 at 22.21.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13923/dca5a91c-988f-3e89-15a4-fd93c13217cc.png">

## OpenTelemetryを入れてみる

では実際にOpenCensusからOpenTelemetryに載せ替えたときにどういう変更を行ったかを見ていきましょう。

### トレース(スパン)を開始、出力する

StartSpanとspan.Endですね。ここは使用するパッケージが変わる程度でdiffはあまり多くありません。

```diff:src/gclient/main.go
- ctx, span := trace.StartSpan(ctx, "gclient/greeting")
+ tr := global.TraceProvider().Tracer("example/grpc")
+ ctx, span := tr.Start(ctx, "gclient/greeting")
```

### スパンにアトリビュートやらを付ける

アトリビュート、ステータスなどを見ていきます。
ここではOpenCensus版でstatusだけにしているところを、OpenTelemetryの方ではstatusがcodeだけを取るようになっているのでattributesも合わせて設定するようにしています。

```diff:src/gclient/main.go
- span.SetStatus(trace.Status{
-         Code:    1,
-         Message: err.Error(),
- })
+ span.SetStatus(1)
+ span.SetAttributes(key.String("message", err.Error()))
```

### Exporterの設定

Stackdriver exporterの設定です。opentelemetry-goに実装が用意されていて助かりました。
ここも使用するパッケージを変更する程度で大丈夫そうです。

```diff:src/gclient/main.go
 func initStackdriver() error {
-       exporter, err := stackdriver.NewExporter(stackdriver.Options{})
+       exporter, err := stackdriver.NewExporter()
        if err != nil {
                return err
        }
 
-       trace.RegisterExporter(exporter)
-       trace.ApplyConfig(trace.Config{
-               DefaultSampler: trace.AlwaysSample(),
-       })
+       tp, err := sdktrace.NewProvider(
+               sdktrace.WithConfig(sdktrace.Config{
+                       DefaultSampler: sdktrace.AlwaysSample()},
+               ),
+               sdktrace.WithSyncer(exporter),
+       )
+       if err != nil {
+               return err
+       }
+       global.SetTraceProvider(tp)
        return nil
 }
```

### gRPC client/server にトレースを仕込む

OpenCensusではgRPC pluginが担保してくれていた部分です。こちらはまだ(?)pluginとしての実装はないので自前でInterceptorを書く必要があります。

```diff:src/gclient/main.go
   ctx,
   fmt.Sprintf("%s", os.Getenv("ADDR")),
   grpc.WithInsecure(),
-  grpc.WithStatsHandler(&ocgrpc.ClientHandler{}),
+  grpc.WithUnaryInterceptor(UnaryClientInterceptor),

...

+
+func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+       requestMetadata, _ := metadata.FromOutgoingContext(ctx)
+       metadataCopy := requestMetadata.Copy()
+
+       err := trace.CurrentSpan(ctx).Tracer().WithSpan(ctx, "send",
+               func(ctx context.Context) error {
+                       grpctrace.Inject(ctx, &metadataCopy)
+                       ctx = metadata.NewOutgoingContext(ctx, metadataCopy)
+
+                       err := invoker(ctx, method, req, reply, cc, opts...)
+                       return err
+               })
+       return err
+}
```

```diff:src/gserver/main.go
  srv := grpc.NewServer(
-   grpc.StatsHandler(&ocgrpc.ServerHandler{}),
+   grpc.UnaryInterceptor(UnaryServerInterceptor),


+}
+
+func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
+       requestMetadata, _ := metadata.FromIncomingContext(ctx)
+       metadataCopy := requestMetadata.Copy()
+
+       entries, spanCtx := grpctrace.Extract(ctx, &metadataCopy)
+       ctx = distributedcontext.WithMap(ctx, distributedcontext.NewMap(distributedcontext.MapUpdate{
+               MultiKV: entries,
+       }))
+
+       tr := global.TraceProvider().Tracer("recv")
+       ctx, span := tr.Start(
+               ctx,
+               info.FullMethod,
+               trace.ChildOf(spanCtx),
+               trace.WithSpanKind(trace.SpanKindServer),
+       )
+       defer span.End()
+
+       return handler(ctx, req)
 }
```

### Stackdriver Trace でトレースを見てみる

ここまででOpenTelemetry実装のトレースが実際にexportされていることが確認出来ました。

<img width="1302" alt="Screen Shot 2019-12-20 at 22.22.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13923/3821a472-ff2a-2b04-7c0c-e8181ad80b57.png">

## おわりに

OpenCensusで実現していた最低限の実装をOpenTelemetryに移行出来るか試してみました。
結果的にまだAlphaなので未実装の部分が多いですが、着実に実装が進んできているという感触を得ました。

今回は言及しなかったですが、この他にも

- Stackdriver exporterのmetricsは未実装
- OpenCensus用のbridge APIは未実装
  - OpenTracing用のbridge APIは現時点で実装がある
- GCPクライアントライブラリに組み込まれているのがOpenCensusなので、OpenTelemetryのspanを入れたcontextを渡しても良い感じにトレースが出ない

といったところがあり、特に最後のライブラリの件は自分でラッパー書けば良いとはいえ面倒なのでしばらくはOpenCensusのままかなぁという感じでした。
とはいえOpenTelemetryはこの界隈のデファクトになると思いますし、もしまだ分散トレーシングはじめ特に何もやってないという場合は、bridge APIも実装予定なのでOpenCensusから初めてみるのも良いのではないでしょうか。

今回使ったコードは https://github.com/takashabe/ot-sandbox にあります。またOpenCensusからOpenTelemetryへのdiffは https://github.com/takashabe/ot-sandbox/pull/1 にまとまっています。