feat: add metrics support to mock server (#606)
* feat: add metrics support to mock server * add unit tests * fix the unit testing --------- Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
This commit is contained in:
parent
664451e6ee
commit
accdb0e4ef
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2023 API Testing Authors.
|
Copyright 2023-2025 API Testing Authors.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -21,6 +21,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -36,7 +37,8 @@ func TestConvert(t *testing.T) {
|
||||||
c.SetOut(io.Discard)
|
c.SetOut(io.Discard)
|
||||||
|
|
||||||
t.Run("normal", func(t *testing.T) {
|
t.Run("normal", func(t *testing.T) {
|
||||||
tmpFile := path.Join(os.TempDir(), time.Now().String())
|
now := strconv.Itoa(int(time.Now().Unix()))
|
||||||
|
tmpFile := path.Join(os.TempDir(), now)
|
||||||
defer os.RemoveAll(tmpFile)
|
defer os.RemoveAll(tmpFile)
|
||||||
|
|
||||||
c.SetArgs([]string{"convert", "-p=testdata/simple-suite.yaml", "--converter=jmeter", "--target", tmpFile})
|
c.SetArgs([]string{"convert", "-p=testdata/simple-suite.yaml", "--converter=jmeter", "--target", tmpFile})
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -37,7 +38,7 @@ func TestExtensionCmd(t *testing.T) {
|
||||||
|
|
||||||
t.Run("normal", func(t *testing.T) {
|
t.Run("normal", func(t *testing.T) {
|
||||||
d := downloader.NewStoreDownloader()
|
d := downloader.NewStoreDownloader()
|
||||||
server := mock.NewInMemoryServer(0)
|
server := mock.NewInMemoryServer(context.Background(), 0)
|
||||||
|
|
||||||
err := server.Start(mock.NewLocalFileReader("../pkg/downloader/testdata/registry.yaml"), "/v2")
|
err := server.Start(mock.NewLocalFileReader("../pkg/downloader/testdata/registry.yaml"), "/v2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
16
cmd/mock.go
16
cmd/mock.go
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2024 API Testing Authors.
|
Copyright 2024-2025 API Testing Authors.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -28,8 +28,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockOption struct {
|
type mockOption struct {
|
||||||
port int
|
port int
|
||||||
prefix string
|
prefix string
|
||||||
|
metrics bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMockCmd() (c *cobra.Command) {
|
func createMockCmd() (c *cobra.Command) {
|
||||||
|
@ -45,12 +46,16 @@ func createMockCmd() (c *cobra.Command) {
|
||||||
flags := c.Flags()
|
flags := c.Flags()
|
||||||
flags.IntVarP(&opt.port, "port", "", 6060, "The mock server port")
|
flags.IntVarP(&opt.port, "port", "", 6060, "The mock server port")
|
||||||
flags.StringVarP(&opt.prefix, "prefix", "", "/mock", "The mock server API prefix")
|
flags.StringVarP(&opt.prefix, "prefix", "", "/mock", "The mock server API prefix")
|
||||||
|
flags.BoolVarP(&opt.metrics, "metrics", "m", true, "Enable request metrics collection")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *mockOption) runE(c *cobra.Command, args []string) (err error) {
|
func (o *mockOption) runE(c *cobra.Command, args []string) (err error) {
|
||||||
reader := mock.NewLocalFileReader(args[0])
|
reader := mock.NewLocalFileReader(args[0])
|
||||||
server := mock.NewInMemoryServer(o.port)
|
server := mock.NewInMemoryServer(c.Context(), o.port)
|
||||||
|
if o.metrics {
|
||||||
|
server.EnableMetrics()
|
||||||
|
}
|
||||||
if err = server.Start(reader, o.prefix); err != nil {
|
if err = server.Start(reader, o.prefix); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -58,6 +63,9 @@ func (o *mockOption) runE(c *cobra.Command, args []string) (err error) {
|
||||||
clean := make(chan os.Signal, 1)
|
clean := make(chan os.Signal, 1)
|
||||||
signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||||
printLocalIPs(c, o.port)
|
printLocalIPs(c, o.port)
|
||||||
|
if o.metrics {
|
||||||
|
c.Printf("Metrics available at http://localhost:%d%s/metrics\n", o.port, o.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-c.Context().Done():
|
case <-c.Context().Done():
|
||||||
|
|
|
@ -275,7 +275,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
|
||||||
mockWriter = mock.NewInMemoryReader("")
|
mockWriter = mock.NewInMemoryReader("")
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicMockServer := mock.NewInMemoryServer(0)
|
dynamicMockServer := mock.NewInMemoryServer(cmd.Context(), 0)
|
||||||
mockServerController := server.NewMockServerController(mockWriter, dynamicMockServer, o.httpPort)
|
mockServerController := server.NewMockServerController(mockWriter, dynamicMockServer, o.httpPort)
|
||||||
|
|
||||||
clean := make(chan os.Signal, 1)
|
clean := make(chan os.Signal, 1)
|
||||||
|
|
|
@ -210,7 +210,7 @@ func TestFrontEndHandlerWithLocation(t *testing.T) {
|
||||||
resp := newFakeResponseWriter()
|
resp := newFakeResponseWriter()
|
||||||
|
|
||||||
opt.getAtestBinary(resp, req, map[string]string{})
|
opt.getAtestBinary(resp, req, map[string]string{})
|
||||||
assert.Equal(t, `failed to read "atest": open : no such file or directory`, resp.GetBody().String())
|
assert.Contains(t, resp.GetBody().String(), `failed to read "atest"`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2024 API Testing Authors.
|
Copyright 2024-2025 API Testing Authors.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package downloader
|
package downloader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -58,7 +59,7 @@ func TestDetectAuthURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownload(t *testing.T) {
|
func TestDownload(t *testing.T) {
|
||||||
server := mock.NewInMemoryServer(0)
|
server := mock.NewInMemoryServer(context.Background(), 0)
|
||||||
err := server.Start(mock.NewLocalFileReader("testdata/registry.yaml"), "/v2")
|
err := server.Start(mock.NewLocalFileReader("testdata/registry.yaml"), "/v2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2024 API Testing Authors.
|
Copyright 2024-2025 API Testing Authors.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -53,15 +53,17 @@ type inMemoryServer struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
reader Reader
|
reader Reader
|
||||||
|
metrics RequestMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInMemoryServer(port int) DynamicServer {
|
func NewInMemoryServer(ctx context.Context, port int) DynamicServer {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
return &inMemoryServer{
|
return &inMemoryServer{
|
||||||
port: port,
|
port: port,
|
||||||
wg: sync.WaitGroup{},
|
wg: sync.WaitGroup{},
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancelFunc: cancel,
|
cancelFunc: cancel,
|
||||||
|
metrics: NewNoopMetrics(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +74,7 @@ func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler htt
|
||||||
s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter()
|
s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter()
|
||||||
s.prefix = prefix
|
s.prefix = prefix
|
||||||
handler = s.mux
|
handler = s.mux
|
||||||
|
s.metrics.AddMetricsHandler(s.mux)
|
||||||
err = s.Load()
|
err = s.Load()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -107,22 +110,31 @@ func (s *inMemoryServer) Load() (err error) {
|
||||||
memLogger.Info("start to proxy", "target", proxy.Target)
|
memLogger.Info("start to proxy", "target", proxy.Target)
|
||||||
s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) {
|
s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) {
|
||||||
api := fmt.Sprintf("%s/%s", proxy.Target, strings.TrimPrefix(req.URL.Path, s.prefix))
|
api := fmt.Sprintf("%s/%s", proxy.Target, strings.TrimPrefix(req.URL.Path, s.prefix))
|
||||||
|
api, err = render.Render("proxy api", api, s)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
memLogger.Error(err, "failed to render proxy api")
|
||||||
|
return
|
||||||
|
}
|
||||||
memLogger.Info("redirect to", "target", api)
|
memLogger.Info("redirect to", "target", api)
|
||||||
|
|
||||||
targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body)
|
targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
memLogger.Error(err, "failed to create proxy request")
|
memLogger.Error(err, "failed to create proxy request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(targetReq)
|
resp, err := http.DefaultClient.Do(targetReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
memLogger.Error(err, "failed to do proxy request")
|
memLogger.Error(err, "failed to do proxy request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
memLogger.Error(err, "failed to read response body")
|
memLogger.Error(err, "failed to read response body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -148,10 +160,15 @@ func (s *inMemoryServer) Start(reader Reader, prefix string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *inMemoryServer) EnableMetrics() {
|
||||||
|
s.metrics = NewInMemoryMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *inMemoryServer) startObject(obj Object) {
|
func (s *inMemoryServer) startObject(obj Object) {
|
||||||
// create a simple CRUD server
|
// create a simple CRUD server
|
||||||
s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) {
|
s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) {
|
||||||
fmt.Println("mock server received request", req.URL.Path)
|
fmt.Println("mock server received request", req.URL.Path)
|
||||||
|
s.metrics.RecordRequest(req.URL.Path)
|
||||||
method := req.Method
|
method := req.Method
|
||||||
w.Header().Set(util.ContentType, util.JSON)
|
w.Header().Set(util.ContentType, util.JSON)
|
||||||
|
|
||||||
|
@ -210,6 +227,7 @@ func (s *inMemoryServer) startObject(obj Object) {
|
||||||
|
|
||||||
// handle a single object
|
// handle a single object
|
||||||
s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) {
|
s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
s.metrics.RecordRequest(req.URL.Path)
|
||||||
w.Header().Set(util.ContentType, util.JSON)
|
w.Header().Set(util.ContentType, util.JSON)
|
||||||
objects := s.data[obj.Name]
|
objects := s.data[obj.Name]
|
||||||
if objects != nil {
|
if objects != nil {
|
||||||
|
@ -278,15 +296,17 @@ func (s *inMemoryServer) startItem(item Item) {
|
||||||
headerSlices = append(headerSlices, k, v)
|
headerSlices = append(headerSlices, k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
adHandler := &advanceHandler{item: &item}
|
adHandler := &advanceHandler{item: &item, metrics: s.metrics}
|
||||||
s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...)
|
s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type advanceHandler struct {
|
type advanceHandler struct {
|
||||||
item *Item
|
item *Item
|
||||||
|
metrics RequestMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) {
|
func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) {
|
||||||
|
h.metrics.RecordRequest(req.URL.Path)
|
||||||
memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path,
|
memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path,
|
||||||
"encoder", h.item.Response.Encoder)
|
"encoder", h.item.Response.Encoder)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2024 API Testing Authors.
|
Copyright 2024-2025 API Testing Authors.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +17,7 @@ package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -27,7 +28,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInMemoryServer(t *testing.T) {
|
func TestInMemoryServer(t *testing.T) {
|
||||||
server := NewInMemoryServer(0)
|
server := NewInMemoryServer(context.Background(), 0)
|
||||||
|
server.EnableMetrics()
|
||||||
|
|
||||||
err := server.Start(NewLocalFileReader("testdata/api.yaml"), "/mock")
|
err := server.Start(NewLocalFileReader("testdata/api.yaml"), "/mock")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -165,13 +167,13 @@ func TestInMemoryServer(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("not found config file", func(t *testing.T) {
|
t.Run("not found config file", func(t *testing.T) {
|
||||||
server := NewInMemoryServer(0)
|
server := NewInMemoryServer(context.Background(), 0)
|
||||||
err := server.Start(NewLocalFileReader("fake"), "/")
|
err := server.Start(NewLocalFileReader("fake"), "/")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid webhook", func(t *testing.T) {
|
t.Run("invalid webhook", func(t *testing.T) {
|
||||||
server := NewInMemoryServer(0)
|
server := NewInMemoryServer(context.Background(), 0)
|
||||||
err := server.Start(NewInMemoryReader(`webhooks:
|
err := server.Start(NewInMemoryReader(`webhooks:
|
||||||
- timer: aa
|
- timer: aa
|
||||||
name: fake`), "/")
|
name: fake`), "/")
|
||||||
|
@ -179,14 +181,14 @@ func TestInMemoryServer(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("missing name or timer in webhook", func(t *testing.T) {
|
t.Run("missing name or timer in webhook", func(t *testing.T) {
|
||||||
server := NewInMemoryServer(0)
|
server := NewInMemoryServer(context.Background(), 0)
|
||||||
err := server.Start(NewInMemoryReader(`webhooks:
|
err := server.Start(NewInMemoryReader(`webhooks:
|
||||||
- timer: 1s`), "/")
|
- timer: 1s`), "/")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid webhook payload", func(t *testing.T) {
|
t.Run("invalid webhook payload", func(t *testing.T) {
|
||||||
server := NewInMemoryServer(0)
|
server := NewInMemoryServer(context.Background(), 0)
|
||||||
err := server.Start(NewInMemoryReader(`webhooks:
|
err := server.Start(NewInMemoryReader(`webhooks:
|
||||||
- name: invalid
|
- name: invalid
|
||||||
timer: 1ms
|
timer: 1ms
|
||||||
|
@ -196,7 +198,7 @@ func TestInMemoryServer(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid webhook api template", func(t *testing.T) {
|
t.Run("invalid webhook api template", func(t *testing.T) {
|
||||||
server := NewInMemoryServer(0)
|
server := NewInMemoryServer(context.Background(), 0)
|
||||||
err := server.Start(NewInMemoryReader(`webhooks:
|
err := server.Start(NewInMemoryReader(`webhooks:
|
||||||
- name: invalid
|
- name: invalid
|
||||||
timer: 1ms
|
timer: 1ms
|
||||||
|
@ -205,4 +207,20 @@ func TestInMemoryServer(t *testing.T) {
|
||||||
path: "{{.fake"`), "/")
|
path: "{{.fake"`), "/")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("proxy", func(t *testing.T) {
|
||||||
|
resp, err = http.Get(api + "/v1/myProjects")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
resp, err = http.Get(api + "/v1/invalid-template")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("metrics", func(t *testing.T) {
|
||||||
|
resp, err = http.Get(api + "/metrics")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
Copyright 2025 API Testing Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ RequestMetrics = &NoopMetrics{}
|
||||||
|
|
||||||
|
// NoopMetrics implements RequestMetrics but does nothing
|
||||||
|
type NoopMetrics struct{}
|
||||||
|
|
||||||
|
// NewNoopMetrics creates a new NoopMetrics instance
|
||||||
|
func NewNoopMetrics() *NoopMetrics {
|
||||||
|
return &NoopMetrics{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordRequest implements RequestMetrics but does nothing
|
||||||
|
func (m *NoopMetrics) RecordRequest(path string) {}
|
||||||
|
|
||||||
|
// GetMetrics implements RequestMetrics but returns empty map
|
||||||
|
func (m *NoopMetrics) GetMetrics() MetricData {
|
||||||
|
return MetricData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMetricsHandler implements RequestMetrics but does nothing
|
||||||
|
func (m *NoopMetrics) AddMetricsHandler(mux MetricsHandler) {}
|
||||||
|
|
||||||
|
type MetricsHandler interface {
|
||||||
|
HandleFunc(path string, f func(http.ResponseWriter, *http.Request)) *mux.Route
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricData struct {
|
||||||
|
FirstRequestTime time.Time
|
||||||
|
LastRequestTime time.Time
|
||||||
|
Requests map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestMetrics represents an interface for collecting request metrics
|
||||||
|
type RequestMetrics interface {
|
||||||
|
RecordRequest(path string)
|
||||||
|
GetMetrics() MetricData
|
||||||
|
AddMetricsHandler(MetricsHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ RequestMetrics = &InMemoryMetrics{}
|
||||||
|
|
||||||
|
// InMemoryMetrics implements RequestMetrics with in-memory storage
|
||||||
|
type InMemoryMetrics struct {
|
||||||
|
MetricData
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInMemoryMetrics creates a new InMemoryMetrics instance
|
||||||
|
func NewInMemoryMetrics() *InMemoryMetrics {
|
||||||
|
return &InMemoryMetrics{
|
||||||
|
MetricData: MetricData{
|
||||||
|
Requests: make(map[string]int),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordRequest records a request for the given path
|
||||||
|
func (m *InMemoryMetrics) RecordRequest(path string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
m.Requests[path]++
|
||||||
|
if m.FirstRequestTime.IsZero() {
|
||||||
|
m.FirstRequestTime = time.Now()
|
||||||
|
}
|
||||||
|
m.LastRequestTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetrics returns a copy of the current metrics
|
||||||
|
func (m *InMemoryMetrics) GetMetrics() MetricData {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.MetricData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InMemoryMetrics) AddMetricsHandler(mux MetricsHandler) {
|
||||||
|
// Add metrics endpoint
|
||||||
|
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
metrics := m.GetMetrics()
|
||||||
|
_ = json.NewEncoder(w).Encode(metrics)
|
||||||
|
})
|
||||||
|
}
|
|
@ -15,7 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
type Loadable interface {
|
type Loadable interface {
|
||||||
Load() error
|
Load() error
|
||||||
|
@ -26,6 +28,7 @@ type DynamicServer interface {
|
||||||
SetupHandler(reader Reader, prefix string) (http.Handler, error)
|
SetupHandler(reader Reader, prefix string) (http.Handler, error)
|
||||||
Stop() error
|
Stop() error
|
||||||
GetPort() string
|
GetPort() string
|
||||||
|
EnableMetrics()
|
||||||
Loadable
|
Loadable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,11 @@ items:
|
||||||
"status": "success"
|
"status": "success"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
proxies:
|
||||||
|
- path: /v1/myProjects
|
||||||
|
target: http://localhost:{{.GetPort}}
|
||||||
|
- path: /v1/invalid-template
|
||||||
|
target: http://localhost:{{.GetPort}
|
||||||
webhooks:
|
webhooks:
|
||||||
- timer: 1ms
|
- timer: 1ms
|
||||||
name: baidu
|
name: baidu
|
||||||
|
|
|
@ -1274,7 +1274,7 @@ func (s *mockServerController) Reload(ctx context.Context, in *MockConfig) (repl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server := mock.NewInMemoryServer(int(in.GetPort()))
|
server := mock.NewInMemoryServer(ctx, int(in.GetPort()))
|
||||||
server.Start(s.mockWriter, in.Prefix)
|
server.Start(s.mockWriter, in.Prefix)
|
||||||
s.loader = server
|
s.loader = server
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue