fix(inputs.gnmi): Handle YANG namespaces in paths correctly (#15565)

This commit is contained in:
Sven Rebhan 2024-06-28 11:11:53 -04:00 committed by GitHub
parent f40578fc16
commit 1b849ebf92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 214 additions and 58 deletions

View File

@ -442,7 +442,7 @@ func (s *Subscription) buildAlias(aliases map[*pathInfo]string) error {
// If the user didn't provide a measurement name, use last path element // If the user didn't provide a measurement name, use last path element
name := s.Name name := s.Name
if name == "" && len(info.segments) > 0 { if name == "" && len(info.segments) > 0 {
name = info.segments[len(info.segments)-1] name = info.segments[len(info.segments)-1].id
} }
if name != "" { if name != "" {
aliases[info] = name aliases[info] = name

View File

@ -166,7 +166,7 @@ func (h *handler) handleSubscribeResponseUpdate(acc telegraf.Accumulator, respon
// Add info to the tags // Add info to the tags
headerTags["source"], _, _ = net.SplitHostPort(h.address) headerTags["source"], _, _ = net.SplitHostPort(h.address)
if !prefix.empty() { if !prefix.empty() {
headerTags["path"] = prefix.String() headerTags["path"] = prefix.FullPath()
} }
// Process and remove tag-updates from the response first so we can // Process and remove tag-updates from the response first so we can
@ -270,15 +270,11 @@ func (h *handler) handleSubscribeResponseUpdate(acc telegraf.Accumulator, respon
// shorter than the full path to avoid an empty key, then strip the // shorter than the full path to avoid an empty key, then strip the
// common part of the field is prefixed with the alias path. Note // common part of the field is prefixed with the alias path. Note
// the origins can match or be empty and be considered equal. // the origins can match or be empty and be considered equal.
if aliasInfo.isSubPathOf(field.path) && len(aliasInfo.segments) < len(field.path.segments) { if relative := aliasInfo.relative(field.path, true); relative != "" {
relative := field.path.segments[len(aliasInfo.segments):len(field.path.segments)] key = relative
key = strings.Join(relative, "/")
} else { } else {
// Otherwise use the last path element as the field key if it // Otherwise use the last path element as the field key
// exists. key = field.path.Base()
if len(field.path.segments) > 0 {
key = field.path.segments[len(field.path.segments)-1]
}
} }
key = strings.ReplaceAll(key, "-", "_") key = strings.ReplaceAll(key, "-", "_")
} }
@ -328,12 +324,11 @@ func guessPrefixFromUpdate(fields []updateField) string {
return "" return ""
} }
if len(fields) == 1 { if len(fields) == 1 {
dir, _ := fields[0].path.split() return fields[0].path.Dir()
return dir
} }
commonPath := &pathInfo{ commonPath := &pathInfo{
origin: fields[0].path.origin, origin: fields[0].path.origin,
segments: append([]string{}, fields[0].path.segments...), segments: append([]segment{}, fields[0].path.segments...),
} }
for _, f := range fields[1:] { for _, f := range fields[1:] {
commonPath.keepCommonPart(f.path) commonPath.keepCommonPart(f.path)

View File

@ -1,25 +1,26 @@
package gnmi package gnmi
import ( import (
"regexp"
"strings" "strings"
gnmiLib "github.com/openconfig/gnmi/proto/gnmi" gnmiLib "github.com/openconfig/gnmi/proto/gnmi"
) )
// Regular expression to see if a path element contains an origin
var originPattern = regexp.MustCompile(`^([\w-]+):`)
type keySegment struct { type keySegment struct {
name string name string
path string path string
kv map[string]string kv map[string]string
} }
type segment struct {
namespace string
id string
}
type pathInfo struct { type pathInfo struct {
origin string origin string
target string target string
segments []string segments []segment
keyValues []keySegment keyValues []keySegment
} }
@ -29,10 +30,11 @@ func newInfoFromString(path string) *pathInfo {
} }
info := &pathInfo{} info := &pathInfo{}
for _, s := range strings.Split(path, "/") { for _, part := range strings.Split(path, "/") {
if s != "" { if part == "" {
info.segments = append(info.segments, s) continue
} }
info.segments = append(info.segments, segment{id: part})
} }
info.normalize() info.normalize()
@ -42,13 +44,13 @@ func newInfoFromString(path string) *pathInfo {
func newInfoFromPathWithoutKeys(path *gnmiLib.Path) *pathInfo { func newInfoFromPathWithoutKeys(path *gnmiLib.Path) *pathInfo {
info := &pathInfo{ info := &pathInfo{
origin: path.Origin, origin: path.Origin,
segments: make([]string, 0, len(path.Elem)), segments: make([]segment, 0, len(path.Elem)),
} }
for _, elem := range path.Elem { for _, elem := range path.Elem {
if elem.Name == "" { if elem.Name == "" {
continue continue
} }
info.segments = append(info.segments, elem.Name) info.segments = append(info.segments, segment{id: elem.Name})
} }
info.normalize() info.normalize()
@ -74,7 +76,7 @@ func newInfoFromPath(paths ...*gnmiLib.Path) *pathInfo {
if elem.Name == "" { if elem.Name == "" {
continue continue
} }
info.segments = append(info.segments, elem.Name) info.segments = append(info.segments, segment{id: elem.Name})
if len(elem.Key) == 0 { if len(elem.Key) == 0 {
continue continue
@ -104,7 +106,7 @@ func (pi *pathInfo) append(paths ...*gnmiLib.Path) *pathInfo {
path := &pathInfo{ path := &pathInfo{
origin: pi.origin, origin: pi.origin,
target: pi.target, target: pi.target,
segments: append([]string{}, pi.segments...), segments: append([]segment{}, pi.segments...),
keyValues: make([]keySegment, 0, len(pi.keyValues)), keyValues: make([]keySegment, 0, len(pi.keyValues)),
} }
for _, elem := range pi.keyValues { for _, elem := range pi.keyValues {
@ -125,7 +127,7 @@ func (pi *pathInfo) append(paths ...*gnmiLib.Path) *pathInfo {
if elem.Name == "" { if elem.Name == "" {
continue continue
} }
path.segments = append(path.segments, elem.Name) path.segments = append(path.segments, segment{id: elem.Name})
if len(elem.Key) == 0 { if len(elem.Key) == 0 {
continue continue
@ -151,7 +153,7 @@ func (pi *pathInfo) appendSegments(segments ...string) *pathInfo {
path := &pathInfo{ path := &pathInfo{
origin: pi.origin, origin: pi.origin,
target: pi.target, target: pi.target,
segments: append([]string{}, pi.segments...), segments: append([]segment{}, pi.segments...),
keyValues: make([]keySegment, 0, len(pi.keyValues)), keyValues: make([]keySegment, 0, len(pi.keyValues)),
} }
for _, elem := range pi.keyValues { for _, elem := range pi.keyValues {
@ -171,7 +173,7 @@ func (pi *pathInfo) appendSegments(segments ...string) *pathInfo {
if s == "" { if s == "" {
continue continue
} }
path.segments = append(path.segments, s) path.segments = append(path.segments, segment{id: s})
} }
path.normalize() path.normalize()
@ -183,18 +185,28 @@ func (pi *pathInfo) normalize() {
return return
} }
// Some devices supply the origin as part of the first path element, // Extract namespaces from segments
// so try to find and extract it there. for i, s := range pi.segments {
groups := originPattern.FindStringSubmatch(pi.segments[0]) if ns, id, found := strings.Cut(s.id, ":"); found {
if len(groups) == 2 { pi.segments[i].namespace = ns
pi.origin = groups[1] pi.segments[i].id = id
pi.segments[0] = pi.segments[0][len(groups[1])+1:]
// if we get empty string back, remove the segment
if pi.segments[0] == "" {
pi.segments = pi.segments[1:]
} }
} }
// Some devices supply the origin as part of the first path element,
// so try to find and extract it there.
if pi.segments[0].namespace != "" {
pi.origin = pi.segments[0].namespace
}
// Remove empty segments
segments := make([]segment, 0, len(pi.segments))
for _, s := range pi.segments {
if s.id != "" {
segments = append(segments, s)
}
}
pi.segments = segments
} }
func (pi *pathInfo) equalsPathNoKeys(path *gnmiLib.Path) bool { func (pi *pathInfo) equalsPathNoKeys(path *gnmiLib.Path) bool {
@ -202,7 +214,7 @@ func (pi *pathInfo) equalsPathNoKeys(path *gnmiLib.Path) bool {
return false return false
} }
for i, s := range pi.segments { for i, s := range pi.segments {
if s != path.Elem[i].Name { if s.id != path.Elem[i].Name {
return false return false
} }
} }
@ -223,7 +235,11 @@ func (pi *pathInfo) isSubPathOf(path *pathInfo) bool {
// Compare the elements and exit if we find a mismatch // Compare the elements and exit if we find a mismatch
for i, p := range pi.segments { for i, p := range pi.segments {
if p != path.segments[i] { ps := path.segments[i]
if p.namespace != "" && ps.namespace != "" && p.namespace != ps.namespace {
return false
}
if p.id != ps.id {
return false return false
} }
} }
@ -231,6 +247,28 @@ func (pi *pathInfo) isSubPathOf(path *pathInfo) bool {
return true return true
} }
func (pi *pathInfo) relative(path *pathInfo, withNamespace bool) string {
if !pi.isSubPathOf(path) || len(pi.segments) == len(path.segments) {
return ""
}
segments := path.segments[len(pi.segments):len(path.segments)]
var r string
if withNamespace && segments[0].namespace != "" {
r = segments[0].namespace + ":" + segments[0].id
} else {
r = segments[0].id
}
for _, s := range segments[1:] {
if withNamespace && s.namespace != "" {
r += "/" + s.namespace + ":" + s.id
} else {
r += "/" + s.id
}
}
return r
}
func (pi *pathInfo) keepCommonPart(path *pathInfo) { func (pi *pathInfo) keepCommonPart(path *pathInfo) {
shortestLen := len(pi.segments) shortestLen := len(pi.segments)
if len(path.segments) < shortestLen { if len(path.segments) < shortestLen {
@ -252,31 +290,35 @@ func (pi *pathInfo) keepCommonPart(path *pathInfo) {
pi.segments = pi.segments[:matchLen] pi.segments = pi.segments[:matchLen]
} }
func (pi *pathInfo) split() (dir, base string) { func (pi *pathInfo) Dir() string {
if len(pi.segments) == 0 { if len(pi.segments) <= 1 {
return "", "" return ""
}
if len(pi.segments) == 1 {
return "", pi.segments[0]
} }
dir = "/" + strings.Join(pi.segments[:len(pi.segments)-1], "/") var dir string
if pi.origin != "" { if pi.origin != "" {
dir = pi.origin + ":" + dir dir = pi.origin + ":"
} }
return dir, pi.segments[len(pi.segments)-1] for _, s := range pi.segments[:len(pi.segments)-1] {
if s.namespace != "" {
dir += "/" + s.namespace + ":" + s.id
} else {
dir += "/" + s.id
}
}
return dir
} }
func (pi *pathInfo) String() string { func (pi *pathInfo) Base() string {
if len(pi.segments) == 0 { if len(pi.segments) == 0 {
return "" return ""
} }
out := "/" + strings.Join(pi.segments, "/") s := pi.segments[len(pi.segments)-1]
if pi.origin != "" { if s.namespace != "" {
out = pi.origin + ":" + out return s.namespace + ":" + s.id
} }
return out return s.id
} }
func (pi *pathInfo) Path() (origin, path string) { func (pi *pathInfo) Path() (origin, path string) {
@ -284,7 +326,45 @@ func (pi *pathInfo) Path() (origin, path string) {
return pi.origin, "/" return pi.origin, "/"
} }
return pi.origin, "/" + strings.Join(pi.segments, "/") for _, s := range pi.segments {
path += "/" + s.id
}
return pi.origin, path
}
func (pi *pathInfo) FullPath() string {
var path string
if pi.origin != "" {
path = pi.origin + ":"
}
if len(pi.segments) == 0 {
return path
}
path += "/" + pi.segments[0].id
for _, s := range pi.segments[1:] {
if s.namespace != "" {
path += "/" + s.namespace + ":" + s.id
} else {
path += "/" + s.id
}
}
return path
}
func (pi *pathInfo) String() string {
if len(pi.segments) == 0 {
return ""
}
origin, path := pi.Path()
if origin != "" {
return origin + ":" + path
}
return path
} }
func (pi *pathInfo) Tags(pathPrefix bool) map[string]string { func (pi *pathInfo) Tags(pathPrefix bool) map[string]string {

View File

@ -44,7 +44,7 @@ func (s *tagStore) insert(subscription TagSubscription, path *pathInfo, values [
for _, f := range values { for _, f := range values {
tagName := subscription.Name tagName := subscription.Name
if len(f.path.segments) > 0 { if len(f.path.segments) > 0 {
key := f.path.segments[len(f.path.segments)-1] key := f.path.Base()
key = strings.ReplaceAll(key, "-", "_") key = strings.ReplaceAll(key, "-", "_")
tagName += "/" + key tagName += "/" + key
} }
@ -74,7 +74,7 @@ func (s *tagStore) insert(subscription TagSubscription, path *pathInfo, values [
for _, f := range values { for _, f := range values {
tagName := subscription.Name tagName := subscription.Name
if len(f.path.segments) > 0 { if len(f.path.segments) > 0 {
key := f.path.segments[len(f.path.segments)-1] key := f.path.Base()
key = strings.ReplaceAll(key, "-", "_") key = strings.ReplaceAll(key, "-", "_")
tagName += "/" + key tagName += "/" + key
} }
@ -103,7 +103,7 @@ func (s *tagStore) insert(subscription TagSubscription, path *pathInfo, values [
for _, f := range values { for _, f := range values {
tagName := subscription.Name tagName := subscription.Name
if len(f.path.segments) > 0 { if len(f.path.segments) > 0 {
key := f.path.segments[len(f.path.segments)-1] key := f.path.Base()
key = strings.ReplaceAll(key, "-", "_") key = strings.ReplaceAll(key, "-", "_")
tagName += "/" + key tagName += "/" + key
} }

View File

@ -0,0 +1 @@
event-stats,path=openconfig-system:/system/openconfig-events:event-stats/state,source=127.0.0.1 state/acked=0u,state/cleared=0u,state/events=4u,state/raised=0u 1718942414831832038

View File

@ -0,0 +1,70 @@
[
{
"update": {
"timestamp": "1718942414831832038",
"prefix": {
"elem": [
{
"name": "openconfig-system:system"
},
{
"name": "openconfig-events:event-stats"
},
{
"name": "state"
}
]
},
"update": [
{
"path": {
"elem": [
{
"name": "acked"
}
]
},
"val": {
"uintVal": "0"
}
},
{
"path": {
"elem": [
{
"name": "cleared"
}
]
},
"val": {
"uintVal": "0"
}
},
{
"path": {
"elem": [
{
"name": "events"
}
]
},
"val": {
"uintVal": "4"
}
},
{
"path": {
"elem": [
{
"name": "raised"
}
]
},
"val": {
"uintVal": "0"
}
}
]
}
}
]

View File

@ -0,0 +1,10 @@
[[inputs.gnmi]]
addresses = ["dummy"]
prefix_tag_key_with_path = true
[[inputs.gnmi.subscription]]
name = "event-stats"
origin = "openconfig-system"
path = "/system/event-stats"
subscription_mode = "sample"
sample_interval = "10s"