diff --git a/consumer/consumerdata/consumerdata.go b/consumer/consumerdata/consumerdata.go index 6859dc1e9a05..e1a61138ec30 100644 --- a/consumer/consumerdata/consumerdata.go +++ b/consumer/consumerdata/consumerdata.go @@ -41,13 +41,17 @@ type TraceData struct { // OTLPTraceData is a struct that groups proto spans with a resource. This is the // newer version of TraceData, using OTLP-based representation. type OTLPTraceData struct { - ResourceSpanList []*otlptrace.ResourceSpans + resourceSpanList []*otlptrace.ResourceSpans +} + +func NewOTLPTraceData(resourceSpanList []*otlptrace.ResourceSpans) OTLPTraceData { + return OTLPTraceData{resourceSpanList} } // SpanCount calculates the total number of spans. func (td OTLPTraceData) SpanCount() int { spanCount := 0 - for _, rsl := range td.ResourceSpanList { + for _, rsl := range td.resourceSpanList { spanCount += len(rsl.Spans) } return spanCount diff --git a/exporter/exporterhelper/tracehelper_test.go b/exporter/exporterhelper/tracehelper_test.go index de5dec2e3ad9..32025bbb7dbe 100644 --- a/exporter/exporterhelper/tracehelper_test.go +++ b/exporter/exporterhelper/tracehelper_test.go @@ -346,7 +346,7 @@ func checkRecordedMetricsForOTLPTraceExporter(t *testing.T, te exporter.OTLPTrac defer doneFn() spans := make([]*otlptrace.Span, 2) - td := consumerdata.OTLPTraceData{ResourceSpanList: []*otlptrace.ResourceSpans{{Spans: spans}}} + td := consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{{Spans: spans}}) ctx := observability.ContextWithReceiverName(context.Background(), fakeTraceReceiverName) const numBatches = 7 for i := 0; i < numBatches; i++ { @@ -361,7 +361,7 @@ func checkRecordedMetricsForOTLPTraceExporter(t *testing.T, te exporter.OTLPTrac } func generateOTLPTraceTraffic(t *testing.T, te exporter.OTLPTraceExporter, numRequests int, wantError error) { - td := consumerdata.OTLPTraceData{ResourceSpanList: []*otlptrace.ResourceSpans{{Spans: []*otlptrace.Span{{}}}}} + td := consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{{Spans: []*otlptrace.Span{{}}}}) ctx, span := trace.StartSpan(context.Background(), fakeTraceParentSpanName, trace.WithSampler(trace.AlwaysSample())) defer span.End() for i := 0; i < numRequests; i++ { diff --git a/go.mod b/go.mod index 9f5101a19317..222f2ecbce75 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/jaegertracing/jaeger v1.14.0 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 github.com/mitchellh/mapstructure v1.1.2 - github.com/open-telemetry/opentelemetry-proto v0.0.0-20200206071824-8310c432e51c + github.com/open-telemetry/opentelemetry-proto v0.0.0-20200211051721-ff5f19c6217d github.com/openzipkin/zipkin-go v0.2.1 github.com/orijtech/prometheus-go-metrics-exporter v0.0.3-0.20190313163149-b321c5297f60 github.com/pavius/impi v0.0.0-20180302134524-c1cbdcb8df2b diff --git a/go.sum b/go.sum index 8f4299a0ca7c..cb011b47398f 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/open-telemetry/opentelemetry-proto v0.0.0-20200206071824-8310c432e51c h1:nDOtl6j2Ei16tlnx/o4qKEelpHtGoZ9ArwU+tux4Ia8= -github.com/open-telemetry/opentelemetry-proto v0.0.0-20200206071824-8310c432e51c/go.mod h1:PMR5GI0F7BSpio+rBGFxNm6SLzg3FypDTcFuQZnO+F8= +github.com/open-telemetry/opentelemetry-proto v0.0.0-20200211051721-ff5f19c6217d h1:hZcHR0at6tb3jBjaPHlfLr6yK7rTrA8xGCS6jlUSLcU= +github.com/open-telemetry/opentelemetry-proto v0.0.0-20200211051721-ff5f19c6217d/go.mod h1:PMR5GI0F7BSpio+rBGFxNm6SLzg3FypDTcFuQZnO+F8= github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= diff --git a/internal/internal.go b/internal/internal.go index 477d243428c6..b5ee550c141f 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -31,3 +31,10 @@ func TimeToTimestamp(t time.Time) *timestamp.Timestamp { Nanos: int32(nanoTime % 1e9), } } + +func TimestampToTime(ts *timestamp.Timestamp) (t time.Time) { + if ts == nil { + return + } + return time.Unix(ts.Seconds, int64(ts.Nanos)) +} diff --git a/testbed/go.sum b/testbed/go.sum index de51bb4a9145..641efad4665e 100644 --- a/testbed/go.sum +++ b/testbed/go.sum @@ -479,6 +479,8 @@ github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/open-telemetry/opentelemetry-proto v0.0.0-20200206071824-8310c432e51c h1:nDOtl6j2Ei16tlnx/o4qKEelpHtGoZ9ArwU+tux4Ia8= github.com/open-telemetry/opentelemetry-proto v0.0.0-20200206071824-8310c432e51c/go.mod h1:PMR5GI0F7BSpio+rBGFxNm6SLzg3FypDTcFuQZnO+F8= +github.com/open-telemetry/opentelemetry-proto v0.0.0-20200211051721-ff5f19c6217d h1:hZcHR0at6tb3jBjaPHlfLr6yK7rTrA8xGCS6jlUSLcU= +github.com/open-telemetry/opentelemetry-proto v0.0.0-20200211051721-ff5f19c6217d/go.mod h1:PMR5GI0F7BSpio+rBGFxNm6SLzg3FypDTcFuQZnO+F8= github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= diff --git a/translator/trace/oc_to_otlp.go b/translator/trace/oc_to_otlp.go new file mode 100644 index 000000000000..8696e4658850 --- /dev/null +++ b/translator/trace/oc_to_otlp.go @@ -0,0 +1,436 @@ +// Copyright 2019 OpenTelemetry 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 ( + "strconv" + "strings" + + occommon "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1" + ocresource "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" + octrace "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/timestamp" + otlpcommon "github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1" + otlpresource "github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1" + otlptrace "github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1" + + "github.com/open-telemetry/opentelemetry-collector/consumer/consumerdata" + "github.com/open-telemetry/opentelemetry-collector/internal" + "github.com/open-telemetry/opentelemetry-collector/translator/conventions" +) + +// OTLP attributes to map certain OpenCensus proto fields. These fields don't have +// corresponding fields in OTLP, nor are defined in OTLP semantic conventions. +// TODO: decide if any of these must be in OTLP semantic conventions. +const ( + ocAttributeProcessStartTime = "opencensus.starttime" + ocAttributeProcessID = "opencensus.pid" + ocAttributeExporterVersion = "opencensus.exporterversion" + ocAttributeResourceType = "opencensus.resourcetype" + ocTimeEventMessageEventType = "opencensus.timeevent.messageevent.type" + ocTimeEventMessageEventID = "opencensus.timeevent.messageevent.id" + ocTimeEventMessageEventUSize = "opencensus.timeevent.messageevent.usize" + ocTimeEventMessageEventCSize = "opencensus.timeevent.messageevent.csize" +) + +func ocToOtlp(td consumerdata.TraceData) consumerdata.OTLPTraceData { + + if td.Node == nil && td.Resource == nil && len(td.Spans) == 0 { + return consumerdata.OTLPTraceData{} + } + + resource := ocNodeResourceToOtlp(td.Node, td.Resource) + + resourceSpans := &otlptrace.ResourceSpans{ + Resource: resource, + } + resourceSpanList := []*otlptrace.ResourceSpans{resourceSpans} + + if len(td.Spans) != 0 { + resourceSpans.Spans = make([]*otlptrace.Span, 0, len(td.Spans)) + + for _, ocSpan := range td.Spans { + if ocSpan == nil { + // Skip nil spans. + continue + } + + otlpSpan := ocSpanToOtlp(ocSpan) + + if ocSpan.Resource != nil { + // Add a separate ResourceSpans item just for this span since it + // has a different Resource. + separateRS := &otlptrace.ResourceSpans{ + Resource: ocNodeResourceToOtlp(nil, ocSpan.Resource), + Spans: []*otlptrace.Span{otlpSpan}, + } + resourceSpanList = append(resourceSpanList, separateRS) + } else { + // Otherwise add the span to the first ResourceSpans item. + resourceSpans.Spans = append(resourceSpans.Spans, otlpSpan) + } + } + } + + return consumerdata.NewOTLPTraceData(resourceSpanList) +} + +func timestampToUnixnano(ts *timestamp.Timestamp) uint64 { + return uint64(internal.TimestampToTime(ts).UnixNano()) +} + +func ocSpanToOtlp(ocSpan *octrace.Span) *otlptrace.Span { + attrs, droppedAttrCount := ocAttrsToOtlp(ocSpan.Attributes) + events, droppedEventCount := ocEventsToOtlp(ocSpan.TimeEvents) + links, droppedLinkCount := ocLinksToOtlp(ocSpan.Links) + + childSpanCount := int32(0) + if ocSpan.ChildSpanCount != nil { + childSpanCount = int32(ocSpan.ChildSpanCount.Value) + } + + otlpSpan := &otlptrace.Span{ + TraceId: ocSpan.TraceId, + SpanId: ocSpan.SpanId, + Tracestate: ocTraceStateToOtlp(ocSpan.Tracestate), + ParentSpanId: ocSpan.ParentSpanId, + Name: truncableStringToStr(ocSpan.Name), + Kind: ocSpanKindToOtlp(ocSpan.Kind, ocSpan.Attributes), + StartTimeUnixnano: timestampToUnixnano(ocSpan.StartTime), + EndTimeUnixnano: timestampToUnixnano(ocSpan.EndTime), + Attributes: attrs, + DroppedAttributesCount: droppedAttrCount, + Events: events, + DroppedEventsCount: droppedEventCount, + Links: links, + DroppedLinksCount: droppedLinkCount, + Status: ocStatusToOtlp(ocSpan.Status), + LocalChildSpanCount: childSpanCount, + } + + return otlpSpan +} + +func ocStatusToOtlp(ocStatus *octrace.Status) *otlptrace.Status { + if ocStatus == nil { + return nil + } + return &otlptrace.Status{ + Code: otlptrace.Status_StatusCode(ocStatus.Code), + Message: ocStatus.Message, + } +} + +// Convert tracestate to W3C format. See the https://w3c.github.io/trace-context/ +func ocTraceStateToOtlp(ocTracestate *octrace.Span_Tracestate) string { + if ocTracestate == nil { + return "" + } + var sb strings.Builder + for i, entry := range ocTracestate.Entries { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(strings.Join([]string{entry.Key, entry.Value}, "=")) + } + return sb.String() +} + +func ocAttrsToOtlp(ocAttrs *octrace.Span_Attributes) (otlpAttrs []*otlpcommon.AttributeKeyValue, droppedCount uint32) { + if ocAttrs == nil { + return + } + + otlpAttrs = make([]*otlpcommon.AttributeKeyValue, 0, len(ocAttrs.AttributeMap)) + for key, ocAttr := range ocAttrs.AttributeMap { + + otlpAttr := &otlpcommon.AttributeKeyValue{Key: key} + + switch attribValue := ocAttr.Value.(type) { + case *octrace.AttributeValue_StringValue: + otlpAttr.StringValue = truncableStringToStr(attribValue.StringValue) + otlpAttr.Type = otlpcommon.AttributeKeyValue_STRING + + case *octrace.AttributeValue_IntValue: + otlpAttr.IntValue = attribValue.IntValue + otlpAttr.Type = otlpcommon.AttributeKeyValue_INT + + case *octrace.AttributeValue_BoolValue: + otlpAttr.BoolValue = attribValue.BoolValue + otlpAttr.Type = otlpcommon.AttributeKeyValue_BOOL + + case *octrace.AttributeValue_DoubleValue: + otlpAttr.DoubleValue = attribValue.DoubleValue + otlpAttr.Type = otlpcommon.AttributeKeyValue_DOUBLE + + default: + str := "" + otlpAttr.StringValue = str + otlpAttr.Type = otlpcommon.AttributeKeyValue_STRING + } + otlpAttrs = append(otlpAttrs, otlpAttr) + } + droppedCount = uint32(ocAttrs.DroppedAttributesCount) + return +} + +func ocSpanKindToOtlp(ocKind octrace.Span_SpanKind, ocAttrs *octrace.Span_Attributes) otlptrace.Span_SpanKind { + switch ocKind { + case octrace.Span_SERVER: + return otlptrace.Span_SERVER + + case octrace.Span_CLIENT: + return otlptrace.Span_CLIENT + + case octrace.Span_SPAN_KIND_UNSPECIFIED: + // Span kind field is unspecified, check if TagSpanKind attribute is set. + // This can happen if span kind had no equivalent in OC, so we could represent it in + // the SpanKind. In that case the span kind may be a special attribute TagSpanKind. + if ocAttrs != nil { + kindAttr := ocAttrs.AttributeMap[TagSpanKind] + if kindAttr != nil { + strVal, ok := kindAttr.Value.(*octrace.AttributeValue_StringValue) + if ok && strVal != nil { + var otlpKind otlptrace.Span_SpanKind + switch OpenTracingSpanKind(truncableStringToStr(strVal.StringValue)) { + case OpenTracingSpanKindConsumer: + otlpKind = otlptrace.Span_CONSUMER + case OpenTracingSpanKindProducer: + otlpKind = otlptrace.Span_PRODUCER + default: + return otlptrace.Span_SPAN_KIND_UNSPECIFIED + } + delete(ocAttrs.AttributeMap, TagSpanKind) + return otlpKind + } + } + } + return otlptrace.Span_SPAN_KIND_UNSPECIFIED + + default: + return otlptrace.Span_SPAN_KIND_UNSPECIFIED + } +} + +func ocEventsToOtlp(ocEvents *octrace.Span_TimeEvents) (otlpEvents []*otlptrace.Span_Event, droppedCount uint32) { + if ocEvents == nil { + return + } + + droppedCount = uint32(ocEvents.DroppedMessageEventsCount + ocEvents.DroppedAnnotationsCount) + if len(ocEvents.TimeEvent) == 0 { + return + } + + otlpEvents = make([]*otlptrace.Span_Event, 0, len(ocEvents.TimeEvent)) + + for _, ocEvent := range ocEvents.TimeEvent { + if ocEvent == nil { + continue + } + + otlpEvent := &otlptrace.Span_Event{ + TimeUnixnano: timestampToUnixnano(ocEvent.Time), + } + + switch teValue := ocEvent.Value.(type) { + case *octrace.Span_TimeEvent_Annotation_: + if teValue.Annotation != nil { + otlpEvent.Name = truncableStringToStr(teValue.Annotation.Description) + attrs, droppedCount := ocAttrsToOtlp(teValue.Annotation.Attributes) + otlpEvent.Attributes = attrs + otlpEvent.DroppedAttributesCount = droppedCount + } + + case *octrace.Span_TimeEvent_MessageEvent_: + otlpEvent.Attributes = ocMessageEventToOtlpAttrs(teValue.MessageEvent) + + default: + otlpEvent.Name = "An unknown OpenCensus TimeEvent type was detected when translating to OTLP" + } + + otlpEvents = append(otlpEvents, otlpEvent) + } + return +} + +func ocLinksToOtlp(ocLinks *octrace.Span_Links) (otlpLinks []*otlptrace.Span_Link, droppedCount uint32) { + if ocLinks == nil { + return nil, 0 + } + + droppedCount = uint32(ocLinks.DroppedLinksCount) + if len(ocLinks.Link) == 0 { + return nil, droppedCount + } + + otlpLinks = make([]*otlptrace.Span_Link, 0, len(ocLinks.Link)) + + for _, ocLink := range ocLinks.Link { + if ocLink == nil { + continue + } + + attrs, droppedCount := ocAttrsToOtlp(ocLink.Attributes) + otlpLink := &otlptrace.Span_Link{ + TraceId: ocLink.TraceId, + SpanId: ocLink.SpanId, + Tracestate: ocTraceStateToOtlp(ocLink.Tracestate), + Attributes: attrs, + DroppedAttributesCount: droppedCount, + } + + otlpLinks = append(otlpLinks, otlpLink) + } + return otlpLinks, droppedCount +} + +func ocMessageEventToOtlpAttrs(msgEvent *octrace.Span_TimeEvent_MessageEvent) []*otlpcommon.AttributeKeyValue { + if msgEvent == nil { + return nil + } + + return []*otlpcommon.AttributeKeyValue{ + { + Key: ocTimeEventMessageEventType, + StringValue: msgEvent.Type.String(), + Type: otlpcommon.AttributeKeyValue_STRING, + }, + { + Key: ocTimeEventMessageEventID, + IntValue: int64(msgEvent.Id), + Type: otlpcommon.AttributeKeyValue_INT, + }, + { + Key: ocTimeEventMessageEventUSize, + IntValue: int64(msgEvent.UncompressedSize), + Type: otlpcommon.AttributeKeyValue_INT, + }, + { + Key: ocTimeEventMessageEventCSize, + IntValue: int64(msgEvent.CompressedSize), + Type: otlpcommon.AttributeKeyValue_INT, + }, + } +} + +func truncableStringToStr(ts *octrace.TruncatableString) string { + if ts == nil { + return "" + } + return ts.Value +} + +func ocNodeResourceToOtlp(node *occommon.Node, resource *ocresource.Resource) *otlpresource.Resource { + otlpResource := &otlpresource.Resource{} + + // Move all special fields in Node and in Resource to a temporary attributes map. + // After that we will build the attributes slice in OTLP format from this map. + // This ensures there are no attributes with duplicate keys. + + // Number of special fields in the Node. See the code below that deals with special fields. + const specialNodeAttrCount = 7 + + // Number of special fields in the Resource. + const specialResourceAttrCount = 1 + + // Calculate maximum total number of attributes. It is OK if we are a bit higher than + // the exact number since this is only needed for capacity reservation. + totalAttrCount := 0 + if node != nil { + totalAttrCount += len(node.Attributes) + specialNodeAttrCount + } + if resource != nil { + totalAttrCount += len(resource.Labels) + specialResourceAttrCount + } + + // Create a temporary map where we will place all attributes from the Node and Resource. + attrs := make(map[string]string, totalAttrCount) + + if node != nil { + // Copy all Attributes. + for k, v := range node.Attributes { + attrs[k] = v + } + + // Add all special fields. + if node.ServiceInfo != nil { + if node.ServiceInfo.Name != "" { + attrs[conventions.AttributeServiceName] = node.ServiceInfo.Name + } + } + if node.Identifier != nil { + if node.Identifier.StartTimestamp != nil { + attrs[ocAttributeProcessStartTime] = ptypes.TimestampString(node.Identifier.StartTimestamp) + } + if node.Identifier.HostName != "" { + attrs[conventions.AttributeHostHostname] = node.Identifier.HostName + } + if node.Identifier.Pid != 0 { + attrs[ocAttributeProcessID] = strconv.Itoa(int(node.Identifier.Pid)) + } + } + if node.LibraryInfo != nil { + if node.LibraryInfo.CoreLibraryVersion != "" { + attrs[conventions.AttributeLibraryVersion] = node.LibraryInfo.CoreLibraryVersion + } + if node.LibraryInfo.ExporterVersion != "" { + attrs[ocAttributeExporterVersion] = node.LibraryInfo.ExporterVersion + } + if node.LibraryInfo.Language != occommon.LibraryInfo_LANGUAGE_UNSPECIFIED { + attrs[conventions.AttributeLibraryLanguage] = node.LibraryInfo.Language.String() + } + } + } + + if resource != nil { + // Copy resource Labels. + for k, v := range resource.Labels { + attrs[k] = v + } + // Add special fields. + if resource.Type != "" { + attrs[ocAttributeResourceType] = resource.Type + } + } + + // Convert everything from the temporary matp to OTLP format. + otlpResource.Attributes = attrMapToOtlp(attrs) + + return otlpResource +} + +func attrMapToOtlp(ocAttrs map[string]string) []*otlpcommon.AttributeKeyValue { + if len(ocAttrs) == 0 { + return nil + } + + otlpAttrs := make([]*otlpcommon.AttributeKeyValue, len(ocAttrs)) + i := 0 + for k, v := range ocAttrs { + otlpAttrs[i] = otlpStringAttr(k, v) + i++ + } + return otlpAttrs +} + +func otlpStringAttr(key string, val string) *otlpcommon.AttributeKeyValue { + return &otlpcommon.AttributeKeyValue{ + Key: key, + Type: otlpcommon.AttributeKeyValue_STRING, + StringValue: val, + } +} diff --git a/translator/trace/oc_to_otlp_test.go b/translator/trace/oc_to_otlp_test.go new file mode 100644 index 000000000000..5c65582b3571 --- /dev/null +++ b/translator/trace/oc_to_otlp_test.go @@ -0,0 +1,521 @@ +// Copyright 2019 OpenTelemetry 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 ( + "strings" + "testing" + "time" + + occommon "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1" + ocresource "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1" + octrace "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" + "github.com/golang/protobuf/ptypes" + otlpcommon "github.com/open-telemetry/opentelemetry-proto/gen/go/common/v1" + otlpresource "github.com/open-telemetry/opentelemetry-proto/gen/go/resource/v1" + otlptrace "github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1" + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-collector/consumer/consumerdata" + "github.com/open-telemetry/opentelemetry-collector/translator/conventions" +) + +func TestAttrMapToOtlp(t *testing.T) { + ocAttrs := map[string]string{} + + otlpAttrs := attrMapToOtlp(ocAttrs) + assert.True(t, otlpAttrs == nil) + + ocAttrs = map[string]string{"abc": "def"} + otlpAttrs = attrMapToOtlp(ocAttrs) + assert.EqualValues(t, []*otlpcommon.AttributeKeyValue{ + { + Key: "abc", + Type: otlpcommon.AttributeKeyValue_STRING, + StringValue: "def", + }, + }, otlpAttrs) +} + +func TestOcNodeResourceToOtlp(t *testing.T) { + otlpResource := ocNodeResourceToOtlp(nil, nil) + assert.EqualValues(t, &otlpresource.Resource{ + Attributes: nil, + }, otlpResource) + + node := &occommon.Node{} + resource := &ocresource.Resource{} + otlpResource = ocNodeResourceToOtlp(node, resource) + assert.EqualValues(t, &otlpresource.Resource{ + Attributes: nil, + }, otlpResource) + + ts, err := ptypes.TimestampProto(time.Date(2020, 2, 11, 20, 26, 0, 0, time.UTC)) + assert.NoError(t, err) + + node = &occommon.Node{ + Identifier: &occommon.ProcessIdentifier{ + HostName: "host1", + Pid: 123, + StartTimestamp: ts, + }, + LibraryInfo: &occommon.LibraryInfo{ + Language: occommon.LibraryInfo_CPP, + ExporterVersion: "v1.2.0", + CoreLibraryVersion: "v2.0.1", + }, + ServiceInfo: &occommon.ServiceInfo{ + Name: "svcA", + }, + Attributes: map[string]string{ + "node-attr": "val1", + }, + } + resource = &ocresource.Resource{ + Type: "good-resource", + Labels: map[string]string{ + "resource-attr": "val2", + }, + } + otlpResource = ocNodeResourceToOtlp(node, resource) + + expectedAttrs := map[string]string{ + conventions.AttributeHostHostname: "host1", + ocAttributeProcessID: "123", + ocAttributeProcessStartTime: "2020-02-11T20:26:00Z", + conventions.AttributeLibraryLanguage: "CPP", + ocAttributeExporterVersion: "v1.2.0", + conventions.AttributeLibraryVersion: "v2.0.1", + conventions.AttributeServiceName: "svcA", + "node-attr": "val1", + ocAttributeResourceType: "good-resource", + "resource-attr": "val2", + } + + assert.EqualValues(t, len(expectedAttrs), len(otlpResource.Attributes)) + for k, v := range expectedAttrs { + assertHasAttrStr(t, otlpResource.Attributes, k, v) + } + + // Make sure hard-coded fields override same-name values in Attributes. + // To do that add Attributes with same-name. + for k := range expectedAttrs { + // Set all except "attr1" which is not a hard-coded field to some bogus values. + + if strings.Index(k, "-attr") < 0 { + node.Attributes[k] = "this will be overridden 1" + } + } + resource.Labels[ocAttributeResourceType] = "this will be overridden 2" + + // Convert again. + otlpResource = ocNodeResourceToOtlp(node, resource) + + // And verify that same-name attributes were ignored. + assert.EqualValues(t, len(expectedAttrs), len(otlpResource.Attributes)) + for k, v := range expectedAttrs { + assertHasAttrStr(t, otlpResource.Attributes, k, v) + } +} + +func assertHasAttrStr(t *testing.T, attrs []*otlpcommon.AttributeKeyValue, key string, val string) { + assertHasAttrVal(t, attrs, key, &otlpcommon.AttributeKeyValue{ + Key: key, + Type: otlpcommon.AttributeKeyValue_STRING, + StringValue: val, + }) +} + +func assertHasAttrVal(t *testing.T, attrs []*otlpcommon.AttributeKeyValue, key string, val *otlpcommon.AttributeKeyValue) { + found := false + for _, attr := range attrs { + if attr != nil && attr.Key == key { + assert.EqualValues(t, false, found, "Duplicate key "+key) + assert.EqualValues(t, val, attr) + found = true + } + } + assert.EqualValues(t, true, found, "Cannot find key "+key) +} + +func TestOcTraceStateToOtlp(t *testing.T) { + assert.EqualValues(t, "", ocTraceStateToOtlp(nil)) + + tracestate := &octrace.Span_Tracestate{ + Entries: []*octrace.Span_Tracestate_Entry{ + { + Key: "abc", + Value: "def", + }, + }, + } + assert.EqualValues(t, "abc=def", ocTraceStateToOtlp(tracestate)) + + tracestate.Entries = append(tracestate.Entries, + &octrace.Span_Tracestate_Entry{ + Key: "123", + Value: "4567", + }) + assert.EqualValues(t, "abc=def,123=4567", ocTraceStateToOtlp(tracestate)) +} + +func TestOcAttrsToOtlp(t *testing.T) { + otlpAttrs, droppedCount := ocAttrsToOtlp(nil) + assert.True(t, otlpAttrs == nil) + assert.EqualValues(t, 0, droppedCount) + + ocAttrs := &octrace.Span_Attributes{} + otlpAttrs, droppedCount = ocAttrsToOtlp(ocAttrs) + assert.EqualValues(t, []*otlpcommon.AttributeKeyValue{}, otlpAttrs) + assert.EqualValues(t, 0, droppedCount) + + ocAttrs = &octrace.Span_Attributes{ + DroppedAttributesCount: 123, + } + otlpAttrs, droppedCount = ocAttrsToOtlp(ocAttrs) + assert.EqualValues(t, []*otlpcommon.AttributeKeyValue{}, otlpAttrs) + assert.EqualValues(t, 123, droppedCount) + + ocAttrs = &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{}, + DroppedAttributesCount: 234, + } + otlpAttrs, droppedCount = ocAttrsToOtlp(ocAttrs) + assert.EqualValues(t, []*otlpcommon.AttributeKeyValue{}, otlpAttrs) + assert.EqualValues(t, 234, droppedCount) + + ocAttrs = &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{ + "abc": { + Value: &octrace.AttributeValue_StringValue{StringValue: &octrace.TruncatableString{Value: "def"}}, + }, + }, + DroppedAttributesCount: 234, + } + otlpAttrs, droppedCount = ocAttrsToOtlp(ocAttrs) + assert.EqualValues(t, []*otlpcommon.AttributeKeyValue{ + { + Key: "abc", + Type: otlpcommon.AttributeKeyValue_STRING, + StringValue: "def", + }, + }, otlpAttrs) + assert.EqualValues(t, 234, droppedCount) + + ocAttrs.AttributeMap["intval"] = &octrace.AttributeValue{ + Value: &octrace.AttributeValue_IntValue{IntValue: 345}, + } + ocAttrs.AttributeMap["boolval"] = &octrace.AttributeValue{ + Value: &octrace.AttributeValue_BoolValue{BoolValue: true}, + } + ocAttrs.AttributeMap["doubleval"] = &octrace.AttributeValue{ + Value: &octrace.AttributeValue_DoubleValue{DoubleValue: 4.5}, + } + otlpAttrs, droppedCount = ocAttrsToOtlp(ocAttrs) + assert.EqualValues(t, 234, droppedCount) + assert.EqualValues(t, 4, len(otlpAttrs)) + assertHasAttrVal(t, otlpAttrs, "abc", &otlpcommon.AttributeKeyValue{ + Key: "abc", + Type: otlpcommon.AttributeKeyValue_STRING, + StringValue: "def", + }) + assertHasAttrVal(t, otlpAttrs, "intval", &otlpcommon.AttributeKeyValue{ + Key: "intval", + Type: otlpcommon.AttributeKeyValue_INT, + IntValue: 345, + }) + assertHasAttrVal(t, otlpAttrs, "boolval", &otlpcommon.AttributeKeyValue{ + Key: "boolval", + Type: otlpcommon.AttributeKeyValue_BOOL, + BoolValue: true, + }) + assertHasAttrVal(t, otlpAttrs, "doubleval", &otlpcommon.AttributeKeyValue{ + Key: "doubleval", + Type: otlpcommon.AttributeKeyValue_DOUBLE, + DoubleValue: 4.5, + }) +} + +func TestOcSpanKindToOtlp(t *testing.T) { + tests := []struct { + ocAttrs *octrace.Span_Attributes + ocKind octrace.Span_SpanKind + otlpKind otlptrace.Span_SpanKind + }{ + { + ocKind: octrace.Span_CLIENT, + otlpKind: otlptrace.Span_CLIENT, + }, + { + ocKind: octrace.Span_SERVER, + otlpKind: otlptrace.Span_SERVER, + }, + { + ocKind: octrace.Span_SPAN_KIND_UNSPECIFIED, + otlpKind: otlptrace.Span_SPAN_KIND_UNSPECIFIED, + }, + { + ocKind: octrace.Span_SPAN_KIND_UNSPECIFIED, + ocAttrs: &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{ + "span.kind": {Value: &octrace.AttributeValue_StringValue{ + StringValue: &octrace.TruncatableString{Value: "consumer"}}}, + }, + }, + otlpKind: otlptrace.Span_CONSUMER, + }, + { + ocKind: octrace.Span_SPAN_KIND_UNSPECIFIED, + ocAttrs: &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{ + "span.kind": {Value: &octrace.AttributeValue_StringValue{ + StringValue: &octrace.TruncatableString{Value: "producer"}}}, + }, + }, + otlpKind: otlptrace.Span_PRODUCER, + }, + { + ocKind: octrace.Span_SPAN_KIND_UNSPECIFIED, + ocAttrs: &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{ + "span.kind": {Value: &octrace.AttributeValue_IntValue{ + IntValue: 123}}, + }, + }, + otlpKind: otlptrace.Span_SPAN_KIND_UNSPECIFIED, + }, + { + ocKind: octrace.Span_CLIENT, + ocAttrs: &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{ + "span.kind": {Value: &octrace.AttributeValue_StringValue{ + StringValue: &octrace.TruncatableString{Value: "consumer"}}}, + }, + }, + otlpKind: otlptrace.Span_CLIENT, + }, + } + + for _, test := range tests { + t.Run(test.otlpKind.String(), func(t *testing.T) { + got := ocSpanKindToOtlp(test.ocKind, test.ocAttrs) + assert.EqualValues(t, test.otlpKind, got, "Expected "+test.otlpKind.String()+", got "+got.String()) + }) + } +} + +func TestOcToOtlp(t *testing.T) { + ocNode := &occommon.Node{} + ocResource := &ocresource.Resource{} + + timestampP, err := ptypes.TimestampProto(time.Date(2020, 2, 11, 20, 26, 0, 0, time.UTC)) + assert.NoError(t, err) + + ocSpan1 := &octrace.Span{ + Name: &octrace.TruncatableString{Value: "operationB"}, + StartTime: timestampP, + EndTime: timestampP, + TimeEvents: &octrace.Span_TimeEvents{ + TimeEvent: []*octrace.Span_TimeEvent{ + { + Time: timestampP, + Value: &octrace.Span_TimeEvent_Annotation_{ + Annotation: &octrace.Span_TimeEvent_Annotation{ + Description: &octrace.TruncatableString{Value: "event1"}, + Attributes: &octrace.Span_Attributes{ + AttributeMap: map[string]*octrace.AttributeValue{ + "eventattr1": { + Value: &octrace.AttributeValue_StringValue{ + StringValue: &octrace.TruncatableString{Value: "eventattrval1"}, + }, + }, + }, + DroppedAttributesCount: 4, + }, + }, + }, + }, + nil, + }, + DroppedAnnotationsCount: 1, + DroppedMessageEventsCount: 2, + }, + Status: &octrace.Status{Message: "status-cancelled", Code: 1}, + } + + ocSpan2 := &octrace.Span{ + Name: &octrace.TruncatableString{Value: "operationC"}, + StartTime: timestampP, + EndTime: timestampP, + Links: &octrace.Span_Links{ + Link: []*octrace.Span_Link{{}, nil}, + DroppedLinksCount: 1, + }, + } + + ocSpan3 := &octrace.Span{ + Name: &octrace.TruncatableString{Value: "operationD"}, + StartTime: timestampP, + EndTime: timestampP, + Resource: ocResource, + } + + otlpSpan1 := &otlptrace.Span{ + Name: "operationB", + StartTimeUnixnano: timestampToUnixnano(timestampP), + EndTimeUnixnano: timestampToUnixnano(timestampP), + Events: []*otlptrace.Span_Event{ + { + TimeUnixnano: timestampToUnixnano(timestampP), + Name: "event1", + Attributes: []*otlpcommon.AttributeKeyValue{ + { + Key: "eventattr1", + Type: otlpcommon.AttributeKeyValue_STRING, + StringValue: "eventattrval1", + }, + }, + DroppedAttributesCount: 4, + }, + }, + DroppedEventsCount: 3, + Status: &otlptrace.Status{Message: "status-cancelled", Code: otlptrace.Status_Cancelled}, + } + + otlpSpan2 := &otlptrace.Span{ + Name: "operationC", + StartTimeUnixnano: timestampToUnixnano(timestampP), + EndTimeUnixnano: timestampToUnixnano(timestampP), + Links: []*otlptrace.Span_Link{{}}, + DroppedLinksCount: 1, + } + + otlpSpan3 := &otlptrace.Span{ + Name: "operationD", + StartTimeUnixnano: timestampToUnixnano(timestampP), + EndTimeUnixnano: timestampToUnixnano(timestampP), + } + + tests := []struct { + name string + oc consumerdata.TraceData + otlp consumerdata.OTLPTraceData + }{ + { + name: "empty", + oc: consumerdata.TraceData{}, + otlp: consumerdata.OTLPTraceData{}, + }, + + { + name: "nil-resource", + oc: consumerdata.TraceData{Node: ocNode}, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + {Resource: &otlpresource.Resource{}}, + }), + }, + + { + name: "nil-node", + oc: consumerdata.TraceData{Resource: ocResource}, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + {Resource: &otlpresource.Resource{}}, + }), + }, + + { + name: "no-spans", + oc: consumerdata.TraceData{Node: ocNode, Resource: ocResource}, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + {Resource: &otlpresource.Resource{}}, + }), + }, + + { + name: "one-spans", + oc: consumerdata.TraceData{ + Node: ocNode, + Resource: ocResource, + Spans: []*octrace.Span{ocSpan1}, + }, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + { + Resource: &otlpresource.Resource{}, + Spans: []*otlptrace.Span{otlpSpan1}, + }, + }), + }, + + { + name: "two-spans", + oc: consumerdata.TraceData{ + Node: ocNode, + Resource: ocResource, + Spans: []*octrace.Span{ocSpan1, nil, ocSpan2}, + }, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + { + Resource: &otlpresource.Resource{}, + Spans: []*otlptrace.Span{otlpSpan1, otlpSpan2}, + }, + }), + }, + + { + name: "two-spans-plus-one-separate", + oc: consumerdata.TraceData{ + Node: ocNode, + Resource: ocResource, + Spans: []*octrace.Span{ocSpan1, ocSpan2, ocSpan3}, + }, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + { + Resource: &otlpresource.Resource{}, + Spans: []*otlptrace.Span{otlpSpan1, otlpSpan2}, + }, + { + Resource: &otlpresource.Resource{}, + Spans: []*otlptrace.Span{otlpSpan3}, + }, + }), + }, + + { + name: "two-spans-and-separate-in-the-middle", + oc: consumerdata.TraceData{ + Node: ocNode, + Resource: ocResource, + Spans: []*octrace.Span{ocSpan1, ocSpan3, ocSpan2}, + }, + otlp: consumerdata.NewOTLPTraceData([]*otlptrace.ResourceSpans{ + { + Resource: &otlpresource.Resource{}, + Spans: []*otlptrace.Span{otlpSpan1, otlpSpan2}, + }, + { + Resource: &otlpresource.Resource{}, + Spans: []*otlptrace.Span{otlpSpan3}, + }, + }), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := ocToOtlp(test.oc) + assert.EqualValues(t, test.otlp, got) + }) + } +} diff --git a/translator/trace/spandata/protospan_to_spandata.go b/translator/trace/spandata/protospan_to_spandata.go index 34393a2f11d1..8184b7ad80f0 100644 --- a/translator/trace/spandata/protospan_to_spandata.go +++ b/translator/trace/spandata/protospan_to_spandata.go @@ -17,13 +17,13 @@ package spandata import ( "errors" - "time" tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1" - "github.com/golang/protobuf/ptypes/timestamp" "github.com/golang/protobuf/ptypes/wrappers" "go.opencensus.io/trace" "go.opencensus.io/trace/tracestate" + + "github.com/open-telemetry/opentelemetry-collector/internal" ) var errNilSpan = errors.New("expected a non-nil span") @@ -47,8 +47,8 @@ func ProtoSpanToOCSpanData(span *tracepb.Span) (*trace.SpanData, error) { sd := &trace.SpanData{ SpanContext: sc, ParentSpanID: parentSpanID, - StartTime: timestampToTime(span.StartTime), - EndTime: timestampToTime(span.EndTime), + StartTime: internal.TimestampToTime(span.StartTime), + EndTime: internal.TimestampToTime(span.EndTime), Name: derefTruncatableString(span.Name), Attributes: protoAttributesToOCAttributes(span.Attributes), Links: protoLinksToOCLinks(span.Links), @@ -62,13 +62,6 @@ func ProtoSpanToOCSpanData(span *tracepb.Span) (*trace.SpanData, error) { return sd, nil } -func timestampToTime(ts *timestamp.Timestamp) (t time.Time) { - if ts == nil { - return - } - return time.Unix(ts.Seconds, int64(ts.Nanos)) -} - func derefTruncatableString(ts *tracepb.TruncatableString) string { if ts == nil { return "" @@ -189,7 +182,7 @@ func protoTimeEventsToOCMessageEvents(tes *tracepb.Span_TimeEvents) []trace.Mess ocme.UncompressedByteSize = int64(me.UncompressedSize) ocme.CompressedByteSize = int64(me.CompressedSize) ocme.EventType = protoMessageEventTypeToOCEventType(me.Type) - ocme.Time = timestampToTime(te.Time) + ocme.Time = internal.TimestampToTime(te.Time) ocmes = append(ocmes, ocme) } @@ -221,7 +214,7 @@ func protoTimeEventsToOCAnnotations(tes *tracepb.Span_TimeEvents) []trace.Annota // they have these attributes as int64 yet the proto definitions // are uint64, this could be a potential loss of precision particularly // in very high traffic systems. - ocann.Time = timestampToTime(te.Time) + ocann.Time = internal.TimestampToTime(te.Time) ocann.Message = me.Description.GetValue() ocann.Attributes = protoSpanAttributesToOCAttributes(me.Attributes) ocanns = append(ocanns, ocann)