Skip to content

Commit

Permalink
[pkg/pdatatest] support Profiles signal comparison (open-telemetry#36273
Browse files Browse the repository at this point in the history
)

<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

<!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. -->
#### Link to tracking issue
Fixes open-telemetry#36232

---------

Signed-off-by: odubajDT <[email protected]>
  • Loading branch information
odubajDT authored Dec 10, 2024
1 parent adef54e commit ccf3cae
Show file tree
Hide file tree
Showing 9 changed files with 3,142 additions and 25 deletions.
27 changes: 27 additions & 0 deletions .chloggen/profiles-pdata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: pkg/pdatatest

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Add support for Profiles signal comparison."

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [36232]

# (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:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
1 change: 1 addition & 0 deletions pkg/pdatatest/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.115.0
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/collector/pdata v1.21.1-0.20241206185113-3f3e208e71b8
go.opentelemetry.io/collector/pdata/pprofile v0.115.1-0.20241206185113-3f3e208e71b8
go.uber.org/goleak v1.3.0
go.uber.org/multierr v1.11.0
)
Expand Down
2 changes: 2 additions & 0 deletions pkg/pdatatest/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions pkg/pdatatest/internal/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"reflect"
"regexp"
"sort"

"go.opentelemetry.io/collector/pdata/pcommon"
"go.uber.org/multierr"
Expand Down Expand Up @@ -88,3 +89,22 @@ func CompareDroppedAttributesCount(expected, actual uint32) error {
}
return nil
}

func OrderMapByKey(input map[string]any) map[string]any {
// Create a slice to hold the keys
keys := make([]string, 0, len(input))
for k := range input {
keys = append(keys, k)
}

// Sort the keys
sort.Strings(keys)

// Create a new map to hold the sorted key-value pairs
orderedMap := make(map[string]any, len(input))
for _, k := range keys {
orderedMap[k] = input[k]
}

return orderedMap
}
30 changes: 5 additions & 25 deletions pkg/pdatatest/pmetrictest/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"math"
"regexp"
"sort"
"time"

"go.opentelemetry.io/collector/pdata/pcommon"
Expand Down Expand Up @@ -275,27 +274,27 @@ func orderDatapointAttributes(metrics pmetric.Metrics) {
switch msl.At(g).Type() {
case pmetric.MetricTypeGauge:
for k := 0; k < msl.At(g).Gauge().DataPoints().Len(); k++ {
rawOrdered := orderMapByKey(msl.At(g).Gauge().DataPoints().At(k).Attributes().AsRaw())
rawOrdered := internal.OrderMapByKey(msl.At(g).Gauge().DataPoints().At(k).Attributes().AsRaw())
_ = msl.At(g).Gauge().DataPoints().At(k).Attributes().FromRaw(rawOrdered)
}
case pmetric.MetricTypeSum:
for k := 0; k < msl.At(g).Sum().DataPoints().Len(); k++ {
rawOrdered := orderMapByKey(msl.At(g).Sum().DataPoints().At(k).Attributes().AsRaw())
rawOrdered := internal.OrderMapByKey(msl.At(g).Sum().DataPoints().At(k).Attributes().AsRaw())
_ = msl.At(g).Sum().DataPoints().At(k).Attributes().FromRaw(rawOrdered)
}
case pmetric.MetricTypeHistogram:
for k := 0; k < msl.At(g).Histogram().DataPoints().Len(); k++ {
rawOrdered := orderMapByKey(msl.At(g).Histogram().DataPoints().At(k).Attributes().AsRaw())
rawOrdered := internal.OrderMapByKey(msl.At(g).Histogram().DataPoints().At(k).Attributes().AsRaw())
_ = msl.At(g).Histogram().DataPoints().At(k).Attributes().FromRaw(rawOrdered)
}
case pmetric.MetricTypeExponentialHistogram:
for k := 0; k < msl.At(g).ExponentialHistogram().DataPoints().Len(); k++ {
rawOrdered := orderMapByKey(msl.At(g).ExponentialHistogram().DataPoints().At(k).Attributes().AsRaw())
rawOrdered := internal.OrderMapByKey(msl.At(g).ExponentialHistogram().DataPoints().At(k).Attributes().AsRaw())
_ = msl.At(g).ExponentialHistogram().DataPoints().At(k).Attributes().FromRaw(rawOrdered)
}
case pmetric.MetricTypeSummary:
for k := 0; k < msl.At(g).Summary().DataPoints().Len(); k++ {
rawOrdered := orderMapByKey(msl.At(g).Summary().DataPoints().At(k).Attributes().AsRaw())
rawOrdered := internal.OrderMapByKey(msl.At(g).Summary().DataPoints().At(k).Attributes().AsRaw())
_ = msl.At(g).Summary().DataPoints().At(k).Attributes().FromRaw(rawOrdered)
}
case pmetric.MetricTypeEmpty:
Expand All @@ -305,25 +304,6 @@ func orderDatapointAttributes(metrics pmetric.Metrics) {
}
}

func orderMapByKey(input map[string]any) map[string]any {
// Create a slice to hold the keys
keys := make([]string, 0, len(input))
for k := range input {
keys = append(keys, k)
}

// Sort the keys
sort.Strings(keys)

// Create a new map to hold the sorted key-value pairs
orderedMap := make(map[string]any, len(input))
for _, k := range keys {
orderedMap[k] = input[k]
}

return orderedMap
}

func maskMetricAttributeValue(metrics pmetric.Metrics, attributeName string, metricNames []string) {
rms := metrics.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
Expand Down
215 changes: 215 additions & 0 deletions pkg/pdatatest/pprofiletest/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package pprofiletest // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/pprofiletest"

import (
"bytes"
"time"

"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pprofile"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/internal"
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil"
)

// CompareProfilesOption can be used to mutate expected and/or actual profiles before comparing.
type CompareProfilesOption interface {
applyOnProfiles(expected, actual pprofile.Profiles)
}

type compareProfilesOptionFunc func(expected, actual pprofile.Profiles)

func (f compareProfilesOptionFunc) applyOnProfiles(expected, actual pprofile.Profiles) {
f(expected, actual)
}

// IgnoreResourceAttributeValue is a CompareProfilesOption that removes a resource attribute
// from all resources.
func IgnoreResourceAttributeValue(attributeName string) CompareProfilesOption {
return ignoreResourceAttributeValue{
attributeName: attributeName,
}
}

type ignoreResourceAttributeValue struct {
attributeName string
}

func (opt ignoreResourceAttributeValue) applyOnProfiles(expected, actual pprofile.Profiles) {
opt.maskProfilesResourceAttributeValue(expected)
opt.maskProfilesResourceAttributeValue(actual)
}

func (opt ignoreResourceAttributeValue) maskProfilesResourceAttributeValue(profiles pprofile.Profiles) {
rls := profiles.ResourceProfiles()
for i := 0; i < rls.Len(); i++ {
internal.MaskResourceAttributeValue(rls.At(i).Resource(), opt.attributeName)
}
}

// IgnoreResourceAttributeValue is a CompareProfilesOption that removes a resource attribute
// from all resources.
func IgnoreScopeAttributeValue(attributeName string) CompareProfilesOption {
return ignoreScopeAttributeValue{
attributeName: attributeName,
}
}

type ignoreScopeAttributeValue struct {
attributeName string
}

func (opt ignoreScopeAttributeValue) applyOnProfiles(expected, actual pprofile.Profiles) {
opt.maskProfilesScopeAttributeValue(expected)
opt.maskProfilesScopeAttributeValue(actual)
}

func (opt ignoreScopeAttributeValue) maskProfilesScopeAttributeValue(profiles pprofile.Profiles) {
rls := profiles.ResourceProfiles()
for i := 0; i < profiles.ResourceProfiles().Len(); i++ {
sls := rls.At(i).ScopeProfiles()
for j := 0; j < sls.Len(); j++ {
lr := sls.At(j)
val, exists := lr.Scope().Attributes().Get(opt.attributeName)
if exists {
val.SetEmptyBytes()
}
}
}
}

// IgnoreProfileAttributeValue is a CompareProfilesOption that sets the value of an attribute
// to empty bytes for every profile
func IgnoreProfileAttributeValue(attributeName string) CompareProfilesOption {
return ignoreProfileAttributeValue{
attributeName: attributeName,
}
}

type ignoreProfileAttributeValue struct {
attributeName string
}

func (opt ignoreProfileAttributeValue) applyOnProfiles(expected, actual pprofile.Profiles) {
opt.maskProfileAttributeValue(expected)
opt.maskProfileAttributeValue(actual)
}

func (opt ignoreProfileAttributeValue) maskProfileAttributeValue(profiles pprofile.Profiles) {
rls := profiles.ResourceProfiles()
for i := 0; i < profiles.ResourceProfiles().Len(); i++ {
sls := rls.At(i).ScopeProfiles()
for j := 0; j < sls.Len(); j++ {
lrs := sls.At(j).Profiles()
for k := 0; k < lrs.Len(); k++ {
lr := lrs.At(k)
val, exists := lr.Attributes().Get(opt.attributeName)
if exists {
val.SetEmptyBytes()
}
}
}
}
}

// IgnoreProfileTimestampValues is a CompareProfilesOption that sets the value of start timestamp
// and duration to empty bytes for every profile
func IgnoreProfileTimestampValues() CompareProfilesOption {
return ignoreProfileTimestampValues{}
}

type ignoreProfileTimestampValues struct{}

func (opt ignoreProfileTimestampValues) applyOnProfiles(expected, actual pprofile.Profiles) {
opt.maskProfileTimestampValues(expected)
opt.maskProfileTimestampValues(actual)
}

func (opt ignoreProfileTimestampValues) maskProfileTimestampValues(profiles pprofile.Profiles) {
rls := profiles.ResourceProfiles()
for i := 0; i < profiles.ResourceProfiles().Len(); i++ {
sls := rls.At(i).ScopeProfiles()
for j := 0; j < sls.Len(); j++ {
lrs := sls.At(j).Profiles()
for k := 0; k < lrs.Len(); k++ {
lr := lrs.At(k)
lr.SetStartTime(pcommon.NewTimestampFromTime(time.Time{}))
lr.SetDuration(pcommon.NewTimestampFromTime(time.Time{}))
}
}
}
}

// IgnoreResourceProfilesOrder is a CompareProfilesOption that ignores the order of resource traces/metrics/profiles.
func IgnoreResourceProfilesOrder() CompareProfilesOption {
return compareProfilesOptionFunc(func(expected, actual pprofile.Profiles) {
sortResourceProfilesSlice(expected.ResourceProfiles())
sortResourceProfilesSlice(actual.ResourceProfiles())
})
}

func sortResourceProfilesSlice(rls pprofile.ResourceProfilesSlice) {
rls.Sort(func(a, b pprofile.ResourceProfiles) bool {
if a.SchemaUrl() != b.SchemaUrl() {
return a.SchemaUrl() < b.SchemaUrl()
}
aAttrs := pdatautil.MapHash(a.Resource().Attributes())
bAttrs := pdatautil.MapHash(b.Resource().Attributes())
return bytes.Compare(aAttrs[:], bAttrs[:]) < 0
})
}

// IgnoreScopeProfilesOrder is a CompareProfilesOption that ignores the order of instrumentation scope traces/metrics/profiles.
func IgnoreScopeProfilesOrder() CompareProfilesOption {
return compareProfilesOptionFunc(func(expected, actual pprofile.Profiles) {
sortScopeProfilesSlices(expected)
sortScopeProfilesSlices(actual)
})
}

func sortScopeProfilesSlices(ls pprofile.Profiles) {
for i := 0; i < ls.ResourceProfiles().Len(); i++ {
ls.ResourceProfiles().At(i).ScopeProfiles().Sort(func(a, b pprofile.ScopeProfiles) bool {
if a.SchemaUrl() != b.SchemaUrl() {
return a.SchemaUrl() < b.SchemaUrl()
}
if a.Scope().Name() != b.Scope().Name() {
return a.Scope().Name() < b.Scope().Name()
}
return a.Scope().Version() < b.Scope().Version()
})
}
}

// IgnoreProfilesOrder is a CompareProfilesOption that ignores the order of profile records.
func IgnoreProfilesOrder() CompareProfilesOption {
return compareProfilesOptionFunc(func(expected, actual pprofile.Profiles) {
sortProfileSlices(expected)
sortProfileSlices(actual)
})
}

func sortProfileSlices(ls pprofile.Profiles) {
for i := 0; i < ls.ResourceProfiles().Len(); i++ {
for j := 0; j < ls.ResourceProfiles().At(i).ScopeProfiles().Len(); j++ {
ls.ResourceProfiles().At(i).ScopeProfiles().At(j).Profiles().Sort(func(a, b pprofile.Profile) bool {
if a.StartTime() != b.StartTime() {
return a.StartTime() < b.StartTime()
}
if a.Duration() != b.Duration() {
return a.Duration() < b.Duration()
}
as := a.ProfileID()
bs := b.ProfileID()
if !bytes.Equal(as[:], bs[:]) {
return bytes.Compare(as[:], bs[:]) < 0
}
aAttrs := pdatautil.MapHash(a.Attributes())
bAttrs := pdatautil.MapHash(b.Attributes())
return bytes.Compare(aAttrs[:], bAttrs[:]) < 0
})
}
}
}
14 changes: 14 additions & 0 deletions pkg/pdatatest/pprofiletest/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package pprofiletest

import (
"testing"

"go.uber.org/goleak"
)

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
Loading

0 comments on commit ccf3cae

Please sign in to comment.