// Copyright 2017, OpenCensus Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package zipkin import ( "encoding/json" "io/ioutil" "net/http" "reflect" "strings" "testing" "time" "github.com/openzipkin/zipkin-go/model" httpreporter "github.com/openzipkin/zipkin-go/reporter/http" "go.opencensus.io/trace" ) type roundTripper func(*http.Request) (*http.Response, error) func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return r(req) } func TestExport(t *testing.T) { // Since Zipkin reports in microsecond resolution let's round our Timestamp, // so when deserializing Zipkin data in this test we can properly compare. now := time.Now().Round(time.Microsecond) tests := []struct { span *trace.SpanData want model.SpanModel }{ { span: &trace.SpanData{ SpanContext: trace.SpanContext{ TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, TraceOptions: 1, }, Name: "name", SpanKind: trace.SpanKindClient, StartTime: now, EndTime: now.Add(24 * time.Hour), Attributes: map[string]interface{}{ "stringkey": "value", "intkey": int64(42), "boolkey1": true, "boolkey2": false, "doublekey": float64(123.456), }, MessageEvents: []trace.MessageEvent{ { Time: now, EventType: trace.MessageEventTypeSent, MessageID: 12, UncompressedByteSize: 99, CompressedByteSize: 98, }, }, Annotations: []trace.Annotation{ { Time: now, Message: "Annotation", Attributes: map[string]interface{}{ "stringkey": "value", "intkey": int64(42), "boolkey1": true, "boolkey2": false, "doublekey": float64(123.456), }, }, }, Status: trace.Status{ Code: 3, Message: "error", }, }, want: model.SpanModel{ SpanContext: model.SpanContext{ TraceID: model.TraceID{ High: 0x0102030405060708, Low: 0x090a0b0c0d0e0f10, }, ID: 0x1112131415161718, Sampled: &sampledTrue, }, Name: "name", Kind: model.Client, Timestamp: now, Duration: 24 * time.Hour, Shared: false, Annotations: []model.Annotation{ { Timestamp: now, Value: "Annotation", }, { Timestamp: now, Value: "SENT", }, }, Tags: map[string]string{ "stringkey": "value", "intkey": "42", "boolkey1": "true", "boolkey2": "false", "doublekey": "123.456", "error": "INVALID_ARGUMENT", "opencensus.status_description": "error", }, }, }, { span: &trace.SpanData{ SpanContext: trace.SpanContext{ TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, TraceOptions: 1, }, Name: "name", StartTime: now, EndTime: now.Add(24 * time.Hour), }, want: model.SpanModel{ SpanContext: model.SpanContext{ TraceID: model.TraceID{ High: 0x0102030405060708, Low: 0x090a0b0c0d0e0f10, }, ID: 0x1112131415161718, Sampled: &sampledTrue, }, Name: "name", Timestamp: now, Duration: 24 * time.Hour, Shared: false, }, }, { span: &trace.SpanData{ SpanContext: trace.SpanContext{ TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, TraceOptions: 1, }, Name: "name", StartTime: now, EndTime: now.Add(24 * time.Hour), Status: trace.Status{ Code: 0, Message: "there is no cause for alarm", }, }, want: model.SpanModel{ SpanContext: model.SpanContext{ TraceID: model.TraceID{ High: 0x0102030405060708, Low: 0x090a0b0c0d0e0f10, }, ID: 0x1112131415161718, Sampled: &sampledTrue, }, Name: "name", Timestamp: now, Duration: 24 * time.Hour, Shared: false, Tags: map[string]string{ "opencensus.status_description": "there is no cause for alarm", }, }, }, { span: &trace.SpanData{ SpanContext: trace.SpanContext{ TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, SpanID: trace.SpanID{17, 18, 19, 20, 21, 22, 23, 24}, TraceOptions: 1, }, Name: "name", StartTime: now, EndTime: now.Add(24 * time.Hour), Status: trace.Status{ Code: 1234, }, }, want: model.SpanModel{ SpanContext: model.SpanContext{ TraceID: model.TraceID{ High: 0x0102030405060708, Low: 0x090a0b0c0d0e0f10, }, ID: 0x1112131415161718, Sampled: &sampledTrue, }, Name: "name", Timestamp: now, Duration: 24 * time.Hour, Shared: false, Tags: map[string]string{ "error": "error code 1234", }, }, }, } for _, tt := range tests { got := zipkinSpan(tt.span, nil) if len(got.Annotations) != len(tt.want.Annotations) { t.Fatalf("zipkinSpan: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations)) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("zipkinSpan:\n\tgot %#v\n\twant %#v", got, tt.want) } } for _, tt := range tests { ch := make(chan []byte) client := http.Client{ Transport: roundTripper(func(req *http.Request) (*http.Response, error) { body, _ := ioutil.ReadAll(req.Body) ch <- body return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader(""))}, nil }), } reporter := httpreporter.NewReporter("foo", httpreporter.Client(&client), httpreporter.BatchInterval(time.Millisecond)) exporter := NewExporter(reporter, nil) exporter.ExportSpan(tt.span) var data []byte select { case data = <-ch: case <-time.After(2 * time.Second): t.Fatalf("span was not exported") } var spans []model.SpanModel json.Unmarshal(data, &spans) if len(spans) != 1 { t.Fatalf("Export: got %d spans, want 1", len(spans)) } got := spans[0] got.SpanContext.Sampled = &sampledTrue // Sampled is not set when the span is reported. if len(got.Annotations) != len(tt.want.Annotations) { t.Fatalf("Export: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations)) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Export:\n\tgot %#v\n\twant %#v", got, tt.want) } } }