@@ -51,7 +51,7 @@ type apiOpts struct {
51
51
logger * slog.Logger
52
52
backoff backoff.Config
53
53
compression Compression
54
- endpoint string
54
+ path string
55
55
retryOnRateLimit bool
56
56
}
57
57
@@ -64,6 +64,7 @@ var defaultAPIOpts = &apiOpts{
64
64
// Hardcoded for now.
65
65
retryOnRateLimit : true ,
66
66
compression : SnappyBlockCompression ,
67
+ path : "api/v1/write" ,
67
68
}
68
69
69
70
// WithAPILogger returns APIOption that allows providing slog logger.
@@ -75,18 +76,18 @@ func WithAPILogger(logger *slog.Logger) APIOption {
75
76
}
76
77
}
77
78
78
- // WithAPIEndpoint returns APIOption that allows providing endpoint .
79
- func WithAPIEndpoint ( endpoint string ) APIOption {
79
+ // WithAPIPath returns APIOption that allows providing path to send remote write requests to .
80
+ func WithAPIPath ( path string ) APIOption {
80
81
return func (o * apiOpts ) error {
81
- o .endpoint = endpoint
82
+ o .path = path
82
83
return nil
83
84
}
84
85
}
85
86
86
- // WithAPIRetryOnRateLimit returns APIOption that allows providing retry on rate limit.
87
- func WithAPIRetryOnRateLimit ( retry bool ) APIOption {
87
+ // WithAPIRetryOnRateLimit returns APIOption that disables retrying on rate limit status code .
88
+ func WithAPINoRetryOnRateLimit ( ) APIOption {
88
89
return func (o * apiOpts ) error {
89
- o .retryOnRateLimit = retry
90
+ o .retryOnRateLimit = false
90
91
return nil
91
92
}
92
93
}
@@ -134,33 +135,67 @@ type vtProtoEnabled interface {
134
135
MarshalToSizedBufferVT (dAtA []byte ) (int , error )
135
136
}
136
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
+
137
150
// Write writes given, non-empty, protobuf message to a remote storage.
138
- // The https://github.com/planetscale/vtprotobuf methods will be used if your msg
139
- // supports those (e.g. SizeVT() and MarshalToSizedBufferVT(...)), for efficiency.
140
- 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 ) {
141
160
// Detect content-type.
142
- cType := WriteProtoFullName (proto .MessageName (msg ))
161
+ cType := WriteProtoFullNameV1
162
+ if _ , ok := msg .(v2Request ); ok {
163
+ cType = WriteProtoFullNameV2
164
+ }
165
+
143
166
if err := cType .Validate (); err != nil {
144
167
return WriteResponseStats {}, err
145
168
}
146
169
147
170
// Encode the payload.
148
- if emsg , ok := msg .(vtProtoEnabled ); ok {
171
+ switch m := msg .(type ) {
172
+ case vtProtoEnabled :
149
173
// Use optimized vtprotobuf if supported.
150
- size := emsg .SizeVT ()
174
+ size := m .SizeVT ()
151
175
if len (r .reqBuf ) < size {
152
176
r .reqBuf = make ([]byte , size )
153
177
}
154
- if _ , err := emsg .MarshalToSizedBufferVT (r .reqBuf [:size ]); err != nil {
178
+ if _ , err := m .MarshalToSizedBufferVT (r .reqBuf [:size ]); err != nil {
155
179
return WriteResponseStats {}, fmt .Errorf ("encoding request %w" , err )
156
180
}
157
- } 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 :
158
191
// Generic proto.
159
192
r .reqBuf = r .reqBuf [:0 ]
160
- r .reqBuf , err = (proto.MarshalOptions {}).MarshalAppend (r .reqBuf , msg )
193
+ r .reqBuf , err = (proto.MarshalOptions {}).MarshalAppend (r .reqBuf , m )
161
194
if err != nil {
162
195
return WriteResponseStats {}, fmt .Errorf ("encoding request %w" , err )
163
196
}
197
+ default :
198
+ return WriteResponseStats {}, fmt .Errorf ("unknown message type %T" , m )
164
199
}
165
200
166
201
payload , err := compressPayload (& r .comprBuf , r .opts .compression , r .reqBuf )
@@ -231,7 +266,7 @@ func compressPayload(tmpbuf *[]byte, enc Compression, inp []byte) (compressed []
231
266
}
232
267
233
268
func (r * API ) attemptWrite (ctx context.Context , compr Compression , proto WriteProtoFullName , payload []byte , attempt int ) (WriteResponseStats , error ) {
234
- u := r .client .URL (r .opts .endpoint , nil )
269
+ u := r .client .URL (r .opts .path , nil )
235
270
req , err := http .NewRequest (http .MethodPost , u .String (), bytes .NewReader (payload ))
236
271
if err != nil {
237
272
// Errors from NewRequest are from unparsable URLs, so are not
@@ -305,15 +340,42 @@ type remoteWriteDecompressor interface {
305
340
}
306
341
307
342
type handler struct {
343
+ store writeStorage
344
+ opts handlerOpts
345
+ }
346
+
347
+ type handlerOpts struct {
308
348
logger * slog.Logger
309
- store writeStorage
310
349
decompressor remoteWriteDecompressor
311
350
}
312
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
+ }
369
+ }
370
+
313
371
// NewRemoteWriteHandler returns HTTP handler that receives Remote Write 2.0
314
372
// protocol https://prometheus.io/docs/specs/remote_write_spec_2_0/.
315
- func NewRemoteWriteHandler (logger * slog.Logger , store writeStorage , decompressor remoteWriteDecompressor ) http.Handler {
316
- return & handler {logger : logger , store : store , decompressor : decompressor }
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 }
317
379
}
318
380
319
381
// ParseProtoMsg parses the content-type header and returns the proto message type.
@@ -359,7 +421,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
359
421
360
422
msgType , err := ParseProtoMsg (contentType )
361
423
if err != nil {
362
- h .logger .Error ("Error decoding remote write request" , "err" , err )
424
+ h .opts . logger .Error ("Error decoding remote write request" , "err" , err )
363
425
http .Error (w , err .Error (), http .StatusUnsupportedMediaType )
364
426
return
365
427
}
@@ -371,14 +433,14 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
371
433
// We could give http.StatusUnsupportedMediaType, but let's assume snappy by default.
372
434
} else if enc != string (SnappyBlockCompression ) {
373
435
err := fmt .Errorf ("%v encoding (compression) is not accepted by this server; only %v is acceptable" , enc , SnappyBlockCompression )
374
- h .logger .Error ("Error decoding remote write request" , "err" , err )
436
+ h .opts . logger .Error ("Error decoding remote write request" , "err" , err )
375
437
http .Error (w , err .Error (), http .StatusUnsupportedMediaType )
376
438
}
377
439
378
440
// Decompress the request body.
379
- decompressed , err := h .decompressor .Decompress (r .Context (), r .Body )
441
+ decompressed , err := h .opts . decompressor .Decompress (r .Context (), r .Body )
380
442
if err != nil {
381
- h .logger .Error ("Error decompressing remote write request" , "err" , err .Error ())
443
+ h .opts . logger .Error ("Error decompressing remote write request" , "err" , err .Error ())
382
444
http .Error (w , err .Error (), http .StatusBadRequest )
383
445
return
384
446
}
@@ -393,7 +455,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
393
455
code = http .StatusInternalServerError
394
456
}
395
457
if code / 5 == 100 { // 5xx
396
- h .logger .Error ("Error while storing the remote write request" , "err" , storeErr .Error ())
458
+ h .opts . logger .Error ("Error while storing the remote write request" , "err" , storeErr .Error ())
397
459
}
398
460
http .Error (w , storeErr .Error (), code )
399
461
return
0 commit comments