From 5611299f599c2ca32491edbb9334e43083ddbff7 Mon Sep 17 00:00:00 2001 From: rick Date: Mon, 27 Jan 2025 11:52:03 +0800 Subject: [PATCH] feat: add metrics support to mock server --- cmd/mock.go | 12 +++- cmd/server.go | 2 +- console/atest-desktop/api-testing.icns | Bin 2985 -> 2984 bytes pkg/mock/in_memory.go | 18 +++-- pkg/mock/metrics.go | 87 +++++++++++++++++++++++++ pkg/mock/server.go | 5 +- pkg/server/remote_server.go | 2 +- 7 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 pkg/mock/metrics.go diff --git a/cmd/mock.go b/cmd/mock.go index aca2b8cd1..589160de1 100644 --- a/cmd/mock.go +++ b/cmd/mock.go @@ -28,8 +28,9 @@ import ( ) type mockOption struct { - port int - prefix string + port int + prefix string + metrics bool } func createMockCmd() (c *cobra.Command) { @@ -45,12 +46,13 @@ func createMockCmd() (c *cobra.Command) { flags := c.Flags() flags.IntVarP(&opt.port, "port", "", 6060, "The mock server port") flags.StringVarP(&opt.prefix, "prefix", "", "/mock", "The mock server API prefix") + flags.BoolVarP(&opt.metrics, "metrics", "m", true, "Enable request metrics collection") return } func (o *mockOption) runE(c *cobra.Command, args []string) (err error) { reader := mock.NewLocalFileReader(args[0]) - server := mock.NewInMemoryServer(o.port) + server := mock.NewInMemoryServer(c.Context(), o.port) if err = server.Start(reader, o.prefix); err != nil { return } @@ -58,6 +60,10 @@ func (o *mockOption) runE(c *cobra.Command, args []string) (err error) { clean := make(chan os.Signal, 1) signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) printLocalIPs(c, o.port) + if o.metrics { + server.EnableMetrics() + c.Printf("Metrics available at http://localhost:%d%s/metrics\n", o.port, o.prefix) + } select { case <-c.Context().Done(): diff --git a/cmd/server.go b/cmd/server.go index 0f4525513..f7afddb33 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -275,7 +275,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) { mockWriter = mock.NewInMemoryReader("") } - dynamicMockServer := mock.NewInMemoryServer(0) + dynamicMockServer := mock.NewInMemoryServer(cmd.Context(), 0) mockServerController := server.NewMockServerController(mockWriter, dynamicMockServer, o.httpPort) clean := make(chan os.Signal, 1) diff --git a/console/atest-desktop/api-testing.icns b/console/atest-desktop/api-testing.icns index fe7ffe63aae3866f622c47be031b4739111b8cea..0be19441cad5fc988be8c036a7d02fe473c14956 100644 GIT binary patch delta 10 RcmZ1}zCwJ0$VSmA+yE6$1Bn0t delta 12 TcmZ1>zEXUG2qW)Ckty5&8Q24s diff --git a/pkg/mock/in_memory.go b/pkg/mock/in_memory.go index 7f24c23e8..81643e6dc 100644 --- a/pkg/mock/in_memory.go +++ b/pkg/mock/in_memory.go @@ -53,15 +53,17 @@ type inMemoryServer struct { ctx context.Context cancelFunc context.CancelFunc reader Reader + metrics RequestMetrics } -func NewInMemoryServer(port int) DynamicServer { - ctx, cancel := context.WithCancel(context.TODO()) +func NewInMemoryServer(ctx context.Context, port int) DynamicServer { + ctx, cancel := context.WithCancel(ctx) return &inMemoryServer{ port: port, wg: sync.WaitGroup{}, ctx: ctx, cancelFunc: cancel, + metrics: NewNoopMetrics(), } } @@ -148,10 +150,15 @@ func (s *inMemoryServer) Start(reader Reader, prefix string) (err error) { return } +func (s *inMemoryServer) EnableMetrics() { + s.metrics = NewInMemoryMetrics() +} + func (s *inMemoryServer) startObject(obj Object) { // create a simple CRUD server s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) { fmt.Println("mock server received request", req.URL.Path) + s.metrics.RecordRequest(req.URL.Path) method := req.Method w.Header().Set(util.ContentType, util.JSON) @@ -210,6 +217,7 @@ func (s *inMemoryServer) startObject(obj Object) { // handle a single object 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) objects := s.data[obj.Name] if objects != nil { @@ -278,15 +286,17 @@ func (s *inMemoryServer) startItem(item Item) { 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...) } type advanceHandler struct { - item *Item + item *Item + metrics RequestMetrics } 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, "encoder", h.item.Response.Encoder) diff --git a/pkg/mock/metrics.go b/pkg/mock/metrics.go new file mode 100644 index 000000000..d29b55331 --- /dev/null +++ b/pkg/mock/metrics.go @@ -0,0 +1,87 @@ +/* +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 ( + "net/http" + "sync" +) + +// 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() map[string]int { + return make(map[string]int) +} + +// AddMetricsHandler implements RequestMetrics but does nothing +func (m *NoopMetrics) AddMetricsHandler(mux *http.ServeMux, prefix string) {} + +// RequestMetrics represents an interface for collecting request metrics +type RequestMetrics interface { + RecordRequest(path string) + GetMetrics() map[string]int + AddMetricsHandler(mux *http.ServeMux, prefix string) +} + +// InMemoryMetrics implements RequestMetrics with in-memory storage +type InMemoryMetrics struct { + requests map[string]int + mu sync.RWMutex +} + +// NewInMemoryMetrics creates a new InMemoryMetrics instance +func NewInMemoryMetrics() *InMemoryMetrics { + return &InMemoryMetrics{ + 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]++ +} + +// GetMetrics returns a copy of the current metrics +func (m *InMemoryMetrics) GetMetrics() map[string]int { + m.mu.RLock() + defer m.mu.RUnlock() + + // Return a copy to avoid map races + result := make(map[string]int) + for k, v := range m.requests { + result[k] = v + } + return result +} + +func (m *InMemoryMetrics) AddMetricsHandler(mux *http.ServeMux, prefix string) { + // Add metrics endpoint + mux.HandleFunc(prefix+"/metrics", func(w http.ResponseWriter, r *http.Request) { + // metrics handling code + }) +} diff --git a/pkg/mock/server.go b/pkg/mock/server.go index 0a2102565..7f098af74 100644 --- a/pkg/mock/server.go +++ b/pkg/mock/server.go @@ -15,7 +15,9 @@ limitations under the License. */ package mock -import "net/http" +import ( + "net/http" +) type Loadable interface { Load() error @@ -26,6 +28,7 @@ type DynamicServer interface { SetupHandler(reader Reader, prefix string) (http.Handler, error) Stop() error GetPort() string + EnableMetrics() Loadable } diff --git a/pkg/server/remote_server.go b/pkg/server/remote_server.go index 68172838a..5fd2ad037 100644 --- a/pkg/server/remote_server.go +++ b/pkg/server/remote_server.go @@ -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) s.loader = server }