動機
話題のO11y(詳しくわかってない)を勉強するために最近使っているgRPCをStackdriver Trace を使ってトレーシングしてみます。
まだ理解しきれていない部分が多々あるのでご指摘などありましたらぜひお願いします!
ちなみに分散トレーシングについてはわかりやすく説明してくださっているスライドが公開されています。
shared 20190115 Advanced Distributed Tracing with Stackdriver Trace - Google スライド
調べる
Stackdriver Trace のGo言語用設定を行う
Go 用の Stackdriver Trace の設定 | Stackdriver Trace | Google Cloud
OpenCensus Quickstart
OpenCensus の stackdriver 用の exporter を設定し、Tracing自体はOpencensusuに従うみたい
以下のような感じで Exporterを設定するみたい
import (
"log"
"os"
"contrib.go.opencensus.io/exporter/stackdriver"
"go.opencensus.io/trace"
)
func main() {
// Create and register a OpenCensus Stackdriver Trace exporter.
exporter, err := stackdriver.NewExporter(stackdriver.Options{
ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"),
})
if err != nil {
log.Fatal(err)
}
trace.RegisterExporter(exporter)
}
OpenCensus Guides gRPC Go
上記の設定をしてこのリンクに沿ってコードを変更すればトレースができる。
やってく
ローカルでサーバーを立ててアクセスしてみたが、ログが正常に出なかったのでGCEでサーバーを立てて検証してみる。
protoc
とか gRPC
についてや GCE の Go 環境についてはわかりやすい記事が沢山あるので適宜参照してください!
(Mac側)
$ go version
go version go1.11 darwin/amd64
(サーバー用 Linux側)
$ go version
go version go1.11.4 linux/amd64
gRPC の Helloworld を改変していく感じで利用させていただきます。
grpc-go/examples/helloworld at master · grpc/grpc-go
ディレクトリ構成が自前のサブモジュールを使っているので少し本家と違いますが適宜置き換えてください(わかりにくくてすみません)
go-grpc-template/
├── README.md
├── client
│ └── main.go
├── go.mod
├── go.sum
├── grpc-gen-circleci-template # コンパイルされた proto コードが入った submodule です
│ ├── doc
│ │ └── readme.md
│ └── helloworld.pb.go
└── server
└── main.go
go get する
$ go get go.opencensus.io/*
$ go get contrib.go.opencensus.io/exporter/stackdriver
変更後 コード
package main
import (
"context"
"contrib.go.opencensus.io/exporter/stackdriver"
pb "go-grpc-template/grpc-gen-circleci-template"
"go.opencensus.io/plugin/ocgrpc"
"go.opencensus.io/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"log"
"net"
"os"
"time"
)
const (
port = "0.0.0.0:50051"
)
// server is used to implement helloworld.GreeterServer.
type server struct{}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("ctx: %v", ctx)
ctx, span := trace.StartSpan(ctx, "grpc-template.server")
defer span.End()
log.Printf("Received: %v", in.Name)
time.Sleep(80 * time.Millisecond)
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
// Create and register a OpenCensus Stackdriver Trace exporter.
exporter, err := stackdriver.NewExporter(stackdriver.Options{
ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"),
})
if err != nil {
log.Fatal(err)
}
trace.RegisterExporter(exporter)
/*
// Register the views to collect server request count.
if err := view.Register(ocgrpc.DefaultServerViews...); err != nil {
log.Fatal(err)
}
*/
// Configure 100% sample rate, otherwise, few traces will be sampled.
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Set up a new server with the OpenCensus
// stats handler to enable stats and tracing.
s := grpc.NewServer(grpc.StatsHandler(new(ocgrpc.ServerHandler)))
pb.RegisterGreeterServer(s, &server{})
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
grpc.StatsHandler(new(ocgrpc.ServerHandler))
がないとTraceID
が伝搬されていかないので注意
(全く本質ではないですがServer側のアクセス制限に気をつけてください。自分はちょいハマりしました。)
package main
import (
"context"
"contrib.go.opencensus.io/exporter/stackdriver"
pb "go-grpc-template/grpc-gen-circleci-template"
"go.opencensus.io/plugin/ocgrpc"
"go.opencensus.io/trace"
"google.golang.org/grpc"
"log"
"os"
"time"
)
const (
address = "<VMの外部IP>:50051"
defaultName = "world"
)
func main() {
// Create and register a OpenCensus Stackdriver Trace exporter.
exporter, err := stackdriver.NewExporter(stackdriver.Options{
ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"),
})
if err != nil {
log.Fatal(err)
}
trace.RegisterExporter(exporter)
// Configure 100% sample rate, otherwise, few traces will be sampled.
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
for i := 0; i < 10; i++ {
// Create a span with the background context, making this the parent span.
// A span must be closed.
ctx, span := trace.StartSpan(context.Background(), "grpc-template.client")
time.Sleep(80 * time.Millisecond)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("trace id: %v", ctx)
log.Printf("Greeting: %s", r.Message)
span.End()
}
}
こちらも同様にgrpc.WithStatsHandler(&ocgrpc.ClientHandler{})
を忘れると伝搬がなされないので注意!
実行する
(client, server それぞれのディレクトリで)
$ export GOOGLE_CLOUD_PROJECT="<YOUR_PROJECT_ID>"
$ go run main.go
コンソールを確認する (Google Cloud Platform Stackdriver Trace)
すごいいいいいいいい!!
僕の知識不足で詰まるところはいくつかあったもののこれだけ簡単にトレースできるのは素晴らしすぎる…
次は gRPC Stream とかでも試してみたい