Skip to content
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
22 changes: 22 additions & 0 deletions plugin/purge/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package purge

import "github.com/prometheus/client_golang/prometheus"

var (
_metricPurgeRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "tr",
Subsystem: "tavern",
Name: "purge_requests_total",
Help: "Total number of purge requests",
}, []string{"code"})
)

func init() {
prometheus.MustRegister(_metricPurgeRequestsTotal)

// docs/purge.md references these labels
_metricPurgeRequestsTotal.WithLabelValues("200")
_metricPurgeRequestsTotal.WithLabelValues("403")
_metricPurgeRequestsTotal.WithLabelValues("404")
_metricPurgeRequestsTotal.WithLabelValues("500")
}
31 changes: 29 additions & 2 deletions plugin/purge/purge.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package purge

import (
"context"
"encoding/binary"
"errors"
"fmt"
"net/http"
Expand All @@ -13,6 +14,7 @@ import (
storagev1 "github.com/omalloc/tavern/api/defined/v1/storage"
"github.com/omalloc/tavern/contrib/log"
"github.com/omalloc/tavern/internal/constants"
"github.com/omalloc/tavern/pkg/encoding"
"github.com/omalloc/tavern/plugin"
"github.com/omalloc/tavern/storage"
)
Expand Down Expand Up @@ -48,10 +50,26 @@ func (r *PurgePlugin) Stop(ctx context.Context) error {
}

func (r *PurgePlugin) AddRouter(router *http.ServeMux) {

codec := encoding.GetDefaultCodec()
sharedkv := storage.Current().SharedKV()

router.Handle("/plugin/purge/tasks", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// TODO: query sharedkv purge task list
// query sharedkv purge task list

purgeTaskMap := make(map[string]uint64)

sharedkv.IteratePrefix(req.Context(), []byte("dir/"), func(key, val []byte) error {
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary.LittleEndian.Uint64(val) call will panic if val is less than 8 bytes long. Add a length check to ensure len(val) >= 8 before calling Uint64, or skip entries with invalid value lengths to prevent a potential runtime panic.

Suggested change
sharedkv.IteratePrefix(req.Context(), []byte("dir/"), func(key, val []byte) error {
sharedkv.IteratePrefix(req.Context(), []byte("dir/"), func(key, val []byte) error {
if len(val) < 8 {
// Skip entries with invalid value lengths to avoid panic in Uint64
return nil
}

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +62
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sharedkv variable is captured from storage.Current().SharedKV() at AddRouter initialization time (outside the handler), rather than being retrieved inside the handler function when it's actually needed. If storage.Current() can change or be reinitialized during the application lifecycle, this could lead to stale references. Consider retrieving the SharedKV inside the handler function to ensure you always have the current storage instance, consistent with the pattern used in HandleFunc at line 113.

Copilot uses AI. Check for mistakes.
purgeTaskMap[string(key)[4:]] = binary.LittleEndian.Uint64(val)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string slicing operation string(key)[4:] will panic if the key length is less than 4 bytes. Before slicing, add a length check to ensure len(key) >= 4, or skip keys that are too short to prevent a potential runtime panic.

Suggested change
purgeTaskMap[string(key)[4:]] = binary.LittleEndian.Uint64(val)
if len(key) >= 4 {
purgeTaskMap[string(key)[4:]] = binary.LittleEndian.Uint64(val)
}

Copilot uses AI. Check for mistakes.
return nil
})
Comment on lines +62 to +65
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error returned by IteratePrefix is being ignored. If the iteration fails, the handler will still return a 200 OK status with potentially incomplete data. Consider checking the error and returning an appropriate HTTP error status if the iteration fails.

Suggested change
sharedkv.IteratePrefix(req.Context(), []byte("dir/"), func(key, val []byte) error {
purgeTaskMap[string(key)[4:]] = binary.LittleEndian.Uint64(val)
return nil
})
if err := sharedkv.IteratePrefix(req.Context(), []byte("dir/"), func(key, val []byte) error {
purgeTaskMap[string(key)[4:]] = binary.LittleEndian.Uint64(val)
return nil
}); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

Copilot uses AI. Check for mistakes.

var payload []byte
// marshal to json
payload, err := codec.Marshal(purgeTaskMap)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Length", fmt.Sprintf("%d", len(payload)))
w.Header().Set("Content-Type", "application/json")
Expand All @@ -72,6 +90,7 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
ipPort := strings.Split(req.RemoteAddr, ":")
if _, ok := r.allowAddr[ipPort[0]]; !ok {
w.WriteHeader(http.StatusForbidden)
_metricPurgeRequestsTotal.WithLabelValues("403").Inc()
return
}

Expand All @@ -83,6 +102,7 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
u, err1 := url.Parse(storeUrl)
if err1 != nil {
r.log.Errorf("failed to parse storeUrl %s: %s", storeUrl, err1)
_metricPurgeRequestsTotal.WithLabelValues("500").Inc()
return
}

Expand All @@ -98,6 +118,7 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
if _, err := current.SharedKV().Get(context.Background(),
[]byte(fmt.Sprintf("if/domain/%s", u.Host))); err != nil && errors.Is(err, storagev1.ErrKeyNotFound) {
r.log.Infof("purge dir %s but is not caching in the service", u.Host)
_metricPurgeRequestsTotal.WithLabelValues("404").Inc()
return
}

Expand All @@ -106,11 +127,13 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Content-Length", "0")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
_metricPurgeRequestsTotal.WithLabelValues("404").Inc()
return
}

r.log.Errorf("purge dir %s failed: %v", storeUrl, err)
w.WriteHeader(http.StatusInternalServerError)
_metricPurgeRequestsTotal.WithLabelValues("500").Inc()
return
}

Expand All @@ -119,6 +142,7 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(payload)
_metricPurgeRequestsTotal.WithLabelValues("200").Inc()
return
}

Expand All @@ -129,12 +153,14 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Content-Length", "0")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
_metricPurgeRequestsTotal.WithLabelValues("404").Inc()
return
}

// others error
r.log.Errorf("purge %s failed: %v", storeUrl, err)
w.WriteHeader(http.StatusInternalServerError)
_metricPurgeRequestsTotal.WithLabelValues("500").Inc()
return
}

Expand All @@ -143,6 +169,7 @@ func (r *PurgePlugin) HandleFunc(next http.HandlerFunc) http.HandlerFunc {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(payload)
_metricPurgeRequestsTotal.WithLabelValues("200").Inc()
}
}

Expand Down
Loading