grpc 환경에서 golang sentry 연동하기

개발 끄적끄적 .. ✍️·2024년 4월 21일
0

Sentry는 실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼입니다. 로그에 대해 다양한 정보를 제공하고 이벤트별, 타임라인으로 얼마나 많은 이벤트가 발생하는지 알 수 있고 설정에 따라 알림을 받을 수 있습니다. (출처: https://tech.kakaopay.com/post/frontend-sentry-monitoring)

grpc, unary protocol 환경에서 golang 애플리케이션과 sentry를 연동하려고 할 때는 grpc 서버 옵션을 추가해주면 됩니다.

서버 띄우기

일반적으로 golang + grpc는 main.go에서 아래와 같이 서버를 띄웁니다.

// main.go

grpcServer := server.NewGRPCServer(opts...)

옵션 추가하기

이 때 opts.. 는 []grpc.ServerOption 타입이고 grpc.ChainUnaryInterceptor() 메서드를 통해 옵션을 추가할 수 있습니다.


// main.go

var opts []grpc.ServerOption
opts = append(opts, grpc.ChainUnaryInterceptor())
grpcServer := server.NewGRPCServer(opts...)

ChainUnaryInterceptor()에는 이때 UnaryServerInterceptor의 타입이 올 수 있는데 이 때 함수 타입이며 아래와 같습니다

type UnaryServerInterceptor func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)

미들웨어 정의

이제 UnaryServerInterceptor와 같은 타입의 센트리 미들웨어를 추가해야합니다. 코드는 간단합니다. grpc handler의 결과의 err가 존재한다면 이를 sentry로 전송하는 미들웨어입니다.

// main.go

// SentryInterceptor is a gRPC interceptor that captures exceptions with Sentry.
func SentryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	resp, err := handler(ctx, req)
	if err != nil {
	    resp = sentry.CaptureException(err)	
	}
	return resp, err
}

unknown만 핸들링하기

하지만 위와 같이 진행하면, 문제가 생깁니다. 의도한 에러를 발생시키더라도 err가 존재하여 모두 센트리에 잡히게 됩니다.
센트리의 경우, 내가 예상하지 못한 에러일 경우에만 알림을 줘야하는데요. 만약 모든 에러가 센트리에 잡혀버린다면 효율적인 이슈 트래킹이 어렵습니다. 그렇기 때문에 아래와 같은 코드를 추가해줘야합니다.

// SentryInterceptor is a gRPC interceptor that captures exceptions with Sentry.
func SentryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	resp, err := handler(ctx, req)
	if err != nil {
		if status.Code(err) == codes.Unknown {
			resp = sentry.CaptureException(err)
		}
	}
	return resp, err
}

err 의 코드가 grpc error 에 정의된 에러 코드 중 2, Unknown일 때만 센트리로 전송할 수 있게 추가했습니다. 만약 그렇다면 Not Found, Internal 등 의도적으로 필요에 의해 에러를 반환한 경우 센트리에 찍히지 않게됩니다.

(참고) grpc error interface

const (
	Canceled Code = 1

	Unknown Code = 2

	InvalidArgument Code = 3

	DeadlineExceeded Code = 4

	NotFound Code = 5

	AlreadyExists Code = 6

	PermissionDenied Code = 7

	ResourceExhausted Code = 8

	FailedPrecondition Code = 9

	Aborted Code = 10

	OutOfRange Code = 11

	Unimplemented Code = 12

	Internal Code = 13

	Unavailable Code = 14

	DataLoss Code = 15

	_maxCode = 17
)

실제 로직에서 적용하기

cursor, mongoErr := r.Mongo.FindOne(ctx, "db", "collection", bson.M{"id": "id"})
if mongoErr != nil {
	if errors.Is(mongoErr, m.ErrNoDocuments) {
		return nil, errors.WithStack(status.Errorf(codes.NotFound, "can not found"))
	} else {
		return nil, errors.WithStack(mongoErr.Error())
	}
}

위 코드는 mongo driver를 통해 데이터를 조회하는 로직입니다. 여기서 만약 error의 타입이 ErrNoDocuments라면, 의도했고 이를 제외한 모든 에러는 핸들링하지 않은 에러입니다. ErrNoDocuments 의 경우는 센트리에 찍히지 않을 것이고, 나머지 경우에는 센트리에 찍혀 이를 확인하고 대응 할 수 있습니다.

0개의 댓글