[MM-55268] Implement ServeMetrics plugins hook (#24249)
* Implement ServeMetrics plugins hook * Update error id * Simplify * Revert "Simplify" This reverts commit c9dc5d5eac6c933ff69e158cf3e34d0973bd3bcd. * Add comment and error handler * Wrap error * Update translation file --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
926142ca22
Коммит
aa3a12f183
@@ -9,7 +9,9 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -19,7 +21,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||
)
|
||||
|
||||
@@ -35,6 +39,8 @@ type platformMetrics struct {
|
||||
|
||||
cfgFn func() *model.Config
|
||||
listenAddr string
|
||||
|
||||
getPluginsEnv func() *plugin.Environment
|
||||
}
|
||||
|
||||
// resetMetrics resets the metrics server. Clears the metrics if the metrics are disabled by the config.
|
||||
@@ -56,6 +62,12 @@ func (ps *PlatformService) resetMetrics() error {
|
||||
cfgFn: ps.Config,
|
||||
metricsImpl: ps.metricsIFace,
|
||||
logger: ps.logger,
|
||||
getPluginsEnv: func() *plugin.Environment {
|
||||
if ps.pluginEnv == nil {
|
||||
return nil
|
||||
}
|
||||
return ps.pluginEnv.GetPluginsEnvironment()
|
||||
},
|
||||
}
|
||||
|
||||
if err := ps.metrics.initMetricsRouter(); err != nil {
|
||||
@@ -166,9 +178,56 @@ func (pm *platformMetrics) initMetricsRouter() error {
|
||||
pm.router.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
||||
pm.router.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||
|
||||
// Plugins metrics route
|
||||
pluginsMetricsRoute := pm.router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/metrics").Subrouter()
|
||||
pluginsMetricsRoute.HandleFunc("", pm.servePluginMetricsRequest)
|
||||
pluginsMetricsRoute.HandleFunc("/{anything:.*}", pm.servePluginMetricsRequest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *platformMetrics) servePluginMetricsRequest(w http.ResponseWriter, r *http.Request) {
|
||||
pluginID := mux.Vars(r)["plugin_id"]
|
||||
|
||||
pluginsEnvironment := pm.getPluginsEnv()
|
||||
if pluginsEnvironment == nil {
|
||||
appErr := model.NewAppError("ServePluginMetricsRequest", "app.plugin.disabled.app_error",
|
||||
nil, "Enable plugins to serve plugin metric requests", http.StatusNotImplemented)
|
||||
mlog.Error(appErr.Error())
|
||||
w.WriteHeader(appErr.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(appErr.ToJSON()))
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := pluginsEnvironment.HooksForPlugin(pluginID)
|
||||
if err != nil {
|
||||
mlog.Debug("Access to route for non-existent plugin",
|
||||
mlog.String("missing_plugin_id", pluginID),
|
||||
mlog.String("url", r.URL.String()),
|
||||
mlog.Err(err))
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
subpath, err := utils.GetSubpathFromConfig(pm.cfgFn())
|
||||
if err != nil {
|
||||
appErr := model.NewAppError("ServePluginMetricsRequest", "app.plugin.subpath_parse.app_error",
|
||||
nil, "Failed to parse SiteURL subpath", http.StatusInternalServerError).Wrap(err)
|
||||
mlog.Error(appErr.Error())
|
||||
w.WriteHeader(appErr.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(appErr.ToJSON()))
|
||||
return
|
||||
}
|
||||
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, path.Join(subpath, "plugins", pluginID, "metrics"))
|
||||
|
||||
// Passing an empty plugin context for the time being. To be decided whether we
|
||||
// should support forms of authentication in the future.
|
||||
hooks.ServeMetrics(&plugin.Context{}, w, r)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) HandleMetrics(route string, h http.Handler) {
|
||||
if ps.metrics != nil {
|
||||
ps.metrics.router.Handle(route, h)
|
||||
|
||||
@@ -2305,3 +2305,63 @@ func TestSendPushNotification(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, 6, numMessages)
|
||||
}
|
||||
|
||||
func TestPluginServeMetrics(t *testing.T) {
|
||||
th := Setup(t, StartMetrics)
|
||||
defer th.TearDown()
|
||||
|
||||
var prevEnable *bool
|
||||
var prevAddress *string
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
prevEnable = cfg.MetricsSettings.Enable
|
||||
prevAddress = cfg.MetricsSettings.ListenAddress
|
||||
cfg.MetricsSettings.Enable = model.NewBool(true)
|
||||
cfg.MetricsSettings.ListenAddress = model.NewString(":30067")
|
||||
})
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.MetricsSettings.Enable = prevEnable
|
||||
cfg.MetricsSettings.ListenAddress = prevAddress
|
||||
})
|
||||
|
||||
testFolder, found := fileutils.FindDir("channels/app/plugin_api_tests")
|
||||
require.True(t, found, "Cannot find tests folder")
|
||||
fullPath := path.Join(testFolder, "manual.test_serve_metrics_plugin", "main.go")
|
||||
|
||||
pluginCode, err := os.ReadFile(fullPath)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pluginCode)
|
||||
|
||||
tearDown, ids, errors := SetAppEnvironmentWithPlugins(t, []string{string(pluginCode)}, th.App, th.NewPluginAPI)
|
||||
defer tearDown()
|
||||
require.NoError(t, errors[0])
|
||||
require.Len(t, ids, 1)
|
||||
|
||||
pluginID := ids[0]
|
||||
require.NotEmpty(t, pluginID)
|
||||
|
||||
reqURL := fmt.Sprintf("http://localhost%s/plugins/%s/metrics", *th.App.Config().MetricsSettings.ListenAddress, pluginID)
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "METRICS", string(body))
|
||||
|
||||
reqURL = fmt.Sprintf("http://localhost%s/plugins/%s/metrics/subpath", *th.App.Config().MetricsSettings.ListenAddress, pluginID)
|
||||
req, err = http.NewRequest("GET", reqURL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "METRICS SUBPATH", string(body))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func (p *Plugin) ServeMetrics(_ *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/subpath" {
|
||||
w.Write([]byte("METRICS SUBPATH"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("METRICS"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&Plugin{})
|
||||
}
|
||||
Ссылка в новой задаче
Block a user