Skip to content

Commit

Permalink
feat: add metrics support to mock server (#606)
Browse files Browse the repository at this point in the history
* feat: add metrics support to mock server

* add unit tests

* fix the unit testing

---------

Co-authored-by: rick <[email protected]>
  • Loading branch information
LinuxSuRen and LinuxSuRen authored Feb 8, 2025
1 parent 664451e commit accdb0e
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 25 deletions.
6 changes: 4 additions & 2 deletions cmd/convert_test.go
Original file line number Diff line number Diff line change
@@ -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");
you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,7 @@ import (
"io"
"os"
"path"
"strconv"
"testing"
"time"

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

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)

c.SetArgs([]string{"convert", "-p=testdata/simple-suite.yaml", "--converter=jmeter", "--target", tmpFile})
Expand Down
3 changes: 2 additions & 1 deletion cmd/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package cmd

import (
"context"
"fmt"
"io"
"os"
Expand All @@ -37,7 +38,7 @@ func TestExtensionCmd(t *testing.T) {

t.Run("normal", func(t *testing.T) {
d := downloader.NewStoreDownloader()
server := mock.NewInMemoryServer(0)
server := mock.NewInMemoryServer(context.Background(), 0)

err := server.Start(mock.NewLocalFileReader("../pkg/downloader/testdata/registry.yaml"), "/v2")
assert.NoError(t, err)
Expand Down
16 changes: 12 additions & 4 deletions cmd/mock.go
Original file line number Diff line number Diff line change
@@ -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");
you may not use this file except in compliance with the License.
Expand Down 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,26 @@ 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 o.metrics {
server.EnableMetrics()
}
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 {
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
2 changes: 1 addition & 1 deletion cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func TestFrontEndHandlerWithLocation(t *testing.T) {
resp := newFakeResponseWriter()

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"`)
})
}

Expand Down
Binary file modified console/atest-desktop/api-testing.icns
Binary file not shown.
5 changes: 3 additions & 2 deletions pkg/downloader/oci_test.go
Original file line number Diff line number Diff line change
@@ -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");
you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@ limitations under the License.
package downloader

import (
"context"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -58,7 +59,7 @@ func TestDetectAuthURL(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")
assert.NoError(t, err)
defer func() {
Expand Down
30 changes: 25 additions & 5 deletions pkg/mock/in_memory.go
Original file line number Diff line number Diff line change
@@ -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");
you may not use this file except in compliance with the License.
Expand Down 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 All @@ -72,6 +74,7 @@ func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler htt
s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter()
s.prefix = prefix
handler = s.mux
s.metrics.AddMetricsHandler(s.mux)
err = s.Load()
return
}
Expand Down Expand Up @@ -107,22 +110,31 @@ func (s *inMemoryServer) Load() (err error) {
memLogger.Info("start to proxy", "target", proxy.Target)
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, 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)

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

resp, err := http.DefaultClient.Do(targetReq)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
memLogger.Error(err, "failed to do proxy request")
return
}

data, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
memLogger.Error(err, "failed to read response body")
return
}
Expand All @@ -148,10 +160,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 +227,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 +296,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
32 changes: 25 additions & 7 deletions pkg/mock/in_memory_test.go
Original file line number Diff line number Diff line change
@@ -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");
you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@ package mock

import (
"bytes"
"context"
"io"
"net/http"
"strings"
Expand All @@ -27,7 +28,8 @@ import (
)

func TestInMemoryServer(t *testing.T) {
server := NewInMemoryServer(0)
server := NewInMemoryServer(context.Background(), 0)
server.EnableMetrics()

err := server.Start(NewLocalFileReader("testdata/api.yaml"), "/mock")
assert.NoError(t, err)
Expand Down Expand Up @@ -165,28 +167,28 @@ func TestInMemoryServer(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"), "/")
assert.Error(t, err)
})

t.Run("invalid webhook", func(t *testing.T) {
server := NewInMemoryServer(0)
server := NewInMemoryServer(context.Background(), 0)
err := server.Start(NewInMemoryReader(`webhooks:
- timer: aa
name: fake`), "/")
assert.Error(t, err)
})

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:
- timer: 1s`), "/")
assert.Error(t, err)
})

t.Run("invalid webhook payload", func(t *testing.T) {
server := NewInMemoryServer(0)
server := NewInMemoryServer(context.Background(), 0)
err := server.Start(NewInMemoryReader(`webhooks:
- name: invalid
timer: 1ms
Expand All @@ -196,7 +198,7 @@ func TestInMemoryServer(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:
- name: invalid
timer: 1ms
Expand All @@ -205,4 +207,20 @@ func TestInMemoryServer(t *testing.T) {
path: "{{.fake"`), "/")
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)
})
}
Loading

0 comments on commit accdb0e

Please sign in to comment.