forked from flashcatcloud/categraf
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdiscovery.go
More file actions
746 lines (631 loc) · 21 KB
/
Copy pathdiscovery.go
File metadata and controls
746 lines (631 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
package snmp_zabbix
import (
"context"
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
"unicode"
"github.com/gosnmp/gosnmp"
)
const (
// DurationNever 代表永不执行 (-1s)
DurationNever = -1 * time.Second
// DurationImmediately 代表立即执行 (0s)
DurationImmediately = 0
)
type DiscoveryEngine struct {
client *SNMPClientManager
template *ZabbixTemplate
cache map[string]DiscoveryCacheEntry
discoveryCache map[string][]DiscoveryItem
discoveryCacheMu sync.RWMutex
}
type DiscoveryCacheEntry struct {
Data []DiscoveryItem
Timestamp time.Time
TTL time.Duration
}
type DiscoveryItem struct {
Macros map[string]string // 发现的宏键值对
Index string // 索引值
}
type MonitorItem struct {
Key string
OID string
Type string
Name string
Units string
Agent string
Description string
ValueType string
Delay time.Duration
LastCollect time.Time
IsDiscovered bool
Tags map[string]string
DiscoveryRuleKey string
IsLabelProvider bool // 是否为标签提供者
LabelKey string // 作为标签输出时,使用的key (e.g., "alias")
DiscoveryIndex string // 从 {#SNMPINDEX} 等宏解析出的唯一索引
Preprocessing []PreprocessStep `json:"preprocessing,omitempty"`
// DependentItems 存储依赖于该 Item 的子 Item
DependentItems []MonitorItem
}
func extractLabelKey(itemKey string) string {
if idx := strings.Index(itemKey, "["); idx > 0 {
return itemKey[:idx] // a.b.c.alias[params] -> a.b.c.alias
}
return itemKey // if [ is not found, return the original string
}
func NewDiscoveryEngine(client *SNMPClientManager, template *ZabbixTemplate) *DiscoveryEngine {
return &DiscoveryEngine{
client: client,
template: template,
cache: make(map[string]DiscoveryCacheEntry),
}
}
func (d *DiscoveryEngine) ExecuteDiscovery(ctx context.Context, agent string, rule DiscoveryRule) ([]DiscoveryItem, error) {
cacheKey := fmt.Sprintf("%s:%s", agent, rule.Key)
// 检查缓存
if entry, exists := d.cache[cacheKey]; exists {
if time.Since(entry.Timestamp) < entry.TTL {
return entry.Data, nil
}
}
// 执行SNMP发现,现在返回一个通用的 interface{} 类型以处理不同类型的原始结果
rawResult, err := d.performSNMPDiscovery(ctx, agent, rule)
if err != nil {
return nil, fmt.Errorf("SNMP discovery failed for rule '%s': %w", rule.Key, err)
}
var discoveries []DiscoveryItem
// 如果发现规则本身有预处理步骤,则执行它们
if len(rule.Preprocessing) > 0 {
var valueForPreprocessing interface{} = rawResult
if discoveryItems, ok := rawResult.([]DiscoveryItem); ok {
flattenedData := make([]map[string]string, 0, len(discoveryItems))
for _, item := range discoveryItems {
flatItem := make(map[string]string)
// 复制所有宏到顶层
for k, v := range item.Macros {
flatItem[k] = v
}
flattenedData = append(flattenedData, flatItem)
}
// Zabbix LLD 格式是一个包含 "data" 键的对象
lldWrapper := map[string]interface{}{
"data": flattenedData,
}
// 将其序列化为 JSON 字节
jsonBytes, err := json.Marshal(lldWrapper)
if err != nil {
return nil, fmt.Errorf("failed to marshal initial discovery result to JSON: %w", err)
}
// 预处理的正确输入应该是这个 JSON 字符串
valueForPreprocessing = string(jsonBytes)
// log.Printf("DEBUG: Serialized discovery result for preprocessing: %s", valueForPreprocessing)
}
processedValue, err := ApplyDiscoveryPreprocessing(valueForPreprocessing, rule.Preprocessing)
if err != nil {
return nil, fmt.Errorf("discovery preprocessing failed for rule '%s': %w", rule.Key, err)
}
// 预处理的最终结果应该是JSON字符串
jsonStr, ok := processedValue.(string)
if !ok {
return nil, fmt.Errorf("expected JSON string from discovery preprocessing, but got %T", processedValue)
}
// 将JSON结果反序列化为DiscoveryItem
// Zabbix LLD格式是一个包含 "data" 键的对象
var lldResult struct {
Data []map[string]string `json:"data"`
}
// 首先尝试解析标准LLD格式
if err := json.Unmarshal([]byte(jsonStr), &lldResult); err != nil {
// 如果失败,尝试直接解析为数组
var rawDiscoveries []map[string]string
if errUnmarshal := json.Unmarshal([]byte(jsonStr), &rawDiscoveries); errUnmarshal != nil {
return nil, fmt.Errorf("failed to unmarshal final discovery JSON from both LLD and raw array formats: %w", errUnmarshal)
}
lldResult.Data = rawDiscoveries
}
for _, itemMacros := range lldResult.Data {
discoveries = append(discoveries, DiscoveryItem{Macros: itemMacros})
}
} else {
// 如果没有预处理,假定结果是 []DiscoveryItem
var ok bool
discoveries, ok = rawResult.([]DiscoveryItem)
if !ok {
return nil, fmt.Errorf("discovery result was not []DiscoveryItem and no preprocessing was defined")
}
}
// 应用过滤器
filtered := d.applyDiscoveryFilter(discoveries, rule.Filter)
log.Printf("I! filtered discovery results: %d items", len(filtered))
ttl := parseZabbixDelay(rule.Delay)
if ttl == 0 {
ttl = time.Hour // 默认缓存1小时
}
// 缓存结果
d.cache[cacheKey] = DiscoveryCacheEntry{
Data: filtered,
Timestamp: time.Now(),
TTL: ttl,
}
return filtered, nil
}
func (d *DiscoveryEngine) performSNMPDiscovery(ctx context.Context, agent string, rule DiscoveryRule) (interface{}, error) {
unlock := d.client.acquire(agent)
defer unlock()
client, err := d.client.GetClient(agent)
if err != nil {
return nil, fmt.Errorf("failed to get SNMP client: %w", err)
}
snmpOID := strings.TrimSpace(rule.SNMPOID)
if snmpOID == "" {
return nil, fmt.Errorf("empty SNMP OID for discovery rule %s", rule.Key)
}
if strings.HasPrefix(snmpOID, "walk[") {
return d.performMultiOidWalkDiscovery(ctx, client, snmpOID)
} else if strings.HasPrefix(snmpOID, "discovery[") {
return d.performZabbixDependentDiscovery(ctx, client, snmpOID, rule)
} else {
return d.performStandardDiscovery(ctx, client, snmpOID, rule)
}
}
// performMultiOidWalkDiscovery 处理 walk[...] 语法
// 改为顺序执行,防止复用 client 导致的 Request ID 错乱
func (d *DiscoveryEngine) performMultiOidWalkDiscovery(ctx context.Context, client *gosnmp.GoSNMP, discoveryOID string) ([][]gosnmp.SnmpPDU, error) {
// 解析出 walk[] 中的所有 OID
content := discoveryOID[5 : len(discoveryOID)-1]
oidStrings := strings.Split(content, ",")
if len(oidStrings) == 0 {
return nil, fmt.Errorf("no OIDs found in walk[] directive: %s", discoveryOID)
}
var allPdus [][]gosnmp.SnmpPDU
// 用于临时存储结果,保证顺序
results := make([][][]gosnmp.SnmpPDU, len(oidStrings))
for i, oidStr := range oidStrings {
oid := strings.TrimSpace(oidStr)
if err := d.validateOID(oid); err != nil {
return nil, fmt.Errorf("invalid OID '%s' in walk[]: %w", oid, err)
}
// 这里不需要 acquire 锁,因为上层 performSNMPDiscovery 已经持有锁了
pdus, err := d.walkOID(client, oid)
if err != nil {
return nil, fmt.Errorf("SNMP walk failed for OID %s: %w", oid, err)
}
results[i] = append(results[i], pdus)
}
for _, pduGroup := range results {
if len(pduGroup) > 0 {
allPdus = append(allPdus, pduGroup[0])
}
}
return allPdus, nil
}
func (d *DiscoveryEngine) performZabbixDependentDiscovery(ctx context.Context, client *gosnmp.GoSNMP, discoveryOID string, rule DiscoveryRule) ([]DiscoveryItem, error) {
macroOIDPairs, err := d.parseZabbixDiscoveryOID(discoveryOID)
if err != nil {
return nil, fmt.Errorf("failed to parse Zabbix discovery OID: %w", err)
}
allResults := make(map[string]map[string]string)
for _, pair := range macroOIDPairs {
type walkResult struct {
pdus []gosnmp.SnmpPDU
err error
}
resultChan := make(chan walkResult, 1)
go func() {
pdus, err := d.walkOID(client, pair.OID)
if err != nil {
log.Printf("W!: %v", err)
}
resultChan <- walkResult{pdus: pdus, err: err}
}()
var results []gosnmp.SnmpPDU
select {
case <-ctx.Done():
return nil, fmt.Errorf("SNMP walk for OID %s was canceled or timed out: %w", pair.OID, ctx.Err())
case res := <-resultChan:
if res.err != nil {
log.Printf("Warning: SNMP walk failed for OID %s: %v", pair.OID, res.err)
continue
}
results = res.pdus
}
for _, result := range results {
index := d.extractIndexFromOID(result.Name, pair.OID)
if index == "" {
continue
}
if allResults[index] == nil {
allResults[index] = make(map[string]string)
}
value := d.convertSNMPValueToString(result)
allResults[index][pair.Macro] = value
}
}
var discoveries []DiscoveryItem
for index, macros := range allResults {
macros["{#SNMPINDEX}"] = index
macros["{#IFINDEX}"] = index
discovery := DiscoveryItem{
Macros: macros,
Index: index,
}
discoveries = append(discoveries, discovery)
}
return discoveries, nil
}
func (d *DiscoveryEngine) parseZabbixDiscoveryOID(discoveryOID string) ([]MacroOIDPair, error) {
// 移除 "discovery[" 前缀和 "]" 后缀
if !strings.HasPrefix(discoveryOID, "discovery[") || !strings.HasSuffix(discoveryOID, "]") {
return nil, fmt.Errorf("invalid discovery OID format: %s", discoveryOID)
}
content := discoveryOID[10 : len(discoveryOID)-1] // 移除 "discovery[" 和 "]"
// 分割参数
parts := strings.Split(content, ",")
if len(parts)%2 != 0 {
return nil, fmt.Errorf("discovery OID must have even number of parameters (macro,oid pairs): %s", discoveryOID)
}
var pairs []MacroOIDPair
for i := 0; i < len(parts); i += 2 {
macro := strings.TrimSpace(parts[i])
oid := strings.TrimSpace(parts[i+1])
// 验证宏格式
if !strings.HasPrefix(macro, "{#") || !strings.HasSuffix(macro, "}") {
return nil, fmt.Errorf("invalid macro format: %s", macro)
}
// 验证 OID 格式
if err := d.validateOID(oid); err != nil {
return nil, fmt.Errorf("invalid OID %s for macro %s: %w", oid, macro, err)
}
pairs = append(pairs, MacroOIDPair{
Macro: macro,
OID: oid,
})
}
return pairs, nil
}
// walkOID 封装了 SNMP Walk 逻辑,包含针对 SNMPv1 的优化和降级策略
func (d *DiscoveryEngine) walkOID(client *gosnmp.GoSNMP, oid string) ([]gosnmp.SnmpPDU, error) {
if client.Version == gosnmp.Version1 {
return client.WalkAll(oid)
}
// 对于 v2c 和 v3,优先尝试 BulkWalkAll
pdus, err := client.BulkWalkAll(oid)
if err != nil {
// 如果 BulkWalk 失败,尝试回退到 WalkAll
pdusRetry, errRetry := client.WalkAll(oid)
if errRetry == nil {
return pdusRetry, nil
}
// 如果降级也失败,返回组合错误
return nil, fmt.Errorf("bulk walk failed: %v, fallback walk failed: %w", err, errRetry)
}
return pdus, nil
}
func (d *DiscoveryEngine) performStandardDiscovery(ctx context.Context, client *gosnmp.GoSNMP, snmpOID string, rule DiscoveryRule) ([]DiscoveryItem, error) {
if err := d.validateOID(snmpOID); err != nil {
return nil, fmt.Errorf("invalid SNMP OID '%s': %w", snmpOID, err)
}
type walkResult struct {
pdus []gosnmp.SnmpPDU
err error
}
resultChan := make(chan walkResult, 1)
go func() {
pdus, err := d.walkOID(client, snmpOID)
resultChan <- walkResult{pdus: pdus, err: err}
}()
var results []gosnmp.SnmpPDU
select {
case <-ctx.Done():
return nil, fmt.Errorf("SNMP walk for OID %s was canceled or timed out: %w", snmpOID, ctx.Err())
case res := <-resultChan:
if res.err != nil {
return nil, fmt.Errorf("SNMP walk failed for OID %s: %w", snmpOID, res.err)
}
results = res.pdus
}
var discoveries []DiscoveryItem
for _, result := range results {
discovery := d.parseStandardDiscoveryResult(result, rule, snmpOID)
if discovery != nil {
discoveries = append(discoveries, *discovery)
}
}
return discoveries, nil
}
func (d *DiscoveryEngine) extractIndexFromOID(fullOID, baseOID string) string {
// 移除前导点
fullOID = strings.TrimPrefix(fullOID, ".")
baseOID = strings.TrimPrefix(baseOID, ".")
if !strings.HasPrefix(fullOID, baseOID) {
return ""
}
// 如果比 baseOID 长,必须紧跟切分符(.)
if len(fullOID) > len(baseOID) && fullOID[len(baseOID)] != '.' {
return ""
}
// 提取索引部分
if len(fullOID) <= len(baseOID) {
return ""
}
index := fullOID[len(baseOID):]
index = strings.TrimPrefix(index, ".")
return index
}
func isPrintableString(b []byte) bool {
for _, c := range string(b) {
// unicode.IsPrint 会将空格也视为可打印
if !unicode.IsPrint(c) {
return false
}
}
return true
}
func (d *DiscoveryEngine) convertSNMPValueToString(pdu gosnmp.SnmpPDU) string {
switch pdu.Type {
case gosnmp.OctetString:
if bytes, ok := pdu.Value.([]byte); ok {
if isPrintableString(bytes) {
return string(bytes)
} else {
hexParts := make([]string, len(bytes))
for i, b := range bytes {
hexParts[i] = fmt.Sprintf("%02X", b)
}
return strings.Join(hexParts, " ")
}
}
return fmt.Sprintf("%v", pdu.Value)
case gosnmp.Integer, gosnmp.Counter32, gosnmp.Counter64, gosnmp.Gauge32:
return gosnmp.ToBigInt(pdu.Value).String()
default:
// Fallback for other types
return fmt.Sprintf("%v", pdu.Value)
}
}
func (d *DiscoveryEngine) parseStandardDiscoveryResult(result gosnmp.SnmpPDU, rule DiscoveryRule, baseOID string) *DiscoveryItem {
macros := make(map[string]string)
// 提取索引
index := d.extractIndexFromOID(result.Name, baseOID)
if index != "" {
macros["{#SNMPINDEX}"] = index
}
// 添加值
value := d.convertSNMPValueToString(result)
// 根据发现规则类型设置相应的宏
switch {
case strings.Contains(rule.Key, "net.if"):
macros["{#IFINDEX}"] = index
macros["{#IFNAME}"] = value
macros["{#IFDESCR}"] = value
case strings.Contains(rule.Key, "vfs.fs"):
macros["{#FSINDEX}"] = index
macros["{#FSNAME}"] = value
macros["{#FSPATH}"] = value
default:
macros["{#VALUE}"] = value
}
return &DiscoveryItem{
Macros: macros,
}
}
type MacroOIDPair struct {
Macro string
OID string
}
func (d *DiscoveryEngine) validateOID(oid string) error {
if oid == "" {
return fmt.Errorf("empty OID")
}
// 移除可能的前导点
oid = strings.TrimPrefix(oid, ".")
// 检查是否是有效的点分十进制格式
parts := strings.Split(oid, ".")
for i, part := range parts {
if part == "" {
return fmt.Errorf("empty OID component at position %d", i)
}
// 检查是否为数字
if _, err := strconv.Atoi(part); err != nil {
return fmt.Errorf("invalid OID component '%s' at position %d: must be numeric", part, i)
}
}
return nil
}
func (d *DiscoveryEngine) applyDiscoveryFilter(discoveries []DiscoveryItem, filter DiscoveryFilter) []DiscoveryItem {
if len(filter.Conditions) == 0 {
return discoveries
}
var filtered []DiscoveryItem
for _, discovery := range discoveries {
if d.template != nil && d.template.ValidateFilter(filter, discovery.Macros) {
filtered = append(filtered, discovery)
}
}
return filtered
}
func (d *DiscoveryEngine) ApplyItemPrototypes(discoveries []DiscoveryItem, rule DiscoveryRule) []MonitorItem {
var items []MonitorItem
prototypes := rule.ItemPrototypes
// Flat list to scope item creation per discovery iteration
type GeneratedItem struct {
MonitorItem
MasterKey string
}
for _, discovery := range discoveries {
discoveryIndex := ""
if idx, ok := discovery.Macros["{#SNMPINDEX}"]; ok {
discoveryIndex = idx
} else if idx, ok := discovery.Macros["{#IFINDEX}"]; ok {
// Fallback for different index macro names
discoveryIndex = idx
}
var currentScopeItems []GeneratedItem
for _, prototype := range prototypes {
if prototype.Status == "DISABLED" || prototype.Status == "UNSUPPORTED" {
continue
}
delay := parseZabbixDelay(prototype.Delay)
tags := map[string]string{}
for _, tag := range prototype.Tags {
tags[tag.Tag] = tag.Value
}
// Expand macros in keys to properly link masters and dependents
expandedKey := d.expandMacros(prototype.Key, discovery.Macros)
expandedMasterKey := ""
if prototype.MasterItem.Key != "" {
expandedMasterKey = d.expandMacros(prototype.MasterItem.Key, discovery.Macros)
}
item := MonitorItem{
Key: expandedKey,
OID: d.expandMacros(prototype.SNMPOID, discovery.Macros),
Type: ConvertZabbixItemType(prototype.Type),
Name: d.expandMacros(prototype.Name, discovery.Macros),
Units: prototype.Units,
Description: d.expandMacros(prototype.Description, discovery.Macros),
ValueType: ConvertZabbixValueType(prototype.ValueType),
Delay: delay,
IsDiscovered: true,
Tags: tags,
DiscoveryRuleKey: rule.Key,
Preprocessing: prototype.Preprocessing,
DiscoveryIndex: discoveryIndex,
}
switch prototype.ValueType {
case "CHAR", "1", "TEXT", "4":
item.IsLabelProvider = true
item.LabelKey = extractLabelKey(prototype.Key)
default:
item.IsLabelProvider = false
}
currentScopeItems = append(currentScopeItems, GeneratedItem{
MonitorItem: item,
MasterKey: expandedMasterKey,
})
}
// Map for O(1) lookups during dependency linkage
localMap := make(map[string]*MonitorItem)
var localList []*MonitorItem
var dependentList []*GeneratedItem
for i := range currentScopeItems {
ptr := ¤tScopeItems[i].MonitorItem
localMap[ptr.Key] = ptr
if currentScopeItems[i].MasterKey != "" {
dependentList = append(dependentList, ¤tScopeItems[i])
} else {
localList = append(localList, ptr)
}
}
depMap := make(map[string][]string)
for _, dep := range dependentList {
depMap[dep.MasterKey] = append(depMap[dep.MasterKey], dep.Key)
}
var buildTree func(key string) MonitorItem
buildTree = func(key string) MonitorItem {
item := *localMap[key]
children := depMap[key]
for _, childKey := range children {
if _, exists := localMap[childKey]; exists {
childItem := buildTree(childKey)
item.DependentItems = append(item.DependentItems, childItem)
}
}
return item
}
// Only return root items; dependents are nested within them
for i := range currentScopeItems {
if currentScopeItems[i].MasterKey == "" {
root := buildTree(currentScopeItems[i].Key)
items = append(items, root)
}
}
}
return items
}
func parseZabbixDelay(delayStr string) time.Duration {
if delayStr == "" {
return 60 * time.Second // 默认60秒
}
// Zabbix支持多种格式:
// - 简单数字(秒): "30"
// - 带单位的: "30s", "5m", "1h"
// - 灵活间隔: "30;wd1-5,h9-18:60;wd6-7:300"
// 简单处理,支持基本格式
if strings.Contains(delayStr, ";") {
// 复杂的灵活间隔,取第一个值
parts := strings.Split(delayStr, ";")
delayStr = parts[0]
}
// 尝试解析数字(秒)
if seconds, err := strconv.Atoi(delayStr); err == nil {
return time.Duration(seconds) * time.Second
}
// 尝试解析带单位的时间
if duration, err := time.ParseDuration(delayStr); err == nil {
return duration
}
return 60 * time.Second
}
// ParseLLDLifetimes 解析 Zabbix 7.0+ 的生命周期配置
// 返回值: (deleteTTL, disableTTL)
// -1 (DurationNever): 永不
// 0 (DurationImmediately): 立即
// >0: 延迟执行的时长
func ParseLLDLifetimes(rule DiscoveryRule) (time.Duration, time.Duration) {
// 1. 解析 Delete 策略 (彻底删除)
deleteTTL := parseStrategy(rule.LifetimeType, rule.Lifetime, 7*24*time.Hour) // 默认删除时间 7d
// 2. 解析 Disable 策略 (停止采集)
// 默认禁用策略:如果未配置,通常 Zabbix 行为是立即禁用(0)或者永不禁用。
// 这里我们设定:如果未显式配置,且旧版 logic (lifetime) 存在,则 Disable 默认为 0 (立即禁用,保持旧版行为一致性)
disableTTL := parseStrategy(rule.EnabledLifetimeType, rule.EnabledLifetime, DurationImmediately)
return deleteTTL, disableTTL
}
// 辅助函数:通用策略解析
// typeStr: DELETE_NEVER, DISABLE_AFTER 等
// durationStr: "7d", "1h"
// defaultValue: 当 typeStr 为空时的默认 duration
func parseStrategy(typeStr, durationStr string, defaultValue time.Duration) time.Duration {
// 归一化
typeStr = strings.ToUpper(strings.TrimSpace(typeStr))
switch typeStr {
case "DELETE_NEVER", "DISABLE_NEVER":
return DurationNever
case "DELETE_IMMEDIATELY", "DISABLE_IMMEDIATELY":
return DurationImmediately
case "DELETE_AFTER", "DISABLE_AFTER":
if durationStr == "" {
return defaultValue
}
return parseZabbixDelay(durationStr)
default:
// 兼容旧版配置或空配置
// 如果 explicit type 为空,但 durationStr 有值,视为 AFTER
if typeStr == "" && durationStr != "" {
return parseZabbixDelay(durationStr)
}
// 否则返回默认值
return defaultValue
}
}
func (d *DiscoveryEngine) expandMacros(text string, macros map[string]string) string {
result := text
for macro, value := range macros {
macroWithBraces := fmt.Sprintf("{#%s}", strings.Trim(macro, "{}#"))
result = strings.ReplaceAll(result, macroWithBraces, value)
}
if d.template != nil {
result = d.template.ExpandMacros(result, macros)
}
return result
}
func (d *DiscoveryEngine) containsMacros(text string) bool {
// 检查是否包含 宏 {#MACRO} 或 {$MACRO}
return strings.Contains(text, "{#") || strings.Contains(text, "{$")
}