Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add util/http #34

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5665107
Bazel-ify Cortex (#342)
tomwilkie May 2, 2017
c1bafed
Support raw snappy encoded requests (#412)
tomwilkie May 10, 2017
1374f77
We should actually try and decompress the requests if reading the bod…
tomwilkie Jun 20, 2017
1bef0dd
Port Cortex to use Prometheus 2 packages (#583)
juliusv Nov 13, 2017
a42693f
Don't pass a http.Request to ParseProtoRequest; pass an io.Reader to …
tomwilkie May 21, 2018
87933e5
Update instrumentation calls to remove deprecated interface
bboreham Jan 2, 2019
1405446
Add more tracing detail when parsing incoming requests
bboreham Jul 18, 2019
9fb0b64
Use pools to reduce allocs in distributor write path (#1526)
gouthamve Aug 8, 2019
888d3dd
Pre-allocate buffer for reading protobufs, for performance
bboreham Sep 28, 2019
26f526f
Rename size->expectedSize for clarity
bboreham Oct 18, 2019
57099a9
Add a maximum receive size to distributor
bboreham Oct 18, 2019
0de2892
Update code to fix almost all pre-existing lint errors (#2008)
zendern Feb 24, 2020
52a6776
Support Accept: application/json on common HTTP endpoints (#2673)
joe-elliott Jun 2, 2020
be57bd8
Don't return raw buffer from util.ParseProtoReader() (#3328)
bboreham Oct 13, 2020
e1f553e
Memberlist: keep a buffer with sent and received messages for trouble…
pstibrany Dec 11, 2020
4a4f89b
feat: Add additional modes for config endpoint (#3645)
simonswine Jan 8, 2021
4f74a0b
Improve decompression within ParseProtoReader. (#3682)
cyriltovena Jan 21, 2021
16c874c
feat: add TLS support for communication from the Ruler-->AM (#3752)
jtlisi Feb 2, 2021
fbe8112
Alertmanager Distributor (#3671)
codesome Feb 18, 2021
3a5ba9a
Removed usage of deprecated pkg/ingester/client stuff (#3915)
pracucci Mar 10, 2021
98420fe
add api to list all alertmanager configs and rule groups (#3529)
Apr 21, 2021
805f1b2
Fix TestStreamWriteYAMLResponse flakyness (#4122)
pracucci Apr 28, 2021
7a6254f
Distributors rings status (#4151)
ethervoid May 6, 2021
0ee8b62
Support ingesting exemplars into TSDB when blocks storage is enabled …
mdisibio May 6, 2021
5282724
Make http package generic
Sep 9, 2021
55f7f93
Mod tidy
Sep 9, 2021
c7a90a3
Add changelog message
Sep 9, 2021
72b91d4
Merge remote-tracking branch 'upstream/main' into util-http
Sep 10, 2021
a568d0d
Review changes
Sep 10, 2021
48e693a
Fix linter error
Sep 10, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* [CHANGE] Removed global metrics for KV package. Making a KV object will now require a prometheus registerer that will
be used to register all relevant KV class metrics. #22
* [CHANGE] Added CHANGELOG.md and Pull Request template to reference the changelog
* [ENHANCEMENT] Add middleware package. #38
* [ENHANCEMENT] Add http package from cortex #34
* [ENHANCEMENT] Add middleware package. #38
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/hashicorp/go-sockaddr v1.0.2
github.com/hashicorp/memberlist v0.2.3
github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect
github.com/opentracing/opentracing-go v1.2.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.26.0
Expand Down
268 changes: 268 additions & 0 deletions http/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package http

import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"html/template"
"io"
"net/http"
"strings"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/opentracing/opentracing-go"
otlog "github.com/opentracing/opentracing-go/log"
"gopkg.in/yaml.v2"
)

const messageSizeLargerErrFmt = "received message larger than max (%d vs %d)"

// IsRequestBodyTooLarge returns true if the error is "http: request body too large".
func IsRequestBodyTooLarge(err error) bool {
return err != nil && strings.Contains(err.Error(), "http: request body too large")
}

// BasicAuth configures basic authentication for HTTP clients.
type BasicAuth struct {
Username string `yaml:"basic_auth_username"`
Password string `yaml:"basic_auth_password"`
}

func (b *BasicAuth) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
f.StringVar(&b.Username, prefix+"basic-auth-username", "", "HTTP Basic authentication username. It overrides the username set in the URL (if any).")
f.StringVar(&b.Password, prefix+"basic-auth-password", "", "HTTP Basic authentication password. It overrides the password set in the URL (if any).")
}

// IsEnabled returns false if basic authentication isn't enabled.
func (b BasicAuth) IsEnabled() bool {
return b.Username != "" || b.Password != ""
}

// WriteJSONResponse writes some JSON as a HTTP response.
func WriteJSONResponse(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")

data, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// We ignore errors here, because we cannot do anything about them.
// Write will trigger sending Status code, so we cannot send a different status code afterwards.
// Also this isn't internal error, but error communicating with client.
_, _ = w.Write(data)
}

// WriteYAMLResponse writes some YAML as a HTTP response.
func WriteYAMLResponse(w http.ResponseWriter, v interface{}) {
// There is not standardised content-type for YAML, text/plain ensures the
// YAML is displayed in the browser instead of offered as a download
w.Header().Set("Content-Type", "text/plain; charset=utf-8")

data, err := yaml.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// We ignore errors here, because we cannot do anything about them.
// Write will trigger sending Status code, so we cannot send a different status code afterwards.
// Also this isn't internal error, but error communicating with client.
_, _ = w.Write(data)
}

// WriteTextResponse sends a message as text/plain response with 200 status code.
func WriteTextResponse(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/plain")

// Ignore inactionable errors.
_, _ = w.Write([]byte(message))
}

// WriteHTMLResponse sends a message as text/html response with 200 status code.
func WriteHTMLResponse(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/html")

// Ignore inactionable errors.
_, _ = w.Write([]byte(message))
}

// RenderHTTPResponse either responds with json or a rendered html page using the passed in template
// by checking the Accepts header
func RenderHTTPResponse(w http.ResponseWriter, v interface{}, t *template.Template, r *http.Request) {
accept := r.Header.Get("Accept")
if strings.Contains(accept, "application/json") {
WriteJSONResponse(w, v)
return
}

err := t.Execute(w, v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

// StreamWriteYAMLResponse stream writes data as http response
func StreamWriteYAMLResponse(w http.ResponseWriter, iter chan interface{}, logger log.Logger) {
w.Header().Set("Content-Type", "application/yaml")
for v := range iter {
data, err := yaml.Marshal(v)
if err != nil {
level.Error(logger).Log("msg", "yaml marshal failed", "err", err)
continue
}
_, err = w.Write(data)
if err != nil {
level.Error(logger).Log("msg", "write http response failed", "err", err)
return
}
}
}

// CompressionType for encoding and decoding requests and responses.
type CompressionType int

// Values for CompressionType
const (
NoCompression CompressionType = iota
RawSnappy
)

// ParseProtoReader parses a compressed proto from an io.Reader.
func ParseProtoReader(ctx context.Context, reader io.Reader, expectedSize, maxSize int, req proto.Message, compression CompressionType) error {
sp := opentracing.SpanFromContext(ctx)
if sp != nil {
sp.LogFields(otlog.String("event", "util.ParseProtoRequest[start reading]"))
}
body, err := decompressRequest(reader, expectedSize, maxSize, compression, sp)
if err != nil {
return err
}

if sp != nil {
sp.LogFields(otlog.String("event", "util.ParseProtoRequest[unmarshal]"), otlog.Int("size", len(body)))
}

// We re-implement proto.Unmarshal here as it calls XXX_Unmarshal first,
// which we can't override without upsetting golint.
req.Reset()
if u, ok := req.(proto.Unmarshaler); ok {
err = u.Unmarshal(body)
} else {
err = proto.NewBuffer(body).Unmarshal(req)
}
if err != nil {
return err
}

return nil
}

func decompressRequest(reader io.Reader, expectedSize, maxSize int, compression CompressionType, sp opentracing.Span) (body []byte, err error) {
defer func() {
if err != nil && len(body) > maxSize {
err = fmt.Errorf(messageSizeLargerErrFmt, len(body), maxSize)
}
}()
if expectedSize > maxSize {
return nil, fmt.Errorf(messageSizeLargerErrFmt, expectedSize, maxSize)
}
buffer, ok := tryBufferFromReader(reader)
if ok {
body, err = decompressFromBuffer(buffer, maxSize, compression, sp)
return
}
body, err = decompressFromReader(reader, expectedSize, maxSize, compression, sp)
return
}

func decompressFromReader(reader io.Reader, expectedSize, maxSize int, compression CompressionType, sp opentracing.Span) ([]byte, error) {
var (
buf bytes.Buffer
body []byte
err error
)
if expectedSize > 0 {
buf.Grow(expectedSize + bytes.MinRead) // extra space guarantees no reallocation
}
// Read from LimitReader with limit max+1. So if the underlying
// reader is over limit, the result will be bigger than max.
reader = io.LimitReader(reader, int64(maxSize)+1)
switch compression {
case NoCompression:
_, err = buf.ReadFrom(reader)
body = buf.Bytes()
case RawSnappy:
_, err = buf.ReadFrom(reader)
if err != nil {
return nil, err
}
body, err = decompressFromBuffer(&buf, maxSize, RawSnappy, sp)
}
return body, err
}

func decompressFromBuffer(buffer *bytes.Buffer, maxSize int, compression CompressionType, sp opentracing.Span) ([]byte, error) {
if len(buffer.Bytes()) > maxSize {
return nil, fmt.Errorf(messageSizeLargerErrFmt, len(buffer.Bytes()), maxSize)
}
switch compression {
case NoCompression:
return buffer.Bytes(), nil
case RawSnappy:
if sp != nil {
sp.LogFields(otlog.String("event", "util.ParseProtoRequest[decompress]"),
otlog.Int("size", len(buffer.Bytes())))
}
size, err := snappy.DecodedLen(buffer.Bytes())
if err != nil {
return nil, err
}
if size > maxSize {
return nil, fmt.Errorf(messageSizeLargerErrFmt, size, maxSize)
}
body, err := snappy.Decode(nil, buffer.Bytes())
if err != nil {
return nil, err
}
return body, nil
}
return nil, nil
}

// tryBufferFromReader attempts to cast the reader to a `*bytes.Buffer` this is possible when using httpgrpc.
// If it fails it will return nil and false.
func tryBufferFromReader(reader io.Reader) (*bytes.Buffer, bool) {
if bufReader, ok := reader.(interface {
BytesBuffer() *bytes.Buffer
}); ok && bufReader != nil {
return bufReader.BytesBuffer(), true
}
return nil, false
}

// SerializeProtoResponse serializes a protobuf response into an HTTP response.
func SerializeProtoResponse(w http.ResponseWriter, resp proto.Message, compression CompressionType) error {
data, err := proto.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return fmt.Errorf("error marshaling proto response: %v", err)
}

switch compression {
case NoCompression:
case RawSnappy:
data = snappy.Encode(nil, data)
}
if _, err := w.Write(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return fmt.Errorf("error sending proto response: %v", err)
}
return nil
}
Loading