feat: sentry integration
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good

ref #3
This commit is contained in:
2023-07-05 12:05:30 -06:00
parent a176b754cd
commit aab5452fa2
17 changed files with 291 additions and 48 deletions

View File

@ -1,11 +1,14 @@
package admin
import (
"context"
"fmt"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/schema"
"github.com/getsentry/sentry-go"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
const ErrCodeAlreadyExist api.ErrorCode = "already-exist"
@ -29,3 +32,8 @@ func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schem
return
}
func logAndCaptureError(ctx context.Context, message string, err error) {
sentry.CaptureException(err)
logger.Error(ctx, message, logger.E(err))
}

View File

@ -1,6 +1,7 @@
package admin
import (
"fmt"
"net/http"
"sort"
@ -10,7 +11,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type QueryLayerResponse struct {
@ -38,7 +38,7 @@ func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
options...,
)
if err != nil {
logger.Error(ctx, "could not list layers", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not list layers", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -85,7 +85,7 @@ func (s *Server) getLayer(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -120,7 +120,7 @@ func (s *Server) deleteLayer(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not delete layer", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not delete layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -156,7 +156,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
layerName, err := store.ValidateName(createLayerReq.Name)
if err != nil {
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "invalid 'name' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return
@ -165,7 +165,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
layerType := store.LayerType(createLayerReq.Type)
if !setup.LayerTypeExists(layerType) {
logger.Error(r.Context(), "unknown layer type", logger.E(errors.WithStack(err)), logger.F("layerType", layerType))
logAndCaptureError(ctx, fmt.Sprintf("unknown layer type '%s'", layerType), errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return
@ -179,7 +179,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not create layer", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not create layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -223,7 +223,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -247,7 +247,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
if updateLayerReq.Options != nil {
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
if err != nil {
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not retrieve layer options schema", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -258,7 +258,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
}(updateLayerReq.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not validate layer options", errors.WithStack(err))
var invalidDataErr *schema.InvalidDataError
if errors.As(err, &invalidDataErr) {
@ -286,7 +286,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not update layer", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not update layer", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -300,21 +300,7 @@ func getLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool
name, err := store.ValidateName(rawLayerName)
if err != nil {
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return "", false
}
return store.LayerName(name), true
}
func geLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) {
rawLayerName := chi.URLParam(r, "layerName")
name, err := store.ValidateName(rawLayerName)
if err != nil {
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
logAndCaptureError(r.Context(), "could not parse layer name", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return "", false

View File

@ -11,7 +11,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
type QueryProxyResponse struct {
@ -37,7 +36,7 @@ func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) {
options...,
)
if err != nil {
logger.Error(ctx, "could not list proxies", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not list proxies", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -79,7 +78,7 @@ func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not get proxy", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not get proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -109,7 +108,7 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not delete proxy", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not delete proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -140,14 +139,14 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
name, err := store.ValidateName(createProxyReq.Name)
if err != nil {
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not parse 'name' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return
}
if _, err := url.Parse(createProxyReq.To); err != nil {
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return
@ -161,7 +160,7 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not create proxy", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not create proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -207,7 +206,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
if updateProxyReq.To != nil {
_, err := url.Parse(*updateProxyReq.To)
if err != nil {
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return
@ -235,7 +234,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
return
}
logger.Error(ctx, "could not update proxy", logger.E(errors.WithStack(err)))
logAndCaptureError(ctx, "could not update proxy", errors.WithStack(err))
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return
@ -249,7 +248,7 @@ func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool
name, err := store.ValidateName(rawProxyName)
if err != nil {
logger.Error(r.Context(), "could not parse proxy name", logger.E(errors.WithStack(err)))
logAndCaptureError(r.Context(), "could not parse proxy name", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return "", false
@ -263,7 +262,7 @@ func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defa
if rawValue != "" {
value, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil {
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.E(errors.WithStack(err)))
logAndCaptureError(r.Context(), "could not parse int param", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return 0, false
@ -296,7 +295,7 @@ func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request,
for _, rv := range rawValues {
v, err := validate(rv)
if err != nil {
logger.Error(r.Context(), "could not parse ids slice param", logger.F("param", param), logger.E(errors.WithStack(err)))
logAndCaptureError(r.Context(), "could not parse ids slice param", errors.WithStack(err))
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
return nil, false

View File

@ -12,6 +12,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"forge.cadoles.com/cadoles/bouncer/internal/store"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
@ -92,6 +93,16 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router.Use(middleware.Logger)
if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware")
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
router.Use(sentryMiddleware.Handle)
}
corsMiddleware := cors.New(cors.Options{
AllowedOrigins: s.serverConfig.CORS.AllowedOrigins,
AllowedMethods: s.serverConfig.CORS.AllowedMethods,

View File

@ -7,6 +7,7 @@ import (
"sort"
"time"
"github.com/getsentry/sentry-go"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
@ -90,6 +91,8 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
return
}
sentry.CaptureException(err)
debug := ctx.Bool("debug")
if !debug {

View File

@ -6,6 +6,7 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/admin"
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
@ -27,6 +28,14 @@ func RunCommand() *cli.Command {
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
projectVersion := ctx.String("projectVersion")
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Admin.Sentry, projectVersion)
if err != nil {
return errors.Wrap(err, "could not initialize sentry client")
}
defer flushSentry()
srv := admin.NewServer(
admin.WithServerConfig(conf.Admin),
admin.WithRedisConfig(conf.Redis),

View File

@ -28,6 +28,14 @@ func RunCommand() *cli.Command {
logger.SetFormat(logger.Format(conf.Logger.Format))
logger.SetLevel(logger.Level(conf.Logger.Level))
projectVersion := ctx.String("projectVersion")
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
if err != nil {
return errors.Wrap(err, "could not initialize sentry client")
}
defer flushSentry()
layers, err := setup.GetLayers(ctx.Context, conf)
if err != nil {
return errors.Wrap(err, "could not initialize director layers")

View File

@ -5,6 +5,7 @@ type AdminServerConfig struct {
CORS CORSConfig `yaml:"cors"`
Auth AuthConfig `yaml:"auth"`
Metrics MetricsConfig `yaml:"metrics"`
Sentry SentryConfig `yaml:"sentry"`
}
func NewDefaultAdminServerConfig() AdminServerConfig {
@ -13,6 +14,7 @@ func NewDefaultAdminServerConfig() AdminServerConfig {
CORS: NewDefaultCORSConfig(),
Auth: NewDefaultAuthConfig(),
Metrics: NewDefaultMetricsConfig(),
Sentry: NewDefaultSentryConfig(),
}
}

View File

@ -53,6 +53,29 @@ func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error {
return nil
}
type InterpolatedFloat float64
func (ifl *InterpolatedFloat) UnmarshalYAML(value *yaml.Node) error {
var str string
if err := value.Decode(&str); err != nil {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
}
if match := reVar.FindStringSubmatch(str); len(match) > 0 {
str = os.Getenv(match[1])
}
floatVal, err := strconv.ParseFloat(str, 10)
if err != nil {
return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line)
}
*ifl = InterpolatedFloat(floatVal)
return nil
}
type InterpolatedBool bool
func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {

View File

@ -7,6 +7,7 @@ type ProxyServerConfig struct {
Metrics MetricsConfig `yaml:"metrics"`
Transport TransportConfig `yaml:"transport"`
Dial DialConfig `yaml:"dial"`
Sentry SentryConfig `yaml:"sentry"`
}
// See https://pkg.go.dev/net/http#Transport
@ -32,6 +33,7 @@ func NewDefaultProxyServerConfig() ProxyServerConfig {
Metrics: NewDefaultMetricsConfig(),
Transport: NewDefaultTransportConfig(),
Dial: NewDefaultDialConfig(),
Sentry: NewDefaultSentryConfig(),
}
}

43
internal/config/sentry.go Normal file
View File

@ -0,0 +1,43 @@
package config
import "time"
// Sentry configuration
// See https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
type SentryConfig struct {
DSN InterpolatedString `yaml:"dsn"`
Debug InterpolatedBool `yaml:"debug"`
FlushTimeout *InterpolatedDuration `yaml:"flushTimeout"`
AttachStacktrace InterpolatedBool `yaml:"attachStacktrace"`
SampleRate InterpolatedFloat `yaml:"sampleRate"`
EnableTracing InterpolatedBool `yaml:"enableTracing"`
TracesSampleRate InterpolatedFloat `yaml:"tracesSampleRate"`
ProfilesSampleRate InterpolatedFloat `yaml:"profilesSampleRate"`
IgnoreErrors InterpolatedStringSlice `yaml:"ignoreErrors"`
SendDefaultPII InterpolatedBool `yaml:"sendDefaultPII"`
ServerName InterpolatedString `yaml:"serverName"`
Environment InterpolatedString `yaml:"environment"`
MaxBreadcrumbs InterpolatedInt `yaml:"maxBreadcrumbs"`
MaxSpans InterpolatedInt `yaml:"maxSpans"`
MaxErrorDepth InterpolatedInt `yaml:"maxErrorDepth"`
}
func NewDefaultSentryConfig() SentryConfig {
return SentryConfig{
DSN: "",
Debug: false,
FlushTimeout: NewInterpolatedDuration(2 * time.Second),
AttachStacktrace: true,
SampleRate: 1,
EnableTracing: true,
TracesSampleRate: 0.2,
ProfilesSampleRate: 1,
IgnoreErrors: []string{},
SendDefaultPII: false,
ServerName: "",
Environment: "",
MaxBreadcrumbs: 0,
MaxSpans: 1000,
MaxErrorDepth: 10,
}
}

43
internal/logger/writer.go Normal file
View File

@ -0,0 +1,43 @@
package logger
import (
"context"
"io"
"gitlab.com/wpetit/goweb/logger"
)
type Writer struct {
ctx context.Context
level logger.Level
}
// Write implements io.Writer.
func (w *Writer) Write(p []byte) (n int, err error) {
w.log(string(p))
return len(p), nil
}
func (w *Writer) log(message string) {
switch w.level {
case logger.LevelDebug:
logger.Debug(w.ctx, message)
case logger.LevelInfo:
logger.Info(w.ctx, message)
case logger.LevelWarn:
logger.Warn(w.ctx, message)
case logger.LevelError:
logger.Error(w.ctx, message)
case logger.LevelCritical:
logger.Critical(w.ctx, message)
default:
logger.Debug(w.ctx, message)
}
}
func NewWriter(ctx context.Context, level logger.Level) *Writer {
return &Writer{ctx, level}
}
var _ io.Writer = &Writer{}

View File

@ -15,6 +15,8 @@ import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
@ -89,6 +91,16 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
if s.serverConfig.Sentry.DSN != "" {
logger.Info(ctx, "enabling sentry http middleware")
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
Repanic: true,
})
router.Use(sentryMiddleware.Handle)
}
if s.serverConfig.Metrics.Enabled {
metrics := s.serverConfig.Metrics
@ -169,7 +181,10 @@ func (s *Server) createReverseProxy(ctx context.Context, target *url.URL) *httpu
}
func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
logger.Error(r.Context(), "proxy error", logger.E(errors.WithStack(err)))
err = errors.WithStack(err)
logger.Error(r.Context(), "proxy error", logger.E(err))
sentry.CaptureException(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

42
internal/setup/sentry.go Normal file
View File

@ -0,0 +1,42 @@
package setup
import (
"context"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config"
loggerWriter "forge.cadoles.com/cadoles/bouncer/internal/logger"
"github.com/getsentry/sentry-go"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func SetupSentry(ctx context.Context, conf config.SentryConfig, release string) (func(), error) {
err := sentry.Init(sentry.ClientOptions{
Dsn: string(conf.DSN),
Debug: bool(conf.Debug),
AttachStacktrace: bool(conf.AttachStacktrace),
SampleRate: float64(conf.SampleRate),
EnableTracing: bool(conf.EnableTracing),
TracesSampleRate: float64(conf.TracesSampleRate),
ProfilesSampleRate: float64(conf.ProfilesSampleRate),
IgnoreErrors: conf.IgnoreErrors,
SendDefaultPII: bool(conf.SendDefaultPII),
ServerName: string(conf.ServerName),
Release: release,
Environment: string(conf.Environment),
MaxBreadcrumbs: int(conf.MaxBreadcrumbs),
MaxSpans: int(conf.MaxSpans),
MaxErrorDepth: int(conf.MaxErrorDepth),
DebugWriter: loggerWriter.NewWriter(ctx, logger.LevelDebug),
})
if err != nil {
return nil, errors.WithStack(err)
}
flush := func() {
sentry.Flush(time.Duration(*conf.FlushTimeout))
}
return flush, nil
}