Skip to content

feat: add metrics support to mock server #606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading