Skip to content

Commit

Permalink
feat: add metrics support to mock server
Browse files Browse the repository at this point in the history
  • Loading branch information
LinuxSuRen committed Jan 27, 2025
1 parent 664451e commit 5611299
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 10 deletions.
12 changes: 9 additions & 3 deletions cmd/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import (
)

type mockOption struct {
port int
prefix string
port int
prefix string
metrics bool
}

func createMockCmd() (c *cobra.Command) {
Expand All @@ -45,19 +46,24 @@ 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
}

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():
Expand Down
2 changes: 1 addition & 1 deletion cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Binary file modified console/atest-desktop/api-testing.icns
Binary file not shown.
18 changes: 14 additions & 4 deletions pkg/mock/in_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down
87 changes: 87 additions & 0 deletions pkg/mock/metrics.go
Original file line number Diff line number Diff line change
@@ -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
})
}
5 changes: 4 additions & 1 deletion pkg/mock/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ limitations under the License.
*/
package mock

import "net/http"
import (
"net/http"
)

type Loadable interface {
Load() error
Expand All @@ -26,6 +28,7 @@ type DynamicServer interface {
SetupHandler(reader Reader, prefix string) (http.Handler, error)
Stop() error
GetPort() string
EnableMetrics()
Loadable
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/server/remote_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 5611299

Please sign in to comment.