본 글은 센트리 계정이 있으며, 센트리 프로젝트를 이미 생성하였다 가정하고 진행됩니다.
에러 추적(Capture Exception)
: 애플리케이션에서 발생하는 예외와 에러를 자동으로 감지하고 기록합니다. 센트리는 스택 트레이스(stack trace)와 함께 에러 발생 시점의 환경 정보를 제공하여, 에러의 원인을 쉽게 파악할 수 있도록 도와줍니다.
성능 모니터링(Transactions)
: 애플리케이션의 성능을 모니터링하며, 웹 요청이나 API 호출 등의 트랜잭션을 추적합니다. 각 트랜잭션의 응답 시간, 성공 및 실패 비율과 같은 세부 정보를 제공하여 성능 병목 현상을 식별하고 개선할 수 있도록 도와줍니다.
트레이싱(Trace)
: 애플리케이션의 트랜잭션을 세부적으로 추적하여, 서비스 간 호출, 데이터베이스 쿼리 등의 작업에 걸리는 시간을 분석합니다. 이를 통해 애플리케이션 전체의 성능을 이해하고, 문제가 되는 부분을 파악할 수 있습니다.
이슈 관리 및 알림
: 센트리는 에러와 성능 문제를 이슈로 관리하며, 발생 시 지정된 이메일이나 슬랙(Slack) 등의 통지 채널을 통해 알림을 보냅니다. 개발자는 이슈를 할당받고, 처리 상태를 업데이트하며, 문제 해결을 위해 협업할 수 있습니다.
릴리즈 추적(Release Tracking)
: 애플리케이션의 버전을 추적하여, 새로운 릴리즈가 에러 비율에 미치는 영향을 분석할 수 있습니다. 이를 통해 최근 배포된 변경사항이 문제를 일으키고 있는지 파악할 수 있습니다.
센트리 알람을 통해 협업 툴과 연동하여 개발자가 쉽게 대응할 수 있도록 합니다.
대시보드 - Alert - Create Alert
트리거 설정
세부 조건 설정
WHEN : 알람이 트리거 되는 시점 정의
IF : 이벤트의 세부 조건
THEN : 액션 선정
[Projects - Settings - Client Keys]
Go-Sentry SDK 사용 시, 최초 초기화 당시에 HTTP Client의 TCP 커넥션을 자동으로 처리 합니다.
Applicaiton 의 main.go 에서 최초로 Init() 설정하는 것이 Best Practice로 소개됩니다.
err := sentry.Init(
sentry.ClientOptions{
Dsn: si.conf.Sentry.DSN,
SampleRate: si.conf.Sentry.SampleRate,
EnableTracing: si.conf.Sentry.EnableTrace,
Debug: si.conf.Sentry.Debug,
TracesSampleRate: si.conf.Sentry.TracesSampleRate,
Environment: si.conf.Sentry.Environment,
AttachStacktrace: true,
Transport: &sentry.HTTPSyncTransport{
Timeout: si.conf.Sentry.Timeout,
},
},
)
발생한 예외(에러)와 관련된 스택 트레이스를 자동으로 캡처하고 추적합니다.
// 현재 컨텍스트와 연관된 Hub 생성 또는 가져오기
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
// 현재 컨텍스트에 Sentry Hub을 설정
ctx = sentry.SetHubOnContext(ctx, hub)
}
// 에러 캡처
hub.CaptureException(err)
정확한 에러의 발생처를 알기 위해서는 pkg/errors 라이브러리를 사용하자!
errors 패키지: 기본적으로 스택 트레이스 정보를 제공하지 않습니다. 에러는 단순히 메시지를 포함하는 값이며, 디버깅을 위한 추가적인 컨텍스트나 위치 정보는 포함되어 있지 않습니다.
github.com/pkg/errors 패키지: 에러에 자동으로
스택 트레이스
를 포함합니다. 이를 통해 에러가 어디서 발생했는지, 에러의 원인을 추적하는 데 필요한 상세한 호출 스택 정보를 얻을 수 있습니다.
test. errors 패키지와 pkg/errors 에 따른 스택 트레이스
package main
import (
"context"
stdErr "errors"
"fmt"
"time"
sentry "github.com/getsentry/sentry-go"
pkgErr "github.com/pkg/errors"
)
var (
PkgErr1 = pkgErr.New("pkg error 1")
PkgErr2 = pkgErr.New("pkg error 2")
StdErr1 = stdErr.New("standard error1")
StdErr2 = stdErr.New("standard error2")
)
func main() {
err := sentry.Init(
sentry.ClientOptions{
Dsn: "",
Debug: true,
AttachStacktrace: true,
},
)
if err != nil {
// sentry 초기화 실패 시, panic 시스템 os exit
panic(err)
}
ctx := context.Background()
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
// 현재 컨텍스트에 Sentry Hub을 설정
ctx = sentry.SetHubOnContext(ctx, hub)
}
err = errPkgNested()
go hub.CaptureException(err)
fmt.Println("Standard error nested")
time.Sleep(5 * time.Second)
}
func errStdNested() error {
return stdErr.Join(PkgErr1, PkgErr2)
}
func errPkgNested() error {
return pkgErr.Wrap(PkgErr1, PkgErr2.Error())
}
Sentry에서 Scope는 특정 에러 또는 이벤트에 추가적인 컨텍스트 정보를 제공하는 메커니즘입니다. 에러의 재현을 위해서 요청 파라미터 등을 Scope에 저장하여 애플리케이션의 고도화가 가능합니다.
Sentry 클라이언트를 통해 생성되는 허브는 Context 단위로 싱글턴 패턴을 유지하며, go context.Context 와 함께 메타데이터를 저장하여, 추적에 용이하도록 돕습니다.
func (rm *sentryScopeMiddleware) Register(originalHandler http.Handler) http.Handler {
if !rm.initializer.Enabled() {
// sentry가 비활성화 되어 있는 경우, 센트리 활성화
rm.initializer.Init()
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hub := sentry.GetHubFromContext(r.Context())
if hub == nil {
hub = sentry.CurrentHub().Clone()
r = r.WithContext(sentry.SetHubOnContext(r.Context(), hub))
}
hub.Scope().SetRequest(r)
})
}
하나의 애플리케이션에서 싱글턴을 유지하기 위해 설정을 통한 초기화 여부를 확인하는 클래스
package core
import (
"sync"
"github.com/getsentry/sentry-go"
)
type (
// SentryInitializer : Sentry 설정 초기화를 담당하는 구현체
SentryInitializer struct {
conf *Config
enabled bool
mutex sync.RWMutex // enabled 필드에 대한 읽기/쓰기 동기화를 위한 RWMutex
}
)
// NewSentryInitializer : SentryInitializer 생성자
func NewSentryInitializer(conf *Config) SentryInitializer {
return SentryInitializer{
conf: conf,
}
}
// Init : SentryInitializer 초기화
//
// - sentry 패키지 변수를 통해 싱글턴 처리하므로 최초 설정만 요구됨
func (si *SentryInitializer) Init() error {
si.mutex.Lock() // enabled lock for writing
defer si.mutex.Unlock()
err := sentry.Init(
sentry.ClientOptions{
Dsn: si.conf.Sentry.DSN,
SampleRate: si.conf.Sentry.SampleRate,
EnableTracing: si.conf.Sentry.EnableTrace,
Debug: si.conf.Sentry.Debug,
TracesSampleRate: si.conf.Sentry.TracesSampleRate,
Environment: si.conf.Sentry.Environment,
AttachStacktrace: true,
Transport: &sentry.HTTPSyncTransport{
Timeout: si.conf.Sentry.Timeout,
},
},
)
if err != nil {
// sentry 초기화 실패 시, panic 시스템 os exit
panic(err)
}
// 활성화 상태로 변경
si.enabled = true
return nil
}
// Enabled : Sentry 활성화 여부
//
// - Init()이 호출된 경우, 활성화 상태로 변경
func (si *SentryInitializer) Enabled() bool {
si.mutex.RLock() // read lock
defer si.mutex.RUnlock()
return si.enabled
}
동시성 문제로 Enabled()의 동기화를 위해 락으로 관리
SentryInitializer를 임베딩하여, 센트리를 통한 센트리 허브에서 에러를 캡쳐링하는 클래스
package core
import (
"context"
"github.com/getsentry/sentry-go"
)
type (
// ErrorCapturer : 에러 캡처 인터페이스
ErrorCapturer interface {
CaptureError(ctx context.Context, err error)
}
sentryErrorCapturer struct {
SentryInitializer
}
)
// NewSentryErrorCapturer : SentryErrorCapturer 생성자
func NewSentryErrorCapturer(initializer SentryInitializer) ErrorCapturer {
return &sentryErrorCapturer{
initializer: initializer,
}
}
// CaptureError : Sentry로 에러 캡처
func (sec *sentryErrorCapturer) CaptureError(ctx context.Context, err error) {
// 에러가 없는 경우, 무시
if err == nil {
return
}
if !sec.SentryInitializer.Enabled() {
// sentry 초기화를 통해 활성화
sec.SentryInitializer.Init()
}
// 현재 컨텍스트와 연관된 Hub 생성 또는 가져오기
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
// 현재 컨텍스트에 Sentry Hub을 설정
ctx = sentry.SetHubOnContext(ctx, hub)
}
// 에러 캡처
hub.CaptureException(err)
}
핸들러에서 Panic 발생 시, Recover(), 요청을 허브의 메타데이터로 저장하는 클래스
// Register panic() 발생 시 스택 스레이스를 로깅하며 센트리를 통해 남기는 메서드
//
// - http handler에 내부적으로 등록
// - API 고루틴 내부적으로 defer()를 통하여 panic 상황에서도 로깅이 가능하도록 한다
//
// Parameters:
// - originalHandler: 원래의 http handler
func (rm *sentryRecoverMiddleware) Register(originalHandler http.Handler) http.Handler {
if !rm.initializer.Enabled() {
// sentry가 비활성화 되어 있는 경우, 센트리 활성화
rm.initializer.Init()
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hub := sentry.GetHubFromContext(r.Context())
if hub == nil {
hub = sentry.CurrentHub().Clone()
r = r.WithContext(sentry.SetHubOnContext(r.Context(), hub))
}
hub.Scope().SetRequest(r)
defer func() {
if err := recover(); err != nil {
hub.RecoverWithContext(r.Context(), err)
stackList := strings.Split(string(debug.Stack()), "\n")
rm.Logger.Error().
Any("error", err).
Any("stack_list", stackList).Send()
resp := dto.ACSErrorResponse{
Code: http.StatusInternalServerError,
ErrMessage: "서버에서 예기치 못한 에러가 발생하였습니다",
Success: false,
}
jsonResp := resp.ToJSON()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(jsonResp)
if err != nil {
rm.Logger.Error().
Any("error", err).Send()
}
return
}
}()
originalHandler.ServeHTTP(w, r)
})
}