package json_v1 import ( "encoding/json" "testing" "time" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf/plugins/inputs/zipkin/codec" ) func TestJSON_Decode(t *testing.T) { addr := func(i int64) *int64 { return &i } tests := []struct { name string octets []byte want []codec.Span wantErr bool }{ { name: "bad json is error", octets: []byte(` [ { ]`), wantErr: true, }, { name: "Decodes simple trace", octets: []byte(` [ { "traceId": "6b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c" } ]`), want: []codec.Span{ &span{ TraceID: "6b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", }, }, }, { name: "Decodes two spans", octets: []byte(` [ { "traceId": "6b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c" }, { "traceId": "6b221d5bc9e6496c", "name": "get-traces", "id": "c6946e9cb5d122b6", "parentId": "6b221d5bc9e6496c", "duration": 10000 } ]`), want: []codec.Span{ &span{ TraceID: "6b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", }, &span{ TraceID: "6b221d5bc9e6496c", SpanName: "get-traces", ID: "c6946e9cb5d122b6", ParentID: "6b221d5bc9e6496c", Dur: addr(10000), }, }, }, { name: "Decodes trace with timestamp", octets: []byte(` [ { "traceId": "6b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "timestamp": 1503031538791000 } ]`), want: []codec.Span{ &span{ TraceID: "6b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", Time: addr(1503031538791000), }, }, }, { name: "Decodes simple trace with high and low trace id", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c" } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", }, }, }, { name: "Error when trace id is null", octets: []byte(` [ { "traceId": null, "name": "get-traces", "id": "6b221d5bc9e6496c" } ]`), wantErr: true, }, { name: "ignore null parentId", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "parentId": null } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", }, }, }, { name: "ignore null timestamp", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "timestamp": null } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", }, }, }, { name: "ignore null duration", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "duration": null } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", }, }, }, { name: "ignore null annotation endpoint", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "annotations": [ { "timestamp": 1461750491274000, "value": "cs", "endpoint": null } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", Anno: []annotation{ { Time: 1461750491274000, Val: "cs", }, }, }, }, }, { name: "ignore null binary annotation endpoint", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "lc", "value": "JDBCSpanStore", "endpoint": null } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", BAnno: []binaryAnnotation{ { K: "lc", V: json.RawMessage(`"JDBCSpanStore"`), }, }, }, }, }, { name: "Error when binary annotation has no key", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "value": "JDBCSpanStore", "endpoint": null } ] } ]`), wantErr: true, }, { name: "Error when binary annotation has no value", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "lc", "endpoint": null } ] } ]`), wantErr: true, }, { name: "binary annotation with endpoint", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "lc", "value": "JDBCSpanStore", "endpoint": { "serviceName": "service", "port": 65535 } } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", BAnno: []binaryAnnotation{ { K: "lc", V: json.RawMessage(`"JDBCSpanStore"`), Endpoint: &endpoint{ ServiceName: "service", Port: 65535, }, }, }, }, }, }, { name: "binary annotation with double value", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "num", "value": 1.23456789, "type": "DOUBLE" } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", BAnno: []binaryAnnotation{ { K: "num", V: json.RawMessage{0x31, 0x2e, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39}, Type: "DOUBLE", }, }, }, }, }, { name: "binary annotation with integer value", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "num", "value": 1, "type": "I16" } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", BAnno: []binaryAnnotation{ { K: "num", V: json.RawMessage{0x31}, Type: "I16", }, }, }, }, }, { name: "binary annotation with bool value", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "num", "value": true, "type": "BOOL" } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", BAnno: []binaryAnnotation{ { K: "num", V: json.RawMessage(`true`), Type: "BOOL", }, }, }, }, }, { name: "binary annotation with bytes value", octets: []byte(` [ { "traceId": "48485a3953bb61246b221d5bc9e6496c", "name": "get-traces", "id": "6b221d5bc9e6496c", "binaryAnnotations": [ { "key": "num", "value": "1", "type": "BYTES" } ] } ]`), want: []codec.Span{ &span{ TraceID: "48485a3953bb61246b221d5bc9e6496c", SpanName: "get-traces", ID: "6b221d5bc9e6496c", BAnno: []binaryAnnotation{ { K: "num", V: json.RawMessage(`"1"`), Type: "BYTES", }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { j := &JSON{} got, err := j.Decode(tt.octets) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, tt.want, got) }) } } func Test_span_Trace(t *testing.T) { tests := []struct { name string TraceID string want string wantErr bool }{ { name: "Trace IDs cannot be null", TraceID: "", wantErr: true, }, { name: "converts hex string correctly", TraceID: "deadbeef", want: "deadbeef", }, { name: "converts high and low trace id correctly", TraceID: "48485a3953bb61246b221d5bc9e6496c", want: "48485a3953bb61246b221d5bc9e6496c", }, { name: "errors when string isn't hex", TraceID: "oxdeadbeef", wantErr: true, }, { name: "errors when id is too long", TraceID: "1234567890abcdef1234567890abcdef1", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &span{ TraceID: tt.TraceID, } got, err := s.Trace() if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, tt.want, got) }) } } func Test_span_SpanID(t *testing.T) { tests := []struct { name string ID string want string wantErr bool }{ { name: "Span IDs cannot be null", ID: "", wantErr: true, }, { name: "validates known id correctly", ID: "b26412d1ac16767d", want: "b26412d1ac16767d", }, { name: "validates hex string correctly", ID: "deadbeef", want: "deadbeef", }, { name: "errors when string isn't hex", ID: "oxdeadbeef", wantErr: true, }, { name: "errors when id is too long", ID: "1234567890abcdef1", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &span{ ID: tt.ID, } got, err := s.SpanID() if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, tt.want, got) }) } } func Test_span_Parent(t *testing.T) { tests := []struct { name string ParentID string want string wantErr bool }{ { name: "when there is no parent return empty string", ParentID: "", want: "", }, { name: "validates hex string correctly", ParentID: "deadbeef", want: "deadbeef", }, { name: "errors when string isn't hex", ParentID: "oxdeadbeef", wantErr: true, }, { name: "errors when parent id is too long", ParentID: "1234567890abcdef1", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &span{ ParentID: tt.ParentID, } got, err := s.Parent() if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, tt.want, got) }) } } func Test_span_Timestamp(t *testing.T) { tests := []struct { name string Time *int64 want time.Time }{ { name: "converts to microseconds", Time: func(i int64) *int64 { return &i }(3000000), want: time.Unix(3, 0).UTC(), }, { name: "nil time should be zero time", Time: nil, want: time.Time{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &span{ Time: tt.Time, } require.Equal(t, tt.want, s.Timestamp()) }) } } func Test_span_Duration(t *testing.T) { tests := []struct { name string dur *int64 want time.Duration }{ { name: "converts from 3 microseconds", dur: func(i int64) *int64 { return &i }(3000000), want: 3 * time.Second, }, { name: "nil time should be zero duration", dur: nil, want: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &span{ Dur: tt.dur, } require.Equal(t, tt.want, s.Duration()) }) } } func Test_annotation(t *testing.T) { type fields struct { Endpoint *endpoint Time int64 Val string } tests := []struct { name string fields fields tm time.Time val string endpoint *endpoint }{ { name: "returns all fields", fields: fields{ Time: 3000000, Val: "myvalue", Endpoint: &endpoint{ ServiceName: "myservice", Ipv4: "127.0.0.1", Port: 443, }, }, tm: time.Unix(3, 0).UTC(), val: "myvalue", endpoint: &endpoint{ ServiceName: "myservice", Ipv4: "127.0.0.1", Port: 443, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { an := annotation(tt.fields) a := &an require.Equal(t, tt.tm, a.Timestamp()) require.Equal(t, tt.val, a.Value()) require.Equal(t, tt.endpoint, a.Host()) }) } } func Test_binaryAnnotation(t *testing.T) { type fields struct { K string V json.RawMessage Type string Endpoint *endpoint } tests := []struct { name string fields fields key string value string endpoint *endpoint }{ { name: "returns all fields", fields: fields{ K: "key", V: json.RawMessage(`"value"`), Endpoint: &endpoint{ ServiceName: "myservice", Ipv4: "127.0.0.1", Port: 443, }, }, key: "key", value: "value", endpoint: &endpoint{ ServiceName: "myservice", Ipv4: "127.0.0.1", Port: 443, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bin := binaryAnnotation(tt.fields) b := &bin require.Equal(t, tt.key, b.Key()) require.Equal(t, tt.value, b.Value()) require.Equal(t, tt.endpoint, b.Host()) }) } } func Test_endpoint_Host(t *testing.T) { type fields struct { Ipv4 string Port int } tests := []struct { name string fields fields want string }{ { name: "with port", fields: fields{ Ipv4: "127.0.0.1", Port: 443, }, want: "127.0.0.1:443", }, { name: "no port", fields: fields{ Ipv4: "127.0.0.1", }, want: "127.0.0.1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := &endpoint{ Ipv4: tt.fields.Ipv4, Port: tt.fields.Port, } require.Equal(t, tt.want, e.Host()) }) } } func Test_endpoint_Name(t *testing.T) { tests := []struct { name string ServiceName string want string }{ { name: "has service name", ServiceName: "myservicename", want: "myservicename", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := &endpoint{ ServiceName: tt.ServiceName, } require.Equal(t, tt.want, e.Name()) }) } } func TestTraceIDFromString(t *testing.T) { tests := []struct { name string s string want string wantErr bool }{ { name: "Convert hex string id", s: "6b221d5bc9e6496c", want: "6b221d5bc9e6496c", }, { name: "error : id too long", s: "1234567890abcdef1234567890abcdef1", wantErr: true, }, { name: "error : not parsable", s: "howdyhowdyhowdy", wantErr: true, }, { name: "Convert hex string with high/low", s: "48485a3953bb61246b221d5bc9e6496c", want: "48485a3953bb61246b221d5bc9e6496c", }, { name: "errors in high", s: "ERR85a3953bb61246b221d5bc9e6496c", wantErr: true, }, { name: "errors in low", s: "48485a3953bb61246b221d5bc9e64ERR", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := traceIDFromString(tt.s) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, tt.want, got) }) } } func TestIDFromString(t *testing.T) { tests := []struct { name string s string want string wantErr bool }{ { name: "validates hex string id", s: "6b221d5bc9e6496c", want: "6b221d5bc9e6496c", }, { name: "error : id too long", s: "1234567890abcdef1", wantErr: true, }, { name: "error : not parsable", s: "howdyhowdyhowdy", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := idFromString(tt.s) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, tt.want, got) }) } }