Skip to content

Commit 052b28d

Browse files
mruegbwplotka
andcommitted
Update prometheus/promhttp/http.go
Co-authored-by: Bartlomiej Plotka <[email protected]> Signed-off-by: Manuel Rüger <[email protected]>
1 parent 03af4da commit 052b28d

File tree

2 files changed

+159
-54
lines changed

2 files changed

+159
-54
lines changed

prometheus/promhttp/http.go

+82-44
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,25 @@ const (
5555
processStartTimeHeader = "Process-Start-Time-Unix"
5656
)
5757

58-
var defaultEncodingOffers = []string{"gzip", "zstd"}
58+
type Compression int
59+
60+
const (
61+
Identity Compression = iota
62+
Gzip
63+
Zstd
64+
)
65+
66+
var compressions = [...]string{
67+
"identity",
68+
"gzip",
69+
"zstd",
70+
}
71+
72+
func (c Compression) String() string {
73+
return compressions[c]
74+
}
75+
76+
var defaultCompressionFormats = []Compression{Identity, Gzip, Zstd}
5977

6078
var gzipPool = sync.Pool{
6179
New: func() interface{} {
@@ -168,46 +186,13 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
168186
} else {
169187
contentType = expfmt.Negotiate(req.Header)
170188
}
171-
header := rsp.Header()
172-
header.Set(contentTypeHeader, string(contentType))
173-
174-
w := io.Writer(rsp)
175-
if !opts.DisableCompression {
176-
offers := defaultEncodingOffers
177-
if len(opts.EncodingOffers) > 0 {
178-
offers = opts.EncodingOffers
179-
}
180-
// TODO(mrueg): Replace internal/github.com/gddo once https://github.com/golang/go/issues/19307 is implemented.
181-
compression := httputil.NegotiateContentEncoding(req, offers)
182-
switch compression {
183-
case "zstd":
184-
header.Set(contentEncodingHeader, "zstd")
185-
// TODO(mrueg): Replace klauspost/compress with stdlib implementation once https://github.com/golang/go/issues/62513 is implemented.
186-
z, err := zstd.NewWriter(rsp, zstd.WithEncoderLevel(zstd.SpeedFastest))
187-
if err != nil {
188-
return
189-
}
190-
191-
z.Reset(w)
192-
defer z.Close()
189+
rsp.Header().Set(contentTypeHeader, string(contentType))
193190

194-
w = z
195-
case "gzip":
196-
header.Set(contentEncodingHeader, "gzip")
197-
gz := gzipPool.Get().(*gzip.Writer)
198-
defer gzipPool.Put(gz)
199-
200-
gz.Reset(w)
201-
defer gz.Close()
202-
203-
w = gz
204-
case "identity":
205-
// This means the content is not encoded.
206-
default:
207-
// The content encoding was not implemented yet.
208-
return
191+
w, err := GetWriter(req, rsp, opts.DisableCompression, opts.OfferedCompressions)
192+
if err != nil {
193+
if opts.ErrorLog != nil {
194+
opts.ErrorLog.Println("error getting writer", err)
209195
}
210-
211196
}
212197

213198
enc := expfmt.NewEncoder(w, contentType)
@@ -373,12 +358,19 @@ type HandlerOpts struct {
373358
// no effect on the HTTP status code because ErrorHandling is set to
374359
// ContinueOnError.
375360
Registry prometheus.Registerer
376-
// If DisableCompression is true, the handler will never compress the
377-
// response, even if requested by the client.
361+
// DisableCompression disables the response encoding (compression) and
362+
// encoding negotiation. If true, the handler will
363+
// never compress the response, even if requested
364+
// by the client and the OfferedCompressions field is ignored.
378365
DisableCompression bool
379-
// If DisableCompression is false, this option will allow to define the
380-
// set of offered encoding algorithms.
381-
EncodingOffers []string
366+
// OfferedCompressions is a set of encodings (compressions) handler will
367+
// try to offer when negotiating with the client. This defaults to zstd,
368+
// gzip and identity.
369+
// NOTE: If handler can't agree on the encodings with the client or
370+
// caller using unsupported or empty encodings in OfferedCompressions,
371+
// handler always fallbacks to no compression (identity), for
372+
// compatibility reasons. In such cases ErrorLog will be used if set.
373+
OfferedCompressions []Compression
382374
// The number of concurrent HTTP requests is limited to
383375
// MaxRequestsInFlight. Additional requests are responded to with 503
384376
// Service Unavailable and a suitable message in the body. If
@@ -426,3 +418,49 @@ func httpError(rsp http.ResponseWriter, err error) {
426418
http.StatusInternalServerError,
427419
)
428420
}
421+
422+
func GetWriter(r *http.Request, rsp http.ResponseWriter, disableCompression bool, offeredCompressions []Compression) (io.Writer, error) {
423+
w := io.Writer(rsp)
424+
rsp.Header().Set(contentEncodingHeader, "identity")
425+
if !disableCompression {
426+
offers := defaultCompressionFormats
427+
if len(offeredCompressions) > 0 {
428+
offers = offeredCompressions
429+
}
430+
var compressions []string
431+
for _, comp := range offers {
432+
compressions = append(compressions, comp.String())
433+
}
434+
// TODO(mrueg): Replace internal/github.com/gddo once https://github.com/golang/go/issues/19307 is implemented.
435+
compression := httputil.NegotiateContentEncoding(r, compressions)
436+
switch compression {
437+
case "zstd":
438+
rsp.Header().Set(contentEncodingHeader, "zstd")
439+
// TODO(mrueg): Replace klauspost/compress with stdlib implementation once https://github.com/golang/go/issues/62513 is implemented.
440+
z, err := zstd.NewWriter(rsp, zstd.WithEncoderLevel(zstd.SpeedFastest))
441+
if err != nil {
442+
return nil, err
443+
}
444+
445+
z.Reset(w)
446+
defer z.Close()
447+
448+
w = z
449+
case "gzip":
450+
rsp.Header().Set(contentEncodingHeader, "gzip")
451+
gz := gzipPool.Get().(*gzip.Writer)
452+
defer gzipPool.Put(gz)
453+
454+
gz.Reset(w)
455+
defer gz.Close()
456+
457+
w = gz
458+
case "identity":
459+
// This means the content is not compressed.
460+
default:
461+
// The content encoding was not implemented yet.
462+
return w, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", compression, defaultCompressionFormats)
463+
}
464+
}
465+
return w, nil
466+
}

prometheus/promhttp/http_test.go

+77-10
Original file line numberDiff line numberDiff line change
@@ -332,22 +332,89 @@ func TestHandlerTimeout(t *testing.T) {
332332
close(c.Block) // To not leak a goroutine.
333333
}
334334

335-
func BenchmarkEncoding(b *testing.B) {
335+
func TestGetWriter(t *testing.T) {
336+
testCases := []struct {
337+
name string
338+
disableCompression bool
339+
offeredCompressions []Compression
340+
acceptEncoding string
341+
expectedCompression string
342+
err error
343+
}{
344+
{
345+
name: "test without compression enabled",
346+
disableCompression: true,
347+
offeredCompressions: defaultCompressionFormats,
348+
acceptEncoding: "",
349+
expectedCompression: "identity",
350+
err: nil,
351+
},
352+
{
353+
name: "test with compression enabled with empty accept-encoding header",
354+
disableCompression: false,
355+
offeredCompressions: defaultCompressionFormats,
356+
acceptEncoding: "",
357+
expectedCompression: "identity",
358+
err: nil,
359+
},
360+
{
361+
name: "test with gzip compression requested",
362+
disableCompression: false,
363+
offeredCompressions: defaultCompressionFormats,
364+
acceptEncoding: "gzip",
365+
expectedCompression: "gzip",
366+
err: nil,
367+
},
368+
{
369+
name: "test with gzip, zstd compression requested",
370+
disableCompression: false,
371+
offeredCompressions: defaultCompressionFormats,
372+
acceptEncoding: "gzip,zstd",
373+
expectedCompression: "gzip",
374+
err: nil,
375+
},
376+
{
377+
name: "test with zstd, gzip compression requested",
378+
disableCompression: false,
379+
offeredCompressions: defaultCompressionFormats,
380+
acceptEncoding: "zstd,gzip",
381+
expectedCompression: "gzip",
382+
err: nil,
383+
},
384+
}
385+
386+
for _, test := range testCases {
387+
request, _ := http.NewRequest("GET", "/", nil)
388+
request.Header.Add(acceptEncodingHeader, test.acceptEncoding)
389+
rr := httptest.NewRecorder()
390+
_, err := GetWriter(request, rr, test.disableCompression, test.offeredCompressions)
391+
392+
if !errors.Is(err, test.err) {
393+
t.Errorf("got error: %v, expected: %v", err, test.err)
394+
}
395+
396+
if rr.Header().Get(contentEncodingHeader) != test.expectedCompression {
397+
t.Errorf("got different compression type: %v, expected: %v", rr.Header().Get(contentEncodingHeader), test.expectedCompression)
398+
}
399+
}
400+
}
401+
402+
func BenchmarkCompression(b *testing.B) {
336403
benchmarks := []struct {
337-
name string
338-
encodingType string
404+
name string
405+
compressionType string
339406
}{
340407
{
341-
name: "test with gzip encoding",
342-
encodingType: "gzip",
408+
name: "test with gzip compression",
409+
compressionType: "gzip",
343410
},
344411
{
345-
name: "test with zstd encoding",
346-
encodingType: "zstd",
412+
name: "test with zstd compression",
413+
compressionType: "zstd",
347414
},
348415
{
349-
name: "test with no encoding",
350-
encodingType: "identity",
416+
name: "test with no compression",
417+
compressionType: "identity",
351418
},
352419
}
353420
sizes := []struct {
@@ -416,7 +483,7 @@ func BenchmarkEncoding(b *testing.B) {
416483
for i := 0; i < b.N; i++ {
417484
writer := httptest.NewRecorder()
418485
request, _ := http.NewRequest("GET", "/", nil)
419-
request.Header.Add("Accept-Encoding", benchmark.encodingType)
486+
request.Header.Add("Accept-Encoding", benchmark.compressionType)
420487
handler.ServeHTTP(writer, request)
421488
}
422489
})

0 commit comments

Comments
 (0)