openTelemetryでGCPのtraceを使う方法

本記事ではopenTelemetryというオープンソースを利用して、GoogleCloudPlatform(GCP)のTrace機能で計測をする方法を記載します。今回のTrace対象はCloudRun上で実行します。

# openTelemetryとは
openTelemetryは、複雑化していくシステムの動作を分析するためのオープンソースです。実行された処理の時間計測をおこなう事で、ボトルネックや障害に繋がる原因を探す事ができます。openCensusという分散トレーシングのオープンソースがあり、openTelemetryはその後継になります。
[openTelemetry](https://opentelemetry.io/)

# Traceの構造
Traceを始める時に、まずはTraceIDという識別用のIDを作成します。このIDによりTraceの対象を一意にしたりSpanという計測区間との紐付けを行います。Spanは親子関係を持ち、計測の細分化をする事が出来ます。下の図は、1番上の親Spanを3分割して中央の子Span2をさらに詳細にトレースする構造になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644751/9ab8322b-85fa-53a2-0499-21eba1121031.png)

# Goを使って実装
端末 : MacBook PRO 
Go : go1.15.2
クラウド : GCP

### インストール
1. goをダウンロード  
https://golang.org/doc/install
2. ダウンロードしたファイルからインストール
3. PATHを設定を追加

```bash
$ export PATH=/usr/local/go/bin:$PATH
```
### 初期化
**go mod**を使ってモジュールの管理をしていくため、初期化を行います。
以下のコマンドを実行すると**go.mod**というファイル作成されます。

```
$ go mod init example.com/trace
```

あとはコード内に読み込むモジュールの設定をしてビルドをする事で、自動でgo.modに読み込むモジュールの設定を追記してくれます。

### 構成
最終的な構成は以下のようになります。

```
.
├── Dockerfile
├── go.mod
├── go.sum
└── main.go
```

### ソースコード
#### main.go

HTTPのリクエストを受けるAPIを作成しています。
トレースをするためのパッケージを読み込み、プロジェクトの設定などの初期設定を行います。
このコードでは、
1. **example.com/trace**というTracerを作成
2. **sample**という名前のspanを開始
3. 各stepでの処理時間を計測
をしています。

```golang

package main

import (
        "context"
        "fmt"
        "log"
        "net/http"
        "os"
        "time"

        texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
        "go.opentelemetry.io/otel/api/global"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func handler(w http.ResponseWriter, r *http.Request) {

        exporter, err := texporter.NewExporter(texporter.WithProjectID("{project id}"))
        if err != nil {
                log.Fatalf("texporter.NewExporter: %v", err)
        }

        tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(exporter))
        if err != nil {
                log.Fatal(err)
        }

        global.SetTraceProvider(tp)
        tracer := global.TraceProvider().Tracer("example.com/trace")

        ctx, span := tracer.Start(context.Background(), "sample")
	    defer span.End()

	    _, step1 := tracer.Start(ctx, "step1")
        time.Sleep(time.Second * 1)
	    step1.End()

	    _, step2 := tracer.Start(ctx, "step2")
        time.Sleep(time.Second * 2)
	    step2.End()

        fmt.Fprintf(w, "Done\n")
}

func main() {

        http.HandleFunc("/", handler)

        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }

        log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

```

#### Dockerfile
マルチステージビルドを用いて、上部で実行ファイルの作成を行います。作成した実行ファイルをのみをalpineのイメージにコピーして、デプロイするイメージを作成します。

```dockerfile

FROM golang:1.15 as builder

WORKDIR /app

COPY go.* ./
RUN go mod download
COPY . ./

RUN CGO_ENABLED=0 GOOS=linux go build -v -o server


FROM alpine:3
RUN apk add --no-cache ca-certificates

COPY --from=builder /app/server /server

CMD ["/server"]
```
### 構築
Cloud Run上にデプロイを行います。
**gcloud builds submit**で、イメージのビルドを行いContainer Registryに保存します。そのあと、**gcloud run deploy**を使用して、保存したイメージからデプロイをします。

```bash

$ gcloud builds submit --tag gcr.io/$1/{イメージ名}
$ gcloud run deploy {サービス名} --image gcr.io/$1/{イメージ名} --platform managed --memory 256M --region {region} --allow-unauthenticated
```

# 結果
デプロイしたAPIを実行してGCPのTraceを見ると、以下のように表示されます。
![スクリーンショット 2020-09-23 19.02.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/644751/b5113007-b1fe-8752-c1b6-c20dc032caf2.png)

sampleのspanの中にstep1とstep2の実行時間が可視化されて表示されます。
トレーサを作成して、計測したい部分を囲む事で簡単に時間の計測をする事が出来ました。また、コンテキスト引数にトレーサを実行する事で入れ子で計測する事が出来るので、APIの中でもどの部分に処理の時間がかかっているか調べる事が出来ます。トレースの結果を集める事で恒常的に速度が遅いのかその時だけ処理が遅くなったかも後から検証する事が出来たりと、メリットは大きいと思います。