Repository: NetApp/harvest
Pull request #1596 (2022-12-06 19:14:26)

ci: unable to make issue labeler work. removing


Patch:
diff --git a/.github/workflows/issue-closed-labeler.yml b/.github/workflows/issue-closed-labeler.yml
deleted file mode 100644
index 9fdc36ee..00000000
--- a/.github/workflows/issue-closed-labeler.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-# https://github.com/RebeccaStevens/issue-closed-labeler-action
-name: Issue Closed Labeler
-
-on:
-  issues:
-    types:
-      - closed
-  pull_request:
-    types:
-      - closed
-
-jobs:
-  run:
-    name: "Label"
-    runs-on: ubuntu-latest
-    permissions:
-      contents: read
-      pull-requests: write
-      issues: write
-
-    steps:
-      - name: Update Labels
-        uses: RebeccaStevens/issue-closed-labeler-action@latest
-        with:
-          rules: '[
-            {
-              "condition": "bug",
-              "add": [
-                "status/testme"
-              ],
-              "remove": "wip"
-            }
-          ]'
\ No newline at end of file
diff --git a/cmd/collectors/restperf/restperf.go b/cmd/collectors/restperf/restperf.go
index 29fad60d..09cdba4b 100644
--- a/cmd/collectors/restperf/restperf.go
+++ b/cmd/collectors/restperf/restperf.go
@@ -852,7 +852,7 @@ func (r *RestPerf) PollData() (map[string]*matrix.Matrix, error) {
 
 	// calculate timestamp delta first since many counters require it for postprocessing.
 	// Timestamp has "raw" property, so it isn't post-processed automatically
-	if _, err = timestamp.Delta(prevMat.GetMetric("timestamp"), prevMat, curMat, r.Logger); err != nil {
+	if _, err = curMat.Delta("timestamp", prevMat, r.Logger); err != nil {
 		r.Logger.Error().Err(err).Msg("(timestamp) calculate delta:")
 	}
 
@@ -878,7 +878,7 @@ func (r *RestPerf) PollData() (map[string]*matrix.Matrix, error) {
 		}
 
 		// all other properties - first calculate delta
-		if skips, err = metric.Delta(prevMat.GetMetric(key), prevMat, curMat, r.Logger); err != nil {
+		if skips, err = curMat.Delta(key, prevMat, r.Logger); err != nil {
 			r.Logger.Error().Err(err).Str("key", key).Msg("Calculate delta")
 			continue
 		}
@@ -919,9 +919,9 @@ func (r *RestPerf) PollData() (map[string]*matrix.Matrix, error) {
 		if property == "average" || property == "percent" {
 
 			if strings.HasSuffix(metric.GetName(), "latency") {
-				skips, err = metric.DivideWithThreshold(base, r.perfProp.latencyIoReqd, r.Logger)
+				skips, err = curMat.DivideWithThreshold(key, counter.denominator, r.perfProp.latencyIoReqd, r.Logger)
 			} else {
-				skips, err = metric.Divide(base, r.Logger)
+				skips, err = curMat.Divide(key, counter.denominator, r.Logger)
 			}
 
 			if err != nil {
@@ -936,7 +936,7 @@ func (r *RestPerf) PollData() (map[string]*matrix.Matrix, error) {
 		}
 
 		if property == "percent" {
-			if skips, err = metric.MultiplyByScalar(100, r.Logger); err != nil {
+			if skips, err = curMat.MultiplyByScalar(key, 100, r.Logger); err != nil {
 				r.Logger.Error().Err(err).Str("key", key).Msg("Multiply by scalar")
 			} else {
 				totalSkips += skips
@@ -958,7 +958,7 @@ func (r *RestPerf) PollData() (map[string]*matrix.Matrix, error) {
 		if counter != nil {
 			property := counter.counterType
 			if property == "rate" {
-				if skips, err = metric.Divide(timestamp, r.Logger); err != nil {
+				if skips, err = curMat.Divide(orderedKeys[i], "timestamp", r.Logger); err != nil {
 					r.Logger.Error().Err(err).
 						Int("i", i).
 						Str("metric", metric.GetName()).
diff --git a/cmd/collectors/zapiperf/zapiperf.go b/cmd/collectors/zapiperf/zapiperf.go
index 1451ad7f..0ce1621a 100644
--- a/cmd/collectors/zapiperf/zapiperf.go
+++ b/cmd/collectors/zapiperf/zapiperf.go
@@ -571,7 +571,7 @@ func (z *ZapiPerf) PollData() (map[string]*matrix.Matrix, error) {
 
 	// calculate timestamp delta first since many counters require it for postprocessing.
 	// Timestamp has "raw" property, so it isn't post-processed automatically
-	if _, err = timestamp.Delta(prevMat.GetMetric("timestamp"), prevMat, curMat, z.Logger); err != nil {
+	if _, err = curMat.Delta("timestamp", prevMat, z.Logger); err != nil {
 		z.Logger.Error().Err(err).Msg("(timestamp) calculate delta:")
 		// @TODO terminate since other counters will be incorrect
 	}
@@ -590,7 +590,7 @@ func (z *ZapiPerf) PollData() (map[string]*matrix.Matrix, error) {
 		}
 
 		// all other properties - first calculate delta
-		if skips, err = metric.Delta(prevMat.GetMetric(key), prevMat, curMat, z.Logger); err != nil {
+		if skips, err = curMat.Delta(key, prevMat, z.Logger); err != nil {
 			z.Logger.Error().Err(err).Str("key", key).Msg("Calculate delta")
 			continue
 		}
@@ -631,9 +631,9 @@ func (z *ZapiPerf) PollData() (map[string]*matrix.Matrix, error) {
 		if property == "average" || property == "percent" {
 
 			if strings.HasSuffix(metric.GetName(), "latency") {
-				skips, err = metric.DivideWithThreshold(base, z.latencyIoReqd, z.Logger)
+				skips, err = curMat.DivideWithThreshold(key, metric.GetComment(), z.latencyIoReqd, z.Logger)
 			} else {
-				skips, err = metric.Divide(base, z.Logger)
+				skips, err = curMat.Divide(key, metric.GetComment(), z.Logger)
 			}
 
 			if err != nil {
@@ -647,7 +647,7 @@ func (z *ZapiPerf) PollData() (map[string]*matrix.Matrix, error) {
 		}
 
 		if property == "percent" {
-			if skips, err = metric.MultiplyByScalar(100, z.Logger); err != nil {
+			if skips, err = curMat.MultiplyByScalar(key, 100, z.Logger); err != nil {
 				z.Logger.Error().Err(err).Str("key", key).Msg("Multiply by scalar")
 			} else {
 				totalSkips += skips
@@ -663,7 +663,7 @@ func (z *ZapiPerf) PollData() (map[string]*matrix.Matrix, error) {
 	// calculate rates (which we deferred to calculate averages/percents first)
 	for i, metric := range orderedMetrics {
 		if metric.GetProperty() == "rate" {
-			if skips, err = metric.Divide(timestamp, z.Logger); err != nil {
+			if skips, err = curMat.Divide(orderedKeys[i], "timestamp", z.Logger); err != nil {
 				z.Logger.Error().Err(err).
 					Int("i", i).
 					Str("key", orderedKeys[i]).
diff --git a/pkg/matrix/matrix.go b/pkg/matrix/matrix.go
index f5e0c426..eadf56fa 100644
--- a/pkg/matrix/matrix.go
+++ b/pkg/matrix/matrix.go
@@ -14,6 +14,7 @@ import (
 	"fmt"
 	"github.com/netapp/harvest/v2/pkg/dict"
 	"github.com/netapp/harvest/v2/pkg/errs"
+	"github.com/netapp/harvest/v2/pkg/logging"
 	"github.com/netapp/harvest/v2/pkg/tree/node"
 	"strings"
 )
@@ -303,3 +304,169 @@ func CreateMetric(key string, data *Matrix) error {
 	}
 	return nil
 }
+
+// Delta vector arithmetics
+func (m *Matrix) Delta(metricKey string, prevMat *Matrix, logger *logging.Logger) (int, error) {
+	var skips int
+	prevMetric := prevMat.GetMetric(metricKey)
+	curMetric := m.GetMetric(metricKey)
+	prevRaw := prevMetric.values
+	prevRecord := prevMetric.GetRecords()
+	for key, currInstance := range m.GetInstances() {
+		// check if this instance key exists in previous matrix
+		prevInstance := prevMat.GetInstance(key)
+		currIndex := currInstance.index
+		curRaw := curMetric.values[currIndex]
+		if prevInstance != nil {
+			prevIndex := prevInstance.index
+			if curMetric.record[currIndex] && prevRecord[prevIndex] {
+				curMetric.values[currIndex] -= prevRaw[prevIndex]
+				// Sometimes ONTAP sends spurious zeroes or values less than the previous poll.
+				// Detect and don't publish negative deltas or the subsequent poll will show a large spike.
+				isInvalidZero := (curRaw == 0 || prevRaw[prevIndex] == 0) && curMetric.values[prevIndex] != 0
+				isNegative := curMetric.values[currIndex] < 0
+				if isInvalidZero || isNegative {
+					curMetric.record[currIndex] = false
+					skips++
+					logger.Trace().
+						Str("metric", curMetric.GetName()).
+						Float64("currentRaw", curRaw).
+						Float64("previousRaw", prevRaw[prevIndex]).
+						Str("instKey", key).
+						Msg("Negative cooked value")
+				}
+			} else {
+				curMetric.record[currIndex] = false
+				skips++
+				logger.Trace().
+					Str("metric", curMetric.GetName()).
+					Float64("currentRaw", curRaw).
+					Float64("previousRaw", prevRaw[prevIndex]).
+					Str("instKey", key).
+					Msg("Delta calculation skipped")
+			}
+		} else {
+			curMetric.record[currIndex] = false
+			skips++
+			logger.Trace().
+				Str("metric", curMetric.GetName()).
+				Float64("currentRaw", curRaw).
+				Str("instKey", key).
+				Msg("New instance added")
+		}
+	}
+	return skips, nil
+}
+
+func (m *Matrix) Divide(metricKey string, baseKey string, logger *logging.Logger) (int, error) {
+	var skips int
+	metric := m.GetMetric(metricKey)
+	base := m.GetMetric(baseKey)
+	sValues := base.values
+	sRecord := base.GetRecords()
+	if len(metric.values) != len(sValues) {
+		return 0, errs.New(ErrUnequalVectors, fmt.Sprintf("numerator=%d, denominator=%d", len(metric.values), len(sValues)))
+	}
+	for i := 0; i < len(metric.values); i++ {
+		if metric.record[i] && sRecord[i] {
+			if sValues[i] != 0 {
+				// Don't pass along the value if the numerator or denominator is < 0
+				// A denominator of zero is fine
+				if metric.values[i] < 0 || sValues[i] < 0 {
+					metric.record[i] = false
+					skips++
+					logger.Trace().
+						Str("metric", metric.GetName()).
+						Float64("numerator", metric.values[i]).
+						Float64("denominator", sValues[i]).
+						Msg("Divide calculation skipped")
+				}
+				metric.values[i] /= sValues[i]
+			} else {
+				metric.values[i] = 0
+			}
+		} else {
+			metric.record[i] = false
+			skips++
+			logger.Trace().
+				Str("metric", metric.GetName()).
+				Float64("numerator", metric.values[i]).
+				Float64("denominator", sValues[i]).
+				Msg("Divide calculation skipped")
+		}
+	}
+	return skips, nil
+}
+
+func (m *Matrix) DivideWithThreshold(metricKey string, baseKey string, threshold int, logger *logging.Logger) (int, error) {
+	var skips int
+	x := float64(threshold)
+	metric := m.GetMetric(metricKey)
+	base := m.GetMetric(baseKey)
+	sValues := base.values
+	sRecord := base.GetRecords()
+	if len(metric.values) != len(sValues) {
+		return 0, errs.New(ErrUnequalVectors, fmt.Sprintf("numerator=%d, denominator=%d", len(metric.values), len(sValues)))
+	}
+	for i := 0; i < len(metric.values); i++ {
+		v := metric.values[i]
+		// Don't pass along the value if the numerator or denominator is < 0
+		// It is important to check sValues[i] < 0 and allow a zero so pass=true and m.values[i] remains unchanged
+		if metric.values[i] < 0 || sValues[i] < 0 {
+			metric.record[i] = false
+			skips++
+			logger.Trace().
+				Str("metric", metric.GetName()).
+				Float64("numerator", v).
+				Float64("denominator", sValues[i]).
+				Msg("Negative values")
+			return skips, nil
+		}
+		if metric.record[i] && sRecord[i] {
+			if sValues[i] >= x {
+				metric.values[i] /= sValues[i]
+			} else {
+				metric.values[i] = 0
+			}
+		} else {
+			metric.record[i] = false
+			skips++
+			logger.Trace().
+				Str("metric", metric.GetName()).
+				Float64("numerator", metric.values[i]).
+				Float64("denominator", sValues[i]).
+				Msg("Divide threshold calculation skipped")
+		}
+	}
+	return skips, nil
+}
+
+func (m *Matrix) MultiplyByScalar(metricKey string, s uint, logger *logging.Logger) (int, error) {
+	var skips int
+	x := float64(s)
+	metric := m.GetMetric(metricKey)
+	for i := 0; i < len(metric.values); i++ {
+		if metric.record[i] {
+			// if current is <= 0
+			if metric.values[i] < 0 {
+				metric.record[i] = false
+				skips++
+				logger.Trace().
+					Str("metric", metric.GetName()).
+					Float64("currentRaw", metric.values[i]).
+					Uint("scalar", s).
+					Msg("Negative value")
+			}
+			metric.values[i] *= x
+		} else {
+			metric.record[i] = false
+			skips++
+			logger.Trace().
+				Str("metric", metric.GetName()).
+				Float64("currentRaw", metric.values[i]).
+				Uint("scalar", s).
+				Msg("Scalar multiplication skipped")
+		}
+	}
+	return skips, nil
+}
diff --git a/pkg/matrix/metric.go b/pkg/matrix/metric.go
index c0d267c4..ed548d99 100644
--- a/pkg/matrix/metric.go
+++ b/pkg/matrix/metric.go
@@ -14,8 +14,6 @@ package matrix
 import (
 	"fmt"
 	"github.com/netapp/harvest/v2/pkg/dict"
-	"github.com/netapp/harvest/v2/pkg/errs"
-	"github.com/netapp/harvest/v2/pkg/logging"
 	"strconv"
 )
 
@@ -288,165 +286,6 @@ func (m *Metric) GetValueBytes(i *Instance) ([]byte, bool) {
 	return []byte(s), ok
 }
 
-// Delta vector arithmetics
-func (m *Metric) Delta(prevMetric *Metric, prevMat *Matrix, curMat *Matrix, logger *logging.Logger) (int, error) {
-	var skips int
-	prevRaw := prevMetric.values
-	prevRecord := prevMetric.GetRecords()
-	for key, currInstance := range curMat.GetInstances() {
-		// check if this instance key exists in previous matrix
-		prevInstance := prevMat.GetInstance(key)
-		currIndex := currInstance.index
-		curRaw := m.values[currIndex]
-		if prevInstance != nil {
-			prevIndex := prevInstance.index
-			if m.record[currIndex] && prevRecord[prevIndex] {
-				m.values[currIndex] -= prevRaw[prevIndex]
-				// Sometimes ONTAP sends spurious zeroes or values less than the previous poll.
-				// Detect and don't publish negative deltas or the subsequent poll will show a large spike.
-				isInvalidZero := (curRaw == 0 || prevRaw[prevIndex] == 0) && m.values[prevIndex] != 0
-				isNegative := m.values[currIndex] < 0
-				if isInvalidZero || isNegative {
-					m.record[currIndex] = false
-					skips++
-					logger.Trace().
-						Str("metric", m.GetName()).
-						Float64("currentRaw", curRaw).
-						Float64("previousRaw", prevRaw[prevIndex]).
-						Str("instKey", key).
-						Msg("Negative cooked value")
-				}
-			} else {
-				m.record[currIndex] = false
-				skips++
-				logger.Trace().
-					Str("metric", m.GetName()).
-					Float64("currentRaw", curRaw).
-					Float64("previousRaw", prevRaw[prevIndex]).
-					Str("instKey", key).
-					Msg("Delta calculation skipped")
-			}
-		} else {
-			m.record[currIndex] = false
-			skips++
-			logger.Trace().
-				Str("metric", m.GetName()).
-				Float64("currentRaw", curRaw).
-				Str("instKey", key).
-				Msg("New instance added")
-		}
-	}
-	return skips, nil
-}
-
-func (m *Metric) Divide(s *Metric, logger *logging.Logger) (int, error) {
-	var skips int
-	sValues := s.values
-	sRecord := s.GetRecords()
-	if len(m.values) != len(sValues) {
-		return 0, errs.New(ErrUnequalVectors, fmt.Sprintf("numerator=%d, denominator=%d", len(m.values), len(sValues)))
-	}
-	for i := 0; i < len(m.values); i++ {
-		if m.record[i] && sRecord[i] {
-			if sValues[i] != 0 {
-				// Don't pass along the value if the numerator or denominator is < 0
-				// A denominator of zero is fine
-				if m.values[i] < 0 || sValues[i] < 0 {
-					m.record[i] = false
-					skips++
-					logger.Trace().
-						Str("metric", m.GetName()).
-						Float64("numerator", m.values[i]).
-						Float64("denominator", sValues[i]).
-						Msg("Divide calculation skipped")
-				}
-				m.values[i] /= sValues[i]
-			} else {
-				m.values[i] = 0
-			}
-		} else {
-			m.record[i] = false
-			skips++
-			logger.Trace().
-				Str("metric", m.GetName()).
-				Float64("numerator", m.values[i]).
-				Float64("denominator", sValues[i]).
-				Msg("Divide calculation skipped")
-		}
-	}
-	return skips, nil
-}
-
-func (m *Metric) DivideWithThreshold(s *Metric, t int, logger *logging.Logger) (int, error) {
-	var skips int
-	x := float64(t)
-	sValues := s.values
-	sRecord := s.GetRecords()
-	if len(m.values) != len(sValues) {
-		return 0, errs.New(ErrUnequalVectors, fmt.Sprintf("numerator=%d, denominator=%d", len(m.values), len(sValues)))
-	}
-	for i := 0; i < len(m.values); i++ {
-		v := m.values[i]
-		// Don't pass along the value if the numerator or denominator is < 0
-		// It's important to check sValues[i] < 0 and allow a zero so pass=true and m.values[i] remains unchanged
-		if m.values[i] < 0 || sValues[i] < 0 {
-			m.record[i] = false
-			skips++
-			logger.Trace().
-				Str("metric", m.GetName()).
-				Float64("numerator", v).
-				Float64("denominator", sValues[i]).
-				Msg("Negative values")
-			return skips, nil
-		}
-		if m.record[i] && sRecord[i] {
-			if sValues[i] >= x {
-				m.values[i] /= sValues[i]
-			} else {
-				m.values[i] = 0
-			}
-		} else {
-			m.record[i] = false
-			skips++
-			logger.Trace().
-				Str("metric", m.GetName()).
-				Float64("numerator", m.values[i]).
-				Float64("denominator", sValues[i]).
-				Msg("Divide threshold calculation skipped")
-		}
-	}
-	return skips, nil
-}
-
-func (m *Metric) MultiplyByScalar(s uint, logger *logging.Logger) (int, error) {
-	var skips int
-	x := float64(s)
-	for i := 0; i < len(m.values); i++ {
-		if m.record[i] {
-			// if current is <= 0
-			if m.values[i] < 0 {
-				m.record[i] = false
-				skips++
-				logger.Trace().
-					Str("metric", m.GetName()).
-					Float64("currentRaw", m.values[i]).
-					Uint("scalar", s).
-					Msg("Negative value")
-			}
-			m.values[i] *= x
-		} else {
-			m.record[i] = false
-			skips++
-			logger.Trace().
-				Str("metric", m.GetName()).
-				Float64("currentRaw", m.values[i]).
-				Uint("scalar", s).
-				Msg("Scalar multiplication skipped")
-		}
-	}
-	return skips, nil
-}
-
 func (m *Metric) Print() {
 	for i := range m.values {
 		if m.record[i] {


Test patch:
diff --git a/pkg/matrix/metric_test.go b/pkg/matrix/metric_test.go
index 4042ec85..32dcc8e1 100644
--- a/pkg/matrix/metric_test.go
+++ b/pkg/matrix/metric_test.go
@@ -81,14 +81,13 @@ func setupMatrixAdv(previousRaw []float64, currentRaw []float64) (*Matrix, *Matr
 }
 
 type test struct {
-	name      string
-	curRaw    float64
-	prevRaw   float64
-	cooked    []float64
-	threshold int
-	scalar    uint
-	skips     int
-	record    []bool
+	name    string
+	curRaw  float64
+	prevRaw float64
+	cooked  []float64
+	scalar  uint
+	skips   int
+	record  []bool
 }
 
 type testAdv struct {
@@ -116,7 +115,7 @@ func TestMetricFloat64_Delta(t *testing.T) {
 	for _, tt := range tests2 {
 		t.Run(tt.name, func(t *testing.T) {
 			previous, current := setupMatrix(tt.prevRaw, tt.curRaw, addInstance)
-			skips, err := current.GetMetric("speed").Delta(previous.GetMetric("speed"), previous, current, logging.Get())
+			skips, err := current.Delta("speed", previous, logging.Get())
 			matrixTest(t, tt, current, skips, err)
 		})
 	}
@@ -132,7 +131,7 @@ func TestMetricFloat64_Delta(t *testing.T) {
 	for _, tt := range tests3 {
 		t.Run(tt.name, func(t *testing.T) {
 			previous, current := setupMatrix(tt.prevRaw, tt.curRaw, addDeleteInstance)
-			skips, err := current.GetMetric("speed").Delta(previous.GetMetric("speed"), previous, current, logging.Get())
+			skips, err := current.Delta("speed", previous, logging.Get())
 			matrixTest(t, tt, current, skips, err)
 		})
 	}
@@ -150,90 +149,60 @@ func testDelta(t *testing.T, op matrixOp) {
 	for _, tt := range tests {
 		t.Run(tt.name+"_"+string(op), func(t *testing.T) {
 			previous, current := setupMatrix(tt.prevRaw, tt.curRaw, op)
-			skips, err := current.GetMetric("speed").Delta(previous.GetMetric("speed"), previous, current, logging.Get())
+			skips, err := current.Delta("speed", previous, logging.Get())
 			matrixTest(t, tt, current, skips, err)
 		})
 	}
 }
 
 func TestMetricFloat64_Divide(t *testing.T) {
-	tests := []test{
-		{prevRaw: 20, curRaw: 10, cooked: []float64{2}, skips: 0, record: []bool{true}, name: "normal"},
-		{prevRaw: -20, curRaw: 10, cooked: []float64{-2}, skips: 1, record: []bool{false}, name: "bug negative num"},
-		{prevRaw: 20, curRaw: -10, cooked: []float64{-2}, skips: 1, record: []bool{false}, name: "bug negative den"},
-		{prevRaw: -20, curRaw: -10, cooked: []float64{2}, skips: 1, record: []bool{false}, name: "bug negative both"},
-		{prevRaw: 20, curRaw: 0, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "allow zero den"},
-		{prevRaw: 0, curRaw: 0, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "allow zero both"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			numerator, denominator := setupMatrix(tt.prevRaw, tt.curRaw, sameInstance)
-			skips, err := numerator.GetMetric("speed").Divide(denominator.GetMetric("speed"), logging.Get())
-			matrixTest(t, tt, numerator, skips, err)
-		})
-	}
-
 	testsAdv := []testAdv{
 		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 10}, cooked: []float64{2}, skips: 0, record: []bool{true}, name: "normal"},
-		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 0}, cooked: []float64{10}, skips: 1, record: []bool{false}, name: "denominator record false"},
-		{prevRaw: []float64{10, 5}, curRaw: []float64{5, 10}, cooked: []float64{-5}, skips: 1, record: []bool{false}, name: "numerator record false"},
-		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 0}, cooked: []float64{10}, skips: 1, record: []bool{false}, name: "numerator denominator record false"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 5}, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "allow zero den"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{10, 5}, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "allow zero both"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 0}, cooked: []float64{10}, skips: 1, record: []bool{false}, name: "bug negative den"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{5, 10}, cooked: []float64{-5}, skips: 1, record: []bool{false}, name: "bug negative num"},
+		{prevRaw: []float64{20, 5}, curRaw: []float64{10, 0}, cooked: []float64{-10}, skips: 1, record: []bool{false}, name: "bug negative both"},
 	}
 
 	for _, tt := range testsAdv {
 		t.Run(tt.name, func(t *testing.T) {
 			prevMat, curMat := setupMatrixAdv(tt.prevRaw, tt.curRaw)
 			for k := range curMat.GetMetrics() {
-				_, err := curMat.GetMetric(k).Delta(prevMat.GetMetric(k), prevMat, curMat, logging.Get())
+				_, err := curMat.Delta(k, prevMat, logging.Get())
 				if err != nil {
 					t.Error("unexpected error", err)
 					return
 				}
 			}
-			skips, err := curMat.GetMetric("average_latency").Divide(curMat.GetMetric("total_ops"), logging.Get())
+			skips, err := curMat.Divide("average_latency", "total_ops", logging.Get())
 			matrixTestAdv(t, tt, curMat, skips, err)
 		})
 	}
 }
 
 func TestMetricFloat64_DivideWithThreshold(t *testing.T) {
-	tests := []test{
-		{prevRaw: 20, curRaw: 10, threshold: 5, cooked: []float64{2}, skips: 0, record: []bool{true}, name: "normal"},
-		{prevRaw: 20, curRaw: 10, threshold: 15, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "normal < threshold"},
-		{prevRaw: 20, curRaw: -10, threshold: 5, cooked: []float64{20}, skips: 1, record: []bool{false}, name: "bug negative den"},
-		{prevRaw: -20, curRaw: -10, threshold: 5, cooked: []float64{-20}, skips: 1, record: []bool{false}, name: "bug negative both"},
-		{prevRaw: 20, curRaw: 0, threshold: 0, cooked: []float64{math.Inf(1)}, skips: 0, record: []bool{true}, name: "allow no threshold"},
-		{prevRaw: 0, curRaw: 0, threshold: 5, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "allow zero both"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			numerator, denominator := setupMatrix(tt.prevRaw, tt.curRaw, sameInstance)
-			skips, err := numerator.GetMetric("speed").
-				DivideWithThreshold(denominator.GetMetric("speed"), tt.threshold, logging.Get())
-			matrixTest(t, tt, numerator, skips, err)
-		})
-	}
-
 	testsAdv := []testAdv{
 		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 10}, threshold: 1, cooked: []float64{2}, skips: 0, record: []bool{true}, name: "normal"},
-		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 0}, threshold: 1, cooked: []float64{10}, skips: 1, record: []bool{false}, name: "denominator record false"},
-		{prevRaw: []float64{10, 5}, curRaw: []float64{5, 10}, threshold: 1, cooked: []float64{-5}, skips: 1, record: []bool{false}, name: "numerator record false"},
-		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 0}, threshold: 1, cooked: []float64{10}, skips: 1, record: []bool{false}, name: "numerator denominator record false"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 10}, threshold: 15, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "normal < threshold"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{20, 0}, threshold: 1, cooked: []float64{10}, skips: 1, record: []bool{false}, name: "bug negative den"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{5, 10}, threshold: 1, cooked: []float64{-5}, skips: 1, record: []bool{false}, name: "bug negative num"},
+		{prevRaw: []float64{20, 5}, curRaw: []float64{10, 0}, threshold: 1, cooked: []float64{-10}, skips: 1, record: []bool{false}, name: "bug negative both"},
+		{prevRaw: []float64{10, 10}, curRaw: []float64{20, 10}, threshold: 0, cooked: []float64{math.Inf(1)}, skips: 0, record: []bool{true}, name: "allow no threshold"},
+		{prevRaw: []float64{10, 5}, curRaw: []float64{10, 5}, threshold: 5, cooked: []float64{0}, skips: 0, record: []bool{true}, name: "allow zero both"},
 	}
 
 	for _, tt := range testsAdv {
 		t.Run(tt.name, func(t *testing.T) {
 			prevMat, curMat := setupMatrixAdv(tt.prevRaw, tt.curRaw)
 			for k := range curMat.GetMetrics() {
-				_, err := curMat.GetMetric(k).Delta(prevMat.GetMetric(k), prevMat, curMat, logging.Get())
+				_, err := curMat.Delta(k, prevMat, logging.Get())
 				if err != nil {
 					t.Error("unexpected error", err)
 					return
 				}
 			}
-			skips, err := curMat.GetMetric("average_latency").DivideWithThreshold(curMat.GetMetric("total_ops"), tt.threshold, logging.Get())
+			skips, err := curMat.DivideWithThreshold("average_latency", "total_ops", tt.threshold, logging.Get())
 			matrixTestAdv(t, tt, curMat, skips, err)
 		})
 	}
@@ -248,7 +217,7 @@ func TestMetricFloat64_MultiplyByScalar(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			_, current := setupMatrix(tt.prevRaw, tt.curRaw, sameInstance)
-			skips, err := current.GetMetric("speed").MultiplyByScalar(tt.scalar, logging.Get())
+			skips, err := current.MultiplyByScalar("speed", tt.scalar, logging.Get())
 			matrixTest(t, tt, current, skips, err)
 		})
 	}