CAFxX/httpcompression

Feature request: add hooks to enable compression metrics

jschaf opened this issue · 1 comments

Hi there. First off, thank you for the high-quality library. I rolled my own fork of the https://github.com/nytimes/gziphandler and was contemplating a brotli version before happily discovering your repo.

I recently did an optimization pass on my server. I noticed the server was spending 200 ms to encode large RPC responses using the Brotli default compression level of 6. It took a while to track down because I had to infer the time spent on compression by adding logs to the handler before and after the httpcompression handler. I figured other folks would benefit from metrics, hence this feature request.

I'd like to be able to emit traces and metrics for compression to answer the following questions:

  • How long did the compression take?
  • Which compression algorithm was used, and at what compression level?
  • What was the compression ratio?
  • What was the input size and output size?

I've seen a few approaches to support tracing.

The struct is advantageous because you can add new methods without breaking existing clients. With interfaces, the library author needs to add new interfaces for additional functionality and check to see if the user-provided tracer supports the new interface, i.e., interface smuggling.

I like the httptrace approach of using a single struct with optional methods. As a straw man, maybe something like:

func handleRequest() {
	compressHandler, err := httpcompression.Adapter(
		httpcompression.BrotliCompressionLevel(brotli.BestSpeed),
		// Need a factory here to create a new trace for each request.
		httpcompression.TraceProvider(func(ctx context.Context) *CompressTrace {
			span := trace.SpanFromContext(ctx)
			return &CompressTrace{
				CompressStart: func(info CompressStartInfo) {
					span.AddEvent("compress start",
						trace.String("content_type", info.ContentType),
						trace.String("content_encoding", info.ContentEncoding),
					)
				},
				CompressDone: func(info CompressDoneInfo) {
					span.AddEvent("compress done",
						trace.Int("bytes_read", info.BytesRead),
						trace.Int("bytes_written", info.BytesWritten),
						trace.Float64("compression_ratio", float64(info.BytesWritten)/float64(info.BytesRead)),
					)
				},
			}
		}),
	)
}

// NOTE: structs are a bit overkill compared to inlining the args into the function call.
// The benefit is the library can add new fields without breaking existing clients.
// httptrace uses a mixture of structs and inlined args.
type CompressStartInfo struct {
	// ContentType is the content type of the response.
	ContentType string
	// ContentEncoding is the encoding used for the response.
	ContentEncoding string
}

type CompressDoneInfo struct {
	BytesRead    int
	BytesWritten int
	// Others:
	// * ContentEncoding - if it can change from CompressStartInfo
	// * allocations - not sure if feasible
}

type CompressTrace struct {
	// StartCompress is called when compression starts.
	CompressStart func(info CompressStartInfo)
	CompressDone  func(info CompressDoneInfo)
	// Others:
	// * WroteHeaders
	// * Close
}

I'll test out an implementation here: https://github.com/simple-circle/httpcompression/commit/134d732e749ba38a26997fb36b380c02b71ba245

Let me know if you're interested in a PR, and I'll send it your way.