0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(令和最新版)GoのgRPCサーバーのトレースからヘルスチェックを除外する

Last updated at Posted at 2025-01-16

はじめに

Go言語でgRPCサーバーを使ってマイクロサービスを開発している方の中には、OpenTelemetryを活用して分散トレーシングを実装している方も多いと思います。

しかし、ヘルスチェックリクエストのトレースが出力されて邪魔に感じることはありませんか?

この記事では、調べても意外と出てこない、ヘルスチェックをトレースから除外する最新の方法を解説します!

OpenTelemetryの計装

以下は動作確認用のgRPCサーバーのコードです。

以前はトレースを出力するために otelgrpc.UnaryServerInterceptor を使用していましたが、現在は非推奨とされています。代わりに otelgrpc.NewServerHandler を使用しましょう。

package main

import (
	"context"
	"errors"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/sdk/trace"
	"google.golang.org/grpc"
	"google.golang.org/grpc/examples/features/proto/echo"
	"google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/reflection"
	"log"
	"net"
)

// HealthService implements the gRPC health checking service.
type HealthService struct{}

// Check メソッドの実装
func (h *HealthService) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
	// 商用サービスなら、ここでデータベースに接続できるかなどをチェックする
	return &grpc_health_v1.HealthCheckResponse{
		// サーバーが正常な状態でなければ NOT_SERVING を返すようにする
		Status: grpc_health_v1.HealthCheckResponse_SERVING,
	}, nil
}

// Watch メソッドの実装
func (h *HealthService) Watch(req *grpc_health_v1.HealthCheckRequest, srv grpc_health_v1.Health_WatchServer) error {
	// ここでは常に SERVING と NOT_SERVING を連続して返し続けてい
	// 本来はサーバーの状態に応じてステータスが変わったときだけ返すようにする
	for {
		if err := srv.Send(&grpc_health_v1.HealthCheckResponse{
			Status: grpc_health_v1.HealthCheckResponse_SERVING,
		}); err != nil {
			return err
		}

		if err := srv.Send(&grpc_health_v1.HealthCheckResponse{
			Status: grpc_health_v1.HealthCheckResponse_NOT_SERVING,
		}); err != nil {
			return err
		}
	}
}

// EchoService implements the Echo service defined in the proto file.
type EchoService struct {
	echo.UnimplementedEchoServer
}

// UnaryEcho returns the same message sent by the client.
func (e *EchoService) UnaryEcho(ctx context.Context, req *echo.EchoRequest) (*echo.EchoResponse, error) {
	return &echo.EchoResponse{Message: req.GetMessage()}, nil
}

func setupOpenTelemetry(ctx context.Context) (shutdown func(context.Context) error, err error) {
	var shutdownFuncs []func(context.Context) error
	shutdown = func(ctx context.Context) error {
		var err error
		for _, fn := range shutdownFuncs {
			err = errors.Join(err, fn(ctx))
		}
		shutdownFuncs = nil
		return err
	}

	exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}

	tp := trace.NewTracerProvider(
		trace.WithBatcher(exporter),
	)
	shutdownFuncs = append(shutdownFuncs, tp.Shutdown)
	otel.SetTracerProvider(tp)

	return shutdown, nil
}

func main() {
	ctx := context.Background()

	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	shutdown, err := setupOpenTelemetry(ctx)
	if err != nil {
		log.Fatalf("error setting up OpenTelemetry: %v", err)
	}

	server := grpc.NewServer(
		grpc.StatsHandler(
			otelgrpc.NewServerHandler(),
		),
	)

	// メインのサービスの登録
	echoServer := &EchoService{}
	echo.RegisterEchoServer(server, echoServer)

	// ヘルスサービスの登録
	healthServer := &HealthService{}
	grpc_health_v1.RegisterHealthServer(server, healthServer)

	// grpcurl で proto ファイルを指定せずにサービスを呼び出すため
	reflection.Register(server)

	log.Println("gRPC server is running on port 50051")
	if err := errors.Join(server.Serve(listener), shutdown(ctx)); err != nil {
		log.Fatalf("server exited with error: %v", err)
	}
}

動作確認

ターミナルを2つ起動し、トレースが正しく出力されることを確認してみましょう。

ターミナル1: gRPCサーバーの起動

以下のコマンドを実行して、gRPCサーバーを起動します。

$ go run ./main.go
2025/01/16 22:22:28 gRPC server is running on port 50051

ターミナル2: ヘルスチェックのリクエスト

grpccurl を使用して、サーバーのヘルスチェックエンドポイントにリクエストを送信します。

$ grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check

ターミナル1: ログの確認

サーバーのログを確認すると、ヘルスチェックとリフレクションに関連するRPCのトレースが出力されていることがわかります。

{
        "Name": "grpc.health.v1.Health/Check",
        ...
}
{
        "Name": "grpc.reflection.v1.ServerReflection/ServerReflectionInfo",
        ...
}

これで、サーバーがヘルスチェックリクエストを受け取り、対応したことがトレースログに記録されているのを確認できます。

ヘルスチェックサービスの除外

このままではすべてのRPCのトレースが出力されてしまいます。しかし、ヘルスチェックのような重要でないトレースを保存する必要はありません。

おそらく2022年以前にgRPCサーバーを実装された方は、独自のInterceptorでトレースを出力しないリクエストをフィルターする処理を実装していたのではないかと思います。

しかし、居間ではこのように特定のRPCのトレースを出力しないために、otelgrpc では filters パッケージが提供されています。

例えば、ヘルスチェックのRPCのトレースを除外する場合は次のように記述します。「ヘルスチェック以外」のトレースを出力するので、filters.Not(filters.HealthCheck()) と書いてください。

	server := grpc.NewServer(
		grpc.StatsHandler(
			otelgrpc.NewServerHandler(
				otelgrpc.WithFilter(
					filters.Not(filters.HealthCheck()),
				),
			),
		),
	)

grpc.health.v1.Health/Check にリクエストしても、grpc.health.v1.Health/Check のトレースが表示されなくなります。

$ go run ./main.go
2025/01/16 22:59:41 gRPC server is running on port 50051
{
        "Name": "grpc.reflection.v1.ServerReflection/ServerReflectionInfo",
}

おわりに

gRPCサーバーにOpenTelemetryを導入する方法についての記事は多く見かけますが、ヘルスチェックのトレースを除外する具体的な方法は意外と見つかりません。そこで、この記事ではその方法を分かりやすく解説しました。参考になれば嬉しいです!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?