Repository: kentik/ktranslate
Pull request #178 (2021-11-03 18:49:23)

Send 0 value metrics sometimes
This sends metrics which are 0 valued to NR -- when they are tagged with `force=true`. 

Also fixes a bug where a format of new_relic sends metrics to the events endpoint, not the metrics one. 

Also allows forcing a particular snmp profile to be used from the yaml. 

Also lets a snmp profile use SysDesc to modify which profile is set. 


Patch:
diff --git a/.github/workflows/clean-stale-issues.yml b/.github/workflows/clean-stale-issues.yml
new file mode 100644
index 0000000..4eceec6
--- /dev/null
+++ b/.github/workflows/clean-stale-issues.yml
@@ -0,0 +1,20 @@
+name: 'Close stale issues and PRs'
+
+on:
+  schedule:
+    - cron: '30 1 * * *'
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@v4
+        with:
+          stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
+          stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
+          close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
+          close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
+          days-before-issue-stale: 30
+          days-before-pr-stale: 45
+          days-before-issue-close: 5
+          days-before-pr-close: 10
\ No newline at end of file
diff --git a/pkg/cat/kkc.go b/pkg/cat/kkc.go
index 7ff5391..9225926 100644
--- a/pkg/cat/kkc.go
+++ b/pkg/cat/kkc.go
@@ -684,6 +684,23 @@ func (kc *KTranslate) monitorInput(ctx context.Context, num int, seri func([]*kt
 	}
 }
 
+func (kc *KTranslate) monitorMetricsInput(ctx context.Context, seri func([]*kt.JCHF, []byte) (*kt.Output, error)) {
+	kc.log.Infof("monitorMetricsInput Starting")
+	serBuf := make([]byte, 0)
+	citycache := map[uint32]string{}
+	regioncache := map[uint32]string{}
+
+	for {
+		select {
+		case msgs := <-kc.config.MetricsChan:
+			kc.handleInput(ctx, msgs, serBuf, citycache, regioncache, nil, seri)
+		case <-ctx.Done():
+			kc.log.Infof("monitorMetricsInput Done")
+			return
+		}
+	}
+}
+
 func (kc *KTranslate) monitorAlphaChan(ctx context.Context, i int, seri func([]*kt.JCHF, []byte) (*kt.Output, error)) {
 	cacheTicker := time.NewTicker(CacheInvalidateDuration)
 	defer cacheTicker.Stop()
@@ -953,15 +970,14 @@ func (kc *KTranslate) Run(ctx context.Context) error {
 		kc.syslog = ss
 	}
 
-	// If we're sending self metrics via a chan to sinks.
+	// If we're sending self metrics via a chan to sinks. This one always get sent via nrm.
 	if kc.config.MetricsChan != nil {
-		assureInput()
-		go func() {
-			for {
-				o := <-kc.config.MetricsChan
-				kc.inputChan <- o
-			}
-		}()
+		// Set up formatter
+		fmtr, err := formats.NewFormat(formats.FORMAT_NRM, kc.log.GetLogger().GetUnderlyingLogger(), kc.config.Compression)
+		if err != nil {
+			return err
+		}
+		go kc.monitorMetricsInput(ctx, fmtr.To)
 	}
 
 	kc.log.Infof("System running with format %s, compression %s, max flows: %d, sample rate %d:1 after %d", kc.config.Format, kc.config.Compression, kc.config.MaxFlowPerMessage, kc.config.SampleRate, kc.config.MaxBeforeSample)
diff --git a/pkg/formats/nrm/nrm.go b/pkg/formats/nrm/nrm.go
index b0df3b8..43f1b03 100644
--- a/pkg/formats/nrm/nrm.go
+++ b/pkg/formats/nrm/nrm.go
@@ -571,7 +571,7 @@ func (f *NRMFormat) fromKtranslate(in *kt.JCHF) []NRMetric {
 
 	switch in.CustomStr["type"] {
 	case "counter":
-		if in.CustomBigInt["count"] > 0 {
+		if in.CustomStr["force"] == "true" || in.CustomBigInt["count"] > 0 {
 			ms = append(ms, NRMetric{
 				Name:       "kentik.ktranslate." + in.CustomStr["name"],
 				Type:       NR_GAUGE_TYPE,
@@ -580,7 +580,7 @@ func (f *NRMFormat) fromKtranslate(in *kt.JCHF) []NRMetric {
 			})
 		}
 	case "gauge":
-		if in.CustomBigInt["value"] > 0 {
+		if in.CustomStr["force"] == "true" || in.CustomBigInt["value"] > 0 {
 			ms = append(ms, NRMetric{
 				Name:       "kentik.ktranslate." + in.CustomStr["name"],
 				Type:       NR_GAUGE_TYPE,
@@ -589,7 +589,7 @@ func (f *NRMFormat) fromKtranslate(in *kt.JCHF) []NRMetric {
 			})
 		}
 	case "histogram":
-		if in.CustomBigInt["95-percentile"] > 0 {
+		if in.CustomStr["force"] == "true" || in.CustomBigInt["95-percentile"] > 0 {
 			ms = append(ms, NRMetric{
 				Name:       "kentik.ktranslate." + in.CustomStr["name"],
 				Type:       NR_GAUGE_TYPE,
@@ -598,7 +598,7 @@ func (f *NRMFormat) fromKtranslate(in *kt.JCHF) []NRMetric {
 			})
 		}
 	case "meter":
-		if in.CustomBigInt["one-minute"] > 0 {
+		if in.CustomStr["force"] == "true" || in.CustomBigInt["one-minute"] > 0 {
 			ms = append(ms, NRMetric{
 				Name:       "kentik.ktranslate." + in.CustomStr["name"],
 				Type:       NR_GAUGE_TYPE,
@@ -607,7 +607,7 @@ func (f *NRMFormat) fromKtranslate(in *kt.JCHF) []NRMetric {
 			})
 		}
 	case "timer":
-		if in.CustomBigInt["value"] > 0 {
+		if in.CustomStr["force"] == "true" || in.CustomBigInt["value"] > 0 {
 			ms = append(ms, NRMetric{
 				Name:       "kentik.ktranslate." + in.CustomStr["95-percentile"],
 				Type:       NR_GAUGE_TYPE,
@@ -637,6 +637,7 @@ var keepAcrossTables = map[string]bool{
 	"eventType":      true,
 	"provider":       true,
 	"sysoid_profile": true,
+	kt.IndexVar:      true,
 }
 
 func copyAttrForSnmp(attr map[string]interface{}, metricName string, name kt.MetricInfo, lm *kt.LastMetadata) map[string]interface{} {
@@ -668,14 +669,8 @@ func copyAttrForSnmp(attr map[string]interface{}, metricName string, name kt.Met
 					}
 				} else {
 					// If this metric comes from a specific table, only show attributes for this table.
-					if strings.HasPrefix(newKey, kt.StringPrefix) {
-						if !strings.HasPrefix(newKey, kt.StringPrefix+name.Table) {
-							continue
-						}
-					} else {
-						if !strings.HasPrefix(newKey, name.Table) {
-							continue
-						}
+					if !strings.HasPrefix(newKey, name.Table) {
+						continue
 					}
 				}
 			}
diff --git a/pkg/inputs/flow/format.go b/pkg/inputs/flow/format.go
index d0377db..3c6c381 100644
--- a/pkg/inputs/flow/format.go
+++ b/pkg/inputs/flow/format.go
@@ -111,7 +111,7 @@ func (t *KentikDriver) toJCHF(fmsg *flowmessage.FlowMessage) *kt.JCHF {
 	}
 
 	if _, ok := t.metrics[in.DeviceName]; !ok {
-		t.metrics[in.DeviceName] = &FlowMetric{Flows: go_metrics.GetOrRegisterMeter(fmt.Sprintf("netflow.flows^fmt=%s^device_name=%s", string(t.proto), in.DeviceName), t.registry)}
+		t.metrics[in.DeviceName] = &FlowMetric{Flows: go_metrics.GetOrRegisterMeter(fmt.Sprintf("netflow.flows^fmt=%s^device_name=%s^force=true", string(t.proto), in.DeviceName), t.registry)}
 	}
 	t.metrics[in.DeviceName].Flows.Mark(1)
 
diff --git a/pkg/inputs/snmp/disco.go b/pkg/inputs/snmp/disco.go
index 89ea326..86df32c 100644
--- a/pkg/inputs/snmp/disco.go
+++ b/pkg/inputs/snmp/disco.go
@@ -198,7 +198,7 @@ func doubleCheckHost(result scan.Result, timeout time.Duration, ctl chan bool, m
 	log.Infof("%s Success connecting to %s -- %v", posit, result.Host.String(), md)
 
 	// Stick in the profile too for future use.
-	mibProfile := mdb.FindProfile(md.SysObjectID)
+	mibProfile := mdb.FindProfile(md.SysObjectID, md.SysDescr, "")
 	if mibProfile != nil {
 		log.Infof("Found profile for %s: %v", md.SysObjectID, mibProfile)
 		device.MibProfile = mibProfile.From
diff --git a/pkg/inputs/snmp/metadata/device_metadata.go b/pkg/inputs/snmp/metadata/device_metadata.go
index 109462d..e3bdbc0 100644
--- a/pkg/inputs/snmp/metadata/device_metadata.go
+++ b/pkg/inputs/snmp/metadata/device_metadata.go
@@ -69,7 +69,7 @@ func GetDeviceMetadata(log logger.ContextL, server *gosnmp.GoSNMP, deviceMetadat
 				log.Infof("Trying to walk %s -> %s as a table", oidInfo.Name, oidVal)
 				err := getTable(log, server, oidVal, oidInfo, &md)
 				if err != nil {
-					log.Warnf("Dropping %s because of nil value or missing object: %+v", oidVal, pdu)
+					log.Warnf("Dropping %s because of nil value or missing object: %+v %v", oidVal, pdu, err)
 				}
 			}
 			continue
diff --git a/pkg/inputs/snmp/mibs/profile.go b/pkg/inputs/snmp/mibs/profile.go
index c0d9bce..7b6c111 100644
--- a/pkg/inputs/snmp/mibs/profile.go
+++ b/pkg/inputs/snmp/mibs/profile.go
@@ -3,6 +3,7 @@ package mibs
 import (
 	"fmt"
 	"os"
+	"regexp"
 	"strings"
 	"time"
 
@@ -46,14 +47,15 @@ type Device struct {
 
 type Profile struct {
 	logger.ContextL  `yaml:"-"`
-	Metrics          []MIB          `yaml:"metrics,omitempty"`
-	Extends          []string       `yaml:"extends,omitempty"`
-	Device           Device         `yaml:"device,omitempty"`
-	MetricTags       []Tag          `yaml:"metric_tags,omitempty"`
-	Sysobjectid      kt.StringArray `yaml:"sysobjectid,omitempty"`
-	From             string         `yaml:"from,omitempty"`
-	Provider         kt.Provider    `yaml:"provider,omitempty"`
-	NoUseBulkWalkAll bool           `yaml:"no_use_bulkwalkall"`
+	Metrics          []MIB             `yaml:"metrics,omitempty"`
+	Extends          []string          `yaml:"extends,omitempty"`
+	Device           Device            `yaml:"device,omitempty"`
+	MetricTags       []Tag             `yaml:"metric_tags,omitempty"`
+	Sysobjectid      kt.StringArray    `yaml:"sysobjectid,omitempty"`
+	From             string            `yaml:"from,omitempty"`
+	Provider         kt.Provider       `yaml:"provider,omitempty"`
+	NoUseBulkWalkAll bool              `yaml:"no_use_bulkwalkall"`
+	Matches          map[string]string `yaml:"matches"`
 	extended         bool
 }
 
@@ -175,14 +177,23 @@ func (mdb *MibDB) loadProfileDir(profileDir string, extends map[string]*Profile)
 	return nil
 }
 
-func (mdb *MibDB) FindProfile(sysid string) *Profile {
+func (mdb *MibDB) FindProfile(sysid string, sysdesc string, mibProfile string) *Profile {
+	if strings.HasPrefix(mibProfile, "!") { // Force the use of this particular profile if it starts with !
+		for _, p := range mdb.profiles {
+			if p.From == mibProfile[1:] {
+				return p
+			}
+		}
+		mdb.log.Errorf("No profile matching %s found", mibProfile[1:])
+	}
+
 	if strings.HasPrefix(sysid, ".") {
 		sysid = sysid[1:] // Strip this out if we start with .
 	}
 
 	// If we have one directly matching, just return this.
 	if p, ok := mdb.profiles[sysid]; ok {
-		return p
+		return mdb.checkMatch(p, sysdesc)
 	}
 
 	// Now walk resursivly up the tree, seeing what profiles are found via a wildcard
@@ -190,7 +201,7 @@ func (mdb *MibDB) FindProfile(sysid string) *Profile {
 	for i := len(pts); i > 0; i-- {
 		check := strings.Join(pts[0:i], ".") + ".*"
 		if p, ok := mdb.profiles[check]; ok {
-			return p
+			return mdb.checkMatch(p, sysdesc)
 		}
 	}
 
@@ -198,6 +209,25 @@ func (mdb *MibDB) FindProfile(sysid string) *Profile {
 	return nil
 }
 
+func (mdb *MibDB) checkMatch(pro *Profile, sysdesc string) *Profile {
+	for m, target := range pro.Matches {
+		r, err := regexp.Compile(m)
+		if err != nil {
+			mdb.log.Errorf("Invalid Regex for Match: %s %s", pro.From, m)
+			return nil
+		}
+		if r.MatchString(strings.ToLower(sysdesc)) {
+			for _, p := range mdb.profiles {
+				if p.From == target {
+					return p
+				}
+			}
+			mdb.log.Errorf("No profile matching %s found", target)
+		}
+	}
+	return pro
+}
+
 func (p *Profile) validate() error {
 	hasErr := false
 	for _, metric := range p.Metrics {
diff --git a/pkg/inputs/snmp/snmp.go b/pkg/inputs/snmp/snmp.go
index 6879225..c205fa8 100644
--- a/pkg/inputs/snmp/snmp.go
+++ b/pkg/inputs/snmp/snmp.go
@@ -155,9 +155,9 @@ func runSnmpPolling(ctx context.Context, snmpFile string, jchfChan chan []*kt.JC
 		cl := logger.NewSubContextL(logger.SContext{S: device.DeviceName}, log)
 		var profile *mibs.Profile
 		if mibdb != nil {
-			profile = mibdb.FindProfile(device.OID)
+			profile = mibdb.FindProfile(device.OID, device.Description, device.MibProfile)
 			if profile != nil {
-				cl.Infof("Found profile for %s: %v", device.OID, profile.From)
+				cl.Infof("Found profile for %s: %v %s", device.OID, profile.From, device.MibProfile)
 				if *dumpMibTable {
 					profile.DumpOids(cl)
 				}
diff --git a/pkg/inputs/syslog/syslog.go b/pkg/inputs/syslog/syslog.go
index 3b306ed..ed2a6dc 100644
--- a/pkg/inputs/syslog/syslog.go
+++ b/pkg/inputs/syslog/syslog.go
@@ -51,9 +51,9 @@ func NewSyslogSource(ctx context.Context, host string, log logger.Underlying, lo
 		ContextL: logger.NewContextLFromUnderlying(logger.SContext{S: "Syslog"}, log),
 		logchan:  logchan,
 		metrics: SyslogMetric{
-			Messages: go_metrics.GetOrRegisterMeter(fmt.Sprintf("syslog_messages"), registry),
-			Errors:   go_metrics.GetOrRegisterMeter(fmt.Sprintf("syslog_errors"), registry),
-			Queue:    go_metrics.GetOrRegisterGauge(fmt.Sprintf("syslog_queue"), registry),
+			Messages: go_metrics.GetOrRegisterMeter(fmt.Sprintf("syslog_messages^force=true"), registry),
+			Errors:   go_metrics.GetOrRegisterMeter(fmt.Sprintf("syslog_errors^force=true"), registry),
+			Queue:    go_metrics.GetOrRegisterGauge(fmt.Sprintf("syslog_queue^force=true"), registry),
 		},
 		apic:    apic,
 		devices: apic.GetDevicesAsMap(0),
diff --git a/pkg/util/cmetrics/cmetrics.go b/pkg/util/cmetrics/cmetrics.go
index f4a2cba..0c76305 100644
--- a/pkg/util/cmetrics/cmetrics.go
+++ b/pkg/util/cmetrics/cmetrics.go
@@ -127,6 +127,8 @@ func SetConfWithRegistry(conf string, l Logger, log_prefix string, tsdb_prefix s
 				} else {
 					l.Errorf(log_prefix, "Only HTTP|TCP|UDP endpoint for influx currently supported")
 				}
+			default:
+				l.Errorf(log_prefix, "Unknown metrics config: %v", conf)
 			}
 		}
 	}


Test patch:
diff --git a/pkg/inputs/snmp/mibs/profile_test.go b/pkg/inputs/snmp/mibs/profile_test.go
index 745a58b..5e04559 100644
--- a/pkg/inputs/snmp/mibs/profile_test.go
+++ b/pkg/inputs/snmp/mibs/profile_test.go
@@ -23,30 +23,94 @@ func TestFindProfile(t *testing.T) {
 			"1.3.6.1.4.1.8072.3.2.8":      &Profile{Device: Device{Vendor: "freebsd"}},
 			"1.3.6.1.4.1.318.1.3.4.*":     &Profile{Device: Device{Vendor: "pdu"}},
 			"1.3.6.1.4.1.318.1.3.*":       &Profile{Device: Device{Vendor: "ups"}},
+			"1.3.6.1.4.1.8072.3.2.*":      &Profile{Matches: map[string]string{"^barracuda": "barracuda.yml"}, Device: Device{Vendor: "net-bsd"}},
+			"1.3.6.1.4.1.1588.2.1.1.*":    &Profile{From: "barracuda.yml", Device: Device{Vendor: "barracuda"}},
+			"1.3.6.1.4.1.2.3.4":           &Profile{From: "direct", Device: Device{Vendor: "direct"}},
 		},
 	}
 
-	inputs := map[string]string{
-		".1.3.6.1.4.1.2435.2.3.9.1":      "generic",
-		".1.3.5.1.4.5":                   "nil",
-		".1.3.6.1.4.1.2636.1.1.1.2.65":   "juniper",
-		".1.3.6.1.4.1.29671.2.65":        "meraki",
-		".1.3.6.1.4.1.2636.1.1.1.2.65.2": "generic",
-		".1.3.6.1.4.1.29671":             "meraki",
-		".1.3.6.1.4.1.318":               "apc",
-		".1.3.6.1.4.1.8072.3.2.8":        "freebsd",
-		".1.3.6.1.4.1.318.1.3.27":        "ups",
-		".1.3.6.1.4.1.318.1.3.5.4":       "ups",
-		".1.3.6.1.4.1.318.1.3.4.5":       "pdu",
-		".1.3.6.1.4.1.318.1.3.4.6":       "pdu",
+	inputs := []struct {
+		Sysoid   string
+		Sysdesc  string
+		Profile  string
+		Expected string
+	}{
+		{
+			Sysoid:   ".1.3.6.1.4.1.2435.2.3.9.1",
+			Expected: "generic",
+		},
+		{
+			Sysoid:   ".1.3.5.1.4.5",
+			Expected: "nil",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.2636.1.1.1.2.65",
+			Expected: "juniper",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.29671.2.65",
+			Expected: "meraki",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.2636.1.1.1.2.65.2",
+			Expected: "generic",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.29671",
+			Expected: "meraki",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.318",
+			Expected: "apc",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.8072.3.2.8",
+			Expected: "freebsd",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.318.1.3.27",
+			Expected: "ups",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.318.1.3.5.4",
+			Expected: "ups",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.318.1.3.4.5",
+			Expected: "pdu",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.318.1.3.4.6",
+			Expected: "pdu",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.318.1.3.4.6",
+			Expected: "pdu",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.8072.3.2.10",
+			Expected: "barracuda",
+			Sysdesc:  "Barracuda Email Security Gateway",
+		},
+		{
+			Sysoid:   ".1.3.6.1.4.1.8072.3.2.10",
+			Expected: "barracuda",
+			Sysdesc:  "Barracuda Email Security Gateway",
+		},
+		{
+			Sysoid:   "",
+			Expected: "direct",
+			Sysdesc:  "Barracuda Email Security Gateway",
+			Profile:  "!direct",
+		},
 	}
 
-	for sysid, vendor := range inputs {
-		res := mdb.FindProfile(sysid)
-		if vendor == "nil" {
+	for _, in := range inputs {
+		res := mdb.FindProfile(in.Sysoid, in.Sysdesc, in.Profile)
+		if in.Expected == "nil" {
 			assert.Nil(t, res)
 		} else {
-			assert.Equal(t, vendor, res.Device.Vendor, "sysid: %s", sysid)
+			assert.Equal(t, in.Expected, res.Device.Vendor, "sysid: %s", in.Sysoid)
 		}
 	}
 }