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
}
하지만 위와 같이 진행하면, 문제가 생깁니다. 의도한 에러를 발생시키더라도 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
의 경우는 센트리에 찍히지 않을 것이고, 나머지 경우에는 센트리에 찍혀 이를 확인하고 대응 할 수 있습니다.