Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2024-05-09 20:49:02 +02:00
коммит произвёл GitHub
родитель 099f704d4f
Коммит 5590e1604a
15 изменённых файлов: 714 добавлений и 0 удалений

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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")

Просмотреть файл

@@ -102,4 +102,14 @@ type MetricsInterface interface {
IncrementNotificationErrorCounter(notificationType model.NotificationType, errorReason model.NotificationReason)
IncrementNotificationNotSentCounter(notificationType model.NotificationType, notSentReason model.NotificationReason)
IncrementNotificationUnsupportedCounter(notificationType model.NotificationType, notSentReason model.NotificationReason)
ObserveClientTimeToFirstByte(platform, agent string, elapsed float64)
ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64)
ObserveClientLargestContentfulPaint(platform, agent string, elapsed float64)
ObserveClientInteractionToNextPaint(platform, agent string, elapsed float64)
ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64)
IncrementClientLongTasks(platform, agent string, inc float64)
ObserveClientChannelSwitchDuration(platform, agent string, elapsed float64)
ObserveClientTeamSwitchDuration(platform, agent string, elapsed float64)
ObserveClientRHSLoadDuration(platform, agent string, elapsed float64)
}

Просмотреть файл

@@ -73,6 +73,11 @@ func (_m *MetricsInterface) IncrementChannelIndexCounter() {
_m.Called()
}
// IncrementClientLongTasks provides a mock function with given fields: platform, agent, inc
func (_m *MetricsInterface) IncrementClientLongTasks(platform string, agent string, inc float64) {
_m.Called(platform, agent, inc)
}
// IncrementClusterEventType provides a mock function with given fields: eventType
func (_m *MetricsInterface) IncrementClusterEventType(eventType model.ClusterEvent) {
_m.Called(eventType)
@@ -293,6 +298,46 @@ func (_m *MetricsInterface) ObserveAPIEndpointDuration(endpoint string, method s
_m.Called(endpoint, method, statusCode, originClient, pageLoadContext, elapsed)
}
// ObserveClientChannelSwitchDuration provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientChannelSwitchDuration(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientCumulativeLayoutShift provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientCumulativeLayoutShift(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientFirstContentfulPaint provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientFirstContentfulPaint(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientInteractionToNextPaint provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientInteractionToNextPaint(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientLargestContentfulPaint provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientLargestContentfulPaint(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientRHSLoadDuration provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientRHSLoadDuration(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientTeamSwitchDuration provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientTeamSwitchDuration(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClientTimeToFirstByte provides a mock function with given fields: platform, agent, elapsed
func (_m *MetricsInterface) ObserveClientTimeToFirstByte(platform string, agent string, elapsed float64) {
_m.Called(platform, agent, elapsed)
}
// ObserveClusterRequestDuration provides a mock function with given fields: elapsed
func (_m *MetricsInterface) ObserveClusterRequestDuration(elapsed float64) {
_m.Called(elapsed)

Просмотреть файл

@@ -42,6 +42,7 @@ const (
MetricsSubsystemSystem = "system"
MetricsSubsystemJobs = "jobs"
MetricsSubsystemNotifications = "notifications"
MetricsSubsystemClientsWeb = "webapp"
MetricsCloudInstallationLabel = "installationId"
MetricsCloudDatabaseClusterLabel = "databaseClusterName"
MetricsCloudInstallationGroupLabel = "installationGroupId"
@@ -209,6 +210,16 @@ type MetricsInterfaceImpl struct {
NotificationErrorCounters *prometheus.CounterVec
NotificationNotSentCounters *prometheus.CounterVec
NotificationUnsupportedCounters *prometheus.CounterVec
ClientTimeToFirstByte *prometheus.HistogramVec
ClientFirstContentfulPaint *prometheus.HistogramVec
ClientLargestContentfulPaint *prometheus.HistogramVec
ClientInteractionToNextPaint *prometheus.HistogramVec
ClientCumulativeLayoutShift *prometheus.HistogramVec
ClientLongTasks *prometheus.CounterVec
ClientChannelSwitchDuration *prometheus.HistogramVec
ClientTeamSwitchDuration *prometheus.HistogramVec
ClientRHSLoadDuration *prometheus.HistogramVec
}
func init() {
@@ -1122,6 +1133,105 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
)
m.Registry.MustRegister(m.NotificationUnsupportedCounters)
m.ClientTimeToFirstByte = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "time_to_first_byte",
Help: "Duration from when a browser starts to request a page from a server until when it starts to receive data in response (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientTimeToFirstByte)
m.ClientFirstContentfulPaint = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "first_contentful_paint",
Help: "Duration of how long it takes for any content to be displayed on screen to a user (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientFirstContentfulPaint)
m.ClientLargestContentfulPaint = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "largest_contentful_paint",
Help: "Duration of how long it takes for large content to be displayed on screen to a user (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientLargestContentfulPaint)
m.ClientInteractionToNextPaint = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "interaction_to_next_paint",
Help: "Measure of how long it takes for a user to see the effects of clicking with a mouse, tapping with a touchscreen, or pressing a key on the keyboard (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientInteractionToNextPaint)
m.ClientCumulativeLayoutShift = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "cumulative_layout_shift",
Help: "Measure of how much a page's content shifts unexpectedly",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientCumulativeLayoutShift)
m.ClientLongTasks = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "long_tasks",
Help: "Counter of the number of times that the browser's main UI thread is blocked for more than 50ms by a single task",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientLongTasks)
m.ClientChannelSwitchDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "channel_switch",
Help: "Duration of the time taken from when a user clicks on a channel in the LHS to when posts in that channel become visible (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientChannelSwitchDuration)
m.ClientTeamSwitchDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "team_switch",
Help: "Duration of the time taken from when a user clicks on a team in the LHS to when posts in that team become visible (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientTeamSwitchDuration)
m.ClientRHSLoadDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemClientsWeb,
Name: "rhs_load",
Help: "Duration of the time taken from when a user clicks to open a thread in the RHS until when posts in that thread become visible (milliseconds)",
},
[]string{"platform", "user_agent"},
)
m.Registry.MustRegister(m.ClientRHSLoadDuration)
return m
}
@@ -1580,6 +1690,42 @@ func (mi *MetricsInterfaceImpl) DecrementHTTPWebSockets(originClient string) {
mi.HTTPWebsocketsGauge.With(prometheus.Labels{"origin_client": originClient}).Dec()
}
func (mi *MetricsInterfaceImpl) ObserveClientTimeToFirstByte(platform, agent string, elapsed float64) {
mi.ClientTimeToFirstByte.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64) {
mi.ClientFirstContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) ObserveClientLargestContentfulPaint(platform, agent string, elapsed float64) {
mi.ClientLargestContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) ObserveClientInteractionToNextPaint(platform, agent string, elapsed float64) {
mi.ClientInteractionToNextPaint.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64) {
mi.ClientCumulativeLayoutShift.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) IncrementClientLongTasks(platform, agent string, inc float64) {
mi.ClientLongTasks.With(prometheus.Labels{"platform": platform, "agent": agent}).Add(inc)
}
func (mi *MetricsInterfaceImpl) ObserveClientChannelSwitchDuration(platform, agent string, elapsed float64) {
mi.ClientChannelSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) ObserveClientTeamSwitchDuration(platform, agent string, elapsed float64) {
mi.ClientTeamSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func (mi *MetricsInterfaceImpl) ObserveClientRHSLoadDuration(platform, agent string, elapsed float64) {
mi.ClientRHSLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
}
func extractDBCluster(driver, connectionString string) (string, error) {
host, err := extractHost(driver, connectionString)
if err != nil {

Просмотреть файл

@@ -588,6 +588,10 @@ func (c *Client4) bookmarkRoute(channelId, bookmarkId string) string {
return fmt.Sprintf(c.bookmarksRoute(channelId)+"/%v", bookmarkId)
}
func (c *Client4) perfMetricsRoute() string {
return "/perf"
}
func (c *Client4) DoAPIGet(ctx context.Context, url string, etag string) (*http.Response, error) {
return c.DoAPIRequest(ctx, http.MethodGet, c.APIURL+url, "", etag)
}
@@ -8848,3 +8852,16 @@ func (c *Client4) ListChannelBookmarksForChannel(ctx context.Context, channelId
}
return b, BuildResponse(r), nil
}
func (c *Client4) SubmitClientMetrics(ctx context.Context, report *PerformanceReport) (*Response, error) {
buf, err := json.Marshal(report)
if err != nil {
return nil, NewAppError("SubmitClientMetrics", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
res, err := c.DoAPIPostBytes(ctx, c.perfMetricsRoute(), buf)
if err != nil {
return BuildResponse(res), err
}
return BuildResponse(res), nil
}

114
server/public/model/metrics.go Обычный файл
Просмотреть файл

@@ -0,0 +1,114 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"strings"
"time"
"github.com/blang/semver/v4"
)
type MetricType string
const (
ClientTimeToFirstByte MetricType = "TTFB"
ClientFirstContentfulPaint MetricType = "FCP"
ClientLargestContentfulPaint MetricType = "LCP"
ClientInteractionToNextPaint MetricType = "INP"
ClientCumulativeLayoutShift MetricType = "CLS"
ClientLongTasks MetricType = "long_tasks"
ClientChannelSwitchDuration MetricType = "channel_switch"
ClientTeamSwitchDuration MetricType = "team_switch"
ClientRHSLoadDuration MetricType = "rhs_load"
performanceReportTTLMilliseconds = 300 * 1000 // 300 seconds/5 minutes
)
var (
performanceReportVersion = semver.MustParse("0.1.0")
acceptedPlatforms = sliceToMapKey("linux", "macos", "ios", "android", "windows", "other")
acceptedAgents = sliceToMapKey("desktop", "firefox", "chrome", "safari", "edge", "other")
)
type MetricSample struct {
Metric MetricType `json:"metric"`
Value int64 `json:"value"`
Timestamp int64 `json:"timestamp,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
// PerformanceReport is a set of samples collected from a client
type PerformanceReport struct {
Version string `json:"version"`
ClientID string `json:"client_id"`
Labels map[string]string `json:"labels"`
Start int64 `json:"start"`
End int64 `json:"end"`
Counters []*MetricSample `json:"counters"`
Histograms []*MetricSample `json:"histograms"`
}
func (r *PerformanceReport) IsValid() error {
if r == nil {
return fmt.Errorf("the report is nil")
}
reportVersion, err := semver.ParseTolerant(r.Version)
if err != nil {
return err
}
if reportVersion.Major != performanceReportVersion.Major || reportVersion.Minor > performanceReportVersion.Minor {
return fmt.Errorf("report version is not supported: server version: %s, report version: %s", performanceReportVersion.String(), r.Version)
}
if r.Start >= r.End {
return fmt.Errorf("report timestamps are erroneous")
}
now := time.Now().UnixMilli()
if r.End < now-performanceReportTTLMilliseconds {
return fmt.Errorf("report is outdated: %d", r.End)
}
return nil
}
func (r *PerformanceReport) ProcessLabels() map[string]string {
var platform, agent string
var ok bool
// check if the platform is specified
platform, ok = r.Labels["platform"]
if !ok {
platform = "other"
}
platform = strings.ToLower(platform)
// check if platform is one of the accepted platforms
_, ok = acceptedPlatforms[platform]
if !ok {
platform = "other"
}
// check if the agent is specified
agent, ok = r.Labels["agent"]
if !ok {
agent = "other"
}
agent = strings.ToLower(agent)
// check if agent is one of the accepted agents
_, ok = acceptedAgents[agent]
if !ok {
agent = "other"
}
return map[string]string{
"platform": platform,
"agent": agent,
}
}

78
server/public/model/metrics_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestPerformanceReport_IsValid(t *testing.T) {
outdatedTimestamp := time.Now().Add(-6 * time.Minute).UnixMilli()
tests := []struct {
name string
report *PerformanceReport
expected error
}{
{
name: "ValidReport",
report: &PerformanceReport{
Version: "0.1.0",
Labels: map[string]string{"platform": "linux"},
Start: time.Now().UnixMilli() - 10000,
End: time.Now().UnixMilli(),
},
expected: nil,
},
{
name: "NilReport",
report: nil,
expected: fmt.Errorf("the report is nil"),
},
{
name: "UnsupportedVersion",
report: &PerformanceReport{
Version: "2.0.0",
Labels: map[string]string{"platform": "linux"},
Start: time.Now().UnixMilli() - 10000,
End: time.Now().UnixMilli(),
},
expected: fmt.Errorf("report version is not supported: server version: 0.1.0, report version: 2.0.0"),
},
{
name: "ErroneousTimestamps",
report: &PerformanceReport{
Version: "0.1.0",
Labels: map[string]string{"platform": "linux"},
Start: time.Now().UnixMilli(),
End: time.Now().Add(-1 * time.Hour).UnixMilli(),
},
expected: fmt.Errorf("report timestamps are erroneous"),
},
{
name: "OutdatedReport",
report: &PerformanceReport{
Version: "0.1.0",
Labels: map[string]string{"platform": "linux"},
Start: time.Now().Add(-7 * time.Minute).UnixMilli(),
End: outdatedTimestamp,
},
expected: fmt.Errorf("report is outdated: %d", outdatedTimestamp),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.report.IsValid()
if tt.expected != nil {
require.EqualError(t, err, tt.expected.Error())
return
}
require.NoError(t, err)
})
}
}

Просмотреть файл

@@ -844,3 +844,16 @@ func filterBlocklist(r rune) rune {
func IsCloud() bool {
return os.Getenv("MM_CLOUD_INSTALLATION_ID") != ""
}
func sliceToMapKey(s ...string) map[string]any {
m := make(map[string]any)
for i := range s {
m[s[i]] = struct{}{}
}
if len(s) != len(m) {
panic("duplicate keys")
}
return m
}

Просмотреть файл

@@ -43,6 +43,7 @@ OUTPUT_EXCLUDING_IGNORED=$(echo "$OUTPUT" | grep -Fv \
-e 'Cannot find /api/v4/hosted_customer/subscribe-newsletter method: POST in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/license/review method: POST in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/license/review/status method: GET in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/perf method: POST in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/posts/{post_id}/edit_history method: GET in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/posts/{post_id}/info method: GET in OpenAPI 3 spec.' \
-e 'Cannot find /api/v4/posts/search method: POST in OpenAPI 3 spec.' \