diff --git a/go.mod b/go.mod index 2146fae57246..6e2bc416686a 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 // indirect github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20181026070331-e7c4bd17b329 - github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7 // indirect + github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7 github.com/aws/aws-sdk-go v1.15.68 // indirect github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b // indirect github.com/census-instrumentation/opencensus-proto v0.1.0 @@ -37,7 +37,7 @@ require ( github.com/stretchr/testify v1.2.2 // indirect github.com/tinylib/msgp v1.0.2 // indirect github.com/uber-go/atomic v1.3.2 // indirect - github.com/uber/jaeger-lib v1.5.0 // indirect + github.com/uber/jaeger-lib v1.5.0 github.com/uber/tchannel-go v1.10.0 github.com/yancl/opencensus-go-exporter-kafka v0.0.0-20181029030031-9c471c1bfbeb go.opencensus.io v0.18.0 diff --git a/go.sum b/go.sum index dde3dbdd8e38..c8a2f860d296 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,7 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jaegertracing/jaeger v1.7.0 h1:RVpmOTj7Zb9QRFHI5CYvkYSTR8Cdn/PhM5kBxA4pZBM= github.com/jaegertracing/jaeger v1.7.0/go.mod h1:LUWPSnzNPGRubM8pk0inANGitpiMOOxihXx0+53llXI= +github.com/jaegertracing/jaeger v1.8.0 h1:8nWwXtXFqCVyEPjKczfnB1Yj9s7DmaDHYbqnfNBORMY= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= diff --git a/receiver/jaeger/trace_receiver_test.go b/receiver/jaeger/trace_receiver_test.go index fd7f23da628e..a3c72a801197 100644 --- a/receiver/jaeger/trace_receiver_test.go +++ b/receiver/jaeger/trace_receiver_test.go @@ -91,7 +91,7 @@ func TestReception(t *testing.T) { { TraceID: trace.TraceID{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x80}, SpanID: trace.SpanID{0xCF, 0xCE, 0xCD, 0xCC, 0xCB, 0xCA, 0xC9, 0xC8}, - Type: trace.LinkTypeChild, + Type: trace.LinkTypeParent, }, }, }, @@ -111,7 +111,7 @@ func TestReception(t *testing.T) { { TraceID: trace.TraceID{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x80}, SpanID: trace.SpanID{0xAF, 0xAE, 0xAD, 0xAC, 0xAB, 0xAA, 0xA9, 0xA8}, - Type: trace.LinkTypeParent, + Type: trace.LinkTypeChild, }, }, }, @@ -154,12 +154,22 @@ func TestReception(t *testing.T) { Code: trace.StatusCodeNotFound, Message: "Stale indices", }, + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "status.code": { + Value: &tracepb.AttributeValue_IntValue{IntValue: trace.StatusCodeNotFound}, + }, + "status.message": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "Stale indices"}}, + }, + }, + }, Links: &tracepb.Span_Links{ Link: []*tracepb.Span_Link{ { TraceId: []byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x80}, SpanId: []byte{0xCF, 0xCE, 0xCD, 0xCC, 0xCB, 0xCA, 0xC9, 0xC8}, - Type: tracepb.Span_Link_CHILD_LINKED_SPAN, + Type: tracepb.Span_Link_PARENT_LINKED_SPAN, }, }, }, @@ -174,6 +184,16 @@ func TestReception(t *testing.T) { Code: trace.StatusCodeInternal, Message: "Frontend crash", }, + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "status.code": { + Value: &tracepb.AttributeValue_IntValue{IntValue: trace.StatusCodeInternal}, + }, + "status.message": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "Frontend crash"}}, + }, + }, + }, Links: &tracepb.Span_Links{ Link: []*tracepb.Span_Link{ { @@ -184,7 +204,7 @@ func TestReception(t *testing.T) { // * Child_of // * Follows_from // yet OpenCensus has Parent too but Jaeger uses a zero-value for LinkCHILD. - Type: tracepb.Span_Link_CHILD_LINKED_SPAN, + Type: tracepb.Span_Link_PARENT_LINKED_SPAN, }, }, }, diff --git a/translator/trace/jaegerthrift_to_protospan.go b/translator/trace/jaegerthrift_to_protospan.go index 99a10f424b3b..c4bb7e86d9cd 100644 --- a/translator/trace/jaegerthrift_to_protospan.go +++ b/translator/trace/jaegerthrift_to_protospan.go @@ -163,7 +163,8 @@ func jReferencesToOCProtoLinks(jrefs []*jaeger.SpanRef) *tracepb.Span_Links { for _, jref := range jrefs { var linkType tracepb.Span_Link_Type if jref.RefType == jaeger.SpanRefType_CHILD_OF { - linkType = tracepb.Span_Link_CHILD_LINKED_SPAN + // Wording on OC for Span_Link_PARENT_LINKED_SPAN: The linked span is a parent of the current span. + linkType = tracepb.Span_Link_PARENT_LINKED_SPAN } else { // TODO: SpanRefType_FOLLOWS_FROM doesn't map well to OC, so treat all other cases as unknown linkType = tracepb.Span_Link_TYPE_UNSPECIFIED @@ -227,15 +228,12 @@ func jtagsToAttributes(tags []*jaeger.Tag) (string, tracepb.Span_SpanKind, *trac case "http.status_code", "status.code": // It is expected to be an int statusCodePtr = new(int32) *statusCodePtr = int32(tag.GetVLong()) - continue case "http.status_message", "status.message": statusMessage = tag.GetVStr() - continue case "message": message = tag.GetVStr() - continue } attrib := &tracepb.AttributeValue{} diff --git a/translator/trace/jaegerthrift_to_protospan_test.go b/translator/trace/jaegerthrift_to_protospan_test.go index a99c96fe1f8f..8073c540ce7c 100644 --- a/translator/trace/jaegerthrift_to_protospan_test.go +++ b/translator/trace/jaegerthrift_to_protospan_test.go @@ -22,12 +22,12 @@ import ( "reflect" "testing" - "github.com/census-instrumentation/opencensus-service/internal/testutils" + "github.com/jaegertracing/jaeger/thrift-gen/jaeger" commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1" agenttracepb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/trace/v1" tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" - "github.com/jaegertracing/jaeger/thrift-gen/jaeger" + "github.com/census-instrumentation/opencensus-service/internal/testutils" ) func TestJaegerThriftBatchToOCProto(t *testing.T) { @@ -118,12 +118,12 @@ func TestConservativeConversions(t *testing.T) { Tags: []*jaeger.Tag{ { Key: "http.status_code", - VLong: func() *int64 { v := int64(5); return &v }(), + VLong: func() *int64 { v := int64(403); return &v }(), VType: jaeger.TagType_LONG, }, { Key: "http.status_message", - VStr: func() *string { v := "cache miss"; return &v }(), + VStr: func() *string { v := "Forbidden"; return &v }(), VType: jaeger.TagType_STRING, }, }, @@ -205,8 +205,22 @@ func TestConservativeConversions(t *testing.T) { TraceId: []byte{0x00, 0x11, 0x12, 0x13, 0x14, 0x11, 0x11, 0x11, 0x01, 0x11, 0x11, 0x11, 0xFF, 0xFF, 0xFF, 0xFF}, // Ensure that the status code was properly translated Status: &tracepb.Status{ - Code: 5, - Message: "cache miss", + Code: 403, + Message: "Forbidden", + }, + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "http.status_code": { + Value: &tracepb.AttributeValue_IntValue{ + IntValue: 403, + }, + }, + "http.status_message": { + Value: &tracepb.AttributeValue_StringValue{ + StringValue: &tracepb.TruncatableString{Value: "Forbidden"}, + }, + }, + }, }, }, { @@ -217,6 +231,20 @@ func TestConservativeConversions(t *testing.T) { Code: 13, Message: "proxy crashed", }, + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "status.code": { + Value: &tracepb.AttributeValue_IntValue{ + IntValue: 13, + }, + }, + "status.message": { + Value: &tracepb.AttributeValue_StringValue{ + StringValue: &tracepb.TruncatableString{Value: "proxy crashed"}, + }, + }, + }, + }, }, }, }, diff --git a/translator/trace/protospan_to_jaegerthrift.go b/translator/trace/protospan_to_jaegerthrift.go new file mode 100644 index 000000000000..fbc0bcf5738b --- /dev/null +++ b/translator/trace/protospan_to_jaegerthrift.go @@ -0,0 +1,406 @@ +// Copyright 2018, 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 tracetranslator + +import ( + "encoding/binary" + "errors" + "fmt" + + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/jaegertracing/jaeger/thrift-gen/jaeger" + + commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1" + agenttracepb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/trace/v1" + tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" +) + +var ( + errZeroTraceID = errors.New("OC span has an all zeros trace ID") + errNilTraceID = errors.New("OC trace ID is nil") + errWrongLenTraceID = errors.New("TraceID does not have 16 bytes") + errZeroSpanID = errors.New("OC span has an all zeros span ID") + errNilID = errors.New("OC ID is nil") + errWrongLenID = errors.New("ID does not have 8 bytes") +) + +// OCProtoToJaegerThrift translates OpenCensus trace data into the Jaeger Thrift format. +func OCProtoToJaegerThrift(ocBatch *agenttracepb.ExportTraceServiceRequest) (*jaeger.Batch, error) { + if ocBatch == nil { + return nil, nil + } + + jSpans, err := ocSpansToJaegerSpans(ocBatch.Spans) + if err != nil { + return nil, err + } + + jb := &jaeger.Batch{ + Process: ocNodeToJaegerProcess(ocBatch.Node), + Spans: jSpans, + } + + return jb, nil +} + +func ocNodeToJaegerProcess(node *commonpb.Node) *jaeger.Process { + if node == nil { + return nil + } + + var jTags []*jaeger.Tag + nodeAttribsLen := len(node.Attributes) + if nodeAttribsLen > 0 { + jTags = make([]*jaeger.Tag, 0, nodeAttribsLen) + for k, v := range node.Attributes { + str := v + jTag := &jaeger.Tag{ + Key: k, + VType: jaeger.TagType_STRING, + VStr: &str, + } + jTags = append(jTags, jTag) + } + } + + if node.Identifier != nil { + if node.Identifier.HostName != "" { + hostTag := &jaeger.Tag{ + Key: "hostname", + VType: jaeger.TagType_STRING, + VStr: &node.Identifier.HostName, + } + jTags = append(jTags, hostTag) + } + if node.Identifier.Pid != 0 { + pid := int64(node.Identifier.Pid) + hostTag := &jaeger.Tag{ + Key: "pid", + VType: jaeger.TagType_LONG, + VLong: &pid, + } + jTags = append(jTags, hostTag) + } + if node.Identifier.StartTimestamp != nil && node.Identifier.StartTimestamp.Seconds != 0 { + startTimeStr := ptypes.TimestampString(node.Identifier.StartTimestamp) + hostTag := &jaeger.Tag{ + Key: "start.time", + VType: jaeger.TagType_STRING, + VStr: &startTimeStr, + } + jTags = append(jTags, hostTag) + } + } + + // Add OpenCensus library information as tags if available + ocLib := node.LibraryInfo + if ocLib != nil { + // Only add language if specified + if ocLib.Language != commonpb.LibraryInfo_LANGUAGE_UNSPECIFIED { + languageStr := ocLib.Language.String() + languageTag := &jaeger.Tag{ + Key: "opencensus.language", + VType: jaeger.TagType_STRING, + VStr: &languageStr, + } + jTags = append(jTags, languageTag) + } + if ocLib.ExporterVersion != "" { + exporterTag := &jaeger.Tag{ + Key: "opencensus.exporterversion", + VType: jaeger.TagType_STRING, + VStr: &ocLib.ExporterVersion, + } + jTags = append(jTags, exporterTag) + } + if ocLib.CoreLibraryVersion != "" { + exporterTag := &jaeger.Tag{ + Key: "opencensus.corelibversion", + VType: jaeger.TagType_STRING, + VStr: &ocLib.CoreLibraryVersion, + } + jTags = append(jTags, exporterTag) + } + } + + var serviceName string + if node.ServiceInfo != nil && node.ServiceInfo.Name != "" { + serviceName = node.ServiceInfo.Name + } + + if serviceName == "" && len(jTags) == 0 { + // No info to put in the process... + return nil + } + + jProc := &jaeger.Process{ + ServiceName: serviceName, + Tags: jTags, + } + + return jProc +} + +func ocSpansToJaegerSpans(ocSpans []*tracepb.Span) ([]*jaeger.Span, error) { + if ocSpans == nil { + return nil, nil + } + + // Pre-allocate assuming that few, if any spans, are nil. + jSpans := make([]*jaeger.Span, 0, len(ocSpans)) + for _, ocSpan := range ocSpans { + traceIDLow, traceIDHigh, err := traceIDBytesToLowAndHigh(ocSpan.TraceId) + if err != nil { + return nil, fmt.Errorf("OC span has invalid trace ID: %v", err) + } + if traceIDLow == 0 && traceIDHigh == 0 { + return nil, errZeroTraceID + } + jReferences, err := ocLinksToJaegerReferences(ocSpan.Links) + if err != nil { + return nil, fmt.Errorf("Error converting OC links to Jaeger references: %v", err) + } + spanID, err := ocIDBytesToJaegerID(ocSpan.SpanId) + if err != nil { + return nil, fmt.Errorf("OC span has invalid span ID: %v", err) + } + if spanID == 0 { + return nil, errZeroSpanID + } + // OC ParentSpanId can be nil/empty: only attempt conversion if not nil/empty. + var parentSpanID int64 + if len(ocSpan.ParentSpanId) != 0 { + parentSpanID, err = ocIDBytesToJaegerID(ocSpan.ParentSpanId) + if err != nil { + return nil, fmt.Errorf("OC span has invalid parent span ID: %v", err) + } + } + startTime := timestampToEpochMicroseconds(ocSpan.StartTime) + jSpan := &jaeger.Span{ + TraceIdLow: traceIDLow, + TraceIdHigh: traceIDHigh, + SpanId: spanID, + ParentSpanId: parentSpanID, + OperationName: truncableStringToStr(ocSpan.Name), + References: jReferences, + // Flags: TODO (@pjanotti) Nothing from OC-Proto seems to match the values for Flags see https://www.jaegertracing.io/docs/1.8/client-libraries/ + StartTime: startTime, + Duration: timestampToEpochMicroseconds(ocSpan.EndTime) - startTime, + Tags: ocSpanAttributesToJaegerTags(ocSpan.Attributes), + Logs: ocTimeEventsToJaegerLogs(ocSpan.TimeEvents), + } + + jSpan.Tags = appendJaegerTagFromOCSpanKind(jSpan.Tags, ocSpan.Kind) + jSpans = append(jSpans, jSpan) + } + + return jSpans, nil +} + +func ocLinksToJaegerReferences(ocSpanLinks *tracepb.Span_Links) ([]*jaeger.SpanRef, error) { + if ocSpanLinks == nil || ocSpanLinks.Link == nil { + return nil, nil + } + + ocLinks := ocSpanLinks.Link + jRefs := make([]*jaeger.SpanRef, 0, len(ocLinks)) + for _, ocLink := range ocLinks { + traceIDLow, traceIDHigh, err := traceIDBytesToLowAndHigh(ocLink.TraceId) + if err != nil { + return nil, fmt.Errorf("OC link has invalid trace ID: %v", err) + } + + var jRefType jaeger.SpanRefType + switch ocLink.Type { + case tracepb.Span_Link_PARENT_LINKED_SPAN: + jRefType = jaeger.SpanRefType_CHILD_OF + default: + // TODO: (@pjanotti) Jaeger doesn't have a unknown SpanRefType, it has FOLLOWS_FROM or CHILD_OF + // at first mapping all others to FOLLOWS_FROM. + jRefType = jaeger.SpanRefType_FOLLOWS_FROM + } + + spanID, err := ocIDBytesToJaegerID(ocLink.SpanId) + if err != nil { + return nil, fmt.Errorf("OC link has invalid span ID: %v", err) + } + + jRef := &jaeger.SpanRef{ + TraceIdLow: traceIDLow, + TraceIdHigh: traceIDHigh, + RefType: jRefType, + SpanId: spanID, + } + jRefs = append(jRefs, jRef) + } + + return jRefs, nil +} + +func appendJaegerTagFromOCSpanKind(jTags []*jaeger.Tag, ocSpanKind tracepb.Span_SpanKind) []*jaeger.Tag { + // We could check if the key is already present but it doesn't seem worth at this point. + // TODO: (@pjanotti): Replace any OpenTracing literals by importing github.com/opentracing/opentracing-go/ext? + var tagValue string + switch ocSpanKind { + case tracepb.Span_CLIENT: + tagValue = "client" + case tracepb.Span_SERVER: + tagValue = "server" + } + + if tagValue != "" { + jTag := &jaeger.Tag{ + Key: "span.kind", + VStr: &tagValue, + } + jTags = append(jTags, jTag) + } + + return jTags +} + +func ocTimeEventsToJaegerLogs(ocSpanTimeEvents *tracepb.Span_TimeEvents) []*jaeger.Log { + if ocSpanTimeEvents == nil || ocSpanTimeEvents.TimeEvent == nil { + return nil + } + + ocTimeEvents := ocSpanTimeEvents.TimeEvent + + // Assume that in general no time events are going to produce nil Jaeger logs. + jLogs := make([]*jaeger.Log, 0, len(ocTimeEvents)) + for _, ocTimeEvent := range ocTimeEvents { + jLog := &jaeger.Log{ + Timestamp: timestampToEpochMicroseconds(ocTimeEvent.Time), + } + switch teValue := ocTimeEvent.Value.(type) { + case *tracepb.Span_TimeEvent_Annotation_: + jLog.Fields = ocAnnotationToJagerTags(teValue.Annotation) + case *tracepb.Span_TimeEvent_MessageEvent_: + jLog.Fields = ocMessageEventToJaegerTags(teValue.MessageEvent) + default: + msg := "An unknown OpenCensus TimeEvent type was detected when translating to Jaeger" + jTag := &jaeger.Tag{ + Key: "unknown.oc.timeevent.type", + VStr: &msg, + } + jLog.Fields = append(jLog.Fields, jTag) + } + + jLogs = append(jLogs, jLog) + } + + return jLogs +} + +func ocAnnotationToJagerTags(annotation *tracepb.Span_TimeEvent_Annotation) []*jaeger.Tag { + if annotation == nil { + return nil + } + + // TODO: (@pjanotti) what about Description? Does it fit as another tag? + + return ocSpanAttributesToJaegerTags(annotation.Attributes) +} + +func ocMessageEventToJaegerTags(msgEvent *tracepb.Span_TimeEvent_MessageEvent) []*jaeger.Tag { + if msgEvent == nil { + return nil + } + + // TODO: (@pjanotti) Not clear how to map those to Jaeger, perhaps some OpenTracing tags... + + return nil +} + +func truncableStringToStr(ts *tracepb.TruncatableString) string { + if ts == nil { + return "" + } + return ts.Value +} + +func traceIDBytesToLowAndHigh(traceID []byte) (traceIDLow, traceIDHigh int64, err error) { + if traceID == nil { + return 0, 0, errNilTraceID + } + if len(traceID) != 16 { + return 0, 0, errWrongLenTraceID + } + traceIDHigh = int64(binary.BigEndian.Uint64(traceID[:8])) + traceIDLow = int64(binary.BigEndian.Uint64(traceID[8:])) + return traceIDLow, traceIDHigh, nil +} + +func ocIDBytesToJaegerID(b []byte) (id int64, err error) { + if b == nil { + return 0, errNilID + } + if len(b) != 8 { + return 0, errWrongLenID + } + id = int64(binary.BigEndian.Uint64(b)) + return id, nil +} + +func timestampToEpochMicroseconds(ts *timestamp.Timestamp) int64 { + if ts == nil { + return 0 + } + return ts.GetSeconds()*1e6 + int64(ts.GetNanos()/1e3) +} + +func ocSpanAttributesToJaegerTags(ocAttribs *tracepb.Span_Attributes) []*jaeger.Tag { + if ocAttribs == nil { + return nil + } + + // Pre-allocate assuming that few attributes, if any at all, are nil. + jTags := make([]*jaeger.Tag, 0, len(ocAttribs.AttributeMap)) + for key, attrib := range ocAttribs.AttributeMap { + if attrib == nil || attrib.Value == nil { + continue + } + + jTag := &jaeger.Tag{Key: key} + switch attribValue := attrib.Value.(type) { + case *tracepb.AttributeValue_StringValue: + // Jaeger-to-OC maps binary tags to string attributes and encodes them as + // base64 strings. Blindingly attempting to decode base64 seems too much. + str := truncableStringToStr(attribValue.StringValue) + jTag.VStr = &str + jTag.VType = jaeger.TagType_STRING + case *tracepb.AttributeValue_IntValue: + i := attribValue.IntValue + jTag.VLong = &i + jTag.VType = jaeger.TagType_LONG + case *tracepb.AttributeValue_BoolValue: + b := attribValue.BoolValue + jTag.VBool = &b + jTag.VType = jaeger.TagType_BOOL + case *tracepb.AttributeValue_DoubleValue: + d := attribValue.DoubleValue + jTag.VDouble = &d + jTag.VType = jaeger.TagType_DOUBLE + default: + str := "" + jTag.VStr = &str + jTag.VType = jaeger.TagType_STRING + } + jTags = append(jTags, jTag) + } + + return jTags +} diff --git a/translator/trace/protospan_to_jaegerthrift_test.go b/translator/trace/protospan_to_jaegerthrift_test.go new file mode 100644 index 000000000000..1ee146cd7203 --- /dev/null +++ b/translator/trace/protospan_to_jaegerthrift_test.go @@ -0,0 +1,307 @@ +// Copyright 2018, 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 tracetranslator + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "testing" + + commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1" + agenttracepb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/trace/v1" + tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" + "github.com/census-instrumentation/opencensus-service/internal/testutils" + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/jaegertracing/jaeger/thrift-gen/jaeger" +) + +func TestJaegerFromOCProtoTraceIDRoundTrip(t *testing.T) { + wl := int64(0x0001020304050607) + wh := int64(0x70605040302010FF) + gl, gh, err := traceIDBytesToLowAndHigh(jTraceIDToOCProtoTraceID(wh, wl)) + if err != nil { + t.Errorf("Error converting from OC trace id: %v", err) + } + if gl != wl || gh != wh { + t.Errorf("Round trip of trace Id failed want: (0x%0x, 0x%0x) got: (0x%0x, 0x%0x)", wl, wh, gl, gh) + } +} + +func TestJaegerFromOCProtoSpanIDRoundTrip(t *testing.T) { + w := int64(0x0001020304050607) + g, err := ocIDBytesToJaegerID(jSpanIDToOCProtoSpanID(w)) + if err != nil { + t.Errorf("Error converting from OC span id: %v", err) + } + if g != w { + t.Errorf("Round trip of span Id failed want: 0x%0x got: 0x%0x", w, g) + } +} + +func TestInvalidOCProtoIDs(t *testing.T) { + fakeTraceID := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + tests := []struct { + name string + ocSpans []*tracepb.Span + wantErr error // nil means that we check for the message of the wrapped error + wrappedError error // when wantErr is nil we expect this error to have been wrapped by the one received + }{ + { + name: "nil TraceID", + ocSpans: []*tracepb.Span{{}}, + wantErr: nil, + wrappedError: errNilTraceID, + }, + { + name: "empty TraceID", + ocSpans: []*tracepb.Span{{TraceId: []byte{}}}, + wantErr: nil, + wrappedError: errWrongLenTraceID, + }, + { + name: "zero TraceID", + ocSpans: []*tracepb.Span{{TraceId: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}}, + wantErr: errZeroTraceID, + }, + { + name: "nil SpanID", + ocSpans: []*tracepb.Span{{TraceId: fakeTraceID}}, + wantErr: nil, + wrappedError: errNilID, + }, + { + name: "empty SpanID", + ocSpans: []*tracepb.Span{{TraceId: fakeTraceID, SpanId: []byte{}}}, + wantErr: nil, + wrappedError: errWrongLenID, + }, + { + name: "zero SpanID", + ocSpans: []*tracepb.Span{{TraceId: fakeTraceID, SpanId: []byte{0, 0, 0, 0, 0, 0, 0, 0}}}, + wantErr: errZeroSpanID, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ocSpansToJaegerSpans(tt.ocSpans) + if err == nil { + t.Error("ocSpansToJaegerSpans() no error, want error") + return + } + if tt.wantErr != nil && err != tt.wantErr { + t.Errorf("ocSpansToJaegerSpans() = %v, want %v", err, tt.wantErr) + } + if tt.wrappedError != nil && !strings.Contains(err.Error(), tt.wrappedError.Error()) { + t.Errorf("ocSpansToJaegerSpans() = %v, want it to wrap error %v", err, tt.wrappedError) + } + }) + } +} + +func TestOCProtoToJaegerThrift(t *testing.T) { + const numOfFiles = 2 + for i := 0; i < numOfFiles; i++ { + ocBatch := ocBatches[i] + + gotJBatch, err := OCProtoToJaegerThrift(ocBatch) + if err != nil { + t.Errorf("Failed to translate OC batch to Jaeger Thrift: %v", err) + continue + } + + wantSpanCount, gotSpanCount := len(ocBatch.Spans), len(gotJBatch.Spans) + if wantSpanCount != gotSpanCount { + t.Errorf("Different number of spans in the batches on pass #%d (want %d, got %d)", i, wantSpanCount, gotSpanCount) + continue + } + + // Jaeger binary tags do not round trip from Jaeger -> OCProto -> Jaeger. + // For tests use data without binary tags. + thriftFile := fmt.Sprintf("./testdata/thrift_batch_no_binary_tags_%02d.json", i+1) + wantJBatch := &jaeger.Batch{} + if err := loadFromJSON(thriftFile, wantJBatch); err != nil { + t.Errorf("Failed load Jaeger Thrift from %q: %v", thriftFile, err) + continue + } + + // Sort tags to help with comparison, not only for jaeger.Process but also + // on each span. + sort.Slice(gotJBatch.Process.Tags, func(i, j int) bool { + return gotJBatch.Process.Tags[i].Key < gotJBatch.Process.Tags[j].Key + }) + sort.Slice(wantJBatch.Process.Tags, func(i, j int) bool { + return wantJBatch.Process.Tags[i].Key < wantJBatch.Process.Tags[j].Key + }) + var jSpans []*jaeger.Span + jSpans = append(jSpans, gotJBatch.Spans...) + jSpans = append(jSpans, wantJBatch.Spans...) + for _, jSpan := range jSpans { + sort.Slice(jSpan.Tags, func(i, j int) bool { + return jSpan.Tags[i].Key < jSpan.Tags[j].Key + }) + } + + gjson, _ := json.Marshal(gotJBatch) + wjson, _ := json.Marshal(wantJBatch) + gjsonStr := testutils.GenerateNormalizedJSON(string(gjson)) + wjsonStr := testutils.GenerateNormalizedJSON(string(wjson)) + if gjsonStr != wjsonStr { + t.Errorf("OC Proto to Jaeger Thrift failed.\nGot:\n%s\nWant:\n%s\n", gjsonStr, wjsonStr) + } + } +} + +// ocBatches has the OpenCensus proto batches used in the test. They are hard coded because +// structs like tracepb.AttributeMap cannot be ready from JSON. +var ocBatches = []*agenttracepb.ExportTraceServiceRequest{ + { + Node: &commonpb.Node{ + Identifier: &commonpb.ProcessIdentifier{ + HostName: "api246-sjc1", + Pid: 13, + StartTimestamp: ×tamp.Timestamp{Seconds: 1485467190, Nanos: 639875000}, + }, + LibraryInfo: &commonpb.LibraryInfo{ExporterVersion: "someVersion"}, + ServiceInfo: &commonpb.ServiceInfo{Name: "api"}, + Attributes: map[string]string{ + "a.binary": "AQIDBAMCAQ==", + "a.bool": "true", + "a.double": "1234.56789", + "a.long": "123456789", + "ip": "10.53.69.61", + }, + }, + Spans: []*tracepb.Span{ + { + TraceId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x96, 0x9A, 0x89, 0x55, 0x57, 0x1A, 0x3F}, + SpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}, + ParentSpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0xC4, 0xE3}, + Name: &tracepb.TruncatableString{Value: "get"}, + Kind: tracepb.Span_SERVER, + StartTime: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 639875000}, + EndTime: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 662813000}, + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "http.url": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "http://127.0.0.1:15598/client_transactions"}}, + }, + "peer.ipv4": { + Value: &tracepb.AttributeValue_IntValue{IntValue: 3224716605}, + }, + "peer.port": { + Value: &tracepb.AttributeValue_IntValue{IntValue: 53931}, + }, + "peer.service": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "rtapi"}}, + }, + "someBool": { + Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}, + }, + "someDouble": { + Value: &tracepb.AttributeValue_DoubleValue{DoubleValue: 129.8}, + }, + }, + }, + TimeEvents: &tracepb.Span_TimeEvents{ + TimeEvent: []*tracepb.Span_TimeEvent{ + { + Time: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 639875000}, + Value: &tracepb.Span_TimeEvent_Annotation_{ + Annotation: &tracepb.Span_TimeEvent_Annotation{ + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "key1": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "value1"}}, + }, + }, + }, + }, + }, + }, + { + Time: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 639875000}, + Value: &tracepb.Span_TimeEvent_Annotation_{ + Annotation: &tracepb.Span_TimeEvent_Annotation{ + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "event": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "nothing"}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Node: &commonpb.Node{ + ServiceInfo: &commonpb.ServiceInfo{Name: "api"}, + }, + Spans: []*tracepb.Span{ + { + TraceId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x96, 0x9A, 0x89, 0x55, 0x57, 0x1A, 0x3F}, + SpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}, + ParentSpanId: nil, + Name: &tracepb.TruncatableString{Value: "get"}, + Kind: tracepb.Span_SERVER, + StartTime: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 639875000}, + EndTime: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 662813000}, + Attributes: &tracepb.Span_Attributes{ + AttributeMap: map[string]*tracepb.AttributeValue{ + "peer.service": { + Value: &tracepb.AttributeValue_StringValue{StringValue: &tracepb.TruncatableString{Value: "AAAAAAAAMDk="}}, + }, + }, + }, + }, + { + TraceId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x96, 0x9A, 0x89, 0x55, 0x57, 0x1A, 0x3F}, + SpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x99}, + ParentSpanId: []byte{}, + Name: &tracepb.TruncatableString{Value: "get"}, + Kind: tracepb.Span_SERVER, + StartTime: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 639875000}, + EndTime: ×tamp.Timestamp{Seconds: 1485467191, Nanos: 662813000}, + Links: &tracepb.Span_Links{ + Link: []*tracepb.Span_Link{ + { + TraceId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x96, 0x9A, 0x89, 0x55, 0x57, 0x1A, 0x3F}, + SpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}, + Type: tracepb.Span_Link_PARENT_LINKED_SPAN, + }, + { + TraceId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x96, 0x9A, 0x89, 0x55, 0x57, 0x1A, 0x3F}, + SpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0xC4, 0xE3}, + }, + }, + }, + }, + { + TraceId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x96, 0x9A, 0x89, 0x55, 0x57, 0x1A, 0x3F}, + SpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x7D, 0x98}, + ParentSpanId: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + Name: &tracepb.TruncatableString{Value: "get2"}, + StartTime: ×tamp.Timestamp{Seconds: 1485467192, Nanos: 639875000}, + EndTime: ×tamp.Timestamp{Seconds: 1485467192, Nanos: 662813000}, + }, + }, + }, +} diff --git a/translator/trace/testdata/ocproto_batch_02.json b/translator/trace/testdata/ocproto_batch_02.json index 6f00158d0c29..a86d69f0c0c9 100644 --- a/translator/trace/testdata/ocproto_batch_02.json +++ b/translator/trace/testdata/ocproto_batch_02.json @@ -54,7 +54,7 @@ { "trace_id": "AAAAAAAAAABSlpqJVVcaPw==", "span_id": "AAAAAABkfZg=", - "type": 1 + "type": 2 }, { "trace_id": "AAAAAAAAAABSlpqJVVcaPw==", diff --git a/translator/trace/testdata/thrift_batch_no_binary_tags_01.json b/translator/trace/testdata/thrift_batch_no_binary_tags_01.json new file mode 100644 index 000000000000..b1239448d446 --- /dev/null +++ b/translator/trace/testdata/thrift_batch_no_binary_tags_01.json @@ -0,0 +1,121 @@ +{ + "process": { + "serviceName": "api", + "tags": [ + { + "key": "hostname", + "vType": "STRING", + "vStr": "api246-sjc1" + }, + { + "key": "pid", + "vType": "LONG", + "vLong": 13 + }, + { + "key": "start.time", + "vType": "STRING", + "vStr": "2017-01-26T21:46:30.639875Z" + }, + { + "key": "ip", + "vType": "STRING", + "vStr": "10.53.69.61" + }, + { + "key": "opencensus.exporterversion", + "vType": "STRING", + "vStr": "someVersion" + }, + { + "key": "a.bool", + "vType": "STRING", + "vStr": "true" + }, + { + "key": "a.double", + "vType": "STRING", + "vStr": "1234.56789" + }, + { + "key": "a.long", + "vType": "STRING", + "vStr": "123456789" + }, + { + "key": "a.binary", + "vType": "STRING", + "vStr": "AQIDBAMCAQ==" + } + ] + }, + "spans": [ + { + "traceIdLow": 5951113872249657919, + "spanId": 6585752, + "parentSpanId": 6866147, + "operationName": "get", + "startTime": 1485467191639875, + "duration": 22938, + "tags": [ + { + "key": "http.url", + "vType": "STRING", + "vStr": "http://127.0.0.1:15598/client_transactions" + }, + { + "key": "span.kind", + "vType": "STRING", + "vStr": "server" + }, + { + "key": "peer.port", + "vType": "LONG", + "vLong": 53931 + }, + { + "key": "someBool", + "vType": "BOOL", + "vBool": true + }, + { + "key": "someDouble", + "vType": "DOUBLE", + "vDouble": 129.8 + }, + { + "key": "peer.service", + "vType": "STRING", + "vStr": "rtapi" + }, + { + "key": "peer.ipv4", + "vType": "LONG", + "vLong": 3224716605 + } + ], + "logs": [ + { + "timestamp": 1485467191639875, + "fields": [ + { + "key": "key1", + "vType": "STRING", + "vStr": "value1" + } + ] + }, + { + "timestamp": 1485467191639875, + "fields": [ + { + "key": "event", + "vType": "STRING", + "vStr": "nothing" + } + ] + } + ] + } + ] +} diff --git a/translator/trace/testdata/thrift_batch_no_binary_tags_02.json b/translator/trace/testdata/thrift_batch_no_binary_tags_02.json new file mode 100644 index 000000000000..af028939557d --- /dev/null +++ b/translator/trace/testdata/thrift_batch_no_binary_tags_02.json @@ -0,0 +1,64 @@ +{ + "process": { + "serviceName": "api", + "tags": [] + }, + "spans": [ + { + "traceIdLow": 5951113872249657919, + "spanId": 6585752, + "parentSpanId": 0, + "operationName": "get", + "startTime": 1485467191639875, + "duration": 22938, + "tags": [ + { + "key": "peer.service", + "vType": "STRING", + "vStr": "AAAAAAAAMDk=" + }, + { + "key": "span.kind", + "vType": "STRING", + "vStr": "server" + } + ] + }, + { + "traceIdLow": 5951113872249657919, + "spanId": 6585753, + "parentSpanId": 0, + "operationName": "get", + "references": [ + { + "refType": "CHILD_OF", + "traceIdLow": 5951113872249657919, + "spanId": 6585752 + }, + { + "refType": "FOLLOWS_FROM", + "traceIdLow": 5951113872249657919, + "spanId": 6866147 + } + ], + "startTime": 1485467191639875, + "duration": 22938, + "tags": [ + { + "key": "span.kind", + "vType": "STRING", + "vStr": "server" + } + ] + }, + { + "traceIdLow": 5951113872249657919, + "spanId": 6585752, + "parentSpanId": 0, + "operationName": "get2", + "startTime": 1485467192639875, + "duration": 22938 + } + ] + } + \ No newline at end of file