diff --git a/prometheus/promhttp/http.go b/prometheus/promhttp/http.go index 28eed2672..763d99e36 100644 --- a/prometheus/promhttp/http.go +++ b/prometheus/promhttp/http.go @@ -41,11 +41,11 @@ import ( "sync" "time" - "github.com/klauspost/compress/zstd" "github.com/prometheus/common/expfmt" "github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp/internal" ) const ( @@ -65,7 +65,13 @@ const ( Zstd Compression = "zstd" ) -var defaultCompressionFormats = []Compression{Identity, Gzip, Zstd} +func defaultCompressionFormats() []Compression { + if internal.NewZstdWriter != nil { + return []Compression{Identity, Gzip, Zstd} + } else { + return []Compression{Identity, Gzip} + } +} var gzipPool = sync.Pool{ New: func() interface{} { @@ -138,7 +144,7 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO // Select compression formats to offer based on default or user choice. var compressions []string if !opts.DisableCompression { - offers := defaultCompressionFormats + offers := defaultCompressionFormats() if len(opts.OfferedCompressions) > 0 { offers = opts.OfferedCompressions } @@ -466,14 +472,12 @@ func negotiateEncodingWriter(r *http.Request, rw io.Writer, compressions []strin switch selected { case "zstd": - // TODO(mrueg): Replace klauspost/compress with stdlib implementation once https://github.com/golang/go/issues/62513 is implemented. - z, err := zstd.NewWriter(rw, zstd.WithEncoderLevel(zstd.SpeedFastest)) - if err != nil { - return nil, "", func() {}, err + if internal.NewZstdWriter == nil { + // The content encoding was not implemented yet. + return nil, "", func() {}, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", selected, defaultCompressionFormats()) } - - z.Reset(rw) - return z, selected, func() { _ = z.Close() }, nil + writer, closeWriter, err := internal.NewZstdWriter(rw) + return writer, selected, closeWriter, err case "gzip": gz := gzipPool.Get().(*gzip.Writer) gz.Reset(rw) @@ -483,6 +487,6 @@ func negotiateEncodingWriter(r *http.Request, rw io.Writer, compressions []strin return rw, selected, func() {}, nil default: // The content encoding was not implemented yet. - return nil, "", func() {}, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", selected, defaultCompressionFormats) + return nil, "", func() {}, fmt.Errorf("content compression format not recognized: %s. Valid formats are: %s", selected, defaultCompressionFormats()) } } diff --git a/prometheus/promhttp/http_test.go b/prometheus/promhttp/http_test.go index a763da469..189ee357c 100644 --- a/prometheus/promhttp/http_test.go +++ b/prometheus/promhttp/http_test.go @@ -30,6 +30,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/client_golang/prometheus" + _ "github.com/prometheus/client_golang/prometheus/promhttp/zstd" ) type errorCollector struct{} @@ -484,7 +485,7 @@ func TestInstrumentMetricHandlerWithCompression(t *testing.T) { func TestNegotiateEncodingWriter(t *testing.T) { var defaultCompressions []string - for _, comp := range defaultCompressionFormats { + for _, comp := range defaultCompressionFormats() { defaultCompressions = append(defaultCompressions, string(comp)) } diff --git a/prometheus/promhttp/internal/compression.go b/prometheus/promhttp/internal/compression.go new file mode 100644 index 000000000..c5039590f --- /dev/null +++ b/prometheus/promhttp/internal/compression.go @@ -0,0 +1,21 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "io" +) + +// NewZstdWriter enables zstd write support if non-nil. +var NewZstdWriter func(rw io.Writer) (_ io.Writer, closeWriter func(), _ error) diff --git a/prometheus/promhttp/zstd/zstd.go b/prometheus/promhttp/zstd/zstd.go new file mode 100644 index 000000000..7e355e338 --- /dev/null +++ b/prometheus/promhttp/zstd/zstd.go @@ -0,0 +1,47 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package zstd activates support for zstd compression. +// To enable zstd compression support, import like this: +// +// import ( +// _ "github.com/prometheus/client_golang/prometheus/promhttp/zstd" +// ) +// +// This support is currently implemented via the github.com/klauspost/compress library, +// so importing this package requires linking and building that library. +// Once stdlib support is added to the Go standard library (https://github.com/golang/go/issues/62513), +// this package is expected to become a no-op, and zstd support will be enabled by default. +package zstd + +import ( + "io" + + "github.com/klauspost/compress/zstd" + + "github.com/prometheus/client_golang/prometheus/promhttp/internal" +) + +func init() { + // Enable zstd support + internal.NewZstdWriter = func(rw io.Writer) (_ io.Writer, closeWriter func(), _ error) { + // TODO(mrueg): Replace klauspost/compress with stdlib implementation once https://github.com/golang/go/issues/62513 is implemented, and move this package to be a no-op / backfill for older go versions. + z, err := zstd.NewWriter(rw, zstd.WithEncoderLevel(zstd.SpeedFastest)) + if err != nil { + return nil, func() {}, err + } + + z.Reset(rw) + return z, func() { _ = z.Close() }, nil + } +}