Skip to content

Commit 433de68

Browse files
authored
Move remote write API to client_golang/exp (#1711)
* Move remote write API to client_golang/exp Signed-off-by: Saswata Mukherjee <[email protected]> * Don't use api.Client structs, add options for middleware Signed-off-by: Saswata Mukherjee <[email protected]> * Fix reqBuf usage Signed-off-by: Saswata Mukherjee <[email protected]> * Fix url path Signed-off-by: Saswata Mukherjee <[email protected]> * Add separate mod file (and workspace file) Signed-off-by: Saswata Mukherjee <[email protected]> * Hook exp tests fmt; Test handler error case; Configure backoff Signed-off-by: Saswata Mukherjee <[email protected]> --------- Signed-off-by: Saswata Mukherjee <[email protected]>
1 parent e0800f5 commit 433de68

File tree

17 files changed

+286
-108
lines changed

17 files changed

+286
-108
lines changed

Makefile

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
include .bingo/Variables.mk
1515
include Makefile.common
1616

17+
.PHONY: deps
18+
deps:
19+
$(GO) work sync
20+
$(MAKE) common-deps
21+
1722
.PHONY: test
18-
test: deps common-test
23+
test: deps common-test test-exp
1924

2025
.PHONY: test-short
21-
test-short: deps common-test-short
22-
26+
test-short: deps common-test-short test-exp-short
2327
# Overriding Makefile.common check_license target to add
2428
# dagger paths
2529
.PHONY: common-check_license
@@ -55,8 +59,17 @@ fmt: common-format $(GOIMPORTS)
5559
proto: ## Regenerate Go from remote write proto.
5660
proto: $(BUF)
5761
@echo ">> regenerating Prometheus Remote Write proto"
58-
@cd api/prometheus/v1/remote/genproto && $(BUF) generate
59-
@cd api/prometheus/v1/remote && find genproto/ -type f -exec sed -i '' 's/protohelpers "github.com\/planetscale\/vtprotobuf\/protohelpers"/protohelpers "github.com\/prometheus\/client_golang\/internal\/github.com\/planetscale\/vtprotobuf\/protohelpers"/g' {} \;
62+
@cd exp/api/remote/genproto && $(BUF) generate
63+
@cd exp/api/remote && find genproto/ -type f -exec sed -i '' 's/protohelpers "github.com\/planetscale\/vtprotobuf\/protohelpers"/protohelpers "github.com\/prometheus\/client_golang\/exp\/internal\/github.com\/planetscale\/vtprotobuf\/protohelpers"/g' {} \;
6064
# For some reasons buf generates this unused import, kill it manually for now and reformat.
61-
@cd api/prometheus/v1/remote && find genproto/ -type f -exec sed -i '' 's/_ "github.com\/gogo\/protobuf\/gogoproto"//g' {} \;
62-
@cd api/prometheus/v1/remote && go fmt ./genproto/...
65+
@cd exp/api/remote && find genproto/ -type f -exec sed -i '' 's/_ "github.com\/gogo\/protobuf\/gogoproto"//g' {} \;
66+
@cd exp/api/remote && go fmt ./genproto/...
67+
$(MAKE) fmt
68+
69+
.PHONY: test-exp
70+
test-exp:
71+
cd exp && $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs)
72+
73+
.PHONY: test-exp-short
74+
test-exp-short:
75+
cd exp && $(GOTEST) -short $(GOOPTS) $(pkgs)

api/prometheus/v1/remote/genproto/v2/types.pb.go renamed to exp/api/remote/genproto/v2/types.pb.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/prometheus/v1/remote/genproto/v2/types_vtproto.pb.go renamed to exp/api/remote/genproto/v2/types_vtproto.pb.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/prometheus/v1/remote/remote_api.go renamed to exp/api/remote/remote_api.go

Lines changed: 108 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,26 @@ import (
2222
"io"
2323
"log/slog"
2424
"net/http"
25+
"net/url"
26+
"path"
2527
"strconv"
2628
"strings"
2729
"time"
2830

2931
"github.com/klauspost/compress/snappy"
3032
"google.golang.org/protobuf/proto"
3133

32-
"github.com/prometheus/client_golang/api"
33-
"github.com/prometheus/client_golang/internal/github.com/efficientgo/core/backoff"
34+
"github.com/prometheus/client_golang/exp/internal/github.com/efficientgo/core/backoff"
3435
)
3536

3637
// API is a client for Prometheus Remote Protocols.
3738
// NOTE(bwplotka): Only https://prometheus.io/docs/specs/remote_write_spec_2_0/ is currently implemented,
3839
// read protocols to be implemented if there will be a demand.
3940
type API struct {
40-
client api.Client
41-
opts apiOpts
41+
baseURL *url.URL
42+
client *http.Client
43+
44+
opts apiOpts
4245

4346
reqBuf, comprBuf []byte
4447
}
@@ -92,6 +95,14 @@ func WithAPINoRetryOnRateLimit() APIOption {
9295
}
9396
}
9497

98+
// WithAPIBackoff returns APIOption that allows overriding backoff configuration.
99+
func WithAPIBackoff(backoff backoff.Config) APIOption {
100+
return func(o *apiOpts) error {
101+
o.backoff = backoff
102+
return nil
103+
}
104+
}
105+
95106
type nopSlogHandler struct{}
96107

97108
func (n nopSlogHandler) Enabled(context.Context, slog.Level) bool { return false }
@@ -103,7 +114,12 @@ func (n nopSlogHandler) WithGroup(string) slog.Handler { return n }
103114
//
104115
// It is not safe to use the returned API from multiple goroutines, create a
105116
// separate *API for each goroutine.
106-
func NewAPI(c api.Client, opts ...APIOption) (*API, error) {
117+
func NewAPI(client *http.Client, baseURL string, opts ...APIOption) (*API, error) {
118+
parsedURL, err := url.Parse(baseURL)
119+
if err != nil {
120+
return nil, fmt.Errorf("invalid base URL: %w", err)
121+
}
122+
107123
o := *defaultAPIOpts
108124
for _, opt := range opts {
109125
if err := opt(&o); err != nil {
@@ -115,9 +131,16 @@ func NewAPI(c api.Client, opts ...APIOption) (*API, error) {
115131
o.logger = slog.New(nopSlogHandler{})
116132
}
117133

134+
if client == nil {
135+
client = http.DefaultClient
136+
}
137+
138+
parsedURL.Path = path.Join(parsedURL.Path, o.path)
139+
118140
return &API{
119-
client: c,
120-
opts: o,
141+
opts: o,
142+
client: client,
143+
baseURL: parsedURL,
121144
}, nil
122145
}
123146

@@ -157,6 +180,9 @@ type v2Request interface {
157180
// - If neither is supported, it will marshaled using generic google.golang.org/protobuf methods and
158181
// error out on unknown scheme.
159182
func (r *API) Write(ctx context.Context, msg any) (_ WriteResponseStats, err error) {
183+
// Reset the buffer.
184+
r.reqBuf = r.reqBuf[:0]
185+
160186
// Detect content-type.
161187
cType := WriteProtoFullNameV1
162188
if _, ok := msg.(v2Request); ok {
@@ -189,7 +215,6 @@ func (r *API) Write(ctx context.Context, msg any) (_ WriteResponseStats, err err
189215
}
190216
case proto.Message:
191217
// Generic proto.
192-
r.reqBuf = r.reqBuf[:0]
193218
r.reqBuf, err = (proto.MarshalOptions{}).MarshalAppend(r.reqBuf, m)
194219
if err != nil {
195220
return WriteResponseStats{}, fmt.Errorf("encoding request %w", err)
@@ -266,8 +291,7 @@ func compressPayload(tmpbuf *[]byte, enc Compression, inp []byte) (compressed []
266291
}
267292

268293
func (r *API) attemptWrite(ctx context.Context, compr Compression, proto WriteProtoFullName, payload []byte, attempt int) (WriteResponseStats, error) {
269-
u := r.client.URL(r.opts.path, nil)
270-
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(payload))
294+
req, err := http.NewRequest(http.MethodPost, r.baseURL.String(), bytes.NewReader(payload))
271295
if err != nil {
272296
// Errors from NewRequest are from unparsable URLs, so are not
273297
// recoverable.
@@ -287,11 +311,17 @@ func (r *API) attemptWrite(ctx context.Context, compr Compression, proto WritePr
287311
req.Header.Set("Retry-Attempt", strconv.Itoa(attempt))
288312
}
289313

290-
resp, body, err := r.client.Do(ctx, req)
314+
resp, err := r.client.Do(req.WithContext(ctx))
291315
if err != nil {
292316
// Errors from Client.Do are likely network errors, so recoverable.
293317
return WriteResponseStats{}, retryableError{err, 0}
294318
}
319+
defer resp.Body.Close()
320+
321+
body, err := io.ReadAll(resp.Body)
322+
if err != nil {
323+
return WriteResponseStats{}, fmt.Errorf("reading response body: %w", err)
324+
}
295325

296326
rs := WriteResponseStats{}
297327
if proto == WriteProtoFullNameV2 {
@@ -334,19 +364,14 @@ type writeStorage interface {
334364
Store(ctx context.Context, proto WriteProtoFullName, serializedRequest []byte) (_ WriteResponseStats, code int, _ error)
335365
}
336366

337-
// remoteWriteDecompressor is an interface that allows decompressing the body of the request.
338-
type remoteWriteDecompressor interface {
339-
Decompress(ctx context.Context, body io.ReadCloser) (decompressed []byte, _ error)
340-
}
341-
342367
type handler struct {
343368
store writeStorage
344369
opts handlerOpts
345370
}
346371

347372
type handlerOpts struct {
348-
logger *slog.Logger
349-
decompressor remoteWriteDecompressor
373+
logger *slog.Logger
374+
middlewares []func(http.Handler) http.Handler
350375
}
351376

352377
// HandlerOption represents an option for the handler.
@@ -360,22 +385,71 @@ func WithHandlerLogger(logger *slog.Logger) HandlerOption {
360385
}
361386
}
362387

363-
// WithHandlerDecompressor returns HandlerOption that allows providing remoteWriteDecompressor.
364-
// By default, SimpleSnappyDecompressor is used.
365-
func WithHandlerDecompressor(decompressor remoteWriteDecompressor) HandlerOption {
388+
// WithHandlerMiddleware returns HandlerOption that allows providing middlewares.
389+
// Multiple middlewares can be provided and will be applied in the order they are passed.
390+
// When using this option, SnappyDecompressorMiddleware is not applied by default so
391+
// it (or any other decompression middleware) needs to be added explicitly.
392+
func WithHandlerMiddlewares(middlewares ...func(http.Handler) http.Handler) HandlerOption {
366393
return func(o *handlerOpts) {
367-
o.decompressor = decompressor
394+
o.middlewares = middlewares
395+
}
396+
}
397+
398+
// SnappyDecompressorMiddleware returns a middleware that checks if the request body is snappy-encoded and decompresses it.
399+
// If the request body is not snappy-encoded, it returns an error.
400+
// Used by default in NewRemoteWriteHandler.
401+
func SnappyDecompressorMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
402+
return func(next http.Handler) http.Handler {
403+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
404+
enc := r.Header.Get("Content-Encoding")
405+
if enc != "" && enc != string(SnappyBlockCompression) {
406+
err := fmt.Errorf("%v encoding (compression) is not accepted by this server; only %v is acceptable", enc, SnappyBlockCompression)
407+
logger.Error("Error decoding remote write request", "err", err)
408+
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
409+
return
410+
}
411+
412+
// Read the request body.
413+
bodyBytes, err := io.ReadAll(r.Body)
414+
if err != nil {
415+
logger.Error("Error reading request body", "err", err)
416+
http.Error(w, err.Error(), http.StatusBadRequest)
417+
return
418+
}
419+
420+
decompressed, err := snappy.Decode(nil, bodyBytes)
421+
if err != nil {
422+
// TODO(bwplotka): Add more context to responded error?
423+
logger.Error("Error snappy decoding remote write request", "err", err)
424+
http.Error(w, err.Error(), http.StatusBadRequest)
425+
return
426+
}
427+
428+
// Replace the body with decompressed data
429+
r.Body = io.NopCloser(bytes.NewReader(decompressed))
430+
next.ServeHTTP(w, r)
431+
})
368432
}
369433
}
370434

371435
// NewRemoteWriteHandler returns HTTP handler that receives Remote Write 2.0
372436
// protocol https://prometheus.io/docs/specs/remote_write_spec_2_0/.
373437
func NewRemoteWriteHandler(store writeStorage, opts ...HandlerOption) http.Handler {
374-
o := handlerOpts{logger: slog.New(nopSlogHandler{}), decompressor: &SimpleSnappyDecompressor{}}
438+
o := handlerOpts{
439+
logger: slog.New(nopSlogHandler{}),
440+
middlewares: []func(http.Handler) http.Handler{SnappyDecompressorMiddleware(slog.New(nopSlogHandler{}))},
441+
}
375442
for _, opt := range opts {
376443
opt(&o)
377444
}
378-
return &handler{opts: o, store: store}
445+
h := &handler{opts: o, store: store}
446+
447+
// Apply all middlewares in order
448+
var handler http.Handler = h
449+
for i := len(o.middlewares) - 1; i >= 0; i-- {
450+
handler = o.middlewares[i](handler)
451+
}
452+
return handler
379453
}
380454

381455
// ParseProtoMsg parses the content-type header and returns the proto message type.
@@ -412,10 +486,13 @@ func ParseProtoMsg(contentType string) (WriteProtoFullName, error) {
412486
}
413487

414488
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
489+
if r.Method != http.MethodPost {
490+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
491+
return
492+
}
493+
415494
contentType := r.Header.Get("Content-Type")
416495
if contentType == "" {
417-
// Don't break yolo 1.0 clients if not needed.
418-
// We could give http.StatusUnsupportedMediaType, but let's assume 1.0 message by default.
419496
contentType = appProtoContentType
420497
}
421498

@@ -426,26 +503,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
426503
return
427504
}
428505

429-
enc := r.Header.Get("Content-Encoding")
430-
if enc == "" {
431-
// Don't break yolo 1.0 clients if not needed. This is similar to what we did
432-
// before 2.0: https://github.com/prometheus/prometheus/blob/d78253319daa62c8f28ed47e40bafcad2dd8b586/storage/remote/write_handler.go#L62
433-
// We could give http.StatusUnsupportedMediaType, but let's assume snappy by default.
434-
} else if enc != string(SnappyBlockCompression) {
435-
err := fmt.Errorf("%v encoding (compression) is not accepted by this server; only %v is acceptable", enc, SnappyBlockCompression)
436-
h.opts.logger.Error("Error decoding remote write request", "err", err)
437-
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
438-
}
439-
440-
// Decompress the request body.
441-
decompressed, err := h.opts.decompressor.Decompress(r.Context(), r.Body)
506+
// Read the already decompressed body
507+
body, err := io.ReadAll(r.Body)
442508
if err != nil {
443-
h.opts.logger.Error("Error decompressing remote write request", "err", err.Error())
509+
h.opts.logger.Error("Error reading request body", "err", err.Error())
444510
http.Error(w, err.Error(), http.StatusBadRequest)
445511
return
446512
}
447513

448-
stats, code, storeErr := h.store.Store(r.Context(), msgType, decompressed)
514+
stats, code, storeErr := h.store.Store(r.Context(), msgType, body)
449515

450516
// Set required X-Prometheus-Remote-Write-Written-* response headers, in all cases.
451517
stats.SetHeaders(w)
@@ -454,32 +520,11 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
454520
if code == 0 {
455521
code = http.StatusInternalServerError
456522
}
457-
if code/5 == 100 { // 5xx
523+
if code/100 == 5 { // 5xx
458524
h.opts.logger.Error("Error while storing the remote write request", "err", storeErr.Error())
459525
}
460526
http.Error(w, storeErr.Error(), code)
461527
return
462528
}
463529
w.WriteHeader(http.StatusNoContent)
464530
}
465-
466-
// SimpleSnappyDecompressor is a simple implementation of the remoteWriteDecompressor interface.
467-
type SimpleSnappyDecompressor struct{}
468-
469-
func (s *SimpleSnappyDecompressor) Decompress(ctx context.Context, body io.ReadCloser) (decompressed []byte, _ error) {
470-
// Read the request body.
471-
bodyBytes, err := io.ReadAll(body)
472-
if err != nil {
473-
return nil, fmt.Errorf("error reading request body: %w", err)
474-
}
475-
476-
decompressed, err = snappy.Decode(nil, bodyBytes)
477-
if err != nil {
478-
// TODO(bwplotka): Add more context to responded error?
479-
return nil, fmt.Errorf("error snappy decoding request body: %w", err)
480-
}
481-
482-
return decompressed, nil
483-
}
484-
485-
var _ remoteWriteDecompressor = &SimpleSnappyDecompressor{}

0 commit comments

Comments
 (0)