feat(telemetry): Do not record telemetry data for invalid requests; replace invalid static asset paths with a slug (#21958)
* feat: record telemetry data only on successful response codes * feat: re-write request path in file handler * fix: use a slug for the replacement path * chore: update CHANGELOG * fix: report for 5XX also * fix: address review commentspull/21971/head
parent
5d84c602c8
commit
4db7978b32
|
@ -52,6 +52,7 @@ This release adds an embedded SQLite database for storing metadata required by t
|
||||||
1. [21936](https://github.com/influxdata/influxdb/pull/21936): Ported the `influxd inspect build-tsi` command from 1.x.
|
1. [21936](https://github.com/influxdata/influxdb/pull/21936): Ported the `influxd inspect build-tsi` command from 1.x.
|
||||||
1. [21910](https://github.com/influxdata/influxdb/pull/21910): Added `--ui-disabled` option to `influxd` to allow for running with the UI disabled.
|
1. [21910](https://github.com/influxdata/influxdb/pull/21910): Added `--ui-disabled` option to `influxd` to allow for running with the UI disabled.
|
||||||
1. [21938](https://github.com/influxdata/influxdb/pull/21938): Added route to delete individual secret.
|
1. [21938](https://github.com/influxdata/influxdb/pull/21938): Added route to delete individual secret.
|
||||||
|
1. [21958](https://github.com/influxdata/influxdb/pull/21958): Telemetry improvements: Do not record telemetry data for non-existant paths; replace invalid static asset paths with a slug.
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|
|
@ -45,14 +45,21 @@ func Metrics(name string, reqMetric *prometheus.CounterVec, durMetric *prometheu
|
||||||
statusW := NewStatusResponseWriter(w)
|
statusW := NewStatusResponseWriter(w)
|
||||||
|
|
||||||
defer func(start time.Time) {
|
defer func(start time.Time) {
|
||||||
|
statusCode := statusW.Code()
|
||||||
|
// only log metrics for 2XX or 5XX requests
|
||||||
|
if !reportFromCode(statusCode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
label := prometheus.Labels{
|
label := prometheus.Labels{
|
||||||
"handler": name,
|
"handler": name,
|
||||||
"method": r.Method,
|
"method": r.Method,
|
||||||
"path": normalizePath(r.URL.Path),
|
"path": normalizePath(r.URL.Path),
|
||||||
"status": statusW.StatusCodeClass(),
|
"status": statusW.StatusCodeClass(),
|
||||||
"response_code": fmt.Sprintf("%d", statusW.Code()),
|
"response_code": fmt.Sprintf("%d", statusCode),
|
||||||
"user_agent": UserAgent(r),
|
"user_agent": UserAgent(r),
|
||||||
}
|
}
|
||||||
|
|
||||||
durMetric.With(label).Observe(time.Since(start).Seconds())
|
durMetric.With(label).Observe(time.Since(start).Seconds())
|
||||||
reqMetric.With(label).Inc()
|
reqMetric.With(label).Inc()
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
@ -239,3 +246,9 @@ func OrgIDFromContext(ctx context.Context) *platform.ID {
|
||||||
id := v.(platform.ID)
|
id := v.(platform.ID)
|
||||||
return &id
|
return &id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reportFromCode is a helper function to determine if telemetry data should be
|
||||||
|
// reported for this response.
|
||||||
|
func reportFromCode(c int) bool {
|
||||||
|
return (c >= 200 && c <= 299) || (c >= 500 && c <= 599)
|
||||||
|
}
|
||||||
|
|
|
@ -2,15 +2,108 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/kit/platform"
|
"github.com/influxdata/influxdb/v2/kit/platform"
|
||||||
|
"github.com/influxdata/influxdb/v2/kit/prom"
|
||||||
|
"github.com/influxdata/influxdb/v2/kit/prom/promtest"
|
||||||
"github.com/influxdata/influxdb/v2/pkg/testttp"
|
"github.com/influxdata/influxdb/v2/pkg/testttp"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMetrics(t *testing.T) {
|
||||||
|
labels := []string{"handler", "method", "path", "status", "response_code", "user_agent"}
|
||||||
|
|
||||||
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/serverError" {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/redirect" {
|
||||||
|
w.WriteHeader(http.StatusPermanentRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
reqPath string
|
||||||
|
wantCount int
|
||||||
|
labelResponse string
|
||||||
|
labelStatus string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "counter increments on OK (2XX) ",
|
||||||
|
reqPath: "/",
|
||||||
|
wantCount: 1,
|
||||||
|
labelResponse: "200",
|
||||||
|
labelStatus: "2XX",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counter does not increment on not found (4XX)",
|
||||||
|
reqPath: "/badpath",
|
||||||
|
wantCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counter increments on server error (5XX)",
|
||||||
|
reqPath: "/serverError",
|
||||||
|
wantCount: 1,
|
||||||
|
labelResponse: "500",
|
||||||
|
labelStatus: "5XX",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "counter does not increment on redirect (3XX)",
|
||||||
|
reqPath: "/redirect",
|
||||||
|
wantCount: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
counter := prometheus.NewCounterVec(prometheus.CounterOpts{Name: "counter"}, labels)
|
||||||
|
hist := prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "hist"}, labels)
|
||||||
|
reg := prom.NewRegistry(zaptest.NewLogger(t))
|
||||||
|
reg.MustRegister(counter, hist)
|
||||||
|
|
||||||
|
metricsMw := Metrics("testing", counter, hist)
|
||||||
|
svr := metricsMw(nextHandler)
|
||||||
|
r := httptest.NewRequest("GET", tt.reqPath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
svr.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
mfs := promtest.MustGather(t, reg)
|
||||||
|
m := promtest.FindMetric(mfs, "counter", map[string]string{
|
||||||
|
"handler": "testing",
|
||||||
|
"method": "GET",
|
||||||
|
"path": tt.reqPath,
|
||||||
|
"response_code": tt.labelResponse,
|
||||||
|
"status": tt.labelStatus,
|
||||||
|
"user_agent": "unknown",
|
||||||
|
})
|
||||||
|
|
||||||
|
if tt.wantCount == 0 {
|
||||||
|
require.Nil(t, m)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.wantCount, int(m.Counter.GetValue()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_normalizePath(t *testing.T) {
|
func Test_normalizePath(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
@ -32,6 +32,11 @@ const (
|
||||||
|
|
||||||
// swaggerFile is the name of the swagger JSON.
|
// swaggerFile is the name of the swagger JSON.
|
||||||
swaggerFile = "swagger.json"
|
swaggerFile = "swagger.json"
|
||||||
|
|
||||||
|
// fallbackPathSlug is the path to re-write on the request if the requested
|
||||||
|
// path does not match a file and the default file is served. For telemetry
|
||||||
|
// and metrics reporting purposes.
|
||||||
|
fallbackPathSlug = "/:fallback_path"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAssetHandler returns an http.Handler to serve files from the provided
|
// NewAssetHandler returns an http.Handler to serve files from the provided
|
||||||
|
@ -94,25 +99,36 @@ func swaggerHandler(fileOpener http.FileSystem) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// assetHandler returns a handler that either serves the file at that path, or
|
// assetHandler returns a handler that either serves the file at that path, or
|
||||||
// the default file if a file cannot be found at that path.
|
// the default file if a file cannot be found at that path. If the default file
|
||||||
|
// is served, the request path is re-written to the root path to simplify
|
||||||
|
// metrics reporting.
|
||||||
func assetHandler(fileOpener http.FileSystem) http.Handler {
|
func assetHandler(fileOpener http.FileSystem) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
name := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
name := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
||||||
// If the root directory is being requested, respond with the default file.
|
// If the root directory is being requested, respond with the default file.
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = defaultFile
|
name = defaultFile
|
||||||
|
r.URL.Path = "/" + defaultFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to open the file requested by name, falling back to the default file.
|
// Try to open the file requested by name, falling back to the default file.
|
||||||
// If even the default file can't be found, the binary must not have been
|
// If even the default file can't be found, the binary must not have been
|
||||||
// built with assets, so respond with not found.
|
// built with assets, so respond with not found.
|
||||||
f, err := openAsset(fileOpener, name)
|
f, fallback, err := openAsset(fileOpener, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
|
// If the default file will be served because the requested path didn't
|
||||||
|
// match any existing files, re-write the request path a placeholder value.
|
||||||
|
// This is to ensure that metrics do not get collected for an arbitrarily
|
||||||
|
// large range of incorrect paths.
|
||||||
|
if fallback {
|
||||||
|
r.URL.Path = fallbackPathSlug
|
||||||
|
}
|
||||||
|
|
||||||
staticFileHandler(f).ServeHTTP(w, r)
|
staticFileHandler(f).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,18 +172,21 @@ func staticFileHandler(f fs.File) http.Handler {
|
||||||
// openAsset attempts to open the asset by name in the given directory, falling
|
// openAsset attempts to open the asset by name in the given directory, falling
|
||||||
// back to the default file if the named asset can't be found. Returns an error
|
// back to the default file if the named asset can't be found. Returns an error
|
||||||
// if even the default asset can't be opened.
|
// if even the default asset can't be opened.
|
||||||
func openAsset(fileOpener http.FileSystem, name string) (fs.File, error) {
|
func openAsset(fileOpener http.FileSystem, name string) (fs.File, bool, error) {
|
||||||
|
var fallback bool
|
||||||
|
|
||||||
f, err := fileOpener.Open(name)
|
f, err := fileOpener.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
fallback = true
|
||||||
f, err = fileOpener.Open(defaultFile)
|
f, err = fileOpener.Open(defaultFile)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fallback, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, fallback, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// modTimeFromInfo gets the modification time from an fs.FileInfo. If this
|
// modTimeFromInfo gets the modification time from an fs.FileInfo. If this
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package static
|
package static
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
"time"
|
"time"
|
||||||
|
@ -11,7 +13,75 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAssetHandler(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
defaultData := []byte("this is the default file")
|
||||||
|
otherData := []byte("this is a different file")
|
||||||
|
|
||||||
|
m := http.FS(fstest.MapFS{
|
||||||
|
defaultFile: {
|
||||||
|
Data: defaultData,
|
||||||
|
ModTime: time.Now(),
|
||||||
|
},
|
||||||
|
"somethingElse.js": {
|
||||||
|
Data: otherData,
|
||||||
|
ModTime: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
reqPath string
|
||||||
|
newPath string
|
||||||
|
wantData []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "path matches default",
|
||||||
|
reqPath: "/" + defaultFile,
|
||||||
|
newPath: "/" + defaultFile,
|
||||||
|
wantData: defaultData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root path",
|
||||||
|
reqPath: "/",
|
||||||
|
newPath: "/" + defaultFile,
|
||||||
|
wantData: defaultData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path matches a file",
|
||||||
|
reqPath: "/somethingElse.js",
|
||||||
|
newPath: "/somethingElse.js",
|
||||||
|
wantData: otherData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path matches nothing",
|
||||||
|
reqPath: "/something_random",
|
||||||
|
newPath: fallbackPathSlug,
|
||||||
|
wantData: defaultData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := assetHandler(m)
|
||||||
|
r := httptest.NewRequest("GET", tt.reqPath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
b, err := io.ReadAll(w.Result().Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Result().StatusCode)
|
||||||
|
require.Equal(t, tt.wantData, b)
|
||||||
|
require.Equal(t, tt.newPath, r.URL.Path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestModTimeFromInfo(t *testing.T) {
|
func TestModTimeFromInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
nowTime := time.Now()
|
nowTime := time.Now()
|
||||||
|
|
||||||
timeFunc := func() (time.Time, error) {
|
timeFunc := func() (time.Time, error) {
|
||||||
|
@ -78,29 +148,34 @@ func TestOpenAsset(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
file string
|
file string
|
||||||
|
fallback bool
|
||||||
want []byte
|
want []byte
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"default file by name",
|
"default file by name",
|
||||||
defaultFile,
|
defaultFile,
|
||||||
|
false,
|
||||||
defaultData,
|
defaultData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"other file by name",
|
"other file by name",
|
||||||
"somethingElse.js",
|
"somethingElse.js",
|
||||||
|
false,
|
||||||
otherData,
|
otherData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"falls back to default if can't find",
|
"falls back to default if can't find",
|
||||||
"badFile.exe",
|
"badFile.exe",
|
||||||
|
true,
|
||||||
defaultData,
|
defaultData,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
gotFile, err := openAsset(m, tt.file)
|
gotFile, fallback, err := openAsset(m, tt.file)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.fallback, fallback)
|
||||||
|
|
||||||
got, err := ioutil.ReadAll(gotFile)
|
got, err := ioutil.ReadAll(gotFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -108,7 +183,6 @@ func TestOpenAsset(t *testing.T) {
|
||||||
require.Equal(t, tt.want, got)
|
require.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEtag(t *testing.T) {
|
func TestEtag(t *testing.T) {
|
||||||
|
|
Loading…
Reference in New Issue