[#321] Use correct owner id in billing metrics #322
11 changed files with 161 additions and 32 deletions
|
@ -10,6 +10,7 @@ This document outlines major changes between releases.
|
|||
- Fix `NextVersionIDMarker` in `list-object-versions` (#248)
|
||||
- Fix possibility of panic during SIGHUP (#288)
|
||||
- Fix flaky `TestErrorTimeoutChecking` (`make test` sometimes failed) (#290)
|
||||
- Fix user owner ID in billing metrics (#321)
|
||||
|
||||
### Added
|
||||
- Add new `frostfs.buffer_max_size_for_put` config param and sync TZ hash for PUT operations (#197)
|
||||
|
|
|
@ -46,6 +46,8 @@ func Auth(center Center, log *zap.Logger) Func {
|
|||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
reqInfo := GetReqInfo(ctx)
|
||||
reqInfo.User = "anon"
|
||||
box, err := center.Authenticate(r)
|
||||
if err != nil {
|
||||
if err == ErrNoAuthorizationHeader {
|
||||
|
@ -64,6 +66,10 @@ func Auth(center Center, log *zap.Logger) Func {
|
|||
ctx = SetClientTime(ctx, box.ClientTime)
|
||||
}
|
||||
ctx = SetAuthHeaders(ctx, box.AuthHeaders)
|
||||
|
||||
if box.AccessBox.Gate.BearerToken != nil {
|
||||
reqInfo.User = bearer.ResolveIssuer(*box.AccessBox.Gate.BearerToken).String()
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -80,9 +79,8 @@ func stats(f http.HandlerFunc, resolveCID cidResolveFunc, appMetrics *metrics.Ap
|
|||
// simply for the fact that it is not human-readable.
|
||||
durationSecs := time.Since(statsWriter.startTime).Seconds()
|
||||
|
||||
user := resolveUser(r.Context())
|
||||
cnrID := resolveCID(r.Context(), reqInfo)
|
||||
appMetrics.Update(user, reqInfo.BucketName, cnrID, settings.ResolveNamespaceAlias(reqInfo.Namespace),
|
||||
appMetrics.UsersAPIStats().Update(reqInfo.User, reqInfo.BucketName, cnrID, settings.ResolveNamespaceAlias(reqInfo.Namespace),
|
||||
requestTypeFromAPI(reqInfo.API), in.countBytes, out.countBytes)
|
||||
|
||||
code := statsWriter.statusCode
|
||||
|
@ -149,14 +147,6 @@ func resolveCID(log *zap.Logger, resolveBucket BucketResolveFunc) cidResolveFunc
|
|||
}
|
||||
}
|
||||
|
||||
func resolveUser(ctx context.Context) string {
|
||||
user := "anon"
|
||||
if bd, err := GetBoxData(ctx); err == nil && bd.Gate.BearerToken != nil {
|
||||
user = bearer.ResolveIssuer(*bd.Gate.BearerToken).String()
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// WriteHeader -- writes http status code.
|
||||
func (w *responseWrapper) WriteHeader(code int) {
|
||||
w.Do(func() {
|
||||
|
|
|
@ -39,6 +39,7 @@ type (
|
|||
TraceID string // Trace ID
|
||||
URL *url.URL // Request url
|
||||
Namespace string
|
||||
User string // User owner id
|
||||
tags []KeyVal // Any additional info not accommodated by above fields
|
||||
}
|
||||
|
||||
|
|
|
@ -8,16 +8,47 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const FrostfsNamespaceHeader = "X-Frostfs-Namespace"
|
||||
|
||||
type poolStatisticMock struct {
|
||||
}
|
||||
|
||||
func (p *poolStatisticMock) Statistic() pool.Statistic {
|
||||
return pool.Statistic{}
|
||||
}
|
||||
|
||||
type centerMock struct {
|
||||
t *testing.T
|
||||
anon bool
|
||||
}
|
||||
dkirillov marked this conversation as resolved
Outdated
|
||||
|
||||
func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
|
||||
return &middleware.Box{}, nil
|
||||
var token *bearer.Token
|
||||
|
||||
if !c.anon {
|
||||
bt := bearertest.Token()
|
||||
token = &bt
|
||||
key, err := keys.NewPrivateKey()
|
||||
require.NoError(c.t, err)
|
||||
require.NoError(c.t, token.Sign(key.PrivateKey))
|
||||
}
|
||||
|
||||
return &middleware.Box{
|
||||
AccessBox: &accessbox.Box{
|
||||
Gate: &accessbox.GateData{
|
||||
BearerToken: token,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type middlewareSettingsMock struct {
|
||||
|
@ -36,6 +67,17 @@ func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
|
|||
return r.denyByDefault
|
||||
}
|
||||
dkirillov marked this conversation as resolved
Outdated
dkirillov
commented
We can write just We can write just `func (f *frostFSIDMock) ValidatePublicKey(*keys.PublicKey) error {` (the same for `GetUserGroupID` )
|
||||
|
||||
type frostFSIDMock struct {
|
||||
}
|
||||
|
||||
func (f *frostFSIDMock) ValidatePublicKey(*keys.PublicKey) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *frostFSIDMock) GetUserGroupIDs(util.Uint160) ([]string, error) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
type handlerMock struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
@ -40,18 +41,28 @@ func prepareRouter(t *testing.T) *routerMock {
|
|||
middlewareSettings := &middlewareSettingsMock{}
|
||||
policyChecker := inmemory.NewInMemoryLocalOverrides()
|
||||
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
metricsConfig := metrics.AppMetricsConfig{
|
||||
Logger: logger,
|
||||
PoolStatistics: &poolStatisticMock{},
|
||||
Registerer: prometheus.NewRegistry(),
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Throttle: middleware.ThrottleOpts{
|
||||
Limit: 10,
|
||||
BacklogTimeout: 30 * time.Second,
|
||||
},
|
||||
Handler: &handlerMock{t: t},
|
||||
Center: ¢erMock{},
|
||||
Log: zaptest.NewLogger(t),
|
||||
Metrics: &metrics.AppMetrics{},
|
||||
Center: ¢erMock{t: t},
|
||||
Log: logger,
|
||||
Metrics: metrics.NewAppMetrics(metricsConfig),
|
||||
MiddlewareSettings: middlewareSettings,
|
||||
PolicyChecker: policyChecker,
|
||||
Domains: []string{"domain1", "domain2"},
|
||||
FrostfsID: &frostFSIDMock{},
|
||||
}
|
||||
return &routerMock{
|
||||
router: NewRouter(cfg),
|
||||
|
@ -253,6 +264,54 @@ func TestDefaultBehaviorPolicyChecker(t *testing.T) {
|
|||
assertAPIError(t, w, apiErrors.ErrAccessDenied)
|
||||
}
|
||||
|
||||
func TestOwnerIDRetrieving(t *testing.T) {
|
||||
chiRouter := prepareRouter(t)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/test-bucket", nil)
|
||||
|
||||
chiRouter.ServeHTTP(w, r)
|
||||
resp := readResponse(t, w)
|
||||
require.NotEqual(t, "anon", resp.ReqInfo.User)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
r = httptest.NewRequest(http.MethodGet, "/test-bucket", nil)
|
||||
|
||||
chiRouter.cfg.Center.(*centerMock).anon = true
|
||||
|
||||
chiRouter.ServeHTTP(w, r)
|
||||
resp = readResponse(t, w)
|
||||
require.Equal(t, "anon", resp.ReqInfo.User)
|
||||
}
|
||||
|
||||
func TestBillingMetrics(t *testing.T) {
|
||||
dkirillov marked this conversation as resolved
Outdated
dkirillov
commented
Can we rename this to not be confused with test that dumps (prints) all supported metrics (test Can we rename this to not be confused with test that dumps (prints) all supported metrics (test `TestDescribeAll` with tag `dump_metrics`)?
|
||||
chiRouter := prepareRouter(t)
|
||||
|
||||
bktName, objName := "test-bucket", "test-object"
|
||||
target := fmt.Sprintf("/%s/%s", bktName, objName)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodPut, target, nil)
|
||||
|
||||
chiRouter.ServeHTTP(w, r)
|
||||
dump := chiRouter.cfg.Metrics.UsersAPIStats().DumpMetrics()
|
||||
require.Len(t, dump.Requests, 1)
|
||||
require.NotEqual(t, "anon", dump.Requests[0].User)
|
||||
require.Equal(t, metrics.PUTRequest, dump.Requests[0].Operation)
|
||||
require.Equal(t, bktName, dump.Requests[0].Bucket)
|
||||
require.Equal(t, 1, dump.Requests[0].Requests)
|
||||
|
||||
chiRouter.cfg.Center.(*centerMock).anon = true
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
r = httptest.NewRequest(http.MethodPut, target, nil)
|
||||
|
||||
chiRouter.ServeHTTP(w, r)
|
||||
dump = chiRouter.cfg.Metrics.UsersAPIStats().DumpMetrics()
|
||||
require.Len(t, dump.Requests, 1)
|
||||
require.Equal(t, "anon", dump.Requests[0].User)
|
||||
}
|
||||
|
||||
func readResponse(t *testing.T, w *httptest.ResponseRecorder) handlerResult {
|
||||
var res handlerResult
|
||||
|
||||
|
|
|
@ -439,7 +439,13 @@ func (a *App) initControlAPI() {
|
|||
}
|
||||
|
||||
func (a *App) initMetrics() {
|
||||
a.metrics = metrics.NewAppMetrics(a.log, frostfs.NewPoolStatistic(a.pool), a.cfg.GetBool(cfgPrometheusEnabled))
|
||||
cfg := metrics.AppMetricsConfig{
|
||||
Logger: a.log,
|
||||
PoolStatistics: frostfs.NewPoolStatistic(a.pool),
|
||||
Enabled: a.cfg.GetBool(cfgPrometheusEnabled),
|
||||
}
|
||||
|
||||
a.metrics = metrics.NewAppMetrics(cfg)
|
||||
a.metrics.State().SetHealth(metrics.HealthStatusStarting)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
@ -16,14 +17,27 @@ type AppMetrics struct {
|
|||
enabled bool
|
||||
}
|
||||
|
||||
func NewAppMetrics(logger *zap.Logger, poolStatistics StatisticScraper, enabled bool) *AppMetrics {
|
||||
if !enabled {
|
||||
logger.Warn(logs.MetricsAreDisabled)
|
||||
type AppMetricsConfig struct {
|
||||
Logger *zap.Logger
|
||||
PoolStatistics StatisticScraper
|
||||
Registerer prometheus.Registerer
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
func NewAppMetrics(cfg AppMetricsConfig) *AppMetrics {
|
||||
if !cfg.Enabled {
|
||||
cfg.Logger.Warn(logs.MetricsAreDisabled)
|
||||
}
|
||||
|
||||
registry := cfg.Registerer
|
||||
if registry == nil {
|
||||
registry = prometheus.DefaultRegisterer
|
||||
}
|
||||
|
||||
return &AppMetrics{
|
||||
logger: logger,
|
||||
gate: NewGateMetrics(poolStatistics),
|
||||
enabled: enabled,
|
||||
logger: cfg.Logger,
|
||||
gate: NewGateMetrics(cfg.PoolStatistics, registry),
|
||||
enabled: cfg.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,12 +79,11 @@ func (m *AppMetrics) Handler() http.Handler {
|
|||
return m.gate.Handler()
|
||||
}
|
||||
|
||||
func (m *AppMetrics) Update(user, bucket, cnrID, ns string, reqType RequestType, in, out uint64) {
|
||||
func (m *AppMetrics) UsersAPIStats() *UsersAPIStats {
|
||||
if !m.isEnabled() {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
m.gate.Billing.apiStat.Update(user, bucket, cnrID, ns, reqType, in, out)
|
||||
return m.gate.Billing.apiStat
|
||||
}
|
||||
|
||||
func (m *AppMetrics) Statistic() *APIStatMetrics {
|
||||
|
|
|
@ -111,6 +111,10 @@ type (
|
|||
)
|
||||
|
||||
func (u *UsersAPIStats) Update(user, bucket, cnrID, ns string, reqType RequestType, in, out uint64) {
|
||||
if u == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
|
||||
|
@ -140,6 +144,10 @@ func (u *UsersAPIStats) Update(user, bucket, cnrID, ns string, reqType RequestTy
|
|||
}
|
||||
|
||||
func (u *UsersAPIStats) DumpMetrics() UserMetrics {
|
||||
if u == nil {
|
||||
return UserMetrics{}
|
||||
}
|
||||
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
|
||||
|
@ -195,7 +203,7 @@ type billingMetrics struct {
|
|||
|
||||
userRequestsDesc *prometheus.Desc
|
||||
userTrafficDesc *prometheus.Desc
|
||||
apiStat UsersAPIStats
|
||||
apiStat *UsersAPIStats
|
||||
}
|
||||
|
||||
func newBillingMetrics() *billingMetrics {
|
||||
|
@ -203,7 +211,7 @@ func newBillingMetrics() *billingMetrics {
|
|||
registry: prometheus.NewRegistry(),
|
||||
userRequestsDesc: newDesc(appMetricsDesc[billingSubsystem][userRequestsMetric]),
|
||||
userTrafficDesc: newDesc(appMetricsDesc[billingSubsystem][userTrafficMetric]),
|
||||
apiStat: UsersAPIStats{},
|
||||
apiStat: &UsersAPIStats{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,12 @@ var metricsPath = flag.String("out", "", "File to export s3 gateway metrics to."
|
|||
|
||||
func TestDescribeAll(t *testing.T) {
|
||||
// to check correct metrics type mapping
|
||||
_ = NewAppMetrics(zaptest.NewLogger(t), mock{}, true)
|
||||
cfg := AppMetricsConfig{
|
||||
Logger: zaptest.NewLogger(t),
|
||||
PoolStatistics: mock{},
|
||||
Enabled: true,
|
||||
}
|
||||
_ = NewAppMetrics(cfg)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
|
|
|
@ -24,9 +24,7 @@ type GateMetrics struct {
|
|||
HTTPServer *httpServerMetrics
|
||||
}
|
||||
|
||||
func NewGateMetrics(scraper StatisticScraper) *GateMetrics {
|
||||
registry := prometheus.DefaultRegisterer
|
||||
|
||||
func NewGateMetrics(scraper StatisticScraper, registry prometheus.Registerer) *GateMetrics {
|
||||
stateMetric := newStateMetrics()
|
||||
registry.MustRegister(stateMetric)
|
||||
|
||||
|
|
Loading…
Reference in a new issue
Let's add flag that determine anon/not anon behavior. Also this flag should be available from test to have opportunity toggle behavior with the same router.
in
TestOwnerIDRetrieving
we can toggle behavior usingchiRouter.cfg.Center.(*centerMock).anon = true