Skip to content

Commit

Permalink
[pdatatest] allow partial matching of resource and metric attributes …
Browse files Browse the repository at this point in the history
…via regular expression (open-telemetry#28654)

**Description:**
Allow to compare metrics resource attributes or metric attribute values
by matching on a portion of the dimension value with a regular
expression.

**Link to tracking Issue:**
Fixes open-telemetry#27690

**Testing:**
Unit tests.
  • Loading branch information
atoulme authored and jmsnll committed Nov 12, 2023
1 parent 992b1ea commit 06c875c
Show file tree
Hide file tree
Showing 8 changed files with 414 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .chloggen/add-match-metric-attribute-value.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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: pdatatest

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Allow to compare metrics resource attributes or metric attribute values by matching on a portion of the dimension value with a regular expression.

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

# (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: |
Use `MatchResourceAttributeValue("node_id", "cloud-node")` to match two metrics with a resource attribute value that starts with "cloud-node".
Use `MatchMetricAttributeValue("hostname", "container-tomcat-", "gauge.one", "sum.one")` to match metrics with the `hostname` attribute starting with `container-tomcat-`.
# 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: []
12 changes: 12 additions & 0 deletions pkg/pdatatest/internal/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package internal // import "github.com/open-telemetry/opentelemetry-collector-co
import (
"fmt"
"reflect"
"regexp"

"go.opentelemetry.io/collector/pdata/pcommon"
"go.uber.org/multierr"
Expand All @@ -25,6 +26,17 @@ func ChangeResourceAttributeValue(res pcommon.Resource, attr string, changeFn fu
}
}

func MatchResourceAttributeValue(res pcommon.Resource, attr string, re *regexp.Regexp) {
if _, ok := res.Attributes().Get(attr); ok {
if v, ok := res.Attributes().Get(attr); ok {
results := re.FindStringSubmatch(v.Str())
if len(results) > 0 {
res.Attributes().PutStr(attr, results[0])
}
}
}
}

// AddErrPrefix adds a prefix to every multierr error.
func AddErrPrefix(prefix string, in error) error {
var out error
Expand Down
31 changes: 31 additions & 0 deletions pkg/pdatatest/pmetrictest/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,26 @@ func TestCompareMetrics(t *testing.T) {
errors.New(`resource "map[]": scope "": metric "sum.one": unexpected datapoint: map[hostname:also random]`),
),
},
{
name: "match-one-attribute-value",
compareOptions: []CompareMetricsOption{
MatchMetricAttributeValue("hostname", "also", "gauge.one", "sum.one"),
},
withoutOptions: multierr.Combine(
errors.New(`resource "map[]": scope "": metric "gauge.one": missing expected datapoint: map[attribute.two:value A hostname:unpredictable]`),
errors.New(`resource "map[]": scope "": metric "gauge.one": missing expected datapoint: map[attribute.two:value B hostname:unpredictable]`),
errors.New(`resource "map[]": scope "": metric "gauge.one": unexpected datapoint: map[attribute.two:value A hostname:random]`),
errors.New(`resource "map[]": scope "": metric "gauge.one": unexpected datapoint: map[attribute.two:value B hostname:random]`),
errors.New(`resource "map[]": scope "": metric "sum.one": missing expected datapoint: map[hostname:also unpredictable]`),
errors.New(`resource "map[]": scope "": metric "sum.one": unexpected datapoint: map[hostname:also random]`),
),
withOptions: multierr.Combine(
errors.New(`resource "map[]": scope "": metric "gauge.one": missing expected datapoint: map[attribute.two:value A hostname:unpredictable]`),
errors.New(`resource "map[]": scope "": metric "gauge.one": missing expected datapoint: map[attribute.two:value B hostname:unpredictable]`),
errors.New(`resource "map[]": scope "": metric "gauge.one": unexpected datapoint: map[attribute.two:value A hostname:random]`),
errors.New(`resource "map[]": scope "": metric "gauge.one": unexpected datapoint: map[attribute.two:value B hostname:random]`),
),
},
{
name: "ignore-one-resource-attribute",
compareOptions: []CompareMetricsOption{
Expand All @@ -315,6 +335,17 @@ func TestCompareMetrics(t *testing.T) {
),
withOptions: nil,
},
{
name: "match-one-resource-attribute",
compareOptions: []CompareMetricsOption{
MatchResourceAttributeValue("node_id", "random"),
},
withoutOptions: multierr.Combine(
errors.New("missing expected resource: map[node_id:a-different-random-id]"),
errors.New("unexpected resource: map[node_id:a-random-id]"),
),
withOptions: nil,
},
{
name: "ignore-resource-order",
compareOptions: []CompareMetricsOption{
Expand Down
76 changes: 76 additions & 0 deletions pkg/pdatatest/pmetrictest/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package pmetrictest // import "github.com/open-telemetry/opentelemetry-collector
import (
"bytes"
"fmt"
"regexp"
"time"

"go.opentelemetry.io/collector/pdata/pcommon"
Expand Down Expand Up @@ -242,6 +243,81 @@ func maskDataPointSliceAttributeValues(dataPoints pmetric.NumberDataPointSlice,
}
}

// MatchMetricAttributeValue is a CompareMetricsOption that transforms a metric attribute value based on a regular expression.
func MatchMetricAttributeValue(attributeName string, pattern string, metricNames ...string) CompareMetricsOption {
re := regexp.MustCompile(pattern)
return compareMetricsOptionFunc(func(expected, actual pmetric.Metrics) {
matchMetricAttributeValue(expected, attributeName, re, metricNames)
matchMetricAttributeValue(actual, attributeName, re, metricNames)
})
}

func matchMetricAttributeValue(metrics pmetric.Metrics, attributeName string, re *regexp.Regexp, metricNames []string) {
rms := metrics.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
ilms := rms.At(i).ScopeMetrics()
for j := 0; j < ilms.Len(); j++ {
matchMetricSliceAttributeValues(ilms.At(j).Metrics(), attributeName, re, metricNames)
}
}
}

func matchMetricSliceAttributeValues(metrics pmetric.MetricSlice, attributeName string, re *regexp.Regexp, metricNames []string) {
metricNameSet := make(map[string]bool, len(metricNames))
for _, metricName := range metricNames {
metricNameSet[metricName] = true
}

for i := 0; i < metrics.Len(); i++ {
if len(metricNames) == 0 || metricNameSet[metrics.At(i).Name()] {
dps := getDataPointSlice(metrics.At(i))
matchDataPointSliceAttributeValues(dps, attributeName, re)

// If attribute values are ignored, some data points may become
// indistinguishable from each other, but sorting by value allows
// for a reasonably thorough comparison and a deterministic outcome.
dps.Sort(func(a, b pmetric.NumberDataPoint) bool {
if a.IntValue() < b.IntValue() {
return true
}
if a.DoubleValue() < b.DoubleValue() {
return true
}
return false
})
}
}
}

func matchDataPointSliceAttributeValues(dataPoints pmetric.NumberDataPointSlice, attributeName string, re *regexp.Regexp) {
for i := 0; i < dataPoints.Len(); i++ {
attributes := dataPoints.At(i).Attributes()
attribute, ok := attributes.Get(attributeName)
if ok {
results := re.FindStringSubmatch(attribute.Str())
if len(results) > 0 {
attribute.SetStr(results[0])
}
}
}
}

// MatchResourceAttributeValue is a CompareMetricsOption that transforms a resource attribute value based on a regular expression.
func MatchResourceAttributeValue(attributeName string, pattern string) CompareMetricsOption {
re := regexp.MustCompile(pattern)
return compareMetricsOptionFunc(func(expected, actual pmetric.Metrics) {
matchResourceAttributeValue(expected, attributeName, re)
matchResourceAttributeValue(actual, attributeName, re)
})
}

func matchResourceAttributeValue(metrics pmetric.Metrics, attributeName string, re *regexp.Regexp) {
rms := metrics.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
internal.MatchResourceAttributeValue(rms.At(i).Resource(), attributeName, re)
}
}

// IgnoreResourceAttributeValue is a CompareMetricsOption that removes a resource attribute
// from all resources.
func IgnoreResourceAttributeValue(attributeName string) CompareMetricsOption {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
resourceMetrics:
- resource: {}
scopeMetrics:
- metrics:
- gauge:
dataPoints:
- attributes:
- key: hostname
value:
stringValue: random
- key: attribute.two
value:
stringValue: value A
- attributes:
- key: hostname
value:
stringValue: random
- key: attribute.two
value:
stringValue: value B
name: gauge.one
- name: sum.one
sum:
dataPoints:
- attributes:
- key: hostname
value:
stringValue: also random
scope: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
resourceMetrics:
- resource: {}
scopeMetrics:
- metrics:
- gauge:
dataPoints:
- attributes:
- key: hostname
value:
stringValue: unpredictable
- key: attribute.two
value:
stringValue: value A
- attributes:
- key: hostname
value:
stringValue: unpredictable
- key: attribute.two
value:
stringValue: value B
name: gauge.one
- name: sum.one
sum:
dataPoints:
- attributes:
- key: hostname
value:
stringValue: also unpredictable
scope: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
resourceMetrics:
- resource:
attributes:
- key: node_id
value:
stringValue: a-random-id
scopeMetrics:
- metrics:
- description: Number of connections opened and closed to the node
name: aerospike.node.connection.count
sum:
aggregationTemporality: 2
dataPoints:
- asInt: "149"
attributes:
- key: type
value:
stringValue: client
- key: op
value:
stringValue: close
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "150"
attributes:
- key: type
value:
stringValue: client
- key: op
value:
stringValue: open
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "0"
attributes:
- key: type
value:
stringValue: fabric
- key: op
value:
stringValue: close
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "48"
attributes:
- key: type
value:
stringValue: fabric
- key: op
value:
stringValue: open
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "0"
attributes:
- key: type
value:
stringValue: heartbeat
- key: op
value:
stringValue: close
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "2"
attributes:
- key: type
value:
stringValue: heartbeat
- key: op
value:
stringValue: open
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
isMonotonic: true
unit: '{connections}'
- description: Number of open connections to the node
name: aerospike.node.connection.open
sum:
aggregationTemporality: 2
dataPoints:
- asInt: "1"
attributes:
- key: type
value:
stringValue: client
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "48"
attributes:
- key: type
value:
stringValue: fabric
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
- asInt: "2"
attributes:
- key: type
value:
stringValue: heartbeat
startTimeUnixNano: "1652734551332787000"
timeUnixNano: "1652734556334562000"
unit: '{connections}'
scope:
name: otelcol/aerospikereceiver
Loading

0 comments on commit 06c875c

Please sign in to comment.