306 lines
7.1 KiB
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)
|
|
}
|