master
go 285 lines 7.77 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "math"
7 "sync"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
11 )
12
13 // Interface metric names we track for the function.
14 var ifaceMetricNames = map[string]bool{
15 "ifTraffic": true,
16 "ifPacketsUcast": true,
17 "ifPacketsBroadcast": true,
18 "ifPacketsMulticast": true,
19 "ifErrors": true,
20 "ifDiscards": true,
21 "ifAdminStatus": true,
22 "ifOperStatus": true,
23 }
24
25 // Tag keys used to identify interfaces.
26 const (
27 tagInterface = "interface"
28 tagIfType = "_if_type"
29 tagIfTypeGrp = "_if_type_group"
30 )
31
32 // ifaceCache holds interface metrics between collections for function queries.
33 type ifaceCache struct {
34 mu sync.RWMutex
35 lastUpdate time.Time
36 updateTime time.Time // current collection cycle time
37 interfaces map[string]*ifaceEntry // key: interface name from m.Tags["interface"]
38 }
39
40 // ifaceEntry holds metrics for a single network interface.
41 type ifaceEntry struct {
42 // Identity
43 name string // interface name (from Tags["interface"])
44 ifType string // interface type (from Tags["_if_type"])
45 ifTypeGroup string // interface type group (from Tags["_if_type_group"])
46
47 // Status (text values extracted from MultiValue)
48 adminStatus string
49 operStatus string
50
51 // Raw counter values (cumulative, stored for next delta calculation)
52 counters ifaceCounters
53
54 // Previous counter values (for delta calculation)
55 prevCounters ifaceCounters
56 prevTime time.Time
57 hasPrev bool // true if we have previous values for delta calculation
58
59 // Computed rates (per-second, nil if not yet calculable)
60 rates ifaceRates
61
62 // Tracking
63 updated bool // true if seen in current collection cycle
64 }
65
66 // ifaceCounters holds raw cumulative counter values.
67 type ifaceCounters struct {
68 trafficIn int64
69 trafficOut int64
70 ucastPktsIn int64
71 ucastPktsOut int64
72 bcastPktsIn int64
73 bcastPktsOut int64
74 mcastPktsIn int64
75 mcastPktsOut int64
76 errorsIn int64
77 errorsOut int64
78 discardsIn int64
79 discardsOut int64
80 }
81
82 // ifaceRates holds computed per-second rates.
83 type ifaceRates struct {
84 trafficIn *float64
85 trafficOut *float64
86 ucastPktsIn *float64
87 ucastPktsOut *float64
88 bcastPktsIn *float64
89 bcastPktsOut *float64
90 mcastPktsIn *float64
91 mcastPktsOut *float64
92 errorsIn *float64
93 errorsOut *float64
94 discardsIn *float64
95 discardsOut *float64
96 }
97
98 // newIfaceCache creates a new interface cache.
99 func newIfaceCache() *ifaceCache {
100 return &ifaceCache{
101 interfaces: make(map[string]*ifaceEntry),
102 }
103 }
104
105 // isIfaceMetric returns true if the metric name is one we track for interface function.
106 func isIfaceMetric(name string) bool {
107 return ifaceMetricNames[name]
108 }
109
110 // resetIfaceCache prepares the cache for a new collection cycle.
111 // Must be called before processing metrics.
112 func (c *Collector) resetIfaceCache() {
113 if c.ifaceCache == nil {
114 return
115 }
116
117 c.ifaceCache.mu.Lock()
118 defer c.ifaceCache.mu.Unlock()
119
120 c.ifaceCache.updateTime = time.Now()
121
122 for _, entry := range c.ifaceCache.interfaces {
123 entry.updated = false
124 }
125 }
126
127 // updateIfaceCacheEntry updates the cache with a single interface metric.
128 // Called during collectProfileTableMetrics for matching metrics.
129 // Caller must ensure m.IsTable is true and m.Tags["interface"] is not empty.
130 func (c *Collector) updateIfaceCacheEntry(m ddsnmp.Metric) {
131 if c.ifaceCache == nil {
132 return
133 }
134
135 ifaceName := m.Tags[tagInterface]
136 if ifaceName == "" {
137 return
138 }
139
140 c.ifaceCache.mu.Lock()
141 defer c.ifaceCache.mu.Unlock()
142
143 entry := c.ifaceCache.interfaces[ifaceName]
144 if entry == nil {
145 entry = &ifaceEntry{
146 name: ifaceName,
147 }
148 c.ifaceCache.interfaces[ifaceName] = entry
149 }
150
151 if ifType := m.Tags[tagIfType]; ifType != "" {
152 entry.ifType = ifType
153 }
154 if ifTypeGroup := m.Tags[tagIfTypeGrp]; ifTypeGroup != "" {
155 entry.ifTypeGroup = ifTypeGroup
156 }
157
158 switch m.Name {
159 case "ifTraffic":
160 if v, ok := m.MultiValue["in"]; ok {
161 entry.counters.trafficIn = v
162 }
163 if v, ok := m.MultiValue["out"]; ok {
164 entry.counters.trafficOut = v
165 }
166 case "ifPacketsUcast":
167 if v, ok := m.MultiValue["in"]; ok {
168 entry.counters.ucastPktsIn = v
169 }
170 if v, ok := m.MultiValue["out"]; ok {
171 entry.counters.ucastPktsOut = v
172 }
173 case "ifPacketsBroadcast":
174 if v, ok := m.MultiValue["in"]; ok {
175 entry.counters.bcastPktsIn = v
176 }
177 if v, ok := m.MultiValue["out"]; ok {
178 entry.counters.bcastPktsOut = v
179 }
180 case "ifPacketsMulticast":
181 if v, ok := m.MultiValue["in"]; ok {
182 entry.counters.mcastPktsIn = v
183 }
184 if v, ok := m.MultiValue["out"]; ok {
185 entry.counters.mcastPktsOut = v
186 }
187 case "ifErrors":
188 if v, ok := m.MultiValue["in"]; ok {
189 entry.counters.errorsIn = v
190 }
191 if v, ok := m.MultiValue["out"]; ok {
192 entry.counters.errorsOut = v
193 }
194 case "ifDiscards":
195 if v, ok := m.MultiValue["in"]; ok {
196 entry.counters.discardsIn = v
197 }
198 if v, ok := m.MultiValue["out"]; ok {
199 entry.counters.discardsOut = v
200 }
201 case "ifAdminStatus":
202 entry.adminStatus = extractStatus(m.MultiValue)
203 case "ifOperStatus":
204 entry.operStatus = extractStatus(m.MultiValue)
205 }
206
207 entry.updated = true
208 }
209
210 // finalizeIfaceCache removes stale entries and calculates rates.
211 // Must be called after all metrics have been processed.
212 func (c *Collector) finalizeIfaceCache() {
213 if c.ifaceCache == nil {
214 return
215 }
216
217 c.ifaceCache.mu.Lock()
218 defer c.ifaceCache.mu.Unlock()
219
220 now := c.ifaceCache.updateTime
221
222 for name, entry := range c.ifaceCache.interfaces {
223 if !entry.updated {
224 delete(c.ifaceCache.interfaces, name)
225 continue
226 }
227
228 if entry.hasPrev {
229 elapsed := now.Sub(entry.prevTime)
230 entry.rates.trafficIn = calcRate(entry.counters.trafficIn, entry.prevCounters.trafficIn, elapsed)
231 entry.rates.trafficOut = calcRate(entry.counters.trafficOut, entry.prevCounters.trafficOut, elapsed)
232 entry.rates.ucastPktsIn = calcRate(entry.counters.ucastPktsIn, entry.prevCounters.ucastPktsIn, elapsed)
233 entry.rates.ucastPktsOut = calcRate(entry.counters.ucastPktsOut, entry.prevCounters.ucastPktsOut, elapsed)
234 entry.rates.bcastPktsIn = calcRate(entry.counters.bcastPktsIn, entry.prevCounters.bcastPktsIn, elapsed)
235 entry.rates.bcastPktsOut = calcRate(entry.counters.bcastPktsOut, entry.prevCounters.bcastPktsOut, elapsed)
236 entry.rates.mcastPktsIn = calcRate(entry.counters.mcastPktsIn, entry.prevCounters.mcastPktsIn, elapsed)
237 entry.rates.mcastPktsOut = calcRate(entry.counters.mcastPktsOut, entry.prevCounters.mcastPktsOut, elapsed)
238 entry.rates.errorsIn = calcRate(entry.counters.errorsIn, entry.prevCounters.errorsIn, elapsed)
239 entry.rates.errorsOut = calcRate(entry.counters.errorsOut, entry.prevCounters.errorsOut, elapsed)
240 entry.rates.discardsIn = calcRate(entry.counters.discardsIn, entry.prevCounters.discardsIn, elapsed)
241 entry.rates.discardsOut = calcRate(entry.counters.discardsOut, entry.prevCounters.discardsOut, elapsed)
242 }
243
244 entry.prevCounters = entry.counters
245 entry.prevTime = now
246 entry.hasPrev = true
247 }
248
249 c.ifaceCache.lastUpdate = now
250 }
251
252 // calcRate computes per-second rate from counter delta.
253 // Returns nil if rate cannot be calculated (zero or negative elapsed time).
254 // Handles counter wrap by treating values as unsigned.
255 func calcRate(current, previous int64, elapsed time.Duration) *float64 {
256 if elapsed <= 0 {
257 return nil
258 }
259
260 // Treat as unsigned for proper counter wrap handling
261 ucurrent := uint64(current)
262 uprevious := uint64(previous)
263
264 var delta uint64
265 if ucurrent >= uprevious {
266 delta = ucurrent - uprevious
267 } else {
268 // Counter wrap - calculate wrapped delta
269 delta = (math.MaxUint64 - uprevious) + ucurrent + 1
270 }
271
272 rate := float64(delta) / elapsed.Seconds()
273 return &rate
274 }
275
276 // extractStatus finds the active status from a MultiValue map.
277 // Returns the key where value == 1, or "unknown" if none found.
278 func extractStatus(mv map[string]int64) string {
279 for k, v := range mv {
280 if v == 1 {
281 return k
282 }
283 }
284 return "unknown"
285 }