From da80fdec9d412998847cad1fc234e1ff82916520 Mon Sep 17 00:00:00 2001 From: Stefan Kurek Date: Thu, 20 Oct 2022 17:36:17 -0400 Subject: [PATCH] [receiver/snmp] SNMP Receiver create OTEL metric helper (#15357) SNMP Recever refactor out OTEL metric helper struct for scraper Also changing snmpData to public SNMPData as there will be CI issues later when mocking. --- .chloggen/snmpreceiver-add-metric-helper.yaml | 16 + receiver/snmpreceiver/client.go | 22 +- receiver/snmpreceiver/client_test.go | 54 +- receiver/snmpreceiver/otel_metric_helper.go | 214 ++++++++ .../snmpreceiver/otel_metric_helper_test.go | 479 ++++++++++++++++++ 5 files changed, 747 insertions(+), 38 deletions(-) create mode 100755 .chloggen/snmpreceiver-add-metric-helper.yaml create mode 100644 receiver/snmpreceiver/otel_metric_helper.go create mode 100644 receiver/snmpreceiver/otel_metric_helper_test.go diff --git a/.chloggen/snmpreceiver-add-metric-helper.yaml b/.chloggen/snmpreceiver-add-metric-helper.yaml new file mode 100755 index 000000000000..9c63c399c703 --- /dev/null +++ b/.chloggen/snmpreceiver-add-metric-helper.yaml @@ -0,0 +1,16 @@ +# 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: snmpreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: adds otel metric helper struct for SNMP metric receiver scraper + +# One or more tracking issues related to the change +issues: [13409] + +# (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/receiver/snmpreceiver/client.go b/receiver/snmpreceiver/client.go index 9aa959c455a0..bdb587da72a2 100644 --- a/receiver/snmpreceiver/client.go +++ b/receiver/snmpreceiver/client.go @@ -37,8 +37,8 @@ const ( stringVal oidDataType = 0x03 // value will be string ) -// snmpData used for processFunc and is a simpler version of gosnmp.SnmpPDU -type snmpData struct { +// SNMPData used for processFunc and is a simpler version of gosnmp.SnmpPDU +type SNMPData struct { parentOID string // optional oid string value interface{} @@ -49,10 +49,10 @@ type snmpData struct { type client interface { // GetScalarData retrieves SNMP scalar data from a list of passed in OIDS, // then returns the retrieved data - GetScalarData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []snmpData + GetScalarData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []SNMPData // GetIndexedData retrieves SNMP indexed data from a list of passed in OIDS, // then returns the retrieved data - GetIndexedData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []snmpData + GetIndexedData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []SNMPData // Connect makes a connection to the SNMP host Connect() error // Close closes a connection to the SNMP host @@ -200,8 +200,8 @@ func (c *snmpClient) Close() error { // GetScalarData retrieves and returns scalar data from passed in scalar OIDs. // Note: These OIDs must all end in ".0" for the SNMP GET to work correctly -func (c *snmpClient) GetScalarData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []snmpData { - scalarData := []snmpData{} +func (c *snmpClient) GetScalarData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []SNMPData { + scalarData := []SNMPData{} // Nothing to do if there are no OIDs if len(oids) == 0 { @@ -255,8 +255,8 @@ func (c *snmpClient) GetScalarData(oids []string, scraperErrors *scrapererror.Sc // GetIndexedData retrieves indexed metrics from passed in column OIDs. The returned data // is then also passed into the provided function. -func (c *snmpClient) GetIndexedData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []snmpData { - indexedData := []snmpData{} +func (c *snmpClient) GetIndexedData(oids []string, scraperErrors *scrapererror.ScrapeErrors) []SNMPData { + indexedData := []SNMPData{} // Nothing to do if there are no OIDs if len(oids) == 0 { @@ -329,9 +329,9 @@ func chunkArray(initArray []string, chunkSize int) [][]string { } // convertSnmpPDUToSnmpData takes a piece of SnmpPDU data and converts it to the -// client's snmpData type. -func (c *snmpClient) convertSnmpPDUToSnmpData(pdu gosnmp.SnmpPDU) snmpData { - clientSNMPData := snmpData{ +// client's SNMPData type. +func (c *snmpClient) convertSnmpPDUToSnmpData(pdu gosnmp.SnmpPDU) SNMPData { + clientSNMPData := SNMPData{ oid: pdu.Name, } diff --git a/receiver/snmpreceiver/client_test.go b/receiver/snmpreceiver/client_test.go index 4468eac561c4..f2e46a2e2aae 100644 --- a/receiver/snmpreceiver/client_test.go +++ b/receiver/snmpreceiver/client_test.go @@ -215,7 +215,7 @@ func TestGetScalarData(t *testing.T) { { desc: "No OIDs does nothing", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) client := &snmpClient{ logger: zap.NewNop(), @@ -230,7 +230,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client failures adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} getError := errors.New("Bad GET") mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("Get", []string{"1"}).Return(nil, getError) @@ -250,7 +250,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client timeout failures tries to reset connection", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} getError := errors.New("request timeout (after 0 retries)") mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("Get", []string{"1"}).Return(nil, getError) @@ -272,7 +272,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client reset connection fails on connect adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} getError := errors.New("request timeout (after 0 retries)") mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("Get", []string{"1"}).Return(nil, getError) @@ -297,7 +297,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client partial failures still return successes", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { oid: "2", value: int64(1), @@ -332,7 +332,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client returned nil value does not return data", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) pdu := gosnmp.SnmpPDU{ Value: nil, @@ -358,7 +358,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client returned unsupported type value does not return data", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) pdu := gosnmp.SnmpPDU{ Value: true, @@ -384,7 +384,7 @@ func TestGetScalarData(t *testing.T) { { desc: "Large amount of OIDs handled in chunks", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { oid: "1", value: int64(1), @@ -444,7 +444,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client float data type properly converted", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { oid: "1", value: 1.0, @@ -473,7 +473,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client float data type with bad value adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) pdu1 := gosnmp.SnmpPDU{ Value: true, @@ -497,7 +497,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client float data type with bad string value adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) pdu1 := gosnmp.SnmpPDU{ Value: "bad", @@ -521,7 +521,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client int data type with bad value adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) pdu1 := gosnmp.SnmpPDU{ Value: uint64(math.MaxUint64), @@ -545,7 +545,7 @@ func TestGetScalarData(t *testing.T) { { desc: "GoSNMP Client string data type properly converted", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { oid: "1", value: "test", @@ -586,7 +586,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "No OIDs does nothing", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) client := &snmpClient{ logger: zap.NewNop(), @@ -601,7 +601,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client failures adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} walkError := errors.New("Bad WALK") mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) @@ -621,7 +621,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client timeout failures tries to reset connection", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} walkError := errors.New("request timeout (after 0 retries)") mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) @@ -643,7 +643,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client reset connection fails on connect adds errors", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} walkError := errors.New("request timeout (after 0 retries)") mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) @@ -668,7 +668,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client partial failures still returns successes", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { parentOID: "2", oid: "2.1", @@ -701,7 +701,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client returned nil value does not return data", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) badOID := "1.1" @@ -726,7 +726,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client returned unsupported type value does not return data", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) badOID := "1.1" @@ -751,7 +751,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "Return multiple good values", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { parentOID: "1", oid: "1.1", @@ -815,7 +815,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client float data type properly converted", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { parentOID: "1", oid: "1.1", @@ -844,7 +844,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client float data type with bad value adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) pdu := gosnmp.SnmpPDU{ @@ -867,7 +867,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client float data type with bad string value adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) pdu := gosnmp.SnmpPDU{ @@ -890,7 +890,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client int data type with bad value adds error", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{} + expectedSNMPData := []SNMPData{} mockGoSNMP := new(mocks.MockGoSNMPWrapper) mockGoSNMP.On("GetVersion", mock.Anything).Return(gosnmp.Version2c) pdu := gosnmp.SnmpPDU{ @@ -913,7 +913,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client string data type properly converted", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { parentOID: "1", oid: "1.1", @@ -942,7 +942,7 @@ func TestGetIndexedData(t *testing.T) { { desc: "GoSNMP Client v1 uses normal Walk function", testFunc: func(t *testing.T) { - expectedSNMPData := []snmpData{ + expectedSNMPData := []SNMPData{ { parentOID: "1", oid: "1.1", diff --git a/receiver/snmpreceiver/otel_metric_helper.go b/receiver/snmpreceiver/otel_metric_helper.go new file mode 100644 index 000000000000..3292e55b350b --- /dev/null +++ b/receiver/snmpreceiver/otel_metric_helper.go @@ -0,0 +1,214 @@ +// Copyright 2020 OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package snmpreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/snmpreceiver" + +import ( + "fmt" + "sort" + "strings" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +// generalResourceKey is the resource key for the no general "no attribute" resource +const generalResourceKey = "" + +// getResourceKey returns a unique key based on all of the relevant resource attributes names +// as well as the current row index for a metric data point (a row index corresponds to values +// for the attributes that all belong to a single resource, so we only need this for uniqueness +// and not each value for each attribute name) +// +// For example if we have 3 resource attribute configs: RA1, RA2, RA3 and 1 metric config M1. +// M1 has a single column OID in some table and has references to all 3 resource attribute configs. +// RA1 and RA2 also have an OID in their config which reference different columns in the same table. +// RA3 has an indexed value prefix. +// +// If that table has 3 rows, we are going to expect 3 different resources each with one 1 metric that contains 1 datapoint. +// +// The three resources will have attributes that might look something like this: +// Resource 1 +// RA1 => small ("small" is the value at index 1 for RA1's OID column in the table) +// RA2 => cold ("cold" is the value at index 1 for RA2's OID column in the table) +// RA3 => prefix.1 ("prefix.1" is created from RA3's indexed prefix value + the index that corresponds with the related metric's datapoint +// Metric M1 has 1 datapoint with value 10.0 (10.0 is the value at index 1 for M1's column OID in the table) +// +// Resource 2 +// RA1 => medium ("medium" is the value at index 2 for RA1's OID column in the table) +// RA2 => temperate ("temperate" is the value at index 2 for RA2's OID column in the table) +// RA3 => prefix.2 ("prefix.2" is created from RA3's indexed prefix value + the index that corresponds with the related metric's datapoint +// Metric M1 has 1 datapoint with value 15.0 (15.0 is the value at index 2 for M1's column OID in the table) +// +// Resource 3 +// RA1 => large ("large" is the value at index 3 for RA1's OID column in the table) +// RA2 => hot ("hot" is the value at index 3 for RA2's OID column in the table) +// RA3 => prefix.3 ("prefix.3" is created from RA3's indexed prefix value + the index that corresponds with the related metric's datapoint +// Metric M1 has 1 datapoint with value 5.0 (5.0 is the value at index 3 for M1's column OID in the table) +// +// So we could identify Resource 1 with a string key of "RA1=>small,RA2=>cold,RA3=>prefix.1". +// But we also can uniquely identify Resource 1 with a "shortcut" string key of "RA1,RA2,RA3,1". +// This is because the row index in the table is what is uniquely identifying a single resource within a single collection. +func getResourceKey( + metricCfgResourceAttributes []string, + indexString string, +) string { + sort.Strings(metricCfgResourceAttributes) + resourceKey := generalResourceKey + if len(metricCfgResourceAttributes) > 0 { + resourceKey = strings.Join(metricCfgResourceAttributes, ",") + indexString + } + + return resourceKey +} + +// otelMetricHelper contains many of the functions required to get and create OTEL resources, metrics, and datapoints +type otelMetricHelper struct { + // This is the metrics that should be returned by scrape + metrics pmetric.Metrics + // This is used as an easy reference to grab existing OTEL resources by unique key + resourcesByKey map[string]*pmetric.ResourceMetrics + // The info in this map will ultimately already be contained within the resourcesByKey, but it is + // more easily accessible to pull out a specific existing Metric by resource and metric name using this + metricsByResource map[string]map[string]*pmetric.Metric + // This is the ResourceMetricsSlice that will contain all newly created resources and metrics + resourceMetricsSlice pmetric.ResourceMetricsSlice + // This is the timestamp that should be added to all created data points + dataPointTime pcommon.Timestamp + // This is used so that we can put the proper version on the scope metrics + settings component.ReceiverCreateSettings +} + +// newOtelMetricHelper returns a new otelMetricHelper with an initialized master Metrics +func newOTELMetricHelper(settings component.ReceiverCreateSettings) *otelMetricHelper { + metrics := pmetric.NewMetrics() + omh := otelMetricHelper{ + metrics: metrics, + resourceMetricsSlice: metrics.ResourceMetrics(), + resourcesByKey: map[string]*pmetric.ResourceMetrics{}, + metricsByResource: map[string]map[string]*pmetric.Metric{}, + dataPointTime: pcommon.NewTimestampFromTime(time.Now()), + settings: settings, + } + + return &omh +} + +// getResource returns a resource (if already created) by the resource key +func (h otelMetricHelper) getResource(resourceKey string) *pmetric.ResourceMetrics { + return h.resourcesByKey[resourceKey] +} + +// createResource creates a new resource using the given resource attributes and resource key +func (h *otelMetricHelper) createResource(resourceKey string, resourceAttributes map[string]string) *pmetric.ResourceMetrics { + resourceMetrics := h.resourceMetricsSlice.AppendEmpty() + for key, value := range resourceAttributes { + resourceMetrics.Resource().Attributes().PutStr(key, value) + } + scopeMetrics := resourceMetrics.ScopeMetrics().AppendEmpty() + scopeMetrics.Scope().SetName("otelcol/snmpreceiver") + scopeMetrics.Scope().SetVersion(h.settings.BuildInfo.Version) + h.resourcesByKey[resourceKey] = &resourceMetrics + h.metricsByResource[resourceKey] = map[string]*pmetric.Metric{} + + return &resourceMetrics +} + +// getMetric returns a metric (if already created) by resource key and metric name +func (h otelMetricHelper) getMetric(resourceKey string, metricName string) *pmetric.Metric { + if h.metricsByResource[resourceKey] == nil { + h.metricsByResource[resourceKey] = map[string]*pmetric.Metric{} + } + + return h.metricsByResource[resourceKey][metricName] +} + +// createResource creates a new metric using on the resource key'd resource using the given metric config data +func (h *otelMetricHelper) createMetric(resourceKey string, metricName string, metricCfg *MetricConfig) (*pmetric.Metric, error) { + resource := h.getResource(resourceKey) + if resource == nil { + return nil, fmt.Errorf("cannot create metric '%s' as no resource exists for it to be attached", metricName) + } + metricSlice := resource.ScopeMetrics().At(0).Metrics() + newMetric := metricSlice.AppendEmpty() + newMetric.SetName(metricName) + newMetric.SetDescription(metricCfg.Description) + newMetric.SetUnit(metricCfg.Unit) + + if metricCfg.Sum != nil { + newMetric.SetEmptySum() + newMetric.Sum().SetIsMonotonic(metricCfg.Sum.Monotonic) + + switch metricCfg.Sum.Aggregation { + case "cumulative": + newMetric.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + case "delta": + newMetric.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityDelta) + } + } else { + newMetric.SetEmptyGauge() + } + h.metricsByResource[resourceKey][metricName] = &newMetric + + return &newMetric, nil +} + +// addMetricDataPoint creates a datapoint on the metric (metricName) attached to a resource (resourceKey) and populates it +// based on the given data +func (h *otelMetricHelper) addMetricDataPoint(resourceKey string, metricName string, metricCfg *MetricConfig, data SNMPData, attributes map[string]string) (*pmetric.NumberDataPoint, error) { + metric := h.getMetric(resourceKey, metricName) + if metric == nil { + return nil, fmt.Errorf("cannot retrieve datapoints from metric '%s' as it does not currently exist", metricName) + } + + var dps pmetric.NumberDataPointSlice + var valueType string + if metricCfg.Gauge != nil { + dps = metric.Gauge().DataPoints() + valueType = metricCfg.Gauge.ValueType + } else { + dps = metric.Sum().DataPoints() + valueType = metricCfg.Sum.ValueType + } + + // Creates a data point based on the SNMP data + dp := dps.AppendEmpty() + dp.SetTimestamp(h.dataPointTime) + // Not explicitly checking these casts as this should be made safe in the client + switch data.valueType { + case floatVal: + rawValue := data.value.(float64) + if valueType == "double" { + dp.SetDoubleValue(rawValue) + } else { + dp.SetIntValue(int64(rawValue)) + } + default: + rawValue := data.value.(int64) + if valueType == "int" { + dp.SetIntValue(rawValue) + } else { + dp.SetDoubleValue(float64(rawValue)) + } + } + + // Add attributes to dp + for key, value := range attributes { + dp.Attributes().PutStr(key, value) + } + + return &dp, nil +} diff --git a/receiver/snmpreceiver/otel_metric_helper_test.go b/receiver/snmpreceiver/otel_metric_helper_test.go new file mode 100644 index 000000000000..22888f300f7a --- /dev/null +++ b/receiver/snmpreceiver/otel_metric_helper_test.go @@ -0,0 +1,479 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package snmpreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/snmpreceiver" + +import ( + "testing" + + // client is an autogenerated mock type for the client type + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +func TestGetResourceKey(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Empty arguments gives empty key", + testFunc: func(t *testing.T) { + expectedKey := "" + actualKey := getResourceKey([]string{}, "") + require.Equal(t, expectedKey, actualKey) + }, + }, + { + desc: "Returns stringified key from slice and index", + testFunc: func(t *testing.T) { + expectedKey := "key1,key2.1" + actualKey := getResourceKey([]string{"key1", "key2"}, ".1") + require.Equal(t, expectedKey, actualKey) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +} + +func TestNewOTELMetricHelper(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Returns a good otelMetricHelper", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + require.NotNil(t, helper) + require.NotNil(t, helper.metrics) + require.NotNil(t, helper.resourceMetricsSlice) + require.NotNil(t, helper.dataPointTime) + require.Equal(t, settings, helper.settings) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +} + +func TestGetResource(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Returns nil when resource not yet created", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + actual := helper.getResource("r1") + require.Nil(t, actual) + }, + }, + { + desc: "Returns resource when already created", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + actual := helper.getResource("r1") + require.Equal(t, &resource, actual) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +} + +func TestCreateResource(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Creates resource with given attributes and saves it for easy reference", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + actual := helper.createResource("r1", map[string]string{"key1": "val1"}) + require.NotNil(t, actual) + val, exists := actual.Resource().Attributes().Get("key1") + require.Equal(t, true, exists) + require.Equal(t, "val1", val.AsString()) + require.Equal(t, actual, helper.resourcesByKey["r1"]) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +} + +func TestGetMetric(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Returns nil when resource not yet created", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + actual := helper.getMetric("r1", "m1") + require.Nil(t, actual) + }, + }, + { + desc: "Returns nil when metric not yet created", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + actual := helper.getMetric("r1", "m1") + require.Nil(t, actual) + }, + }, + { + desc: "Returns metric when already created", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + metric := resource.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + metric.SetName("Metric 1") + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + helper.metricsByResource["r1"]["m1"] = &metric + actual := helper.getMetric("r1", "m1") + require.Equal(t, &metric, actual) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +} + +func TestCreateMetric(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Returns error when resource does not exist", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "int", + }, + } + actual, err := helper.createMetric("r1", "m1", &metricCfg) + require.Nil(t, actual) + require.EqualError(t, err, "cannot create metric 'm1' as no resource exists for it to be attached") + }, + }, + { + desc: "Creates gauge metric and saves it for easy reference", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "int", + }, + } + actual, err := helper.createMetric("r1", "m1", &metricCfg) + require.NoError(t, err) + require.NotNil(t, actual) + require.Equal(t, "description", actual.Description()) + require.NotNil(t, actual.Gauge()) + require.Equal(t, "m1", actual.Name()) + require.Equal(t, "1", actual.Unit()) + require.Equal(t, actual, helper.metricsByResource["r1"]["m1"]) + }, + }, + { + desc: "Creates sum metric and saves it for easy reference", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Sum: &SumMetric{ + Aggregation: "delta", + Monotonic: false, + ValueType: "double", + }, + } + actual, err := helper.createMetric("r1", "m1", &metricCfg) + require.NoError(t, err) + require.NotNil(t, actual) + require.Equal(t, "description", actual.Description()) + require.Equal(t, pmetric.AggregationTemporalityDelta, actual.Sum().AggregationTemporality()) + require.Equal(t, false, actual.Sum().IsMonotonic()) + require.Equal(t, "m1", actual.Name()) + require.Equal(t, "1", actual.Unit()) + require.Equal(t, actual, helper.metricsByResource["r1"]["m1"]) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +} + +func TestAddMetricDataPoint(t *testing.T) { + testCases := []struct { + desc string + testFunc func(*testing.T) + }{ + { + desc: "Returns error when resource does not exist", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "int", + }, + } + data := SNMPData{ + valueType: integerVal, + value: int64(10), + } + attributes := map[string]string{"key1": "val1"} + actual, err := helper.addMetricDataPoint("r2", "m2", &metricCfg, data, attributes) + require.Nil(t, actual) + require.EqualError(t, err, "cannot retrieve datapoints from metric 'm2' as it does not currently exist") + }, + }, + { + desc: "Returns error when metric does not exist", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "int", + }, + } + data := SNMPData{ + valueType: integerVal, + value: int64(10), + } + attributes := map[string]string{"key1": "val1"} + actual, err := helper.addMetricDataPoint("r1", "m1", &metricCfg, data, attributes) + require.Nil(t, actual) + require.EqualError(t, err, "cannot retrieve datapoints from metric 'm1' as it does not currently exist") + }, + }, + { + desc: "Creates data points on existing gauge metric using passed in data", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + metric := resource.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + metric.SetName("Metric 1") + metric.SetEmptyGauge() + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + helper.metricsByResource["r1"]["m1"] = &metric + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "int", + }, + } + data := SNMPData{ + valueType: integerVal, + value: int64(10), + } + attributes := map[string]string{"key1": "val1"} + actual, err := helper.addMetricDataPoint("r1", "m1", &metricCfg, data, attributes) + require.NoError(t, err) + require.Equal(t, data.value, actual.IntValue()) + val, exists := actual.Attributes().Get("key1") + require.Equal(t, true, exists) + require.Equal(t, "val1", val.AsString()) + metricDataPoint := metric.Gauge().DataPoints().At(0) + require.Equal(t, &metricDataPoint, actual) + }, + }, + { + desc: "Creates data points on existing sum metric using passed in data", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + metric := resource.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + metric.SetName("Metric 1") + metric.SetEmptySum() + metric.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + metric.Sum().SetIsMonotonic(true) + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + helper.metricsByResource["r1"]["m1"] = &metric + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Sum: &SumMetric{ + Aggregation: "cumulative", + Monotonic: true, + ValueType: "double", + }, + } + data := SNMPData{ + valueType: floatVal, + value: float64(10.0), + } + attributes := map[string]string{"key1": "val1"} + actual, err := helper.addMetricDataPoint("r1", "m1", &metricCfg, data, attributes) + require.NoError(t, err) + require.Equal(t, data.value, actual.DoubleValue()) + val, exists := actual.Attributes().Get("key1") + require.Equal(t, true, exists) + require.Equal(t, "val1", val.AsString()) + metricDataPoint := metric.Sum().DataPoints().At(0) + require.Equal(t, &metricDataPoint, actual) + }, + }, + { + desc: "Creates data points on existing metric converting float to int", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + metric := resource.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + metric.SetName("Metric 1") + metric.SetEmptyGauge() + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + helper.metricsByResource["r1"]["m1"] = &metric + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "int", + }, + } + data := SNMPData{ + valueType: floatVal, + value: float64(10.0), + } + attributes := map[string]string{"key1": "val1"} + actual, err := helper.addMetricDataPoint("r1", "m1", &metricCfg, data, attributes) + require.NoError(t, err) + require.Equal(t, int64(10), actual.IntValue()) + val, exists := actual.Attributes().Get("key1") + require.Equal(t, true, exists) + require.Equal(t, "val1", val.AsString()) + metricDataPoint := metric.Gauge().DataPoints().At(0) + require.Equal(t, &metricDataPoint, actual) + }, + }, + { + desc: "Creates data points on existing metric converting int to float", + testFunc: func(t *testing.T) { + settings := component.ReceiverCreateSettings{} + helper := newOTELMetricHelper(settings) + resource := helper.resourceMetricsSlice.AppendEmpty() + resource.ScopeMetrics().AppendEmpty() + resource.Resource().Attributes().PutStr("key1", "val1") + helper.resourcesByKey["r1"] = &resource + metric := resource.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty() + metric.SetName("Metric 1") + metric.SetEmptyGauge() + helper.metricsByResource["r1"] = map[string]*pmetric.Metric{} + helper.metricsByResource["r1"]["m1"] = &metric + metricCfg := MetricConfig{ + Description: "description", + Unit: "1", + Gauge: &GaugeMetric{ + ValueType: "double", + }, + } + data := SNMPData{ + valueType: integerVal, + value: int64(10), + } + attributes := map[string]string{"key1": "val1"} + actual, err := helper.addMetricDataPoint("r1", "m1", &metricCfg, data, attributes) + require.NoError(t, err) + require.Equal(t, float64(10.0), actual.DoubleValue()) + val, exists := actual.Attributes().Get("key1") + require.Equal(t, true, exists) + require.Equal(t, "val1", val.AsString()) + metricDataPoint := metric.Gauge().DataPoints().At(0) + require.Equal(t, &metricDataPoint, actual) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, tc.testFunc) + } +}