Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pkg/pdatatest] support Profiles signal comparison #36273

Merged
merged 22 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading