Skip to content

Commit 2aa2178

Browse files
kongfei605Copilot
andauthored
feat(snmp_zabbix): support dependent item type and LLD lifetime manag… (#1378)
* feat(snmp_zabbix): support dependent item type and LLD lifetime management * chore(snmp_zabbix): comparison fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(snmp_zabbix): remove duplicate item metrics Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore(snmp_zabbix): update comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor(snmp_zbx): robust macro replacement Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore(snmp_zbx): update comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore(snmp_zabbix): update comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5137433 commit 2aa2178

File tree

5 files changed

+578
-69
lines changed

5 files changed

+578
-69
lines changed

inputs/snmp_zabbix/collector.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ func (c *SNMPCollector) collectFromAgent(ctx context.Context, agent string, item
170170

171171
func (c *SNMPCollector) collectIndividually(client *gosnmp.GoSNMP, agent string, items []MonitorItem, resultChan chan<- CollectionResult) {
172172
for _, item := range items {
173+
// Skip dependent items in individual collection, they are handled by their masters
174+
if strings.EqualFold(item.Type, "dependent") {
175+
continue
176+
}
177+
173178
// 使用 Get 而不是 BulkGet
174179
pduResult, err := client.Get([]string{item.OID})
175180

@@ -220,11 +225,14 @@ func (c *SNMPCollector) processSingleResult(agent string, item MonitorItem, resu
220225
Tags: c.buildTags(agent, item),
221226
}
222227

228+
var masterValue interface{}
229+
223230
if result.Error != nil {
224231
collectionResult.Error = result.Error
225232
} else {
226233
rawValue := c.convertSNMPValue(result.Value, item)
227234
processedValue := rawValue
235+
228236
if len(item.Preprocessing) > 0 {
229237
processed, err := ApplyPreprocessingWithContext(
230238
rawValue,
@@ -243,6 +251,8 @@ func (c *SNMPCollector) processSingleResult(agent string, item MonitorItem, resu
243251
}
244252
}
245253

254+
masterValue = processedValue
255+
246256
finalValue, fields, err := c.processValue(processedValue, item)
247257
if err != nil {
248258
collectionResult.Error = err
@@ -259,6 +269,66 @@ func (c *SNMPCollector) processSingleResult(agent string, item MonitorItem, resu
259269
}
260270
}
261271
resultChan <- collectionResult
272+
273+
if len(item.DependentItems) > 0 && result.Error == nil {
274+
c.processDependentItems(agent, item.DependentItems, masterValue, resultChan)
275+
}
276+
}
277+
278+
func (c *SNMPCollector) processDependentItems(agent string, dependents []MonitorItem, masterValue interface{}, resultChan chan<- CollectionResult) {
279+
for _, depItem := range dependents {
280+
c.processSingleDependentItem(agent, depItem, masterValue, resultChan)
281+
}
282+
}
283+
284+
func (c *SNMPCollector) processSingleDependentItem(agent string, item MonitorItem, inputValue interface{}, resultChan chan<- CollectionResult) {
285+
processedValue := inputValue
286+
var err error
287+
288+
if len(item.Preprocessing) > 0 {
289+
processedValue, err = ApplyPreprocessingWithContext(
290+
inputValue,
291+
item.Preprocessing,
292+
c.preprocessingCtx,
293+
item.Key,
294+
agent,
295+
)
296+
}
297+
298+
collectionResult := CollectionResult{
299+
Agent: agent,
300+
Key: item.Key,
301+
Time: time.Now(),
302+
Tags: c.buildTags(agent, item),
303+
}
304+
305+
if err != nil {
306+
if c.config.DebugMode {
307+
log.Printf("D! dependent preprocessing failed for %s: %v", item.Key, err)
308+
}
309+
return
310+
}
311+
312+
finalValue, fields, err := c.processValue(processedValue, item)
313+
if err != nil {
314+
collectionResult.Error = err
315+
} else {
316+
if floatVal, convErr := ConvertToFloat64(finalValue); convErr == nil {
317+
fields["value"] = floatVal
318+
} else {
319+
fields["value"] = finalValue
320+
}
321+
322+
collectionResult.Value = finalValue
323+
collectionResult.Fields = fields
324+
collectionResult.Type = item.Type
325+
}
326+
327+
resultChan <- collectionResult
328+
329+
if len(item.DependentItems) > 0 && err == nil {
330+
c.processDependentItems(agent, item.DependentItems, processedValue, resultChan)
331+
}
262332
}
263333

264334
type BulkGetResult struct {

inputs/snmp_zabbix/discovery.go

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import (
1414
"github.com/gosnmp/gosnmp"
1515
)
1616

17+
const (
18+
// DurationNever 代表永不执行 (-1s)
19+
DurationNever = -1 * time.Second
20+
// DurationImmediately 代表立即执行 (0s)
21+
DurationImmediately = 0
22+
)
23+
1724
type DiscoveryEngine struct {
1825
client *SNMPClientManager
1926
template *ZabbixTemplate
@@ -55,6 +62,9 @@ type MonitorItem struct {
5562
DiscoveryIndex string // 从 {#SNMPINDEX} 等宏解析出的唯一索引
5663

5764
Preprocessing []PreprocessStep `json:"preprocessing,omitempty"`
65+
66+
// DependentItems 存储依赖于该 Item 的子 Item
67+
DependentItems []MonitorItem
5868
}
5969

6070
func extractLabelKey(itemKey string) string {
@@ -404,6 +414,10 @@ func (d *DiscoveryEngine) extractIndexFromOID(fullOID, baseOID string) string {
404414
if !strings.HasPrefix(fullOID, baseOID) {
405415
return ""
406416
}
417+
// 如果比 baseOID 长,必须紧跟切分符(.)
418+
if len(fullOID) > len(baseOID) && fullOID[len(baseOID)] != '.' {
419+
return ""
420+
}
407421

408422
// 提取索引部分
409423
if len(fullOID) <= len(baseOID) {
@@ -529,6 +543,12 @@ func (d *DiscoveryEngine) ApplyItemPrototypes(discoveries []DiscoveryItem, rule
529543
var items []MonitorItem
530544
prototypes := rule.ItemPrototypes
531545

546+
// Flat list to scope item creation per discovery iteration
547+
type GeneratedItem struct {
548+
MonitorItem
549+
MasterKey string
550+
}
551+
532552
for _, discovery := range discoveries {
533553
discoveryIndex := ""
534554
if idx, ok := discovery.Macros["{#SNMPINDEX}"]; ok {
@@ -538,9 +558,10 @@ func (d *DiscoveryEngine) ApplyItemPrototypes(discoveries []DiscoveryItem, rule
538558
discoveryIndex = idx
539559
}
540560

561+
var currentScopeItems []GeneratedItem
562+
541563
for _, prototype := range prototypes {
542-
if prototype.Status == "DISABLED" ||
543-
prototype.Status == "UNSUPPORTED" {
564+
if prototype.Status == "DISABLED" || prototype.Status == "UNSUPPORTED" {
544565
continue
545566
}
546567
delay := parseZabbixDelay(prototype.Delay)
@@ -549,8 +570,15 @@ func (d *DiscoveryEngine) ApplyItemPrototypes(discoveries []DiscoveryItem, rule
549570
tags[tag.Tag] = tag.Value
550571
}
551572

573+
// Expand macros in keys to properly link masters and dependents
574+
expandedKey := d.expandMacros(prototype.Key, discovery.Macros)
575+
expandedMasterKey := ""
576+
if prototype.MasterItem.Key != "" {
577+
expandedMasterKey = d.expandMacros(prototype.MasterItem.Key, discovery.Macros)
578+
}
579+
552580
item := MonitorItem{
553-
Key: d.expandMacros(prototype.Key, discovery.Macros),
581+
Key: expandedKey,
554582
OID: d.expandMacros(prototype.SNMPOID, discovery.Macros),
555583
Type: ConvertZabbixItemType(prototype.Type),
556584
Name: d.expandMacros(prototype.Name, discovery.Macros),
@@ -571,10 +599,55 @@ func (d *DiscoveryEngine) ApplyItemPrototypes(discoveries []DiscoveryItem, rule
571599
default:
572600
item.IsLabelProvider = false
573601
}
574-
items = append(items, item)
602+
603+
currentScopeItems = append(currentScopeItems, GeneratedItem{
604+
MonitorItem: item,
605+
MasterKey: expandedMasterKey,
606+
})
607+
}
608+
609+
// Map for O(1) lookups during dependency linkage
610+
localMap := make(map[string]*MonitorItem)
611+
var localList []*MonitorItem
612+
var dependentList []*GeneratedItem
613+
614+
for i := range currentScopeItems {
615+
ptr := &currentScopeItems[i].MonitorItem
616+
localMap[ptr.Key] = ptr
617+
618+
if currentScopeItems[i].MasterKey != "" {
619+
dependentList = append(dependentList, &currentScopeItems[i])
620+
} else {
621+
localList = append(localList, ptr)
622+
}
575623
}
576-
}
577624

625+
depMap := make(map[string][]string)
626+
for _, dep := range dependentList {
627+
depMap[dep.MasterKey] = append(depMap[dep.MasterKey], dep.Key)
628+
}
629+
630+
var buildTree func(key string) MonitorItem
631+
buildTree = func(key string) MonitorItem {
632+
item := *localMap[key]
633+
children := depMap[key]
634+
for _, childKey := range children {
635+
if _, exists := localMap[childKey]; exists {
636+
childItem := buildTree(childKey)
637+
item.DependentItems = append(item.DependentItems, childItem)
638+
}
639+
}
640+
return item
641+
}
642+
643+
// Only return root items; dependents are nested within them
644+
for i := range currentScopeItems {
645+
if currentScopeItems[i].MasterKey == "" {
646+
root := buildTree(currentScopeItems[i].Key)
647+
items = append(items, root)
648+
}
649+
}
650+
}
578651
return items
579652
}
580653

@@ -608,6 +681,61 @@ func parseZabbixDelay(delayStr string) time.Duration {
608681
return 60 * time.Second
609682
}
610683

684+
// ParseLLDLifetimes 解析 Zabbix 7.0+ 的生命周期配置
685+
// 返回值: (deleteTTL, disableTTL)
686+
// -1 (DurationNever): 永不
687+
// 0 (DurationImmediately): 立即
688+
// >0: 延迟执行的时长
689+
func ParseLLDLifetimes(rule DiscoveryRule) (time.Duration, time.Duration) {
690+
// 1. 解析 Delete 策略 (彻底删除)
691+
deleteTTL := parseStrategy(rule.LifetimeType, rule.Lifetime, 7*24*time.Hour) // 默认删除时间 7d
692+
693+
// 2. 解析 Disable 策略 (停止采集)
694+
// 默认禁用策略:如果未配置,通常 Zabbix 行为是立即禁用(0)或者永不禁用。
695+
// 这里我们设定:如果未显式配置,且旧版 logic (lifetime) 存在,则 Disable 默认为 0 (立即禁用,保持旧版行为一致性)
696+
disableTTL := parseStrategy(rule.EnabledLifetimeType, rule.EnabledLifetime, DurationImmediately)
697+
698+
return deleteTTL, disableTTL
699+
}
700+
701+
// 辅助函数:通用策略解析
702+
// typeStr:
703+
// - "DELETE_NEVER", "DISABLE_NEVER" -> 返回 DurationNever (-1s),表示“永不执行”
704+
// - "DELETE_IMMEDIATELY", "DISABLE_IMMEDIATELY" -> 返回 DurationImmediately (0s),表示“立即执行”
705+
// - "DELETE_AFTER", "DISABLE_AFTER" -> 解析 durationStr,得到一个 >0 的延迟时长
706+
// - "" (空字符串) -> 若 durationStr 非空,则按 *AFTER* 处理;否则返回 defaultValue
707+
// - 其他未知值 -> 返回 defaultValue
708+
// durationStr: 期望为 Zabbix 风格的延迟字符串,例如 "7d", "1h", "30m" 等;当 typeStr 为 *_AFTER 或为空且 durationStr 非空时生效
709+
// defaultValue: 当无法从 typeStr 和 durationStr 推导策略时使用的默认时长。
710+
// 返回值含义:
711+
// - DurationNever (-1s): 永不执行
712+
// - DurationImmediately (0s): 立即执行
713+
// - >0: 表示延迟执行的时长
714+
func parseStrategy(typeStr, durationStr string, defaultValue time.Duration) time.Duration {
715+
// 归一化
716+
typeStr = strings.ToUpper(strings.TrimSpace(typeStr))
717+
718+
switch typeStr {
719+
case "DELETE_NEVER", "DISABLE_NEVER":
720+
return DurationNever
721+
case "DELETE_IMMEDIATELY", "DISABLE_IMMEDIATELY":
722+
return DurationImmediately
723+
case "DELETE_AFTER", "DISABLE_AFTER":
724+
if durationStr == "" {
725+
return defaultValue
726+
}
727+
return parseZabbixDelay(durationStr)
728+
default:
729+
// 兼容旧版配置或空配置
730+
// 如果 explicit type 为空,但 durationStr 有值,视为 AFTER
731+
if typeStr == "" && durationStr != "" {
732+
return parseZabbixDelay(durationStr)
733+
}
734+
// 否则返回默认值
735+
return defaultValue
736+
}
737+
}
738+
611739
func (d *DiscoveryEngine) expandMacros(text string, macros map[string]string) string {
612740
result := text
613741
for macro, value := range macros {

0 commit comments

Comments
 (0)