Skip to content

Commit 12e6e53

Browse files
committed
feat: Support zstd encoding
This allows endpoints to respond with zstd compressed metric data, if the requester supports it. For backwards compatibility, gzip compression will take precedence. Signed-off-by: Manuel Rüger <manuel@rueg.eu>
1 parent e133e49 commit 12e6e53

File tree

4 files changed

+122
-10
lines changed

4 files changed

+122
-10
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/cespare/xxhash/v2 v2.2.0
88
github.com/davecgh/go-spew v1.1.1
99
github.com/json-iterator/go v1.1.12
10+
github.com/klauspost/compress v1.17.8
1011
github.com/prometheus/client_model v0.6.0
1112
github.com/prometheus/common v0.48.0
1213
github.com/prometheus/procfs v0.13.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E
1717
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
1818
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
1919
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
20+
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
21+
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
2022
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
2123
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
2224
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

prometheus/promhttp/http.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"sync"
4343
"time"
4444

45+
"github.com/klauspost/compress/zstd"
4546
"github.com/prometheus/common/expfmt"
4647

4748
"github.com/prometheus/client_golang/prometheus"
@@ -169,15 +170,31 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
169170
header.Set(contentTypeHeader, string(contentType))
170171

171172
w := io.Writer(rsp)
172-
if !opts.DisableCompression && gzipAccepted(req.Header) {
173-
header.Set(contentEncodingHeader, "gzip")
174-
gz := gzipPool.Get().(*gzip.Writer)
175-
defer gzipPool.Put(gz)
173+
if !opts.DisableCompression {
174+
// Gzip takes precedence over zstd
175+
// TODO(mrueg): Replace klauspost/compress with stdlib implementation once https://github.com/golang/go/issues/62513 is implemented.
176+
if encodingAccepted(req.Header, "zstd") {
177+
header.Set(contentEncodingHeader, "zstd")
178+
z, err := zstd.NewWriter(rsp, zstd.WithEncoderLevel(zstd.SpeedFastest))
179+
if err != nil {
180+
return
181+
}
182+
z.Reset(w)
183+
defer z.Close()
184+
185+
w = z
186+
}
187+
if encodingAccepted(req.Header, "gzip") {
188+
header.Set(contentEncodingHeader, "gzip")
189+
gz := gzipPool.Get().(*gzip.Writer)
190+
defer gzipPool.Put(gz)
191+
192+
gz.Reset(w)
193+
defer gz.Close()
176194

177-
gz.Reset(w)
178-
defer gz.Close()
195+
w = gz
196+
}
179197

180-
w = gz
181198
}
182199

183200
enc := expfmt.NewEncoder(w, contentType)
@@ -381,13 +398,13 @@ type HandlerOpts struct {
381398
ProcessStartTime time.Time
382399
}
383400

384-
// gzipAccepted returns whether the client will accept gzip-encoded content.
385-
func gzipAccepted(header http.Header) bool {
401+
// encodingAccepted returns whether the client will accept encoded content.
402+
func encodingAccepted(header http.Header, encoding string) bool {
386403
a := header.Get(acceptEncodingHeader)
387404
parts := strings.Split(a, ",")
388405
for _, part := range parts {
389406
part = strings.TrimSpace(part)
390-
if part == "gzip" || strings.HasPrefix(part, "gzip;") {
407+
if part == encoding || strings.HasPrefix(part, encoding+";") {
391408
return true
392409
}
393410
}

prometheus/promhttp/http_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,95 @@ func TestHandlerTimeout(t *testing.T) {
331331

332332
close(c.Block) // To not leak a goroutine.
333333
}
334+
335+
func BenchmarkEncoding(b *testing.B) {
336+
benchmarks := []struct {
337+
name string
338+
encodingType string
339+
}{
340+
{
341+
name: "test with gzip encoding",
342+
encodingType: "gzip",
343+
},
344+
{
345+
name: "test with zstd encoding",
346+
encodingType: "zstd",
347+
},
348+
{
349+
name: "test with no encoding",
350+
encodingType: "identity",
351+
},
352+
}
353+
sizes := []struct {
354+
name string
355+
metricCount int
356+
labelCount int
357+
labelLength int
358+
metricLength int
359+
}{
360+
{
361+
name: "small",
362+
metricCount: 50,
363+
labelCount: 5,
364+
labelLength: 5,
365+
metricLength: 5,
366+
},
367+
{
368+
name: "medium",
369+
metricCount: 500,
370+
labelCount: 10,
371+
labelLength: 5,
372+
metricLength: 10,
373+
},
374+
{
375+
name: "large",
376+
metricCount: 5000,
377+
labelCount: 10,
378+
labelLength: 5,
379+
metricLength: 10,
380+
},
381+
{
382+
name: "extra-large",
383+
metricCount: 50000,
384+
labelCount: 20,
385+
labelLength: 5,
386+
metricLength: 10,
387+
},
388+
}
389+
390+
for _, size := range sizes {
391+
reg := prometheus.NewRegistry()
392+
handler := HandlerFor(reg, HandlerOpts{})
393+
394+
// Generate Metrics
395+
// Original source: https://github.com/prometheus-community/avalanche/blob/main/metrics/serve.go
396+
labelKeys := make([]string, size.labelCount)
397+
for idx := 0; idx < size.labelCount; idx++ {
398+
labelKeys[idx] = fmt.Sprintf("label_key_%s_%v", strings.Repeat("k", size.labelLength), idx)
399+
}
400+
labelValues := make([]string, size.labelCount)
401+
for idx := 0; idx < size.labelCount; idx++ {
402+
labelValues[idx] = fmt.Sprintf("label_val_%s_%v", strings.Repeat("v", size.labelLength), idx)
403+
}
404+
metrics := make([]*prometheus.GaugeVec, size.metricCount)
405+
for idx := 0; idx < size.metricCount; idx++ {
406+
gauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
407+
Name: fmt.Sprintf("avalanche_metric_%s_%v_%v", strings.Repeat("m", size.metricLength), 0, idx),
408+
Help: "A tasty metric morsel",
409+
}, append([]string{"series_id", "cycle_id"}, labelKeys...))
410+
reg.MustRegister(gauge)
411+
metrics[idx] = gauge
412+
}
413+
414+
for _, benchmark := range benchmarks {
415+
b.Run(benchmark.name+"_"+size.name, func(b *testing.B) {
416+
for i := 0; i < b.N; i++ {
417+
writer := httptest.NewRecorder()
418+
request, _ := http.NewRequest("GET", "/", nil)
419+
request.Header.Add("Accept-Encoding", benchmark.encodingType)
420+
handler.ServeHTTP(writer, request)
421+
}
422+
})
423+
}
424+
}
425+
}

0 commit comments

Comments
 (0)