diff --git a/config/config.go b/config/config.go index 5050239f6..e75c026ea 100644 --- a/config/config.go +++ b/config/config.go @@ -57,6 +57,7 @@ type Proxy struct { TLSHeader string TLSHeaderValue string GZIPContentTypes *regexp.Regexp + RequestID string } type Runtime struct { diff --git a/config/load.go b/config/load.go index d1e696ce8..6cb4a4ed2 100644 --- a/config/load.go +++ b/config/load.go @@ -126,6 +126,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.StringVar(&cfg.Proxy.ClientIPHeader, "proxy.header.clientip", defaultConfig.Proxy.ClientIPHeader, "header for the request ip") f.StringVar(&cfg.Proxy.TLSHeader, "proxy.header.tls", defaultConfig.Proxy.TLSHeader, "header for TLS connections") f.StringVar(&cfg.Proxy.TLSHeaderValue, "proxy.header.tls.value", defaultConfig.Proxy.TLSHeaderValue, "value for TLS connection header") + f.StringVar(&cfg.Proxy.RequestID, "proxy.header.requestid", defaultConfig.Proxy.RequestID, "header for reqest id") f.StringVar(&gzipContentTypesValue, "proxy.gzip.contenttype", defaultValues.GZIPContentTypesValue, "regexp of content types to compress") f.StringSliceVar(&listenerValue, "proxy.addr", defaultValues.ListenerValue, "listener config") f.KVSliceVar(&certSourcesValue, "proxy.cs", defaultValues.CertSourcesValue, "certificate sources") diff --git a/config/load_test.go b/config/load_test.go index af70f30d5..53d246242 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -302,6 +302,13 @@ func TestLoad(t *testing.T) { return cfg }, }, + { + args: []string{"-proxy.header.requestid", "value"}, + cfg: func(cfg *Config) *Config { + cfg.Proxy.RequestID = "value" + return cfg + }, + }, { args: []string{"-proxy.gzip.contenttype", `^text/.*$`}, cfg: func(cfg *Config) *Config { diff --git a/fabio.properties b/fabio.properties index e35f74282..4bbf269d4 100644 --- a/fabio.properties +++ b/fabio.properties @@ -353,6 +353,15 @@ # proxy.header.tls.value = +# proxy.header.requestid configures the header for the adding a unique request id. +# When set non-empty value the proxy will set this header on every request to the +# unique UUID value. +# +# The default is +# +# proxy.header.requestid = + + # proxy.gzip.contenttype configures which responses should be compressed. # # By default, responses sent to the client are not compressed even if the diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index 22741c5a4..8a629cdbf 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -50,6 +50,31 @@ func TestProxyProducesCorrectXffHeader(t *testing.T) { } } +func TestProxyRequestIDHeader(t *testing.T) { + got := "not called" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = r.Header.Get("X-Request-ID") + })) + defer server.Close() + + proxy := httptest.NewServer(&HTTPProxy{ + Config: config.Proxy{RequestID: "X-Request-Id"}, + Transport: http.DefaultTransport, + UUID: func() string { return "f47ac10b-58cc-0372-8567-0e02b2c3d479" }, + Lookup: func(r *http.Request) *route.Target { + return &route.Target{URL: mustParse(server.URL)} + }, + }) + defer proxy.Close() + + req, _ := http.NewRequest("GET", proxy.URL, nil) + mustDo(req) + + if want := "f47ac10b-58cc-0372-8567-0e02b2c3d479"; got != want { + t.Errorf("got %v, but want %v", got, want) + } +} + func TestProxyNoRouteStaus(t *testing.T) { proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{NoRouteStatus: 999}, diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index 7e84254c7..4056d07be 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -15,6 +15,7 @@ import ( "github.com/fabiolb/fabio/metrics" "github.com/fabiolb/fabio/proxy/gzip" "github.com/fabiolb/fabio/route" + "github.com/fabiolb/fabio/uuid" ) // HTTPProxy is a dynamic reverse proxy for HTTP and HTTPS protocols. @@ -48,6 +49,10 @@ type HTTPProxy struct { // Logger is the access logger for the requests. Logger logger.Logger + + // UUID returns a unique id in uuid format. + // If UUID is nil, uuid.NewUUID() is used. + UUID func() string } func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -66,6 +71,14 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if p.Config.RequestID != "" { + id := p.UUID + if id == nil { + id = uuid.NewUUID + } + r.Header.Set(p.Config.RequestID, id()) + } + // build the request url since r.URL will get modified // by the reverse proxy and contains only the RequestURI anyway requestURL := &url.URL{ diff --git a/uuid/format.go b/uuid/format.go new file mode 100644 index 000000000..542424139 --- /dev/null +++ b/uuid/format.go @@ -0,0 +1,62 @@ +package uuid + +// Fast UUID formatting adapted from +// https://github.com/m4rw3r/uuid/blob/master/uuid.go + +// hexchar2byte contains the integer byte-value represented by a hexadecimal character, +// 255 if it is an invalid character. +var hexchar2byte = []byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// halfbyte2hexchar contains an array of character values corresponding to +// hexadecimal values for the position in the array, 0 to 15 (0x0-0xf, half-byte). +var halfbyte2hexchar = []byte{ + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, +} + +// ToString formats raw UUID bytes as a standard UUID string +func ToString(u [24]byte) string { + /* It is a lot (~10x) faster to allocate a byte slice of specific size and + then use a lookup table to write the characters to the byte-array and + finally cast to string instead of using fmt.Sprintf() */ + /* Slightly faster to not use make([]byte, 36), guessing either call + overhead or slice-header overhead is the cause */ + b := [36]byte{} + + for i, n := range []int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + b[n] = halfbyte2hexchar[(u[i]>>4)&0x0f] + b[n+1] = halfbyte2hexchar[u[i]&0x0f] + } + + b[8] = '-' + b[13] = '-' + b[18] = '-' + b[23] = '-' + + /* Oddly does not seem to cause a memory allocation, + internal data-array is most likely just moved over + to the string-header: */ + return string(b[:]) +} diff --git a/uuid/uuid.go b/uuid/uuid.go new file mode 100644 index 000000000..68dcb1308 --- /dev/null +++ b/uuid/uuid.go @@ -0,0 +1,12 @@ +package uuid + +import ( + "github.com/rogpeppe/fastuuid" +) + +var generator = fastuuid.MustNewGenerator() + +// NewUUID return UUID in string fromat +func NewUUID() string { + return ToString(generator.Next()) +}