@@ -51,6 +51,7 @@ type apiOpts struct {
51
51
logger * slog.Logger
52
52
backoff backoff.Config
53
53
compression Compression
54
+ path string
54
55
retryOnRateLimit bool
55
56
}
56
57
@@ -63,6 +64,7 @@ var defaultAPIOpts = &apiOpts{
63
64
// Hardcoded for now.
64
65
retryOnRateLimit : true ,
65
66
compression : SnappyBlockCompression ,
67
+ path : "api/v1/write" ,
66
68
}
67
69
68
70
// WithAPILogger returns APIOption that allows providing slog logger.
@@ -74,6 +76,22 @@ func WithAPILogger(logger *slog.Logger) APIOption {
74
76
}
75
77
}
76
78
79
+ // WithAPIPath returns APIOption that allows providing path to send remote write requests to.
80
+ func WithAPIPath (path string ) APIOption {
81
+ return func (o * apiOpts ) error {
82
+ o .path = path
83
+ return nil
84
+ }
85
+ }
86
+
87
+ // WithAPIRetryOnRateLimit returns APIOption that disables retrying on rate limit status code.
88
+ func WithAPINoRetryOnRateLimit () APIOption {
89
+ return func (o * apiOpts ) error {
90
+ o .retryOnRateLimit = false
91
+ return nil
92
+ }
93
+ }
94
+
77
95
type nopSlogHandler struct {}
78
96
79
97
func (n nopSlogHandler ) Enabled (context.Context , slog.Level ) bool { return false }
@@ -117,33 +135,67 @@ type vtProtoEnabled interface {
117
135
MarshalToSizedBufferVT (dAtA []byte ) (int , error )
118
136
}
119
137
138
+ type gogoProtoEnabled interface {
139
+ Size () (n int )
140
+ MarshalToSizedBuffer (dAtA []byte ) (n int , err error )
141
+ }
142
+
143
+ // Sort of a hack to identify v2 requests.
144
+ // Under any marshaling scheme, v2 requests have a `Symbols` field of type []string.
145
+ // So would always have a `GetSymbols()` method which doesn't rely on any other types.
146
+ type v2Request interface {
147
+ GetSymbols () []string
148
+ }
149
+
120
150
// Write writes given, non-empty, protobuf message to a remote storage.
121
- // The https://github.com/planetscale/vtprotobuf methods will be used if your msg
122
- // supports those (e.g. SizeVT() and MarshalToSizedBufferVT(...)), for efficiency.
123
- func (r * API ) Write (ctx context.Context , msg proto.Message ) (_ WriteResponseStats , err error ) {
151
+ //
152
+ // Depending on serialization methods,
153
+ // - https://github.com/planetscale/vtprotobuf methods will be used if your msg
154
+ // supports those (e.g. SizeVT() and MarshalToSizedBufferVT(...)), for efficiency
155
+ // - Otherwise https://github.com/gogo/protobuf methods (e.g. Size() and MarshalToSizedBuffer(...))
156
+ // will be used
157
+ // - If neither is supported, it will marshaled using generic google.golang.org/protobuf methods and
158
+ // error out on unknown scheme.
159
+ func (r * API ) Write (ctx context.Context , msg any ) (_ WriteResponseStats , err error ) {
124
160
// Detect content-type.
125
- cType := WriteProtoFullName (proto .MessageName (msg ))
161
+ cType := WriteProtoFullNameV1
162
+ if _ , ok := msg .(v2Request ); ok {
163
+ cType = WriteProtoFullNameV2
164
+ }
165
+
126
166
if err := cType .Validate (); err != nil {
127
167
return WriteResponseStats {}, err
128
168
}
129
169
130
170
// Encode the payload.
131
- if emsg , ok := msg .(vtProtoEnabled ); ok {
171
+ switch m := msg .(type ) {
172
+ case vtProtoEnabled :
132
173
// Use optimized vtprotobuf if supported.
133
- size := emsg .SizeVT ()
174
+ size := m .SizeVT ()
134
175
if len (r .reqBuf ) < size {
135
176
r .reqBuf = make ([]byte , size )
136
177
}
137
- if _ , err := emsg .MarshalToSizedBufferVT (r .reqBuf [:size ]); err != nil {
178
+ if _ , err := m .MarshalToSizedBufferVT (r .reqBuf [:size ]); err != nil {
138
179
return WriteResponseStats {}, fmt .Errorf ("encoding request %w" , err )
139
180
}
140
- } else {
181
+ case gogoProtoEnabled :
182
+ // Gogo proto if supported.
183
+ size := m .Size ()
184
+ if len (r .reqBuf ) < size {
185
+ r .reqBuf = make ([]byte , size )
186
+ }
187
+ if _ , err := m .MarshalToSizedBuffer (r .reqBuf [:size ]); err != nil {
188
+ return WriteResponseStats {}, fmt .Errorf ("encoding request %w" , err )
189
+ }
190
+ case proto.Message :
141
191
// Generic proto.
142
192
r .reqBuf = r .reqBuf [:0 ]
143
- r .reqBuf , err = (proto.MarshalOptions {}).MarshalAppend (r .reqBuf , msg )
193
+ r .reqBuf , err = (proto.MarshalOptions {}).MarshalAppend (r .reqBuf , m )
144
194
if err != nil {
145
195
return WriteResponseStats {}, fmt .Errorf ("encoding request %w" , err )
146
196
}
197
+ default :
198
+ return WriteResponseStats {}, fmt .Errorf ("unknown message type %T" , m )
147
199
}
148
200
149
201
payload , err := compressPayload (& r .comprBuf , r .opts .compression , r .reqBuf )
@@ -214,7 +266,7 @@ func compressPayload(tmpbuf *[]byte, enc Compression, inp []byte) (compressed []
214
266
}
215
267
216
268
func (r * API ) attemptWrite (ctx context.Context , compr Compression , proto WriteProtoFullName , payload []byte , attempt int ) (WriteResponseStats , error ) {
217
- u := r .client .URL ("api/v1/write" , nil )
269
+ u := r .client .URL (r . opts . path , nil )
218
270
req , err := http .NewRequest (http .MethodPost , u .String (), bytes .NewReader (payload ))
219
271
if err != nil {
220
272
// Errors from NewRequest are from unparsable URLs, so are not
@@ -241,9 +293,12 @@ func (r *API) attemptWrite(ctx context.Context, compr Compression, proto WritePr
241
293
return WriteResponseStats {}, retryableError {err , 0 }
242
294
}
243
295
244
- rs , err := parseWriteResponseStats (resp )
245
- if err != nil {
246
- r .opts .logger .Warn ("parsing rw write statistics failed; partial or no stats" , "err" , err )
296
+ rs := WriteResponseStats {}
297
+ if proto == WriteProtoFullNameV2 {
298
+ rs , err = parseWriteResponseStats (resp )
299
+ if err != nil {
300
+ r .opts .logger .Warn ("parsing rw write statistics failed; partial or no stats" , "err" , err )
301
+ }
247
302
}
248
303
249
304
if resp .StatusCode / 100 == 2 {
@@ -279,18 +334,59 @@ type writeStorage interface {
279
334
Store (ctx context.Context , proto WriteProtoFullName , serializedRequest []byte ) (_ WriteResponseStats , code int , _ error )
280
335
}
281
336
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
+
282
342
type handler struct {
283
- logger * slog.Logger
284
- store writeStorage
343
+ store writeStorage
344
+ opts handlerOpts
345
+ }
346
+
347
+ type handlerOpts struct {
348
+ logger * slog.Logger
349
+ decompressor remoteWriteDecompressor
350
+ }
351
+
352
+ // HandlerOption represents an option for the handler.
353
+ type HandlerOption func (o * handlerOpts )
354
+
355
+ // WithHandlerLogger returns HandlerOption that allows providing slog logger.
356
+ // By default, nothing is logged.
357
+ func WithHandlerLogger (logger * slog.Logger ) HandlerOption {
358
+ return func (o * handlerOpts ) {
359
+ o .logger = logger
360
+ }
361
+ }
362
+
363
+ // WithHandlerDecompressor returns HandlerOption that allows providing remoteWriteDecompressor.
364
+ // By default, SimpleSnappyDecompressor is used.
365
+ func WithHandlerDecompressor (decompressor remoteWriteDecompressor ) HandlerOption {
366
+ return func (o * handlerOpts ) {
367
+ o .decompressor = decompressor
368
+ }
285
369
}
286
370
287
371
// NewRemoteWriteHandler returns HTTP handler that receives Remote Write 2.0
288
372
// protocol https://prometheus.io/docs/specs/remote_write_spec_2_0/.
289
- func NewRemoteWriteHandler (logger * slog.Logger , store writeStorage ) http.Handler {
290
- return & handler {logger : logger , store : store }
373
+ func NewRemoteWriteHandler (store writeStorage , opts ... HandlerOption ) http.Handler {
374
+ o := handlerOpts {logger : slog .New (nopSlogHandler {}), decompressor : & SimpleSnappyDecompressor {}}
375
+ for _ , opt := range opts {
376
+ opt (& o )
377
+ }
378
+ return & handler {opts : o , store : store }
291
379
}
292
380
293
- func parseProtoMsg (contentType string ) (WriteProtoFullName , error ) {
381
+ // ParseProtoMsg parses the content-type header and returns the proto message type.
382
+ //
383
+ // The expected content-type will be of the form,
384
+ // - `application/x-protobuf;proto=io.prometheus.write.v2.Request` which will be treated as RW2.0 request,
385
+ // - `application/x-protobuf;proto=prometheus.WriteRequest` which will be treated as RW1.0 request,
386
+ // - `application/x-protobuf` which will be treated as RW1.0 request.
387
+ //
388
+ // If the content-type is not of the above forms, it will return an error.
389
+ func ParseProtoMsg (contentType string ) (WriteProtoFullName , error ) {
294
390
contentType = strings .TrimSpace (contentType )
295
391
296
392
parts := strings .Split (contentType , ";" )
@@ -323,9 +419,9 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
323
419
contentType = appProtoContentType
324
420
}
325
421
326
- msgType , err := parseProtoMsg (contentType )
422
+ msgType , err := ParseProtoMsg (contentType )
327
423
if err != nil {
328
- h .logger .Error ("Error decoding remote write request" , "err" , err )
424
+ h .opts . logger .Error ("Error decoding remote write request" , "err" , err )
329
425
http .Error (w , err .Error (), http .StatusUnsupportedMediaType )
330
426
return
331
427
}
@@ -337,22 +433,14 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
337
433
// We could give http.StatusUnsupportedMediaType, but let's assume snappy by default.
338
434
} else if enc != string (SnappyBlockCompression ) {
339
435
err := fmt .Errorf ("%v encoding (compression) is not accepted by this server; only %v is acceptable" , enc , SnappyBlockCompression )
340
- h .logger .Error ("Error decoding remote write request" , "err" , err )
436
+ h .opts . logger .Error ("Error decoding remote write request" , "err" , err )
341
437
http .Error (w , err .Error (), http .StatusUnsupportedMediaType )
342
438
}
343
439
344
- // Read the request body.
345
- body , err := io . ReadAll ( r .Body )
440
+ // Decompress the request body.
441
+ decompressed , err := h . opts . decompressor . Decompress ( r . Context (), r .Body )
346
442
if err != nil {
347
- h .logger .Error ("Error decoding remote write request" , "err" , err .Error ())
348
- http .Error (w , err .Error (), http .StatusBadRequest )
349
- return
350
- }
351
-
352
- decompressed , err := snappy .Decode (nil , body )
353
- if err != nil {
354
- // TODO(bwplotka): Add more context to responded error?
355
- h .logger .Error ("Error decompressing remote write request" , "err" , err .Error ())
443
+ h .opts .logger .Error ("Error decompressing remote write request" , "err" , err .Error ())
356
444
http .Error (w , err .Error (), http .StatusBadRequest )
357
445
return
358
446
}
@@ -367,10 +455,31 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
367
455
code = http .StatusInternalServerError
368
456
}
369
457
if code / 5 == 100 { // 5xx
370
- h .logger .Error ("Error while remote writing the v2 request" , "err" , storeErr .Error ())
458
+ h .opts . logger .Error ("Error while storing the remote write request" , "err" , storeErr .Error ())
371
459
}
372
460
http .Error (w , storeErr .Error (), code )
373
461
return
374
462
}
375
463
w .WriteHeader (http .StatusNoContent )
376
464
}
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