diff --git a/.chloggen/TEMPLATE.yaml b/.chloggen/TEMPLATE.yaml deleted file mode 100644 index 1e30ebc730e4..000000000000 --- a/.chloggen/TEMPLATE.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' -change_type: - -# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) -component: - -# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). -note: - -# One or more tracking issues related to the change -issues: [] - -# (Optional) One or more lines of additional information to render under the primary note. -# These lines will be padded with 2 spaces and then inserted directly into the document. -# Use pipe (|) for multiline entries. -subtext: diff --git a/.gitignore b/.gitignore index 9d39ea0b8bcc..477e34a76840 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ local/ +vendor/ # GoLand IDEA /.idea/ diff --git a/exporter/awsemfexporter/datapoint.go b/exporter/awsemfexporter/datapoint.go index f788903a20ac..aad3ee3906c6 100644 --- a/exporter/awsemfexporter/datapoint.go +++ b/exporter/awsemfexporter/datapoint.go @@ -16,6 +16,7 @@ package awsemfexporter // import "github.com/open-telemetry/opentelemetry-collec import ( "fmt" + "math" "strconv" "time" @@ -97,6 +98,13 @@ type histogramDataPointSlice struct { pmetric.HistogramDataPointSlice } +type exponentialHistogramDataPointSlice struct { + // TODO: Calculate delta value for count and sum value with exponential histogram + // https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/18245 + deltaMetricMetadata + pmetric.ExponentialHistogramDataPointSlice +} + // summaryDataPointSlice is a wrapper for pmetric.SummaryDataPointSlice type summaryDataPointSlice struct { deltaMetricMetadata @@ -167,6 +175,88 @@ func (dps histogramDataPointSlice) CalculateDeltaDatapoints(i int, instrumentati }}, true } +// CalculateDeltaDatapoints retrieves the ExponentialHistogramDataPoint at the given index. +func (dps exponentialHistogramDataPointSlice) CalculateDeltaDatapoints(idx int, instrumentationScopeName string, _ bool) ([]dataPoint, bool) { + metric := dps.ExponentialHistogramDataPointSlice.At(idx) + + scale := metric.Scale() + base := math.Pow(2, math.Pow(2, float64(-scale))) + arrayValues := []float64{} + arrayCounts := []float64{} + var bucketBegin float64 + var bucketEnd float64 + + // Set mid-point of positive buckets in values/counts array. + positiveBuckets := metric.Positive() + positiveOffset := positiveBuckets.Offset() + positiveBucketCounts := positiveBuckets.BucketCounts() + bucketBegin = 0 + bucketEnd = 0 + for i := 0; i < positiveBucketCounts.Len(); i++ { + index := i + int(positiveOffset) + if bucketBegin == 0 { + bucketBegin = math.Pow(base, float64(index)) + } else { + bucketBegin = bucketEnd + } + bucketEnd = math.Pow(base, float64(index+1)) + metricVal := (bucketBegin + bucketEnd) / 2 + count := positiveBucketCounts.At(i) + if count > 0 { + arrayValues = append(arrayValues, metricVal) + arrayCounts = append(arrayCounts, float64(count)) + } + } + + // Set count of zero bucket in values/counts array. + if metric.ZeroCount() > 0 { + arrayValues = append(arrayValues, 0) + arrayCounts = append(arrayCounts, float64(metric.ZeroCount())) + } + + // Set mid-point of negative buckets in values/counts array. + // According to metrics spec, the value in histogram is expected to be non-negative. + // https://opentelemetry.io/docs/specs/otel/metrics/api/#histogram + // However, the negative support is defined in metrics data model. + // https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram + // The negative is also supported but only verified with unit test. + + negativeBuckets := metric.Negative() + negativeOffset := negativeBuckets.Offset() + negativeBucketCounts := negativeBuckets.BucketCounts() + bucketBegin = 0 + bucketEnd = 0 + for i := 0; i < negativeBucketCounts.Len(); i++ { + index := i + int(negativeOffset) + if bucketEnd == 0 { + bucketEnd = -math.Pow(base, float64(index)) + } else { + bucketEnd = bucketBegin + } + bucketBegin = -math.Pow(base, float64(index+1)) + metricVal := (bucketBegin + bucketEnd) / 2 + count := negativeBucketCounts.At(i) + if count > 0 { + arrayValues = append(arrayValues, metricVal) + arrayCounts = append(arrayCounts, float64(count)) + } + } + + return []dataPoint{{ + name: dps.metricName, + value: &cWMetricHistogram{ + Values: arrayValues, + Counts: arrayCounts, + Count: metric.Count(), + Sum: metric.Sum(), + Max: metric.Max(), + Min: metric.Min(), + }, + labels: createLabels(metric.Attributes(), instrumentationScopeName), + timestampMs: unixNanoToMilliseconds(metric.Timestamp()), + }}, true +} + // CalculateDeltaDatapoints retrieves the SummaryDataPoint at the given index and perform calculation with sum and count while retain the quantile value. func (dps summaryDataPointSlice) CalculateDeltaDatapoints(i int, instrumentationScopeName string, detailedMetrics bool) ([]dataPoint, bool) { metric := dps.SummaryDataPointSlice.At(i) @@ -274,6 +364,12 @@ func getDataPoints(pmd pmetric.Metric, metadata cWMetricMetadata, logger *zap.Lo metricMetadata, metric.DataPoints(), } + case pmetric.MetricTypeExponentialHistogram: + metric := pmd.ExponentialHistogram() + dps = exponentialHistogramDataPointSlice{ + metricMetadata, + metric.DataPoints(), + } case pmetric.MetricTypeSummary: metric := pmd.Summary() // For summaries coming from the prometheus receiver, the sum and count are cumulative, whereas for summaries diff --git a/exporter/awsemfexporter/datapoint_test.go b/exporter/awsemfexporter/datapoint_test.go index 2e3c0a59b774..71bad7c4142c 100644 --- a/exporter/awsemfexporter/datapoint_test.go +++ b/exporter/awsemfexporter/datapoint_test.go @@ -102,6 +102,34 @@ func generateTestHistogramMetric(name string) pmetric.Metrics { return otelMetrics } +func generateTestExponentialHistogramMetric(name string) pmetric.Metrics { + otelMetrics := pmetric.NewMetrics() + rs := otelMetrics.ResourceMetrics().AppendEmpty() + metrics := rs.ScopeMetrics().AppendEmpty().Metrics() + metric := metrics.AppendEmpty() + metric.SetName(name) + metric.SetUnit("Seconds") + exponentialHistogramMetric := metric.SetEmptyExponentialHistogram() + + exponentialHistogramDatapoint := exponentialHistogramMetric.DataPoints().AppendEmpty() + exponentialHistogramDatapoint.SetCount(4) + exponentialHistogramDatapoint.SetSum(0) + exponentialHistogramDatapoint.SetMin(-4) + exponentialHistogramDatapoint.SetMax(4) + exponentialHistogramDatapoint.SetZeroCount(0) + exponentialHistogramDatapoint.SetScale(1) + exponentialHistogramDatapoint.Positive().SetOffset(1) + exponentialHistogramDatapoint.Positive().BucketCounts().FromRaw([]uint64{ + 1, 0, 1, + }) + exponentialHistogramDatapoint.Negative().SetOffset(1) + exponentialHistogramDatapoint.Negative().BucketCounts().FromRaw([]uint64{ + 1, 0, 1, + }) + exponentialHistogramDatapoint.Attributes().PutStr("label1", "value1") + return otelMetrics +} + func generateTestSummaryMetric(name string) pmetric.Metrics { otelMetrics := pmetric.NewMetrics() rs := otelMetrics.ResourceMetrics().AppendEmpty() @@ -358,6 +386,106 @@ func TestCalculateDeltaDatapoints_HistogramDataPointSlice(t *testing.T) { } +func TestCalculateDeltaDatapoints_ExponentialHistogramDataPointSlice(t *testing.T) { + deltaMetricMetadata := generateDeltaMetricMetadata(false, "foo", false) + + testCases := []struct { + name string + histogramDPS pmetric.ExponentialHistogramDataPointSlice + expectedDatapoint dataPoint + }{ + { + name: "Exponential histogram with min and max", + histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice { + histogramDPS := pmetric.NewExponentialHistogramDataPointSlice() + histogramDP := histogramDPS.AppendEmpty() + histogramDP.SetCount(uint64(17)) + histogramDP.SetSum(17.13) + histogramDP.SetMin(10) + histogramDP.SetMax(30) + histogramDP.Attributes().PutStr("label1", "value1") + return histogramDPS + }(), + expectedDatapoint: dataPoint{ + name: "foo", + value: &cWMetricHistogram{Values: []float64{}, Counts: []float64{}, Sum: 17.13, Count: 17, Min: 10, Max: 30}, + labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1"}, + }, + }, + { + name: "Exponential histogram without min and max", + histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice { + histogramDPS := pmetric.NewExponentialHistogramDataPointSlice() + histogramDP := histogramDPS.AppendEmpty() + histogramDP.SetCount(uint64(17)) + histogramDP.SetSum(17.13) + histogramDP.Attributes().PutStr("label1", "value1") + return histogramDPS + + }(), + expectedDatapoint: dataPoint{ + name: "foo", + value: &cWMetricHistogram{Values: []float64{}, Counts: []float64{}, Sum: 17.13, Count: 17, Min: 0, Max: 0}, + labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1"}, + }, + }, + { + name: "Exponential histogram with buckets", + histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice { + histogramDPS := pmetric.NewExponentialHistogramDataPointSlice() + histogramDP := histogramDPS.AppendEmpty() + histogramDP.Positive().BucketCounts().FromRaw([]uint64{1, 2, 3}) + histogramDP.SetZeroCount(4) + histogramDP.Negative().BucketCounts().FromRaw([]uint64{1, 2, 3}) + histogramDP.Attributes().PutStr("label1", "value1") + return histogramDPS + }(), + expectedDatapoint: dataPoint{ + name: "foo", + value: &cWMetricHistogram{Values: []float64{1.5, 3, 6, 0, -1.5, -3, -6}, Counts: []float64{1, 2, 3, 4, 1, 2, 3}}, + labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1"}, + }, + }, + { + name: "Exponential histogram with different scale/offset/labels", + histogramDPS: func() pmetric.ExponentialHistogramDataPointSlice { + histogramDPS := pmetric.NewExponentialHistogramDataPointSlice() + histogramDP := histogramDPS.AppendEmpty() + histogramDP.SetScale(-1) + histogramDP.Positive().SetOffset(-1) + histogramDP.Positive().BucketCounts().FromRaw([]uint64{1, 2, 3}) + histogramDP.SetZeroCount(4) + histogramDP.Negative().SetOffset(-1) + histogramDP.Negative().BucketCounts().FromRaw([]uint64{1, 2, 3}) + histogramDP.Attributes().PutStr("label1", "value1") + histogramDP.Attributes().PutStr("label2", "value2") + return histogramDPS + }(), + expectedDatapoint: dataPoint{ + name: "foo", + value: &cWMetricHistogram{Values: []float64{0.625, 2.5, 10, 0, -0.625, -2.5, -10}, Counts: []float64{1, 2, 3, 4, 1, 2, 3}}, + labels: map[string]string{oTellibDimensionKey: instrLibName, "label1": "value1", "label2": "value2"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(_ *testing.T) { + // Given the histogram datapoints + exponentialHistogramDatapointSlice := exponentialHistogramDataPointSlice{deltaMetricMetadata, tc.histogramDPS} + + // When calculate the delta datapoints for histograms + dps, retained := exponentialHistogramDatapointSlice.CalculateDeltaDatapoints(0, instrLibName, false) + + // Then receiving the following datapoint with an expected length + assert.True(t, retained) + assert.Equal(t, 1, exponentialHistogramDatapointSlice.Len()) + assert.Equal(t, tc.expectedDatapoint, dps[0]) + }) + } + +} + func TestCalculateDeltaDatapoints_SummaryDataPointSlice(t *testing.T) { for _, retainInitialValueOfDeltaMetric := range []bool{true, false} { deltaMetricMetadata := generateDeltaMetricMetadata(true, "foo", retainInitialValueOfDeltaMetric) @@ -497,6 +625,13 @@ func TestGetDataPoints(t *testing.T) { expectedDatapointSlice: histogramDataPointSlice{cumulativeDeltaMetricMetadata, pmetric.HistogramDataPointSlice{}}, expectedAttributes: map[string]interface{}{"label1": "value1"}, }, + { + name: "ExponentialHistogram", + isPrometheusMetrics: false, + metric: generateTestExponentialHistogramMetric("foo"), + expectedDatapointSlice: exponentialHistogramDataPointSlice{cumulativeDeltaMetricMetadata, pmetric.ExponentialHistogramDataPointSlice{}}, + expectedAttributes: map[string]interface{}{"label1": "value1"}, + }, { name: "Summary from SDK", isPrometheusMetrics: false, @@ -551,6 +686,15 @@ func TestGetDataPoints(t *testing.T) { assert.Equal(t, uint64(18), dp.Count()) assert.Equal(t, []float64{0, 10}, dp.ExplicitBounds().AsRaw()) assert.Equal(t, tc.expectedAttributes, dp.Attributes().AsRaw()) + case exponentialHistogramDataPointSlice: + assert.Equal(t, 1, convertedDPS.Len()) + dp := convertedDPS.ExponentialHistogramDataPointSlice.At(0) + assert.Equal(t, float64(0), dp.Sum()) + assert.Equal(t, uint64(4), dp.Count()) + assert.Equal(t, []uint64{1, 0, 1}, dp.Positive().BucketCounts().AsRaw()) + assert.Equal(t, []uint64{1, 0, 1}, dp.Negative().BucketCounts().AsRaw()) + assert.Equal(t, uint64(0), dp.ZeroCount()) + assert.Equal(t, tc.expectedAttributes, dp.Attributes().AsRaw()) case summaryDataPointSlice: expectedDPS := tc.expectedDatapointSlice.(summaryDataPointSlice) assert.Equal(t, expectedDPS.deltaMetricMetadata, convertedDPS.deltaMetricMetadata) @@ -598,6 +742,7 @@ func BenchmarkGetAndCalculateDeltaDataPoints(b *testing.B) { generateTestGaugeMetric("int-gauge", intValueType), generateTestGaugeMetric("int-gauge", doubleValueType), generateTestHistogramMetric("histogram"), + generateTestExponentialHistogramMetric("exponential-histogram"), generateTestSumMetric("int-sum", intValueType), generateTestSumMetric("double-sum", doubleValueType), generateTestSummaryMetric("summary"), diff --git a/exporter/awsemfexporter/metric_translator.go b/exporter/awsemfexporter/metric_translator.go index 40778c883fad..22f23d0a7ec5 100644 --- a/exporter/awsemfexporter/metric_translator.go +++ b/exporter/awsemfexporter/metric_translator.go @@ -67,6 +67,17 @@ type cWMetricStats struct { Sum float64 } +// The SampleCount of CloudWatch metrics will be calculated by the sum of the 'Counts' array. +// The 'Count' field should be same as the sum of the 'Counts' array and will be ignored in CloudWatch. +type cWMetricHistogram struct { + Values []float64 + Counts []float64 + Max float64 + Min float64 + Count uint64 + Sum float64 +} + type groupedMetricMetadata struct { namespace string timestampMs int64 diff --git a/exporter/awsxrayexporter/internal/translator/segment.go b/exporter/awsxrayexporter/internal/translator/segment.go index f45288267602..89cd14c81e47 100644 --- a/exporter/awsxrayexporter/internal/translator/segment.go +++ b/exporter/awsxrayexporter/internal/translator/segment.go @@ -44,6 +44,12 @@ const ( OriginAppRunner = "AWS::AppRunner::Service" ) +// x-ray only span attributes - https://github.com/open-telemetry/opentelemetry-java-contrib/pull/802 +const ( + awsLocalService = "aws.local.service" + awsRemoteService = "aws.remote.service" +) + var ( // reInvalidSpanCharacters defines the invalid letters in a span name as per // https://docs.aws.amazon.com/xray/latest/devguide/xray-api-segmentdocuments.html @@ -112,15 +118,35 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str sqlfiltered, sql = makeSQL(span, awsfiltered) additionalAttrs = addSpecialAttributes(sqlfiltered, indexedAttrs, attributes) user, annotations, metadata = makeXRayAttributes(additionalAttrs, resource, storeResource, indexedAttrs, indexAllAttrs) + spanLinks, makeSpanLinkErr = makeSpanLinks(span.Links()) name string namespace string ) + if makeSpanLinkErr != nil { + return nil, makeSpanLinkErr + } + // X-Ray segment names are service names, unlike span names which are methods. Try to find a service name. - // peer.service should always be prioritized for segment names when set because it is what the user decided. - if peerService, ok := attributes.Get(conventions.AttributePeerService); ok { - name = peerService.Str() + // support x-ray specific service name attributes as segment name if it exists + if span.Kind() == ptrace.SpanKindServer || span.Kind() == ptrace.SpanKindConsumer { + if localServiceName, ok := attributes.Get(awsLocalService); ok { + name = localServiceName.Str() + } + } + if span.Kind() == ptrace.SpanKindClient || span.Kind() == ptrace.SpanKindProducer { + if remoteServiceName, ok := attributes.Get(awsRemoteService); ok { + name = remoteServiceName.Str() + } + } + + // peer.service should always be prioritized for segment names when it set by users and + // the new x-ray specific service name attributes are not found + if name == "" { + if peerService, ok := attributes.Get(conventions.AttributePeerService); ok { + name = peerService.Str() + } } if namespace == "" { @@ -211,6 +237,7 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str Annotations: annotations, Metadata: metadata, Type: awsxray.String(segmentType), + Links: spanLinks, }, nil } diff --git a/exporter/awsxrayexporter/internal/translator/segment_test.go b/exporter/awsxrayexporter/internal/translator/segment_test.go index 8fb4f4bae125..f3d43c9e8700 100644 --- a/exporter/awsxrayexporter/internal/translator/segment_test.go +++ b/exporter/awsxrayexporter/internal/translator/segment_test.go @@ -902,6 +902,117 @@ func TestSegmentWith2LogGroupsFromConfig(t *testing.T) { assert.Equal(t, cwl, segment.AWS.CWLogs) } +func TestClientSpanWithAwsRemoteServiceName(t *testing.T) { + spanName := "ABC.payment" + parentSpanID := newSegmentID() + user := "testingT" + attributes := make(map[string]interface{}) + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeHTTPScheme] = "https" + attributes[conventions.AttributeHTTPHost] = "payment.amazonaws.com" + attributes[conventions.AttributeHTTPTarget] = "/" + attributes[conventions.AttributeRPCService] = "ABC" + attributes[awsRemoteService] = "PaymentService" + + resource := constructDefaultResource() + span := constructClientSpan(parentSpanID, spanName, 0, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + assert.Equal(t, "PaymentService", *segment.Name) + assert.Equal(t, "subsegment", *segment.Type) + + jsonStr, err := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.NotNil(t, jsonStr) + assert.Nil(t, err) + assert.True(t, strings.Contains(jsonStr, "PaymentService")) + assert.False(t, strings.Contains(jsonStr, user)) + assert.False(t, strings.Contains(jsonStr, "user")) +} + +func TestProducerSpanWithAwsRemoteServiceName(t *testing.T) { + spanName := "ABC.payment" + parentSpanID := newSegmentID() + user := "testingT" + attributes := make(map[string]interface{}) + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeHTTPScheme] = "https" + attributes[conventions.AttributeHTTPHost] = "payment.amazonaws.com" + attributes[conventions.AttributeHTTPTarget] = "/" + attributes[conventions.AttributeRPCService] = "ABC" + attributes[awsRemoteService] = "ProducerService" + + resource := constructDefaultResource() + span := constructProducerSpan(parentSpanID, spanName, 0, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + assert.Equal(t, "ProducerService", *segment.Name) + assert.Equal(t, "subsegment", *segment.Type) + + jsonStr, err := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.NotNil(t, jsonStr) + assert.Nil(t, err) + assert.True(t, strings.Contains(jsonStr, "ProducerService")) + assert.False(t, strings.Contains(jsonStr, user)) + assert.False(t, strings.Contains(jsonStr, "user")) +} + +func TestConsumerSpanWithAwsRemoteServiceName(t *testing.T) { + spanName := "ABC.payment" + parentSpanID := newSegmentID() + user := "testingT" + attributes := make(map[string]interface{}) + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeHTTPScheme] = "https" + attributes[conventions.AttributeHTTPHost] = "payment.amazonaws.com" + attributes[conventions.AttributeHTTPTarget] = "/" + attributes[conventions.AttributeRPCService] = "ABC" + attributes[awsLocalService] = "ConsumerService" + + resource := constructDefaultResource() + span := constructConsumerSpan(parentSpanID, spanName, 0, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + assert.Equal(t, "ConsumerService", *segment.Name) + + jsonStr, err := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.NotNil(t, jsonStr) + assert.Nil(t, err) + assert.True(t, strings.Contains(jsonStr, "ConsumerService")) + assert.False(t, strings.Contains(jsonStr, user)) + assert.False(t, strings.Contains(jsonStr, "user")) +} + +func TestServerSpanWithAwsLocalServiceName(t *testing.T) { + spanName := "ABC.payment" + parentSpanID := newSegmentID() + user := "testingT" + attributes := make(map[string]interface{}) + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeHTTPScheme] = "https" + attributes[conventions.AttributeHTTPHost] = "payment.amazonaws.com" + attributes[conventions.AttributeHTTPTarget] = "/" + attributes[conventions.AttributeRPCService] = "ABC" + attributes[awsLocalService] = "PaymentLocalService" + attributes[awsRemoteService] = "PaymentService" + + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, 0, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + assert.Equal(t, "PaymentLocalService", *segment.Name) + + jsonStr, err := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.NotNil(t, jsonStr) + assert.Nil(t, err) + assert.True(t, strings.Contains(jsonStr, "PaymentLocalService")) + assert.False(t, strings.Contains(jsonStr, user)) + assert.False(t, strings.Contains(jsonStr, "user")) +} + func constructClientSpan(parentSpanID pcommon.SpanID, name string, code ptrace.StatusCode, message string, attributes map[string]interface{}) ptrace.Span { var ( traceID = newTraceID() @@ -956,6 +1067,60 @@ func constructServerSpan(parentSpanID pcommon.SpanID, name string, code ptrace.S return span } +func constructConsumerSpan(parentSpanID pcommon.SpanID, name string, code ptrace.StatusCode, message string, attributes map[string]interface{}) ptrace.Span { + var ( + traceID = newTraceID() + spanID = newSegmentID() + endTime = time.Now() + startTime = endTime.Add(-215 * time.Millisecond) + spanAttributes = constructSpanAttributes(attributes) + ) + + span := ptrace.NewSpan() + span.SetTraceID(traceID) + span.SetSpanID(spanID) + span.SetParentSpanID(parentSpanID) + span.SetName(name) + span.SetKind(ptrace.SpanKindConsumer) + span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) + span.SetEndTimestamp(pcommon.NewTimestampFromTime(endTime)) + + status := ptrace.NewStatus() + status.SetCode(code) + status.SetMessage(message) + status.CopyTo(span.Status()) + + spanAttributes.CopyTo(span.Attributes()) + return span +} + +func constructProducerSpan(parentSpanID pcommon.SpanID, name string, code ptrace.StatusCode, message string, attributes map[string]interface{}) ptrace.Span { + var ( + traceID = newTraceID() + spanID = newSegmentID() + endTime = time.Now() + startTime = endTime.Add(-215 * time.Millisecond) + spanAttributes = constructSpanAttributes(attributes) + ) + + span := ptrace.NewSpan() + span.SetTraceID(traceID) + span.SetSpanID(spanID) + span.SetParentSpanID(parentSpanID) + span.SetName(name) + span.SetKind(ptrace.SpanKindProducer) + span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) + span.SetEndTimestamp(pcommon.NewTimestampFromTime(endTime)) + + status := ptrace.NewStatus() + status.SetCode(code) + status.SetMessage(message) + status.CopyTo(span.Status()) + + spanAttributes.CopyTo(span.Attributes()) + return span +} + func constructSpanAttributes(attributes map[string]interface{}) pcommon.Map { attrs := pcommon.NewMap() for key, value := range attributes { diff --git a/exporter/awsxrayexporter/internal/translator/span_links.go b/exporter/awsxrayexporter/internal/translator/span_links.go new file mode 100644 index 000000000000..cae2ed11b72e --- /dev/null +++ b/exporter/awsxrayexporter/internal/translator/span_links.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package translator // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter/internal/translator" + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + + awsxray "github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/xray" +) + +func makeSpanLinks(links ptrace.SpanLinkSlice) ([]awsxray.SpanLinkData, error) { + var spanLinkDataArray []awsxray.SpanLinkData + + for i := 0; i < links.Len(); i++ { + var spanLinkData awsxray.SpanLinkData + var link = links.At(i) + + var spanID = link.SpanID().String() + traceID, err := convertToAmazonTraceID(link.TraceID()) + + if err != nil { + return nil, err + } + + spanLinkData.SpanID = &spanID + spanLinkData.TraceID = &traceID + + if link.Attributes().Len() > 0 { + spanLinkData.Attributes = make(map[string]interface{}) + + link.Attributes().Range(func(k string, v pcommon.Value) bool { + spanLinkData.Attributes[k] = v.AsRaw() + return true + }) + } + + spanLinkDataArray = append(spanLinkDataArray, spanLinkData) + } + + return spanLinkDataArray, nil +} diff --git a/exporter/awsxrayexporter/internal/translator/span_links_test.go b/exporter/awsxrayexporter/internal/translator/span_links_test.go new file mode 100644 index 000000000000..1f7e4db54ca7 --- /dev/null +++ b/exporter/awsxrayexporter/internal/translator/span_links_test.go @@ -0,0 +1,231 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package translator // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter/internal/translator" + +import ( + "encoding/binary" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +func TestSpanLinkSimple(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + var traceID = newTraceID() + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(traceID) + spanLink.SetSpanID(newSegmentID()) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + var convertedTraceID, _ = convertToAmazonTraceID(traceID) + + assert.Equal(t, 1, len(segment.Links)) + assert.Equal(t, spanLink.SpanID().String(), *segment.Links[0].SpanID) + assert.Equal(t, convertedTraceID, *segment.Links[0].TraceID) + assert.Equal(t, 0, len(segment.Links[0].Attributes)) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.True(t, strings.Contains(jsonStr, "links")) + assert.False(t, strings.Contains(jsonStr, "attributes")) + assert.True(t, strings.Contains(jsonStr, convertedTraceID)) + assert.True(t, strings.Contains(jsonStr, spanLink.SpanID().String())) +} + +func TestSpanLinkEmpty(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + assert.Equal(t, 0, len(segment.Links)) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.False(t, strings.Contains(jsonStr, "links")) +} + +func TestOldSpanLinkError(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + const maxAge = 60 * 60 * 24 * 30 + ExpiredEpoch := time.Now().Unix() - maxAge - 1 + + var traceID = newTraceID() + binary.BigEndian.PutUint32(traceID[0:4], uint32(ExpiredEpoch)) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(traceID) + spanLink.SetSpanID(newSegmentID()) + + _, error1 := MakeSegment(span, resource, nil, false, nil) + + assert.NotNil(t, error1) + + _, error2 := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.NotNil(t, error2) +} + +func TestTwoSpanLinks(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + var traceID1 = newTraceID() + + spanLink1 := span.Links().AppendEmpty() + spanLink1.SetTraceID(traceID1) + spanLink1.SetSpanID(newSegmentID()) + spanLink1.Attributes().PutStr("myKey1", "ABC") + + var traceID2 = newTraceID() + + spanLink2 := span.Links().AppendEmpty() + spanLink2.SetTraceID(traceID2) + spanLink2.SetSpanID(newSegmentID()) + spanLink2.Attributes().PutInt("myKey2", 1234) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + var convertedTraceID1, _ = convertToAmazonTraceID(traceID1) + var convertedTraceID2, _ = convertToAmazonTraceID(traceID2) + + assert.Equal(t, 2, len(segment.Links)) + assert.Equal(t, spanLink1.SpanID().String(), *segment.Links[0].SpanID) + assert.Equal(t, convertedTraceID1, *segment.Links[0].TraceID) + + assert.Equal(t, 1, len(segment.Links[0].Attributes)) + assert.Equal(t, "ABC", segment.Links[0].Attributes["myKey1"]) + + assert.Equal(t, spanLink2.SpanID().String(), *segment.Links[1].SpanID) + assert.Equal(t, convertedTraceID2, *segment.Links[1].TraceID) + assert.Equal(t, 1, len(segment.Links[0].Attributes)) + assert.Equal(t, int64(1234), segment.Links[1].Attributes["myKey2"]) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.True(t, strings.Contains(jsonStr, "attributes")) + assert.True(t, strings.Contains(jsonStr, "links")) + assert.True(t, strings.Contains(jsonStr, "myKey1")) + assert.True(t, strings.Contains(jsonStr, "myKey2")) + assert.True(t, strings.Contains(jsonStr, "ABC")) + assert.True(t, strings.Contains(jsonStr, "1234")) + assert.True(t, strings.Contains(jsonStr, convertedTraceID1)) + assert.True(t, strings.Contains(jsonStr, convertedTraceID2)) +} + +func TestSpanLinkComplexAttributes(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(newTraceID()) + spanLink.SetSpanID(newSegmentID()) + spanLink.Attributes().PutStr("myKey1", "myValue") + spanLink.Attributes().PutBool("myKey2", true) + spanLink.Attributes().PutInt("myKey3", 112233) + spanLink.Attributes().PutDouble("myKey4", 3.1415) + + var slice1 = spanLink.Attributes().PutEmptySlice("myKey5") + slice1.AppendEmpty().SetStr("apple") + slice1.AppendEmpty().SetStr("pear") + slice1.AppendEmpty().SetStr("banana") + + var slice2 = spanLink.Attributes().PutEmptySlice("myKey6") + slice2.AppendEmpty().SetBool(true) + slice2.AppendEmpty().SetBool(false) + slice2.AppendEmpty().SetBool(false) + slice2.AppendEmpty().SetBool(true) + + var slice3 = spanLink.Attributes().PutEmptySlice("myKey7") + slice3.AppendEmpty().SetInt(1234) + slice3.AppendEmpty().SetInt(5678) + slice3.AppendEmpty().SetInt(9012) + + var slice4 = spanLink.Attributes().PutEmptySlice("myKey8") + slice4.AppendEmpty().SetDouble(2.718) + slice4.AppendEmpty().SetDouble(1.618) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + assert.Equal(t, 1, len(segment.Links)) + assert.Equal(t, 8, len(segment.Links[0].Attributes)) + + assert.Equal(t, "myValue", segment.Links[0].Attributes["myKey1"]) + assert.Equal(t, true, segment.Links[0].Attributes["myKey2"]) + assert.Equal(t, int64(112233), segment.Links[0].Attributes["myKey3"]) + assert.Equal(t, 3.1415, segment.Links[0].Attributes["myKey4"]) + + assert.Equal(t, "apple", segment.Links[0].Attributes["myKey5"].([]interface{})[0]) + assert.Equal(t, "pear", segment.Links[0].Attributes["myKey5"].([]interface{})[1]) + assert.Equal(t, "banana", segment.Links[0].Attributes["myKey5"].([]interface{})[2]) + + assert.Equal(t, true, segment.Links[0].Attributes["myKey6"].([]interface{})[0]) + assert.Equal(t, false, segment.Links[0].Attributes["myKey6"].([]interface{})[1]) + assert.Equal(t, false, segment.Links[0].Attributes["myKey6"].([]interface{})[2]) + assert.Equal(t, true, segment.Links[0].Attributes["myKey6"].([]interface{})[0]) + + assert.Equal(t, int64(1234), segment.Links[0].Attributes["myKey7"].([]interface{})[0]) + assert.Equal(t, int64(5678), segment.Links[0].Attributes["myKey7"].([]interface{})[1]) + assert.Equal(t, int64(9012), segment.Links[0].Attributes["myKey7"].([]interface{})[2]) + + assert.Equal(t, 2.718, segment.Links[0].Attributes["myKey8"].([]interface{})[0]) + assert.Equal(t, 1.618, segment.Links[0].Attributes["myKey8"].([]interface{})[1]) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.True(t, strings.Contains(jsonStr, "links")) + + assert.True(t, strings.Contains(jsonStr, "myKey1")) + assert.True(t, strings.Contains(jsonStr, "myValue")) + + assert.True(t, strings.Contains(jsonStr, "myKey2")) + assert.True(t, strings.Contains(jsonStr, "true")) + + assert.True(t, strings.Contains(jsonStr, "myKey3")) + assert.True(t, strings.Contains(jsonStr, "112233")) + + assert.True(t, strings.Contains(jsonStr, "myKey4")) + assert.True(t, strings.Contains(jsonStr, "3.1415")) + + assert.True(t, strings.Contains(jsonStr, "myKey5")) + assert.True(t, strings.Contains(jsonStr, "apple")) + assert.True(t, strings.Contains(jsonStr, "pear")) + assert.True(t, strings.Contains(jsonStr, "banana")) + + assert.True(t, strings.Contains(jsonStr, "myKey6")) + assert.True(t, strings.Contains(jsonStr, "false")) + + assert.True(t, strings.Contains(jsonStr, "myKey7")) + assert.True(t, strings.Contains(jsonStr, "1234")) + assert.True(t, strings.Contains(jsonStr, "5678")) + assert.True(t, strings.Contains(jsonStr, "9012")) + + assert.True(t, strings.Contains(jsonStr, "myKey8")) + assert.True(t, strings.Contains(jsonStr, "2.718")) + assert.True(t, strings.Contains(jsonStr, "1.618")) +} diff --git a/internal/aws/xray/tracesegment.go b/internal/aws/xray/tracesegment.go index fc98ce3c5ce1..761bee7c8881 100644 --- a/internal/aws/xray/tracesegment.go +++ b/internal/aws/xray/tracesegment.go @@ -45,10 +45,11 @@ type Segment struct { StartTime *float64 `json:"start_time"` // Segment-only optional fields - Service *ServiceData `json:"service,omitempty"` - Origin *string `json:"origin,omitempty"` - User *string `json:"user,omitempty"` - ResourceARN *string `json:"resource_arn,omitempty"` + Service *ServiceData `json:"service,omitempty"` + Origin *string `json:"origin,omitempty"` + User *string `json:"user,omitempty"` + ResourceARN *string `json:"resource_arn,omitempty"` + Links []SpanLinkData `json:"links,omitempty"` // Optional fields for both Segment and subsegments TraceID *string `json:"trace_id,omitempty"` @@ -281,3 +282,10 @@ type ServiceData struct { CompilerVersion *string `json:"compiler_version,omitempty"` Compiler *string `json:"compiler,omitempty"` } + +// SpanLinkData provides the shape for unmarshalling the span links in the span link field. +type SpanLinkData struct { + TraceID *string `json:"trace_id"` + SpanID *string `json:"id"` + Attributes map[string]interface{} `json:"attributes,omitempty"` +}