Add new Metrics API (#26919)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
099f704d4f
Коммит
5590e1604a
@@ -331,6 +331,7 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.InitReports()
|
||||
api.InitLimits()
|
||||
api.InitOutgoingOAuthConnection()
|
||||
api.InitClientPerformanceMetrics()
|
||||
|
||||
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||
|
||||
|
||||
@@ -359,6 +359,25 @@ func SetupWithServerOptions(tb testing.TB, options []app.Option) *TestHelper {
|
||||
return th
|
||||
}
|
||||
|
||||
func SetupEnterpriseWithServerOptions(tb testing.TB, options []app.Option) *TestHelper {
|
||||
if testing.Short() {
|
||||
tb.SkipNow()
|
||||
}
|
||||
|
||||
if mainHelper == nil {
|
||||
tb.SkipNow()
|
||||
}
|
||||
|
||||
dbStore := mainHelper.GetStore()
|
||||
dbStore.DropAllTables()
|
||||
dbStore.MarkSystemRanUnitTests()
|
||||
mainHelper.PreloadMigrations()
|
||||
searchEngine := mainHelper.GetSearchEngine()
|
||||
th := setupTestHelper(dbStore, searchEngine, true, true, nil, options)
|
||||
th.InitLogin()
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) ShutdownApp() {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
|
||||
38
server/channels/api4/metrics.go
Обычный файл
38
server/channels/api4/metrics.go
Обычный файл
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func (api *API) InitClientPerformanceMetrics() {
|
||||
api.BaseRoutes.APIRoot.Handle("/perf", api.APISessionRequired(submitPerformanceReport)).Methods("POST")
|
||||
}
|
||||
|
||||
func submitPerformanceReport(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
// we return early if server does not have any metrics infra available
|
||||
if c.App.Metrics() == nil || !*c.App.Config().MetricsSettings.EnableClientMetrics {
|
||||
return
|
||||
}
|
||||
|
||||
var report model.PerformanceReport
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&report); jsonErr != nil {
|
||||
c.SetInvalidParamWithErr("submitPerformanceReport", jsonErr)
|
||||
return
|
||||
}
|
||||
|
||||
if err := report.IsValid(); err != nil {
|
||||
c.SetInvalidParamWithErr("submitPerformanceReport", err)
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := c.App.RegisterPerformanceReport(c.AppContext, &report); appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
}
|
||||
158
server/channels/api4/metrics_test.go
Обычный файл
158
server/channels/api4/metrics_test.go
Обычный файл
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupMetricsMock() *mocks.MetricsInterface {
|
||||
metricsMock := &mocks.MetricsInterface{}
|
||||
metricsMock.On("IncrementWebsocketEvent", mock.AnythingOfType("model.WebsocketEventType")).Return()
|
||||
metricsMock.On("IncrementWebSocketBroadcastBufferSize", mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
||||
metricsMock.On("DecrementWebSocketBroadcastBufferSize", mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
||||
metricsMock.On("IncrementMemCacheInvalidationCounter", mock.AnythingOfType("string")).Return()
|
||||
metricsMock.On("IncrementMemCacheMissCounter", mock.AnythingOfType("string")).Return()
|
||||
metricsMock.On("IncrementMemCacheHitCounter", mock.AnythingOfType("string")).Return()
|
||||
metricsMock.On("GetLoggerMetricsCollector").Return(nil)
|
||||
metricsMock.On("IncrementMemCacheHitCounterSession").Return()
|
||||
metricsMock.On("IncrementHTTPError").Return()
|
||||
metricsMock.On("IncrementHTTPRequest").Return()
|
||||
metricsMock.On("ObserveAPIEndpointDuration", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
||||
metricsMock.On("Register").Return()
|
||||
|
||||
return metricsMock
|
||||
}
|
||||
func TestSubmitMetrics(t *testing.T) {
|
||||
t.Run("unauthenticated user should not submit metrics", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
_, err := th.Client.Logout(th.Context.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := th.Client.SubmitClientMetrics(th.Context.Context(), nil)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
// if the metrics is not enabled on server, we don't want to return
|
||||
// an error code.
|
||||
t.Run("metrics not enabled", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
resp, err := th.Client.SubmitClientMetrics(th.Context.Context(), nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("metrics enabled but invalid version", func(t *testing.T) {
|
||||
metricsMock := setupMetricsMock()
|
||||
|
||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||
return metricsMock
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
th := SetupEnterpriseWithServerOptions(t, []app.Option{app.StartMetrics})
|
||||
defer th.TearDown()
|
||||
|
||||
// enable metrics and add the license
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.Enable = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.ListenAddress = ":0" })
|
||||
|
||||
resp, err := th.Client.SubmitClientMetrics(th.Context.Context(), &model.PerformanceReport{
|
||||
Version: "0.1",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("metrics enabled and valid", func(t *testing.T) {
|
||||
metricsMock := setupMetricsMock()
|
||||
metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
||||
|
||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||
return metricsMock
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
th := SetupEnterpriseWithServerOptions(t, []app.Option{app.StartMetrics})
|
||||
defer th.TearDown()
|
||||
|
||||
// enable metrics and add the license
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.Enable = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.ListenAddress = ":0" })
|
||||
|
||||
resp, err := th.Client.SubmitClientMetrics(th.Context.Context(), &model.PerformanceReport{
|
||||
Version: "0.1",
|
||||
Start: time.Now().Add(-1 * time.Minute).UnixMilli(),
|
||||
End: time.Now().UnixMilli(),
|
||||
Counters: []*model.MetricSample{
|
||||
{Metric: model.ClientLongTasks, Value: 1},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("metrics enabled but client metrics are disabled", func(t *testing.T) {
|
||||
metricsMock := setupMetricsMock()
|
||||
|
||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||
return metricsMock
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
th := SetupEnterpriseWithServerOptions(t, []app.Option{app.StartMetrics})
|
||||
defer th.TearDown()
|
||||
|
||||
// enable metrics and add the license
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.Enable = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.EnableClientMetrics = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.MetricsSettings.ListenAddress = ":0" })
|
||||
|
||||
resp, err := th.Client.SubmitClientMetrics(th.Context.Context(), &model.PerformanceReport{
|
||||
Version: "0.1",
|
||||
Start: time.Now().Add(-1 * time.Minute).UnixMilli(),
|
||||
End: time.Now().UnixMilli(),
|
||||
Counters: []*model.MetricSample{
|
||||
{Metric: model.ClientLongTasks, Value: 1},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -1004,6 +1004,7 @@ type AppIface interface {
|
||||
RegenOutgoingWebhookToken(hook *model.OutgoingWebhook) (*model.OutgoingWebhook, *model.AppError)
|
||||
RegenerateOAuthAppSecret(app *model.OAuthApp) (*model.OAuthApp, *model.AppError)
|
||||
RegenerateTeamInviteId(teamID string) (*model.Team, *model.AppError)
|
||||
RegisterPerformanceReport(rctx request.CTX, report *model.PerformanceReport) *model.AppError
|
||||
RegisterPluginCommand(pluginID string, command *model.Command) error
|
||||
RegisterPluginForSharedChannels(rctx request.CTX, opts model.RegisterPluginOpts) (remoteID string, err error)
|
||||
ReloadConfig() error
|
||||
|
||||
51
server/channels/app/metrics.go
Обычный файл
51
server/channels/app/metrics.go
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
)
|
||||
|
||||
func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.PerformanceReport) *model.AppError {
|
||||
if a.Metrics() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commonLabels := report.ProcessLabels()
|
||||
|
||||
for _, c := range report.Counters {
|
||||
switch c.Metric {
|
||||
case model.ClientLongTasks:
|
||||
a.Metrics().IncrementClientLongTasks(commonLabels["platform"], commonLabels["agent"], float64(c.Value))
|
||||
default:
|
||||
// we intentionally skip unknown metrics
|
||||
}
|
||||
}
|
||||
|
||||
for _, h := range report.Histograms {
|
||||
switch h.Metric {
|
||||
case model.ClientTimeToFirstByte:
|
||||
a.Metrics().ObserveClientTimeToFirstByte(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientFirstContentfulPaint:
|
||||
a.Metrics().ObserveClientFirstContentfulPaint(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientLargestContentfulPaint:
|
||||
a.Metrics().ObserveClientLargestContentfulPaint(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientInteractionToNextPaint:
|
||||
a.Metrics().ObserveClientInteractionToNextPaint(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientCumulativeLayoutShift:
|
||||
a.Metrics().ObserveClientCumulativeLayoutShift(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientChannelSwitchDuration:
|
||||
a.Metrics().ObserveClientChannelSwitchDuration(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientTeamSwitchDuration:
|
||||
a.Metrics().ObserveClientTeamSwitchDuration(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
case model.ClientRHSLoadDuration:
|
||||
a.Metrics().ObserveClientRHSLoadDuration(commonLabels["platform"], commonLabels["agent"], float64(h.Value))
|
||||
default:
|
||||
// we intentionally skip unknown metrics
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -14095,6 +14095,28 @@ func (a *OpenTracingAppLayer) RegenerateTeamInviteId(teamID string) (*model.Team
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) RegisterPerformanceReport(rctx request.CTX, report *model.PerformanceReport) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterPerformanceReport")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.RegisterPerformanceReport(rctx, report)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) RegisterPluginCommand(pluginID string, command *model.Command) error {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RegisterPluginCommand")
|
||||
|
||||
Ссылка в новой задаче
Block a user