[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>
Этот коммит содержится в:
Claudio Costa
2023-11-17 14:39:06 -06:00
коммит произвёл GitHub
родитель 926142ca22
Коммит aa3a12f183
10 изменённых файлов: 297 добавлений и 0 удалений

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

@@ -953,3 +953,107 @@ func (s *apiRPCServer) UploadData(args *Z_UploadDataArgs, returns *Z_UploadDataR
returns.A, returns.B = hook.UploadData(args.A, pluginReader)
return nil
}
func init() {
hookNameToId["ServeMetrics"] = ServeMetricsID
}
type Z_ServeMetricsArgs struct {
ResponseWriterStream uint32
Request *http.Request
Context *Context
RequestBodyStream uint32
}
func (g *hooksRPCClient) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) {
if !g.implemented[ServeMetricsID] {
http.NotFound(w, r)
return
}
serveMetricsStreamId := g.muxBroker.NextId()
go func() {
connection, err := g.muxBroker.Accept(serveMetricsStreamId)
if err != nil {
g.log.Error("Plugin failed to ServeMetrics, muxBroker couldn't accept connection", mlog.Uint32("serve_http_stream_id", serveMetricsStreamId), mlog.Err(err))
return
}
defer connection.Close()
rpcServer := rpc.NewServer()
if err := rpcServer.RegisterName("Plugin", &httpResponseWriterRPCServer{w: w, log: g.log}); err != nil {
g.log.Error("Plugin failed to ServeMetrics, couldn't register RPC name", mlog.Err(err))
return
}
rpcServer.ServeConn(connection)
}()
requestBodyStreamId := uint32(0)
if r.Body != nil {
requestBodyStreamId = g.muxBroker.NextId()
go func() {
bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
if err != nil {
g.log.Error("Plugin failed to ServeMetrics, muxBroker couldn't Accept request body connection", mlog.Err(err))
return
}
defer bodyConnection.Close()
serveIOReader(r.Body, bodyConnection)
}()
}
forwardedRequest := &http.Request{
Method: r.Method,
URL: r.URL,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Header: r.Header,
Host: r.Host,
RemoteAddr: r.RemoteAddr,
RequestURI: r.RequestURI,
}
if err := g.client.Call("Plugin.ServeMetrics", Z_ServeMetricsArgs{
Context: c,
ResponseWriterStream: serveMetricsStreamId,
Request: forwardedRequest,
RequestBodyStream: requestBodyStreamId,
}, nil); err != nil {
g.log.Error("Plugin failed to ServeMetrics, RPC call failed", mlog.Err(err))
http.Error(w, "500 internal server error", http.StatusInternalServerError)
}
}
func (s *hooksRPCServer) ServeMetrics(args *Z_ServeMetricsArgs, returns *struct{}) error {
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote response writer stream, error: %v", err.Error())
return err
}
w := connectHTTPResponseWriter(connection)
defer w.Close()
r := args.Request
if args.RequestBodyStream != 0 {
connection, err := s.muxBroker.Dial(args.RequestBodyStream)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote request body stream, error: %v", err.Error())
return err
}
r.Body = connectIOReader(connection)
} else {
r.Body = io.NopCloser(&bytes.Buffer{})
}
defer r.Body.Close()
if hook, ok := s.impl.(interface {
ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request)
}); ok {
hook.ServeMetrics(args.Context, w, r)
} else {
http.NotFound(w, r)
}
return nil
}

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

@@ -54,6 +54,7 @@ const (
UserHasBeenDeactivatedID = 36
MessageHasBeenDeletedID = 37
MessagesWillBeConsumedID = 38
ServeMetricsID = 39
TotalHooksID = iota
)
@@ -322,4 +323,11 @@ type Hooks interface {
//
// Minimum server version: 9.1
UserHasBeenDeactivated(c *Context, user *model.User)
// ServeMetrics allows plugins to expose their own metrics endpoint through
// the server's metrics HTTP listener (e.g. "localhost:8067").
// Requests destined to the /plugins/{id}/metrics path will be routed to the plugin.
//
// Minimum server version: 9.2
ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request)
}

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

@@ -244,3 +244,9 @@ func (hooks *hooksTimerLayer) UserHasBeenDeactivated(c *Context, user *model.Use
hooks.hooksImpl.UserHasBeenDeactivated(c, user)
hooks.recordTime(startTime, "UserHasBeenDeactivated", true)
}
func (hooks *hooksTimerLayer) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) {
startTime := timePkg.Now()
hooks.hooksImpl.ServeMetrics(c, w, r)
hooks.recordTime(startTime, "ServeMetrics", true)
}

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

@@ -37,6 +37,7 @@ var excludedPluginHooks = []string{
"PluginHTTP",
"ServeHTTP",
"UploadData",
"ServeMetrics",
}
var excludedProductHooks = []string{

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

@@ -360,6 +360,11 @@ func (_m *Hooks) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
_m.Called(c, w, r)
}
// ServeMetrics provides a mock function with given fields: c, w, r
func (_m *Hooks) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
_m.Called(c, w, r)
}
// UserHasBeenCreated provides a mock function with given fields: c, user
func (_m *Hooks) UserHasBeenCreated(c *plugin.Context, user *model.User) {
_m.Called(c, user)

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

@@ -9,6 +9,7 @@ package plugin
import (
"errors"
"io"
"net/http"
"reflect"
"github.com/mattermost/mattermost/server/public/model"
@@ -134,6 +135,10 @@ type UserHasBeenDeactivatedIFace interface {
UserHasBeenDeactivated(c *Context, user *model.User)
}
type ServeMetricsIFace interface {
ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request)
}
type HooksAdapter struct {
implemented map[int]struct{}
productHooks any
@@ -417,6 +422,15 @@ func NewAdapter(productHooks any) (*HooksAdapter, error) {
return nil, errors.New("hook has UserHasBeenDeactivated method but does not implement plugin.UserHasBeenDeactivated interface")
}
// Assessing the type of the productHooks if it individually implements ServeMetrics interface.
tt = reflect.TypeOf((*ServeMetricsIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ServeMetricsID] = struct{}{}
} else if _, ok := ft.MethodByName("ServeMetrics"); ok {
return nil, errors.New("hook has ServeMetrics method but does not implement plugin.ServeMetrics interface")
}
return a, nil
}
@@ -689,3 +703,12 @@ func (a *HooksAdapter) UserHasBeenDeactivated(c *Context, user *model.User) {
a.productHooks.(UserHasBeenDeactivatedIFace).UserHasBeenDeactivated(c, user)
}
func (a *HooksAdapter) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) {
if _, ok := a.implemented[ServeMetricsID]; !ok {
panic("product hooks must implement ServeMetrics")
}
a.productHooks.(ServeMetricsIFace).ServeMetrics(c, w, r)
}