influxdb/telemetry/handler_test.go

306 lines
7.1 KiB
Go

package telemetry
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
pr "github.com/influxdata/influxdb/v2/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"go.uber.org/zap/zaptest"
"google.golang.org/protobuf/proto"
)
func TestPushGateway_Handler(t *testing.T) {
type fields struct {
Store *mockStore
now func() time.Time
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
contentType string
wantStatus int
want []byte
}{
{
name: "unknown content-type is a bad request",
fields: fields{
Store: &mockStore{},
},
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("POST", "/", nil),
},
wantStatus: http.StatusBadRequest,
},
{
name: "bad metric with timestamp is a bad request",
fields: fields{
Store: &mockStore{},
now: func() time.Time { return time.Unix(0, 0) },
},
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("POST", "/",
mustEncode(t,
[]*dto.MetricFamily{badMetric()},
),
),
},
contentType: string(expfmt.FmtProtoDelim),
wantStatus: http.StatusBadRequest,
},
{
name: "store error is an internal server error",
fields: fields{
Store: &mockStore{
err: fmt.Errorf("e1"),
},
now: func() time.Time { return time.Unix(0, 0) },
},
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("POST", "/",
mustEncode(t,
[]*dto.MetricFamily{NewCounter("mf1", 1.0, pr.L("n1", "v1"))},
),
),
},
contentType: string(expfmt.FmtProtoDelim),
wantStatus: http.StatusInternalServerError,
want: []byte(`[{"name":"mf1","type":0,"metric":[{"label":[{"name":"n1","value":"v1"}],"counter":{"value":1},"timestamp_ms":0}]}]`),
},
{
name: "metric store in store",
fields: fields{
Store: &mockStore{},
now: func() time.Time { return time.Unix(0, 0) },
},
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("POST", "/",
mustEncode(t,
[]*dto.MetricFamily{NewCounter("mf1", 1.0, pr.L("n1", "v1"))},
),
),
},
contentType: string(expfmt.FmtProtoDelim),
wantStatus: http.StatusAccepted,
want: []byte(`[{"name":"mf1","type":0,"metric":[{"label":[{"name":"n1","value":"v1"}],"counter":{"value":1},"timestamp_ms":0}]}]`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewPushGateway(
zaptest.NewLogger(t),
tt.fields.Store,
&AddTimestamps{
now: tt.fields.now,
},
)
p.Encoder = &pr.JSON{}
tt.args.r.Header.Set("Content-Type", tt.contentType)
p.Handler(tt.args.w, tt.args.r)
if tt.args.w.Code != http.StatusAccepted {
t.Logf("Body: %s", tt.args.w.Body.String())
}
if got, want := tt.args.w.Code, tt.wantStatus; got != want {
t.Errorf("PushGateway.Handler() StatusCode = %v, want %v", got, want)
}
if got, want := tt.fields.Store.data, tt.want; string(got) != string(want) {
t.Errorf("PushGateway.Handler() Data = %s, want %s", got, want)
}
})
}
}
func Test_decodePostMetricsRequest(t *testing.T) {
type args struct {
req *http.Request
maxBytes int64
}
tests := []struct {
name string
args args
contentType string
want []*dto.MetricFamily
wantErr bool
}{
{
name: "bad body returns no metrics",
args: args{
req: httptest.NewRequest("POST", "/", bytes.NewBuffer([]byte{0x10})),
maxBytes: 10,
},
contentType: string(expfmt.FmtProtoDelim),
want: []*dto.MetricFamily{},
},
{
name: "no body returns no metrics",
args: args{
req: httptest.NewRequest("POST", "/", nil),
maxBytes: 10,
},
contentType: string(expfmt.FmtProtoDelim),
want: []*dto.MetricFamily{},
},
{
name: "metrics are returned from POST",
args: args{
req: httptest.NewRequest("POST", "/",
mustEncode(t,
[]*dto.MetricFamily{NewCounter("mf1", 1.0, pr.L("n1", "v1"))},
),
),
maxBytes: 31,
},
contentType: string(expfmt.FmtProtoDelim),
want: []*dto.MetricFamily{NewCounter("mf1", 1.0, pr.L("n1", "v1"))},
},
{
name: "max bytes limits on record boundary returns a single record",
args: args{
req: httptest.NewRequest("POST", "/",
mustEncode(t,
[]*dto.MetricFamily{
NewCounter("mf1", 1.0, pr.L("n1", "v1")),
NewCounter("mf2", 1.0, pr.L("n2", "v2")),
},
),
),
maxBytes: 31,
},
contentType: string(expfmt.FmtProtoDelim),
want: []*dto.MetricFamily{NewCounter("mf1", 1.0, pr.L("n1", "v1"))},
},
{
name: "exceeding max bytes returns an error",
args: args{
req: httptest.NewRequest("POST", "/",
mustEncode(t,
[]*dto.MetricFamily{
NewCounter("mf1", 1.0, pr.L("n1", "v1")),
NewCounter("mf2", 1.0, pr.L("n2", "v2")),
},
),
),
maxBytes: 33,
},
contentType: string(expfmt.FmtProtoDelim),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.req.Header.Set("Content-Type", tt.contentType)
got, err := decodePostMetricsRequest(tt.args.req.Body, expfmt.Format(tt.contentType), tt.args.maxBytes)
if (err != nil) != tt.wantErr {
t.Errorf("decodePostMetricsRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("decodePostMetricsRequest() = %v, want %v", got, tt.want)
}
})
}
}
func badMetric() *dto.MetricFamily {
return &dto.MetricFamily{
Name: proto.String("bad"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{pr.L("n1", "v1")},
Counter: &dto.Counter{
Value: proto.Float64(1.0),
},
TimestampMs: proto.Int64(1),
},
},
}
}
func goodMetric() *dto.MetricFamily {
return &dto.MetricFamily{
Name: proto.String("good"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Label: []*dto.LabelPair{pr.L("n1", "v1")},
Counter: &dto.Counter{
Value: proto.Float64(1.0),
},
},
},
}
}
func Test_valid(t *testing.T) {
type args struct {
mfs []*dto.MetricFamily
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "metric with timestamp is invalid",
args: args{
mfs: []*dto.MetricFamily{badMetric()},
},
wantErr: true,
},
{
name: "metric without timestamp is valid",
args: args{
mfs: []*dto.MetricFamily{goodMetric()},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := valid(tt.args.mfs); (err != nil) != tt.wantErr {
t.Errorf("valid() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
type mockStore struct {
data []byte
err error
}
func (m *mockStore) WriteMessage(ctx context.Context, data []byte) error {
m.data = data
return m.err
}
func mustEncode(t *testing.T, mfs []*dto.MetricFamily) io.Reader {
b, err := pr.EncodeExpfmt(mfs)
if err != nil {
t.Fatalf("unable to encode %v", err)
}
return bytes.NewBuffer(b)
}