diff --git a/plugins/inputs/gnmi/gnmi.go b/plugins/inputs/gnmi/gnmi.go index 8e9782a700425..8d3303328c593 100644 --- a/plugins/inputs/gnmi/gnmi.go +++ b/plugins/inputs/gnmi/gnmi.go @@ -12,6 +12,7 @@ import ( "math" "net" "path" + "regexp" "strings" "sync" "time" @@ -34,6 +35,9 @@ import ( //go:embed sample.conf var sampleConfig string +// Regular expression to see if a path element contains an origin +var originPattern = regexp.MustCompile(`^([\w-_]+):`) + // gNMI plugin instance type GNMI struct { Addresses []string `toml:"addresses"` @@ -187,6 +191,7 @@ func (c *GNMI) Start(acc telegraf.Accumulator) error { for alias, encodingPath := range c.Aliases { c.internalAliases[encodingPath] = alias } + c.Log.Debugf("Internal alias mapping: %+v", c.internalAliases) // Create a goroutine for each device, dial and subscribe c.wg.Add(len(c.Addresses)) @@ -331,6 +336,7 @@ func (c *GNMI) handleSubscribeResponseUpdate(worker *Worker, response *gnmiLib.S c.Log.Errorf("handling path %q failed: %v", response.Update.Prefix, err) } } + prefixTags["source"], _, _ = net.SplitHostPort(worker.address) prefixTags["path"] = prefix @@ -383,6 +389,12 @@ func (c *GNMI) handleSubscribeResponseUpdate(worker *Worker, response *gnmiLib.S } } + // Check for empty names + if name == "" { + c.acc.AddError(fmt.Errorf("got empty name for update %+v", update)) + continue + } + // Group metrics for k, v := range fields { key := k @@ -402,7 +414,6 @@ func (c *GNMI) handleSubscribeResponseUpdate(worker *Worker, response *gnmiLib.S continue } } - grouper.Add(name, tags, timestamp, key, v) } @@ -432,6 +443,16 @@ func (c *GNMI) handleTelemetryField(update *gnmiLib.Update, tags map[string]stri func handlePath(gnmiPath *gnmiLib.Path, tags map[string]string, aliases map[string]string, prefix string) (pathBuffer string, aliasPath string, err error) { builder := bytes.NewBufferString(prefix) + // Some devices do report the origin in the first path element + // so try to find out if this is the case. + if gnmiPath.Origin == "" && len(gnmiPath.Elem) > 0 { + groups := originPattern.FindStringSubmatch(gnmiPath.Elem[0].Name) + if len(groups) == 2 { + gnmiPath.Origin = groups[1] + gnmiPath.Elem[0].Name = gnmiPath.Elem[0].Name[len(groups[1])+1:] + } + } + // Prefix with origin if len(gnmiPath.Origin) > 0 { if _, err := builder.WriteString(gnmiPath.Origin); err != nil { diff --git a/plugins/inputs/gnmi/gnmi_test.go b/plugins/inputs/gnmi/gnmi_test.go index fa1c1706fc513..df1680c943380 100644 --- a/plugins/inputs/gnmi/gnmi_test.go +++ b/plugins/inputs/gnmi/gnmi_test.go @@ -491,7 +491,7 @@ func TestNotification(t *testing.T) { }, }, { - name: "iss #11011", + name: "issue #11011", plugin: &GNMI{ Log: testutil.Logger{}, Encoding: "proto", @@ -648,6 +648,254 @@ func TestNotification(t *testing.T) { ), }, }, + { + name: "issue #12257 Arista", + plugin: &GNMI{ + Log: testutil.Logger{}, + Encoding: "proto", + Redial: config.Duration(1 * time.Second), + Subscriptions: []Subscription{ + { + Name: "interfaces", + Origin: "openconfig", + Path: "/interfaces/interface/state/counters", + SubscriptionMode: "sample", + SampleInterval: config.Duration(1 * time.Second), + }, + }, + }, + server: &MockServer{ + SubscribeF: func(server gnmiLib.GNMI_SubscribeServer) error { + if err := server.Send(&gnmiLib.SubscribeResponse{Response: &gnmiLib.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil { + return err + } + response := &gnmiLib.SubscribeResponse{ + Response: &gnmiLib.SubscribeResponse_Update{ + Update: &gnmiLib.Notification{ + Timestamp: 1668762813698611837, + Prefix: &gnmiLib.Path{ + Origin: "openconfig", + Elem: []*gnmiLib.PathElem{ + {Name: "interfaces"}, + {Name: "interface", Key: map[string]string{"name": "Ethernet1"}}, + {Name: "state"}, + {Name: "counters"}, + }, + Target: "OC-YANG", + }, + Update: []*gnmiLib.Update{ + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "in-broadcast-pkts"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "in-discards"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "in-errors"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "in-fcs-errors"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "in-unicast-pkts"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-broadcast-pkts"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-discards"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-errors"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-multicast-pkts"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-octets"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-pkts"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + { + Path: &gnmiLib.Path{Elem: []*gnmiLib.PathElem{{Name: "out-unicast-pkts"}}}, + Val: &gnmiLib.TypedValue{Value: &gnmiLib.TypedValue_UintVal{UintVal: 0}}, + }, + }, + }, + }, + } + return server.Send(response) + }, + }, + expected: []telegraf.Metric{ + testutil.MustMetric( + "interfaces", + map[string]string{ + "path": "openconfig:/interfaces/interface/state/counters", + "source": "127.0.0.1", + "name": "Ethernet1", + }, + map[string]interface{}{ + "in_broadcast_pkts": uint64(0), + "in_discards": uint64(0), + "in_errors": uint64(0), + "in_fcs_errors": uint64(0), + "in_unicast_pkts": uint64(0), + "out_broadcast_pkts": uint64(0), + "out_discards": uint64(0), + "out_errors": uint64(0), + "out_multicast_pkts": uint64(0), + "out_octets": uint64(0), + "out_pkts": uint64(0), + "out_unicast_pkts": uint64(0), + }, + time.Unix(0, 0), + ), + }, + }, + { + name: "issue #12257 Sonic", + plugin: &GNMI{ + Log: testutil.Logger{}, + Encoding: "proto", + Redial: config.Duration(1 * time.Second), + Subscriptions: []Subscription{ + { + Name: "temperature", + Origin: "openconfig-platform", + Path: "/components/component[name=TEMP 1]/state", + SubscriptionMode: "sample", + SampleInterval: config.Duration(1 * time.Second), + }, + }, + }, + server: &MockServer{ + SubscribeF: func(server gnmiLib.GNMI_SubscribeServer) error { + if err := server.Send(&gnmiLib.SubscribeResponse{Response: &gnmiLib.SubscribeResponse_SyncResponse{SyncResponse: true}}); err != nil { + return err + } + response := &gnmiLib.SubscribeResponse{ + Response: &gnmiLib.SubscribeResponse_Update{ + Update: &gnmiLib.Notification{ + Timestamp: 1668771585733542546, + Prefix: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "openconfig-platform:components"}, + {Name: "component", Key: map[string]string{"name": "TEMP 1"}}, + {Name: "state"}, + }, + Target: "OC-YANG", + }, + Update: []*gnmiLib.Update{ + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "temperature"}, + {Name: "low-threshold"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_FloatVal{FloatVal: 0}, + }, + }, + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "temperature"}, + {Name: "timestamp"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_StringVal{StringVal: "2022-11-18T11:39:26Z"}, + }, + }, + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "temperature"}, + {Name: "warning-status"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_BoolVal{BoolVal: false}, + }, + }, + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "name"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_StringVal{StringVal: "CPU On-board"}, + }, + }, + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "temperature"}, + {Name: "critical-high-threshold"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_FloatVal{FloatVal: 94}, + }, + }, + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "temperature"}, + {Name: "current"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_FloatVal{FloatVal: 29}, + }, + }, + { + Path: &gnmiLib.Path{ + Elem: []*gnmiLib.PathElem{ + {Name: "temperature"}, + {Name: "high-threshold"}, + }}, + Val: &gnmiLib.TypedValue{ + Value: &gnmiLib.TypedValue_FloatVal{FloatVal: 90}, + }, + }, + }, + }, + }, + } + return server.Send(response) + }, + }, + expected: []telegraf.Metric{ + testutil.MustMetric( + "temperature", + map[string]string{ + "path": "openconfig-platform:/components/component/state", + "source": "127.0.0.1", + "name": "TEMP 1", + }, + map[string]interface{}{ + "temperature/timestamp": "2022-11-18T11:39:26Z", + "temperature/low_threshold": float64(0), + "temperature/current": float64(29), + "temperature/high_threshold": float64(90), + "temperature/critical_high_threshold": float64(94), + "temperature/warning_status": false, + "name": "CPU On-board", + }, + time.Unix(0, 0), + ), + }, + }, } for _, tt := range tests { @@ -704,6 +952,7 @@ func TestRedial(t *testing.T) { Addresses: []string{listener.Addr().String()}, Encoding: "proto", Redial: config.Duration(10 * time.Millisecond), + Aliases: map[string]string{"dummy": "type:/model"}, } grpcServer := grpc.NewServer()