Introduce usage metrics for all API endpoints

This commit is contained in:
ErickSkrauch 2024-03-13 01:29:26 +01:00
parent 4e9a145f74
commit 680effa47a
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
16 changed files with 345 additions and 178 deletions

15
go.mod
View File

@ -15,17 +15,17 @@ require (
github.com/gorilla/mux v1.8.1
github.com/jellydator/ttlcache/v3 v3.1.1
github.com/mediocregopher/radix/v4 v4.1.4
github.com/mono83/slf v0.0.0-20170919161409-79153e9636db
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.1
github.com/valyala/fastjson v1.6.4
go.opentelemetry.io/contrib/exporters/autoexport v0.49.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.48.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.49.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.49.0
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/metric v1.24.0
go.opentelemetry.io/otel/sdk v1.24.0
go.opentelemetry.io/otel/sdk/metric v1.24.0
go.opentelemetry.io/otel/trace v1.24.0
go.uber.org/multierr v1.11.0
)
@ -49,7 +49,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -81,18 +81,17 @@ require (
go.opentelemetry.io/otel/exporters/prometheus v0.46.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect
google.golang.org/grpc v1.61.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

14
go.sum
View File

@ -49,6 +49,8 @@ github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -80,8 +82,6 @@ github.com/mediocregopher/radix/v4 v4.1.4 h1:Uze6DEbEAvL+VHXUEu/EDBTkUk5CLct5h3n
github.com/mediocregopher/radix/v4 v4.1.4/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mono83/slf v0.0.0-20170919161409-79153e9636db h1:tlz4fTklh5mttoq5M+0yEc5Lap8W/02A2HCXCJn5iz0=
github.com/mono83/slf v0.0.0-20170919161409-79153e9636db/go.mod h1:MfF+zNMZz+5IGY9h8jpFaGLyGoJ2ZPri2FmUVftBoUU=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
@ -136,8 +136,12 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.49.0 h1:SPuRs5SgCd9loXBBY5Hu
go.opentelemetry.io/contrib/exporters/autoexport v0.49.0/go.mod h1:BDsrww+PTgwfvBjsZQMstsE1n5dS3hDCtAfYG1t3wag=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.48.0 h1:7rkdNoXgScpSUIqBch/VOB24fk9g0wl3Tr5WPtshi9o=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.48.0/go.mod h1:U3t9uswWhDzieXHMNWP6zk87J4HNondiibKMdNLpnMk=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.49.0 h1:h+c4WbSjBBc3j+IsxwB2mWvkm2nDh0SyGLa5Y5+V9cw=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.49.0/go.mod h1:FObmJ0epY1FcwMR7aq7sRkrCfwwV3d0GBGFfyV5JUBg=
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0 h1:dJlCKeq+zmO5Og4kgxqPvvJrzuD/mygs1g/NYM9dAsU=
go.opentelemetry.io/contrib/instrumentation/runtime v0.48.0/go.mod h1:p+hpBCpLHpuUrR0lHgnHbUnbCBll1IhrcMIlycC+xYs=
go.opentelemetry.io/contrib/instrumentation/runtime v0.49.0 h1:dg9y+7ArpumB6zwImJv47RHfdgOGQ1EMkzP5vLkEnTU=
go.opentelemetry.io/contrib/instrumentation/runtime v0.49.0/go.mod h1:Ul4MtXqu/hJBM+v7a6dCF0nHwckPMLpIpLeCi4+zfdw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0 h1:f2jriWfOdldanBwS9jNBdeOKAQN7b4ugAMaNu1/1k9g=
@ -172,6 +176,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
@ -180,6 +186,8 @@ golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -195,6 +203,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -108,28 +108,39 @@ func newSkinsystemHandler(
config *viper.Viper,
profilesProvider ProfilesProvider,
texturesSigner SignerService,
) *mux.Router {
) (*mux.Router, error) {
config.SetDefault("textures.extra_param_name", "chrly")
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
return (&Skinsystem{
ProfilesProvider: profilesProvider,
SignerService: texturesSigner,
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
}).Handler()
skinsystem, err := NewSkinsystemApi(
profilesProvider,
texturesSigner,
config.GetString("textures.extra_param_name"),
config.GetString("textures.extra_param_value"),
)
if err != nil {
return nil, err
}
return skinsystem.Handler(), nil
}
func newProfilesApiHandler(profilesManager ProfilesManager) *mux.Router {
return (&ProfilesApi{
ProfilesManager: profilesManager,
}).Handler()
func newProfilesApiHandler(profilesManager ProfilesManager) (*mux.Router, error) {
profilesApi, err := NewProfilesApi(profilesManager)
if err != nil {
return nil, err
}
return profilesApi.Handler(), nil
}
func newSignerApiHandler(signer Signer) *mux.Router {
return (&SignerApi{
Signer: signer,
}).Handler()
func newSignerApiHandler(signer Signer) (*mux.Router, error) {
signerApi, err := NewSignerApi(signer)
if err != nil {
return nil, err
}
return signerApi.Handler(), nil
}
func mount(router *mux.Router, path string, handler http.Handler) {

View File

@ -3,50 +3,15 @@ package di
import (
"github.com/defval/di"
"github.com/getsentry/raven-go"
"github.com/mono83/slf"
"github.com/mono83/slf/rays"
"github.com/mono83/slf/recievers/sentry"
"github.com/mono83/slf/recievers/writer"
"github.com/mono83/slf/wd"
"github.com/spf13/viper"
"ely.by/chrly/internal/version"
)
var loggerDiOptions = di.Options(
di.Provide(newLogger),
di.Provide(newSentry),
)
type loggerParams struct {
di.Inject
SentryRaven *raven.Client `di:"" optional:"true"`
}
func newLogger(params loggerParams) slf.Logger {
dispatcher := &slf.Dispatcher{}
dispatcher.AddReceiver(writer.New(writer.Options{
Marker: false,
TimeFormat: "15:04:05.000",
}))
if params.SentryRaven != nil {
sentryReceiver, _ := sentry.NewReceiverWithCustomRaven(
params.SentryRaven,
&sentry.Config{
MinLevel: "warn",
},
)
dispatcher.AddReceiver(sentryReceiver)
}
logger := wd.Custom("", "", dispatcher)
logger.WithParams(rays.Host)
return logger
}
func newSentry(config *viper.Viper) (*raven.Client, error) {
sentryAddr := config.GetString("sentry.dsn")
if sentryAddr == "" {

View File

@ -3,36 +3,37 @@ package http
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/mono83/slf"
"github.com/mono83/slf/wd"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"ely.by/chrly/internal/security"
)
func StartServer(ctx context.Context, server *http.Server, logger slf.Logger) {
func StartServer(ctx context.Context, server *http.Server) {
srvErr := make(chan error, 1)
go func() {
logger.Info("Starting the server, HTTP on: :addr", wd.StringParam("addr", server.Addr))
slog.Info("Starting the server", slog.String("addr", server.Addr))
srvErr <- server.ListenAndServe()
close(srvErr)
}()
select {
case err := <-srvErr:
logger.Emergency("Error in main(): :err", wd.ErrParam(err))
slog.Error("Error in the server", slog.Any("error", err))
case <-ctx.Done():
logger.Info("Got stop signal, starting graceful shutdown: :ctx")
slog.Info("Got stop signal, starting graceful shutdown")
stopCtx, cancelFunc := context.WithTimeout(context.Background(), 3*time.Second)
defer cancelFunc()
_ = server.Shutdown(stopCtx)
logger.Info("Graceful shutdown succeed, exiting")
slog.Info("Graceful shutdown succeed, exiting")
}
}
@ -88,7 +89,11 @@ func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string)
var internalServerError = []byte("Internal server error")
func apiServerError(resp http.ResponseWriter, err error) {
func apiServerError(resp http.ResponseWriter, req *http.Request, err error) {
span := trace.SpanFromContext(req.Context())
span.SetStatus(codes.Error, "")
span.RecordError(err)
resp.WriteHeader(http.StatusInternalServerError)
resp.Header().Set("Content-Type", "text/plain")
_, _ = resp.Write(internalServerError)

View File

@ -7,8 +7,11 @@ import (
"net/http"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/db"
"ely.by/chrly/internal/otel"
"ely.by/chrly/internal/profiles"
)
@ -17,19 +20,35 @@ type ProfilesManager interface {
RemoveProfileByUuid(ctx context.Context, uuid string) error
}
type ProfilesApi struct {
ProfilesManager
func NewProfilesApi(profilesManager ProfilesManager) (*ProfilesApi, error) {
metrics, err := newProfilesApiMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
return &ProfilesApi{
ProfilesManager: profilesManager,
metrics: metrics,
}, nil
}
func (ctx *ProfilesApi) Handler() *mux.Router {
type ProfilesApi struct {
ProfilesManager
metrics *profilesApiMetrics
}
func (p *ProfilesApi) Handler() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", ctx.postProfileHandler).Methods(http.MethodPost)
router.HandleFunc("/{uuid}", ctx.deleteProfileByUuidHandler).Methods(http.MethodDelete)
router.HandleFunc("/", p.postProfileHandler).Methods(http.MethodPost)
router.HandleFunc("/{uuid}", p.deleteProfileByUuidHandler).Methods(http.MethodDelete)
return router
}
func (ctx *ProfilesApi) postProfileHandler(resp http.ResponseWriter, req *http.Request) {
func (p *ProfilesApi) postProfileHandler(resp http.ResponseWriter, req *http.Request) {
p.metrics.UploadProfileRequest.Add(req.Context(), 1)
err := req.ParseForm()
if err != nil {
apiBadRequest(resp, map[string][]string{
@ -48,7 +67,7 @@ func (ctx *ProfilesApi) postProfileHandler(resp http.ResponseWriter, req *http.R
MojangSignature: req.Form.Get("mojangSignature"),
}
err = ctx.PersistProfile(req.Context(), profile)
err = p.PersistProfile(req.Context(), profile)
if err != nil {
var v *profiles.ValidationError
if errors.As(err, &v) {
@ -56,20 +75,40 @@ func (ctx *ProfilesApi) postProfileHandler(resp http.ResponseWriter, req *http.R
return
}
apiServerError(resp, fmt.Errorf("unable to save profile to db: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to save profile to db: %w", err))
return
}
resp.WriteHeader(http.StatusCreated)
}
func (ctx *ProfilesApi) deleteProfileByUuidHandler(resp http.ResponseWriter, req *http.Request) {
func (p *ProfilesApi) deleteProfileByUuidHandler(resp http.ResponseWriter, req *http.Request) {
p.metrics.DeleteProfileRequest.Add(req.Context(), 1)
uuid := mux.Vars(req)["uuid"]
err := ctx.ProfilesManager.RemoveProfileByUuid(req.Context(), uuid)
err := p.ProfilesManager.RemoveProfileByUuid(req.Context(), uuid)
if err != nil {
apiServerError(resp, fmt.Errorf("unable to delete profile from db: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to delete profile from db: %w", err))
return
}
resp.WriteHeader(http.StatusNoContent)
}
func newProfilesApiMetrics(meter metric.Meter) (*profilesApiMetrics, error) {
m := &profilesApiMetrics{}
var errors, err error
m.UploadProfileRequest, err = meter.Int64Counter("chrly.app.profiles.upload.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.DeleteProfileRequest, err = meter.Int64Counter("chrly.app.profiles.delete.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
return m, errors
}
type profilesApiMetrics struct {
UploadProfileRequest metric.Int64Counter
DeleteProfileRequest metric.Int64Counter
}

View File

@ -40,9 +40,7 @@ type ProfilesTestSuite struct {
func (t *ProfilesTestSuite) SetupSubTest() {
t.ProfilesManager = &ProfilesManagerMock{}
t.App = &ProfilesApi{
ProfilesManager: t.ProfilesManager,
}
t.App, _ = NewProfilesApi(t.ProfilesManager)
}
func (t *ProfilesTestSuite) TearDownSubTest() {

View File

@ -7,6 +7,10 @@ import (
"net/http"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/otel"
)
type Signer interface {
@ -14,8 +18,22 @@ type Signer interface {
GetPublicKey(format string) ([]byte, error)
}
func NewSignerApi(signer Signer) (*SignerApi, error) {
metrics, err := newSignerApiMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
return &SignerApi{
Signer: signer,
metrics: metrics,
}, nil
}
type SignerApi struct {
Signer
metrics *signerApiMetrics
}
func (s *SignerApi) Handler() *mux.Router {
@ -29,7 +47,7 @@ func (s *SignerApi) Handler() *mux.Router {
func (s *SignerApi) signHandler(resp http.ResponseWriter, req *http.Request) {
signature, err := s.Signer.Sign(req.Body)
if err != nil {
apiServerError(resp, fmt.Errorf("unable to sign the value: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to sign the value: %w", err))
return
}
@ -44,7 +62,7 @@ func (s *SignerApi) getPublicKeyHandler(resp http.ResponseWriter, req *http.Requ
format := mux.Vars(req)["format"]
publicKey, err := s.Signer.GetPublicKey(format)
if err != nil {
apiServerError(resp, fmt.Errorf("unable to retrieve public key: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve public key: %w", err))
return
}
@ -58,3 +76,21 @@ func (s *SignerApi) getPublicKeyHandler(resp http.ResponseWriter, req *http.Requ
_, _ = resp.Write(publicKey)
}
func newSignerApiMetrics(meter metric.Meter) (*signerApiMetrics, error) {
m := &signerApiMetrics{}
var errors, err error
m.SignRequest, err = meter.Int64Counter("chrly.app.signer.sign.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.GetPublicKeyRequest, err = meter.Int64Counter("chrly.app.signer.get_public_key.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
return m, errors
}
type signerApiMetrics struct {
SignRequest metric.Int64Counter
GetPublicKeyRequest metric.Int64Counter
}

View File

@ -48,9 +48,7 @@ type SignerApiTestSuite struct {
func (t *SignerApiTestSuite) SetupSubTest() {
t.Signer = &SignerMock{}
t.App = &SignerApi{
Signer: t.Signer,
}
t.App, _ = NewSignerApi(t.Signer)
}
func (t *SignerApiTestSuite) TearDownSubTest() {

View File

@ -11,9 +11,12 @@ import (
"time"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/db"
"ely.by/chrly/internal/mojang"
"ely.by/chrly/internal/otel"
"ely.by/chrly/internal/utils"
)
@ -29,11 +32,32 @@ type SignerService interface {
GetPublicKey(ctx context.Context, format string) (string, error)
}
func NewSkinsystemApi(
profilesProvider ProfilesProvider,
signerService SignerService,
texturesExtraParamName string,
texturesExtraParamValue string,
) (*Skinsystem, error) {
metrics, err := newSkinsystemMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
return &Skinsystem{
ProfilesProvider: profilesProvider,
SignerService: signerService,
TexturesExtraParamName: texturesExtraParamName,
TexturesExtraParamValue: texturesExtraParamValue,
metrics: metrics,
}, nil
}
type Skinsystem struct {
ProfilesProvider
SignerService
TexturesExtraParamName string
TexturesExtraParamValue string
metrics *skinsystemApiMetrics
}
func (s *Skinsystem) Handler() *mux.Router {
@ -46,8 +70,8 @@ func (s *Skinsystem) Handler() *mux.Router {
router.HandleFunc("/textures/signed/{username}", s.signedTexturesHandler).Methods(http.MethodGet)
router.HandleFunc("/profile/{username}", s.profileHandler).Methods(http.MethodGet)
// Legacy
router.HandleFunc("/skins", s.skinGetHandler).Methods(http.MethodGet)
router.HandleFunc("/cloaks", s.capeGetHandler).Methods(http.MethodGet)
router.HandleFunc("/skins", s.legacySkinHandler).Methods(http.MethodGet)
router.HandleFunc("/cloaks", s.legacyCapeHandler).Methods(http.MethodGet)
// Utils
router.HandleFunc("/signature-verification-key.{format:(?:pem|der)}", s.signatureVerificationKeyHandler).Methods(http.MethodGet)
@ -55,99 +79,115 @@ func (s *Skinsystem) Handler() *mux.Router {
}
func (s *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
profile, err := s.ProfilesProvider.FindProfileByUsername(request.Context(), parseUsername(mux.Vars(request)["username"]), true)
s.metrics.SkinRequest.Add(request.Context(), 1)
s.skinHandlerWithUsername(response, request, mux.Vars(request)["username"])
}
func (s *Skinsystem) legacySkinHandler(response http.ResponseWriter, request *http.Request) {
s.metrics.LegacySkinRequest.Add(request.Context(), 1)
username := request.URL.Query().Get("name")
if username == "" {
response.WriteHeader(http.StatusBadRequest)
return
}
s.skinHandlerWithUsername(response, request, username)
}
func (s *Skinsystem) skinHandlerWithUsername(resp http.ResponseWriter, req *http.Request, username string) {
profile, err := s.ProfilesProvider.FindProfileByUsername(req.Context(), parseUsername(username), true)
if err != nil {
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve a profile: %w", err))
return
}
if profile == nil || profile.SkinUrl == "" {
response.WriteHeader(http.StatusNotFound)
resp.WriteHeader(http.StatusNotFound)
}
http.Redirect(response, request, profile.SkinUrl, http.StatusMovedPermanently)
http.Redirect(resp, req, profile.SkinUrl, http.StatusMovedPermanently)
}
func (s *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
func (s *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
s.metrics.CapeRequest.Add(request.Context(), 1)
s.capeHandlerWithUsername(response, request, mux.Vars(request)["username"])
}
func (s *Skinsystem) legacyCapeHandler(response http.ResponseWriter, request *http.Request) {
s.metrics.CapeRequest.Add(request.Context(), 1)
username := request.URL.Query().Get("name")
if username == "" {
response.WriteHeader(http.StatusBadRequest)
return
}
mux.Vars(request)["username"] = username
s.skinHandler(response, request)
s.capeHandlerWithUsername(response, request, username)
}
func (s *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
profile, err := s.ProfilesProvider.FindProfileByUsername(request.Context(), parseUsername(mux.Vars(request)["username"]), true)
func (s *Skinsystem) capeHandlerWithUsername(resp http.ResponseWriter, req *http.Request, username string) {
profile, err := s.ProfilesProvider.FindProfileByUsername(req.Context(), parseUsername(username), true)
if err != nil {
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve a profile: %w", err))
return
}
if profile == nil || profile.CapeUrl == "" {
response.WriteHeader(http.StatusNotFound)
resp.WriteHeader(http.StatusNotFound)
}
http.Redirect(response, request, profile.CapeUrl, http.StatusMovedPermanently)
http.Redirect(resp, req, profile.CapeUrl, http.StatusMovedPermanently)
}
func (s *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
username := request.URL.Query().Get("name")
if username == "" {
response.WriteHeader(http.StatusBadRequest)
return
}
func (s *Skinsystem) texturesHandler(resp http.ResponseWriter, req *http.Request) {
s.metrics.TexturesRequest.Add(req.Context(), 1)
mux.Vars(request)["username"] = username
s.capeHandler(response, request)
}
func (s *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
profile, err := s.ProfilesProvider.FindProfileByUsername(request.Context(), mux.Vars(request)["username"], true)
profile, err := s.ProfilesProvider.FindProfileByUsername(req.Context(), mux.Vars(req)["username"], true)
if err != nil {
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve a profile: %w", err))
return
}
if profile == nil {
response.WriteHeader(http.StatusNotFound)
resp.WriteHeader(http.StatusNotFound)
return
}
if profile.SkinUrl == "" && profile.CapeUrl == "" {
response.WriteHeader(http.StatusNoContent)
resp.WriteHeader(http.StatusNoContent)
return
}
textures := texturesFromProfile(profile)
responseData, _ := json.Marshal(textures)
response.Header().Set("Content-Type", "application/json")
_, _ = response.Write(responseData)
resp.Header().Set("Content-Type", "application/json")
_, _ = resp.Write(responseData)
}
func (s *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
func (s *Skinsystem) signedTexturesHandler(resp http.ResponseWriter, req *http.Request) {
s.metrics.SignedTexturesRequest.Add(req.Context(), 1)
profile, err := s.ProfilesProvider.FindProfileByUsername(
request.Context(),
mux.Vars(request)["username"],
getToBool(request.URL.Query().Get("proxy")),
req.Context(),
mux.Vars(req)["username"],
getToBool(req.URL.Query().Get("proxy")),
)
if err != nil {
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve a profile: %w", err))
return
}
if profile == nil {
response.WriteHeader(http.StatusNotFound)
resp.WriteHeader(http.StatusNotFound)
return
}
if profile.MojangTextures == "" {
response.WriteHeader(http.StatusNoContent)
resp.WriteHeader(http.StatusNoContent)
return
}
@ -168,19 +208,21 @@ func (s *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request
}
responseJson, _ := json.Marshal(profileResponse)
response.Header().Set("Content-Type", "application/json")
_, _ = response.Write(responseJson)
resp.Header().Set("Content-Type", "application/json")
_, _ = resp.Write(responseJson)
}
func (s *Skinsystem) profileHandler(response http.ResponseWriter, request *http.Request) {
profile, err := s.ProfilesProvider.FindProfileByUsername(request.Context(), mux.Vars(request)["username"], true)
func (s *Skinsystem) profileHandler(resp http.ResponseWriter, req *http.Request) {
s.metrics.ProfileRequest.Add(req.Context(), 1)
profile, err := s.ProfilesProvider.FindProfileByUsername(req.Context(), mux.Vars(req)["username"], true)
if err != nil {
apiServerError(response, fmt.Errorf("unable to retrieve a profile: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve a profile: %w", err))
return
}
if profile == nil {
response.WriteHeader(http.StatusNotFound)
resp.WriteHeader(http.StatusNotFound)
return
}
@ -199,10 +241,10 @@ func (s *Skinsystem) profileHandler(response http.ResponseWriter, request *http.
Value: texturesPropEncodedValue,
}
if request.URL.Query().Has("unsigned") && !getToBool(request.URL.Query().Get("unsigned")) {
signature, err := s.SignerService.Sign(request.Context(), texturesProp.Value)
if req.URL.Query().Has("unsigned") && !getToBool(req.URL.Query().Get("unsigned")) {
signature, err := s.SignerService.Sign(req.Context(), texturesProp.Value)
if err != nil {
apiServerError(response, fmt.Errorf("unable to sign textures: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to sign textures: %w", err))
return
}
@ -222,27 +264,29 @@ func (s *Skinsystem) profileHandler(response http.ResponseWriter, request *http.
}
responseJson, _ := json.Marshal(profileResponse)
response.Header().Set("Content-Type", "application/json")
_, _ = response.Write(responseJson)
resp.Header().Set("Content-Type", "application/json")
_, _ = resp.Write(responseJson)
}
func (s *Skinsystem) signatureVerificationKeyHandler(response http.ResponseWriter, request *http.Request) {
format := mux.Vars(request)["format"]
publicKey, err := s.SignerService.GetPublicKey(request.Context(), format)
func (s *Skinsystem) signatureVerificationKeyHandler(resp http.ResponseWriter, req *http.Request) {
s.metrics.SigningKeyRequest.Add(req.Context(), 1)
format := mux.Vars(req)["format"]
publicKey, err := s.SignerService.GetPublicKey(req.Context(), format)
if err != nil {
apiServerError(response, fmt.Errorf("unable to retrieve public key: %w", err))
apiServerError(resp, req, fmt.Errorf("unable to retrieve public key: %w", err))
return
}
if format == "pem" {
response.Header().Set("Content-Type", "application/x-pem-file")
response.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.pem"`)
resp.Header().Set("Content-Type", "application/x-pem-file")
resp.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.pem"`)
} else {
response.Header().Set("Content-Type", "application/octet-stream")
response.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.der"`)
resp.Header().Set("Content-Type", "application/octet-stream")
resp.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.der"`)
}
_, _ = io.WriteString(response, publicKey)
_, _ = io.WriteString(resp, publicKey)
}
func parseUsername(username string) string {
@ -250,7 +294,7 @@ func parseUsername(username string) string {
}
func getToBool(v string) bool {
return v == "true" || v == "1" || v == "yes"
return v == "1" || v == "true" || v == "yes"
}
func texturesFromProfile(profile *db.Profile) *mojang.TexturesResponse {
@ -278,3 +322,45 @@ func texturesFromProfile(profile *db.Profile) *mojang.TexturesResponse {
Cape: cape,
}
}
func newSkinsystemMetrics(meter metric.Meter) (*skinsystemApiMetrics, error) {
m := &skinsystemApiMetrics{}
var errors, err error
m.SkinRequest, err = meter.Int64Counter("chrly.app.skinsystem.skin.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.LegacySkinRequest, err = meter.Int64Counter("chrly.app.skinsystem.legacy_skin.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.CapeRequest, err = meter.Int64Counter("chrly.app.skinsystem.cape.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.LegacyCapeRequest, err = meter.Int64Counter("chrly.app.skinsystem.legacy_cape.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.TexturesRequest, err = meter.Int64Counter("chrly.app.skinsystem.textures.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.SignedTexturesRequest, err = meter.Int64Counter("chrly.app.skinsystem.signed_textures.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.ProfileRequest, err = meter.Int64Counter("chrly.app.skinsystem.profile.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.SigningKeyRequest, err = meter.Int64Counter("chrly.app.skinsystem.signing_key.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
return m, errors
}
type skinsystemApiMetrics struct {
SkinRequest metric.Int64Counter
LegacySkinRequest metric.Int64Counter
CapeRequest metric.Int64Counter
LegacyCapeRequest metric.Int64Counter
TexturesRequest metric.Int64Counter
SignedTexturesRequest metric.Int64Counter
ProfileRequest metric.Int64Counter
SigningKeyRequest metric.Int64Counter
}

View File

@ -66,12 +66,12 @@ func (t *SkinsystemTestSuite) SetupSubTest() {
t.ProfilesProvider = &ProfilesProviderMock{}
t.SignerService = &SignerServiceMock{}
t.App = &Skinsystem{
ProfilesProvider: t.ProfilesProvider,
SignerService: t.SignerService,
TexturesExtraParamName: "texturesParamName",
TexturesExtraParamValue: "texturesParamValue",
}
t.App, _ = NewSkinsystemApi(
t.ProfilesProvider,
t.SignerService,
"texturesParamName",
"texturesParamValue",
)
}
func (t *SkinsystemTestSuite) TearDownSubTest() {

View File

@ -6,10 +6,10 @@ import (
"sync"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/otel"
"ely.by/chrly/internal/utils"
)
@ -36,7 +36,7 @@ func NewBatchUuidsProvider(
) (*BatchUuidsProvider, error) {
queue := utils.NewQueue[*job]()
metrics, err := newBatchUuidsProviderMetrics(otel.GetMeterProvider().Meter(ScopeName), queue)
metrics, err := newBatchUuidsProviderMetrics(otel.GetMeter(), queue)
if err != nil {
return nil, err
}
@ -167,21 +167,21 @@ func newBatchUuidsProviderMetrics(meter metric.Meter, queue *utils.Queue[*job])
var errors, err error
m.Requests, err = meter.Int64Counter(
"uuids.batch.request.sent",
"chrly.mojang.uuids.batch.request.sent",
metric.WithDescription("Number of UUIDs requests sent to Mojang API"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.BatchSize, err = meter.Int64Histogram(
"uuids.batch.request.batch_size",
"chrly.mojang.uuids.batch.request.batch_size",
metric.WithDescription("The number of usernames in the query"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.QueueLength, err = meter.Int64ObservableGauge(
"uuids.batch.queue.length",
"chrly.mojang.uuids.batch.queue.length",
metric.WithDescription("Number of tasks in the queue waiting for execution"),
metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error {
o.Observe(int64(queue.Len()))
@ -191,7 +191,7 @@ func newBatchUuidsProviderMetrics(meter metric.Meter, queue *utils.Queue[*job])
errors = multierr.Append(errors, err)
m.QueueTime, err = meter.Float64Histogram(
"uuids.batch.queue.lag",
"chrly.mojang.uuids.batch.queue.lag",
metric.WithDescription("Lag between placing a job in the queue and starting its processing"),
metric.WithUnit("ms"),
)

View File

@ -7,9 +7,10 @@ import (
"strings"
"github.com/brunomvsouza/singleflight"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/otel"
)
const ScopeName = "ely.by/chrly/internal/mojang"
@ -31,7 +32,7 @@ func NewMojangTexturesProvider(
uuidsProvider UuidsProvider,
texturesProvider TexturesProvider,
) (*MojangTexturesProvider, error) {
meter, err := newProviderMetrics(otel.GetMeterProvider().Meter(ScopeName))
meter, err := newProviderMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
@ -119,42 +120,42 @@ func newProviderMetrics(meter metric.Meter) (*providerMetrics, error) {
var errors, err error
m.UsernameFound, err = meter.Int64Counter(
"provider.username_found",
"mojang.provider.username_found",
metric.WithDescription("Number of queries for which username was found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.UsernameMissed, err = meter.Int64Counter(
"provider.username_missed",
"chrly.mojang.provider.username_missed",
metric.WithDescription("Number of queries for which username was not found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.TextureFound, err = meter.Int64Counter(
"provider.textures_found",
"chrly.mojang.provider.textures_found",
metric.WithDescription("Number of queries for which textures were successfully found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.TextureMissed, err = meter.Int64Counter(
"provider.textures_missed",
"chrly.mojang.provider.textures_missed",
metric.WithDescription("Number of queries for which no textures were found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.Failed, err = meter.Int64Counter(
"provider.failed",
"chrly.mojang.provider.failed",
metric.WithDescription("Number of requests that ended in an error"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.Shared, err = meter.Int64Counter(
"provider.singleflight.shared",
"chrly.mojang.provider.singleflight.shared",
metric.WithDescription("Number of requests that are already being processed in another thread"),
metric.WithUnit("1"),
)

View File

@ -6,15 +6,16 @@ import (
"time"
"github.com/jellydator/ttlcache/v3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/otel"
)
type MojangApiTexturesProviderFunc func(ctx context.Context, uuid string, signed bool) (*ProfileResponse, error)
func NewMojangApiTexturesProvider(endpoint MojangApiTexturesProviderFunc) (*MojangApiTexturesProvider, error) {
metrics, err := newMojangApiTexturesProviderMetrics(otel.GetMeterProvider().Meter(ScopeName))
metrics, err := newMojangApiTexturesProviderMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
@ -46,7 +47,7 @@ type TexturesProviderWithInMemoryCache struct {
}
func NewTexturesProviderWithInMemoryCache(provider TexturesProvider) (*TexturesProviderWithInMemoryCache, error) {
metrics, err := newTexturesProviderWithInMemoryCacheMetrics(otel.GetMeterProvider().Meter(ScopeName))
metrics, err := newTexturesProviderWithInMemoryCacheMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
@ -100,7 +101,7 @@ func newMojangApiTexturesProviderMetrics(meter metric.Meter) (*mojangApiTextures
var errors, err error
m.Requests, err = meter.Int64Counter(
"textures.request.sent",
"chrly.mojang.textures.request.sent",
metric.WithDescription("Number of textures requests sent to Mojang API"),
metric.WithUnit("1"),
)
@ -118,14 +119,14 @@ func newTexturesProviderWithInMemoryCacheMetrics(meter metric.Meter) (*texturesP
var errors, err error
m.Hits, err = meter.Int64Counter(
"textures.cache.hit",
"chrly.mojang.textures.cache.hit",
metric.WithDescription("Number of Mojang textures found in the local cache"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.Misses, err = meter.Int64Counter(
"textures.cache.miss",
"chrly.mojang.textures.cache.miss",
metric.WithDescription("Number of Mojang textures missing from local cache"),
metric.WithUnit("1"),
)

View File

@ -3,9 +3,10 @@ package mojang
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/otel"
)
type MojangUuidsStorage interface {
@ -17,7 +18,7 @@ type MojangUuidsStorage interface {
}
func NewUuidsProviderWithCache(o UuidsProvider, s MojangUuidsStorage) (*UuidsProviderWithCache, error) {
metrics, err := newUuidsProviderWithCacheMetrics(otel.GetMeterProvider().Meter(ScopeName))
metrics, err := newUuidsProviderWithCacheMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
@ -88,14 +89,14 @@ func newUuidsProviderWithCacheMetrics(meter metric.Meter) (*uuidsProviderWithCac
var errors, err error
m.Hits, err = meter.Int64Counter(
"uuids.cache.hit",
"chrly.mojang.uuids.cache.hit",
metric.WithDescription("Number of Mojang UUIDs found in the local cache"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.Misses, err = meter.Int64Counter(
"uuids.cache.miss",
"chrly.mojang.uuids.cache.miss",
metric.WithDescription("Number of Mojang UUIDs missing from local cache"),
metric.WithUnit("1"),
)

17
internal/otel/otel.go Normal file
View File

@ -0,0 +1,17 @@
package otel
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
const Scope = "ely.by/chrly"
func GetMeter(opts ...metric.MeterOption) metric.Meter {
return otel.GetMeterProvider().Meter(Scope, opts...)
}
func GetTracer(opts ...trace.TracerOption) trace.Tracer {
return otel.GetTracerProvider().Tracer(Scope, opts...)
}