mirror of
https://github.com/hanzoai/telemetry.git
synced 2026-10-30 20:01:49 +00:00
Follow-up to https://github.com/open-telemetry/opentelemetry-collector/pull/15253. No need for another changelog item
309 lines
9.6 KiB
Go
309 lines
9.6 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// This file contains helper functions regarding compression/decompression for confighttp.
|
|
|
|
package confighttp // import "go.opentelemetry.io/collector/config/confighttp"
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/golang/snappy"
|
|
"github.com/klauspost/compress/zstd"
|
|
"github.com/pierrec/lz4/v4"
|
|
|
|
"go.opentelemetry.io/collector/config/configcompression"
|
|
)
|
|
|
|
func defaultCompressionAlgorithms() []string {
|
|
return []string{"", "gzip", "zstd", "zlib", "snappy", "deflate", "lz4", "x-snappy-framed"}
|
|
}
|
|
|
|
type compressRoundTripper struct {
|
|
rt http.RoundTripper
|
|
compressionType configcompression.Type
|
|
compressionParams configcompression.CompressionParams
|
|
compressor *compressor
|
|
}
|
|
|
|
var zstdReaderPool sync.Pool
|
|
|
|
type pooledZstdReadCloser struct {
|
|
inner *zstd.Decoder
|
|
}
|
|
|
|
func (pzrc *pooledZstdReadCloser) Read(dst []byte) (int, error) {
|
|
if pzrc.inner == nil {
|
|
return 0, zstd.ErrDecoderClosed
|
|
}
|
|
return pzrc.inner.Read(dst)
|
|
}
|
|
|
|
func (pzrc *pooledZstdReadCloser) Close() error {
|
|
if pzrc.inner != nil {
|
|
err := pzrc.inner.Reset(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
zstdReaderPool.Put(pzrc.inner)
|
|
pzrc.inner = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var availableDecoders = map[string]func(body io.ReadCloser) (io.ReadCloser, error){
|
|
"": func(io.ReadCloser) (io.ReadCloser, error) {
|
|
// Not a compressed payload. Nothing to do.
|
|
return nil, nil
|
|
},
|
|
"gzip": func(body io.ReadCloser) (io.ReadCloser, error) {
|
|
gr, err := gzip.NewReader(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gr, nil
|
|
},
|
|
"zstd": func(body io.ReadCloser) (io.ReadCloser, error) {
|
|
v := zstdReaderPool.Get()
|
|
var zr *zstd.Decoder
|
|
var err error
|
|
if v == nil {
|
|
// NOTE(tigrannajaryan):
|
|
// Concurrency 1 disables async decoding. We don't need async decoding, it is pointless
|
|
// for our use-case (a server accepting decoding http requests).
|
|
// Disabling async improves performance (I benchmarked it previously when working
|
|
// on https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/23257).
|
|
zr, err = zstd.NewReader(body, zstd.WithDecoderConcurrency(1))
|
|
} else {
|
|
zr = v.(*zstd.Decoder)
|
|
err = zr.Reset(body)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &pooledZstdReadCloser{inner: zr}, nil
|
|
},
|
|
"zlib": func(body io.ReadCloser) (io.ReadCloser, error) {
|
|
zr, err := zlib.NewReader(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return zr, nil
|
|
},
|
|
"snappy": newSnappyHandler(0),
|
|
//nolint:unparam // Ignoring the linter request to remove error return since it needs to match the method signature
|
|
"lz4": func(body io.ReadCloser) (io.ReadCloser, error) {
|
|
return &compressReadCloser{
|
|
Reader: lz4.NewReader(body),
|
|
orig: body,
|
|
}, nil
|
|
},
|
|
//nolint:unparam // Ignoring the linter request to remove error return since it needs to match the method signature
|
|
"x-snappy-framed": func(body io.ReadCloser) (io.ReadCloser, error) {
|
|
return &compressReadCloser{
|
|
Reader: snappy.NewReader(body),
|
|
orig: body,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
// snappyFramingHeader is always the first 10 bytes of a snappy framed stream.
|
|
var snappyFramingHeader = []byte{
|
|
0xff, 0x06, 0x00, 0x00,
|
|
0x73, 0x4e, 0x61, 0x50, 0x70, 0x59, // "sNaPpY"
|
|
}
|
|
|
|
// newSnappyHandler returns a factory for io.ReadCloser that auto-detects the snappy format.
|
|
// This is necessary because the collector previously used "content-encoding: snappy"
|
|
// but decompressed and compressed the payloads using the snappy framing format.
|
|
// However, "content-encoding: snappy" is uses the block format, and "x-snappy-framed"
|
|
// is the framing format. This handler is a (hopefully temporary) hack to
|
|
// make this work in a backwards-compatible way.
|
|
//
|
|
// See https://github.com/google/snappy/blob/6af9287fbdb913f0794d0148c6aa43b58e63c8e3/framing_format.txt#L27-L36
|
|
// for more details on the framing format.
|
|
//
|
|
// maxRequestBodySize bounds the decoded allocation in the block-format path
|
|
// by checking snappy.DecodedLen before snappy.Decode, so a small compressed
|
|
// body cannot amplify into a large in-memory buffer.
|
|
func newSnappyHandler(maxRequestBodySize int64) func(io.ReadCloser) (io.ReadCloser, error) {
|
|
return func(body io.ReadCloser) (io.ReadCloser, error) {
|
|
br := bufio.NewReader(body)
|
|
|
|
peekBytes, err := br.Peek(len(snappyFramingHeader))
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, err
|
|
}
|
|
|
|
isFramed := len(peekBytes) >= len(snappyFramingHeader) && bytes.Equal(peekBytes[:len(snappyFramingHeader)], snappyFramingHeader)
|
|
if isFramed {
|
|
return &compressReadCloser{
|
|
Reader: snappy.NewReader(br),
|
|
orig: body,
|
|
}, nil
|
|
}
|
|
defer body.Close()
|
|
|
|
if maxRequestBodySize > 0 {
|
|
// Peek MaxVarintLen64 bytes so we can read the decoded length
|
|
// before reading the full compressed request body.
|
|
lenBytes, peakErr := br.Peek(binary.MaxVarintLen64)
|
|
if peakErr != nil && !errors.Is(peakErr, io.EOF) {
|
|
return nil, peakErr
|
|
}
|
|
decodedLen, decErr := snappy.DecodedLen(lenBytes)
|
|
if decErr != nil {
|
|
return nil, decErr
|
|
}
|
|
if int64(decodedLen) > maxRequestBodySize {
|
|
return nil, errors.New("snappy: decoded size exceeds max request body size")
|
|
}
|
|
}
|
|
compressed, err := io.ReadAll(br)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decoded, err := snappy.Decode(nil, compressed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return io.NopCloser(bytes.NewReader(decoded)), nil
|
|
}
|
|
}
|
|
|
|
func newCompressionParams(level configcompression.Level) configcompression.CompressionParams {
|
|
return configcompression.CompressionParams{
|
|
Level: level,
|
|
}
|
|
}
|
|
|
|
func newCompressRoundTripper(rt http.RoundTripper, compressionType configcompression.Type, compressionParams configcompression.CompressionParams) (*compressRoundTripper, error) {
|
|
encoder, err := newCompressor(compressionType, compressionParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &compressRoundTripper{
|
|
rt: rt,
|
|
compressionType: compressionType,
|
|
compressionParams: compressionParams,
|
|
compressor: encoder,
|
|
}, nil
|
|
}
|
|
|
|
func (r *compressRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if req.Header.Get(headerContentEncoding) != "" {
|
|
// If the header already specifies a content encoding then skip compression
|
|
// since we don't want to compress it again. This is a safeguard that normally
|
|
// should not happen since CompressRoundTripper is not intended to be used
|
|
// with http clients which already do their own compression.
|
|
return r.rt.RoundTrip(req)
|
|
}
|
|
|
|
// Compress the body.
|
|
buf := bytes.NewBuffer([]byte{})
|
|
if err := r.compressor.compress(buf, req.Body); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create a new request since the docs say that we cannot modify the "req"
|
|
// (see https://golang.org/pkg/net/http/#RoundTripper).
|
|
cReq, err := http.NewRequestWithContext(req.Context(), req.Method, req.URL.String(), buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Clone the headers and add the encoding header.
|
|
cReq.Header = req.Header.Clone()
|
|
cReq.Header.Add(headerContentEncoding, string(r.compressionType))
|
|
|
|
return r.rt.RoundTrip(cReq)
|
|
}
|
|
|
|
type decompressor struct {
|
|
errHandler func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int)
|
|
base http.Handler
|
|
decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error)
|
|
maxRequestBodySize int64
|
|
}
|
|
|
|
// httpContentDecompressor offloads the task of handling compressed HTTP requests
|
|
// by identifying the compression format in the "Content-Encoding" header and re-writing
|
|
// request body so that the handlers further in the chain can work on decompressed data.
|
|
func httpContentDecompressor(h http.Handler, maxRequestBodySize int64, eh func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int), enableDecoders []string, decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error)) http.Handler {
|
|
errHandler := defaultErrorHandler
|
|
if eh != nil {
|
|
errHandler = eh
|
|
}
|
|
|
|
enabled := map[string]func(body io.ReadCloser) (io.ReadCloser, error){}
|
|
for _, dec := range enableDecoders {
|
|
enabled[dec] = availableDecoders[dec]
|
|
|
|
if dec == "deflate" {
|
|
enabled["deflate"] = availableDecoders["zlib"]
|
|
}
|
|
if dec == "snappy" {
|
|
enabled["snappy"] = newSnappyHandler(maxRequestBodySize)
|
|
}
|
|
}
|
|
|
|
d := &decompressor{
|
|
maxRequestBodySize: maxRequestBodySize,
|
|
errHandler: errHandler,
|
|
base: h,
|
|
decoders: enabled,
|
|
}
|
|
|
|
maps.Copy(d.decoders, decoders)
|
|
|
|
return d
|
|
}
|
|
|
|
func (d *decompressor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
newBody, err := d.newBodyReader(r)
|
|
if err != nil {
|
|
d.errHandler(w, r, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if newBody != nil {
|
|
newBody = &panicRecoverReadCloser{inner: newBody}
|
|
defer newBody.Close()
|
|
// "Content-Encoding" header is removed to avoid decompressing twice
|
|
// in case the next handler(s) have implemented a similar mechanism.
|
|
r.Header.Del("Content-Encoding")
|
|
// "Content-Length" is set to -1 as the size of the decompressed body is unknown.
|
|
r.Header.Del("Content-Length")
|
|
r.ContentLength = -1
|
|
r.Body = http.MaxBytesReader(w, newBody, d.maxRequestBodySize)
|
|
}
|
|
d.base.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (d *decompressor) newBodyReader(r *http.Request) (io.ReadCloser, error) {
|
|
if len(d.decoders) == 0 {
|
|
return nil, nil // Signal: don't replace r.Body
|
|
}
|
|
encoding := r.Header.Get(headerContentEncoding)
|
|
|
|
decoder, ok := d.decoders[encoding]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported %s: %s", headerContentEncoding, encoding)
|
|
}
|
|
return decoder(r.Body)
|
|
}
|
|
|
|
// defaultErrorHandler writes the error message in plain text.
|
|
func defaultErrorHandler(w http.ResponseWriter, _ *http.Request, errMsg string, statusCode int) {
|
|
http.Error(w, errMsg, statusCode)
|
|
}
|