Skip to content

Commit accdb0e

Browse files
authored
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 <[email protected]>
1 parent 664451e commit accdb0e

File tree

13 files changed

+189
-25
lines changed

13 files changed

+189
-25
lines changed

cmd/convert_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2023 API Testing Authors.
2+
Copyright 2023-2025 API Testing Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import (
2121
"io"
2222
"os"
2323
"path"
24+
"strconv"
2425
"testing"
2526
"time"
2627

@@ -36,7 +37,8 @@ func TestConvert(t *testing.T) {
3637
c.SetOut(io.Discard)
3738

3839
t.Run("normal", func(t *testing.T) {
39-
tmpFile := path.Join(os.TempDir(), time.Now().String())
40+
now := strconv.Itoa(int(time.Now().Unix()))
41+
tmpFile := path.Join(os.TempDir(), now)
4042
defer os.RemoveAll(tmpFile)
4143

4244
c.SetArgs([]string{"convert", "-p=testdata/simple-suite.yaml", "--converter=jmeter", "--target", tmpFile})

cmd/extension_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package cmd
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"io"
2223
"os"
@@ -37,7 +38,7 @@ func TestExtensionCmd(t *testing.T) {
3738

3839
t.Run("normal", func(t *testing.T) {
3940
d := downloader.NewStoreDownloader()
40-
server := mock.NewInMemoryServer(0)
41+
server := mock.NewInMemoryServer(context.Background(), 0)
4142

4243
err := server.Start(mock.NewLocalFileReader("../pkg/downloader/testdata/registry.yaml"), "/v2")
4344
assert.NoError(t, err)

cmd/mock.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 API Testing Authors.
2+
Copyright 2024-2025 API Testing Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -28,8 +28,9 @@ import (
2828
)
2929

3030
type mockOption struct {
31-
port int
32-
prefix string
31+
port int
32+
prefix string
33+
metrics bool
3334
}
3435

3536
func createMockCmd() (c *cobra.Command) {
@@ -45,19 +46,26 @@ func createMockCmd() (c *cobra.Command) {
4546
flags := c.Flags()
4647
flags.IntVarP(&opt.port, "port", "", 6060, "The mock server port")
4748
flags.StringVarP(&opt.prefix, "prefix", "", "/mock", "The mock server API prefix")
49+
flags.BoolVarP(&opt.metrics, "metrics", "m", true, "Enable request metrics collection")
4850
return
4951
}
5052

5153
func (o *mockOption) runE(c *cobra.Command, args []string) (err error) {
5254
reader := mock.NewLocalFileReader(args[0])
53-
server := mock.NewInMemoryServer(o.port)
55+
server := mock.NewInMemoryServer(c.Context(), o.port)
56+
if o.metrics {
57+
server.EnableMetrics()
58+
}
5459
if err = server.Start(reader, o.prefix); err != nil {
5560
return
5661
}
5762

5863
clean := make(chan os.Signal, 1)
5964
signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
6065
printLocalIPs(c, o.port)
66+
if o.metrics {
67+
c.Printf("Metrics available at http://localhost:%d%s/metrics\n", o.port, o.prefix)
68+
}
6169

6270
select {
6371
case <-c.Context().Done():

cmd/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
275275
mockWriter = mock.NewInMemoryReader("")
276276
}
277277

278-
dynamicMockServer := mock.NewInMemoryServer(0)
278+
dynamicMockServer := mock.NewInMemoryServer(cmd.Context(), 0)
279279
mockServerController := server.NewMockServerController(mockWriter, dynamicMockServer, o.httpPort)
280280

281281
clean := make(chan os.Signal, 1)

cmd/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func TestFrontEndHandlerWithLocation(t *testing.T) {
210210
resp := newFakeResponseWriter()
211211

212212
opt.getAtestBinary(resp, req, map[string]string{})
213-
assert.Equal(t, `failed to read "atest": open : no such file or directory`, resp.GetBody().String())
213+
assert.Contains(t, resp.GetBody().String(), `failed to read "atest"`)
214214
})
215215
}
216216

-1 Bytes
Binary file not shown.

pkg/downloader/oci_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 API Testing Authors.
2+
Copyright 2024-2025 API Testing Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ limitations under the License.
1717
package downloader
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"io"
2223
"os"
@@ -58,7 +59,7 @@ func TestDetectAuthURL(t *testing.T) {
5859
}
5960

6061
func TestDownload(t *testing.T) {
61-
server := mock.NewInMemoryServer(0)
62+
server := mock.NewInMemoryServer(context.Background(), 0)
6263
err := server.Start(mock.NewLocalFileReader("testdata/registry.yaml"), "/v2")
6364
assert.NoError(t, err)
6465
defer func() {

pkg/mock/in_memory.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 API Testing Authors.
2+
Copyright 2024-2025 API Testing Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -53,15 +53,17 @@ type inMemoryServer struct {
5353
ctx context.Context
5454
cancelFunc context.CancelFunc
5555
reader Reader
56+
metrics RequestMetrics
5657
}
5758

58-
func NewInMemoryServer(port int) DynamicServer {
59-
ctx, cancel := context.WithCancel(context.TODO())
59+
func NewInMemoryServer(ctx context.Context, port int) DynamicServer {
60+
ctx, cancel := context.WithCancel(ctx)
6061
return &inMemoryServer{
6162
port: port,
6263
wg: sync.WaitGroup{},
6364
ctx: ctx,
6465
cancelFunc: cancel,
66+
metrics: NewNoopMetrics(),
6567
}
6668
}
6769

@@ -72,6 +74,7 @@ func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler htt
7274
s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter()
7375
s.prefix = prefix
7476
handler = s.mux
77+
s.metrics.AddMetricsHandler(s.mux)
7578
err = s.Load()
7679
return
7780
}
@@ -107,22 +110,31 @@ func (s *inMemoryServer) Load() (err error) {
107110
memLogger.Info("start to proxy", "target", proxy.Target)
108111
s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) {
109112
api := fmt.Sprintf("%s/%s", proxy.Target, strings.TrimPrefix(req.URL.Path, s.prefix))
113+
api, err = render.Render("proxy api", api, s)
114+
if err != nil {
115+
w.WriteHeader(http.StatusInternalServerError)
116+
memLogger.Error(err, "failed to render proxy api")
117+
return
118+
}
110119
memLogger.Info("redirect to", "target", api)
111120

112121
targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body)
113122
if err != nil {
123+
w.WriteHeader(http.StatusInternalServerError)
114124
memLogger.Error(err, "failed to create proxy request")
115125
return
116126
}
117127

118128
resp, err := http.DefaultClient.Do(targetReq)
119129
if err != nil {
130+
w.WriteHeader(http.StatusInternalServerError)
120131
memLogger.Error(err, "failed to do proxy request")
121132
return
122133
}
123134

124135
data, err := io.ReadAll(resp.Body)
125136
if err != nil {
137+
w.WriteHeader(http.StatusInternalServerError)
126138
memLogger.Error(err, "failed to read response body")
127139
return
128140
}
@@ -148,10 +160,15 @@ func (s *inMemoryServer) Start(reader Reader, prefix string) (err error) {
148160
return
149161
}
150162

163+
func (s *inMemoryServer) EnableMetrics() {
164+
s.metrics = NewInMemoryMetrics()
165+
}
166+
151167
func (s *inMemoryServer) startObject(obj Object) {
152168
// create a simple CRUD server
153169
s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) {
154170
fmt.Println("mock server received request", req.URL.Path)
171+
s.metrics.RecordRequest(req.URL.Path)
155172
method := req.Method
156173
w.Header().Set(util.ContentType, util.JSON)
157174

@@ -210,6 +227,7 @@ func (s *inMemoryServer) startObject(obj Object) {
210227

211228
// handle a single object
212229
s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) {
230+
s.metrics.RecordRequest(req.URL.Path)
213231
w.Header().Set(util.ContentType, util.JSON)
214232
objects := s.data[obj.Name]
215233
if objects != nil {
@@ -278,15 +296,17 @@ func (s *inMemoryServer) startItem(item Item) {
278296
headerSlices = append(headerSlices, k, v)
279297
}
280298

281-
adHandler := &advanceHandler{item: &item}
299+
adHandler := &advanceHandler{item: &item, metrics: s.metrics}
282300
s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...)
283301
}
284302

285303
type advanceHandler struct {
286-
item *Item
304+
item *Item
305+
metrics RequestMetrics
287306
}
288307

289308
func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) {
309+
h.metrics.RecordRequest(req.URL.Path)
290310
memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path,
291311
"encoder", h.item.Response.Encoder)
292312

pkg/mock/in_memory_test.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 API Testing Authors.
2+
Copyright 2024-2025 API Testing Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package mock
1717

1818
import (
1919
"bytes"
20+
"context"
2021
"io"
2122
"net/http"
2223
"strings"
@@ -27,7 +28,8 @@ import (
2728
)
2829

2930
func TestInMemoryServer(t *testing.T) {
30-
server := NewInMemoryServer(0)
31+
server := NewInMemoryServer(context.Background(), 0)
32+
server.EnableMetrics()
3133

3234
err := server.Start(NewLocalFileReader("testdata/api.yaml"), "/mock")
3335
assert.NoError(t, err)
@@ -165,28 +167,28 @@ func TestInMemoryServer(t *testing.T) {
165167
})
166168

167169
t.Run("not found config file", func(t *testing.T) {
168-
server := NewInMemoryServer(0)
170+
server := NewInMemoryServer(context.Background(), 0)
169171
err := server.Start(NewLocalFileReader("fake"), "/")
170172
assert.Error(t, err)
171173
})
172174

173175
t.Run("invalid webhook", func(t *testing.T) {
174-
server := NewInMemoryServer(0)
176+
server := NewInMemoryServer(context.Background(), 0)
175177
err := server.Start(NewInMemoryReader(`webhooks:
176178
- timer: aa
177179
name: fake`), "/")
178180
assert.Error(t, err)
179181
})
180182

181183
t.Run("missing name or timer in webhook", func(t *testing.T) {
182-
server := NewInMemoryServer(0)
184+
server := NewInMemoryServer(context.Background(), 0)
183185
err := server.Start(NewInMemoryReader(`webhooks:
184186
- timer: 1s`), "/")
185187
assert.Error(t, err)
186188
})
187189

188190
t.Run("invalid webhook payload", func(t *testing.T) {
189-
server := NewInMemoryServer(0)
191+
server := NewInMemoryServer(context.Background(), 0)
190192
err := server.Start(NewInMemoryReader(`webhooks:
191193
- name: invalid
192194
timer: 1ms
@@ -196,7 +198,7 @@ func TestInMemoryServer(t *testing.T) {
196198
})
197199

198200
t.Run("invalid webhook api template", func(t *testing.T) {
199-
server := NewInMemoryServer(0)
201+
server := NewInMemoryServer(context.Background(), 0)
200202
err := server.Start(NewInMemoryReader(`webhooks:
201203
- name: invalid
202204
timer: 1ms
@@ -205,4 +207,20 @@ func TestInMemoryServer(t *testing.T) {
205207
path: "{{.fake"`), "/")
206208
assert.NoError(t, err)
207209
})
210+
211+
t.Run("proxy", func(t *testing.T) {
212+
resp, err = http.Get(api + "/v1/myProjects")
213+
assert.NoError(t, err)
214+
assert.Equal(t, http.StatusOK, resp.StatusCode)
215+
216+
resp, err = http.Get(api + "/v1/invalid-template")
217+
assert.NoError(t, err)
218+
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
219+
})
220+
221+
t.Run("metrics", func(t *testing.T) {
222+
resp, err = http.Get(api + "/metrics")
223+
assert.NoError(t, err)
224+
assert.Equal(t, http.StatusOK, resp.StatusCode)
225+
})
208226
}

0 commit comments

Comments
 (0)