diff --git a/go.mod b/go.mod index d82a25b..0704fdf 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 github.com/Masterminds/sprig/v3 v3.2.3 github.com/btcsuite/btcd/btcutil v1.1.3 - github.com/davecgh/go-spew v1.1.1 + github.com/getsentry/sentry-go v0.22.0 github.com/go-chi/chi/v5 v5.0.8 github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/mitchellh/mapstructure v1.4.1 @@ -53,11 +53,12 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/text v0.9.0 // indirect google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -71,12 +72,12 @@ require ( github.com/dlclark/regexp2 v1.9.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-chi/cors v1.2.1 - github.com/go-playground/locales v0.12.1 // indirect - github.com/go-playground/universal-translator v0.16.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/uuid v1.3.0 - github.com/leodido/go-urn v1.1.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect diff --git a/go.sum b/go.sum index 48b868d..c961c56 100644 --- a/go.sum +++ b/go.sum @@ -178,19 +178,24 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM= +github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -314,8 +319,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -382,6 +388,7 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -419,8 +426,9 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -667,6 +675,7 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -695,6 +704,7 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/admin/error.go b/internal/admin/error.go index 223ed5e..82fff75 100644 --- a/internal/admin/error.go +++ b/internal/admin/error.go @@ -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)) +} diff --git a/internal/admin/layer_route.go b/internal/admin/layer_route.go index d8af607..59d72bd 100644 --- a/internal/admin/layer_route.go +++ b/internal/admin/layer_route.go @@ -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 diff --git a/internal/admin/proxy_route.go b/internal/admin/proxy_route.go index 816d802..a40b88c 100644 --- a/internal/admin/proxy_route.go +++ b/internal/admin/proxy_route.go @@ -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 diff --git a/internal/admin/server.go b/internal/admin/server.go index 683145c..84bfc64 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -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, diff --git a/internal/command/main.go b/internal/command/main.go index ffe9f93..9b1592b 100644 --- a/internal/command/main.go +++ b/internal/command/main.go @@ -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 { diff --git a/internal/command/server/admin/run.go b/internal/command/server/admin/run.go index 99b568e..1a326e5 100644 --- a/internal/command/server/admin/run.go +++ b/internal/command/server/admin/run.go @@ -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), diff --git a/internal/command/server/proxy/run.go b/internal/command/server/proxy/run.go index 8c27bf3..aba5089 100644 --- a/internal/command/server/proxy/run.go +++ b/internal/command/server/proxy/run.go @@ -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") diff --git a/internal/config/admin_server.go b/internal/config/admin_server.go index 35b90b4..dbcbda0 100644 --- a/internal/config/admin_server.go +++ b/internal/config/admin_server.go @@ -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(), } } diff --git a/internal/config/environment.go b/internal/config/environment.go index 82470d8..201f9a8 100644 --- a/internal/config/environment.go +++ b/internal/config/environment.go @@ -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 { diff --git a/internal/config/proxy_server.go b/internal/config/proxy_server.go index 8af31ae..f4c509a 100644 --- a/internal/config/proxy_server.go +++ b/internal/config/proxy_server.go @@ -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(), } } diff --git a/internal/config/sentry.go b/internal/config/sentry.go new file mode 100644 index 0000000..5da2cdf --- /dev/null +++ b/internal/config/sentry.go @@ -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, + } +} diff --git a/internal/logger/writer.go b/internal/logger/writer.go new file mode 100644 index 0000000..bfeef91 --- /dev/null +++ b/internal/logger/writer.go @@ -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{} diff --git a/internal/proxy/server.go b/internal/proxy/server.go index a2823ec..eeee775 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -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) } diff --git a/internal/setup/sentry.go b/internal/setup/sentry.go new file mode 100644 index 0000000..0bb742b --- /dev/null +++ b/internal/setup/sentry.go @@ -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 +} diff --git a/misc/packaging/common/config.yml b/misc/packaging/common/config.yml index e040818..426e1f0 100644 --- a/misc/packaging/common/config.yml +++ b/misc/packaging/common/config.yml @@ -45,6 +45,25 @@ admin: # de publication # Mettre à null pour désactiver l'authentification basicAuth: null + + # Configuration de l'intégration Sentry + # Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions + sentry: + dsn: "" + debug: false + flushTimeout: 2s + attachStacktrace: true + sampleRate: 1 + enableTracing: true + tracesSampleRate: 0.2 + profilesSampleRate: 1 + ignoreErrors: [] + sendDefaultPII: false + serverName: "" + environment: "" + maxBreadcrumbs: 0 + maxSpans: 1000 + maxErrorDepth: 10 # Configuration du service "proxy" proxy: @@ -85,6 +104,25 @@ proxy: readBufferSize: 4096 maxResponseHeaderBytes: 0 + # Configuration de l'intégration Sentry + # Voir https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions + sentry: + dsn: "" + debug: false + flushTimeout: 2s + attachStacktrace: true + sampleRate: 1 + enableTracing: true + tracesSampleRate: 0.2 + profilesSampleRate: 1 + ignoreErrors: [] + sendDefaultPII: false + serverName: "" + environment: "" + maxBreadcrumbs: 0 + maxSpans: 1000 + maxErrorDepth: 10 + # Configuration des connexions TCP # Voir https://pkg.go.dev/net#Dialer dial: