master
go 412 lines 12.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ddsnmp
4
5 import (
6 "errors"
7 "fmt"
8 "maps"
9 "math"
10 "net"
11 "strconv"
12 "strings"
13 "text/template"
14 "time"
15
16 "github.com/Masterminds/sprig/v3"
17
18 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
19 )
20
21 // CompileTransforms exported for testing purposes only
22 func CompileTransforms(prof *Profile) error {
23 var errs []error
24
25 for i := range prof.Definition.Metrics {
26 metric := &prof.Definition.Metrics[i]
27
28 switch {
29 case metric.IsScalar():
30 if metric.Symbol.Transform == "" {
31 continue
32 }
33 tmpl, err := compileTransform(metric.Symbol.Transform)
34 if err != nil {
35 errs = append(errs, fmt.Errorf("metric_transform parsing failed for scalar metric '%s': %v",
36 metric.Symbol.Name, err))
37 continue
38 }
39 metric.Symbol.TransformCompiled = tmpl
40 case metric.IsColumn():
41 for j := range metric.Symbols {
42 sym := &metric.Symbols[j]
43 if sym.Transform == "" {
44 continue
45 }
46 tmpl, err := compileTransform(sym.Transform)
47 if err != nil {
48 errs = append(errs, fmt.Errorf("metric_transform parsing failed for table '%s' metric '%s': %v",
49 metric.Table.Name, sym.Name, err))
50 continue
51 }
52 sym.TransformCompiled = tmpl
53 }
54 }
55 }
56
57 if len(errs) > 0 {
58 return errors.Join(errs...)
59
60 }
61
62 return nil
63 }
64
65 func compileTransform(transform string) (*template.Template, error) {
66 return template.New("metric_transform").
67 Option("missingkey=zero").
68 Funcs(newMetricTransformFuncMap()).
69 Parse(transform)
70 }
71
72 func newMetricTransformFuncMap() template.FuncMap {
73 fm := sprig.TxtFuncMap()
74
75 extra := map[string]any{
76 "deleteTag": func(m *Metric, key string) any {
77 delete(m.Tags, key)
78 return nil
79 },
80 "setName": func(m *Metric, name string) string {
81 m.Name = name
82 return ""
83 },
84 "setUnit": func(m *Metric, unit string) string {
85 m.Unit = unit
86 return ""
87 },
88 "setFamily": func(m *Metric, family string) string {
89 m.Family = family
90 return ""
91 },
92 "setDesc": func(m *Metric, desc string) string {
93 m.Description = desc
94 return ""
95 },
96 "setValue": func(m *Metric, value int64) string {
97 m.Value = value
98 return ""
99 },
100 "setTag": func(m *Metric, key, value string) string {
101 if m.Tags == nil {
102 m.Tags = make(map[string]string)
103 }
104 m.Tags[key] = value
105 return ""
106 },
107 "setMultivalue": func(m *Metric, mappings map[int64]string) string {
108 m.MultiValue = make(map[string]int64)
109 for k, v := range mappings {
110 if _, ok := m.MultiValue[v]; !ok || m.Value == k {
111 m.MultiValue[v] = oldmetrix.Bool(m.Value == k)
112 }
113 }
114 return ""
115 },
116 "i64map": func(pairs ...any) map[int64]string {
117 m := make(map[int64]string)
118 for i := 0; i < len(pairs)-1; i += 2 {
119 key := int64(0)
120 switch k := pairs[i].(type) {
121 case int:
122 key = int64(k)
123 case int64:
124 key = k
125 case float64:
126 key = int64(k)
127 }
128
129 if val, ok := pairs[i+1].(string); ok {
130 m[key] = val
131 }
132 }
133 return m
134 },
135 "mask2cidr": func(mask string) string {
136 ip := net.ParseIP(mask)
137 if ip == nil {
138 return ""
139 }
140 ip = ip.To4()
141 if ip == nil {
142 return ""
143 }
144 prefixLen, bits := net.IPv4Mask(ip[0], ip[1], ip[2], ip[3]).Size()
145 if bits == 0 {
146 return ""
147 }
148 return strconv.Itoa(prefixLen)
149 },
150 "pow": func(base float64, exp int) float64 {
151 return math.Pow(base, float64(exp))
152 },
153 "transformEntitySensorValue": func(m *Metric) string {
154 /*
155 transformEntitySensorValue normalizes metrics from ENTITY-SENSOR-MIB and
156 CISCO-ENTITY-SENSOR-MIB.
157
158 Supported MIBs:
159 - ENTITY-SENSOR-MIB (RFC 3433): .1.3.6.1.2.1.99.1.1
160 - CISCO-ENTITY-SENSOR-MIB: .1.3.6.1.4.1.9.9.91.1.1.1
161
162 Expected metric:
163 - entPhySensorValue / entSensorValue: .*.1.4
164
165 Required metric tags:
166 - sensor_type (entPhySensorType / entSensorType)
167 - sensor_scale (entPhySensorScale / entSensorScale)
168 - sensor_precision (entPhySensorPrecision / entSensorPrecision)
169
170 What it does:
171 - Sets a descriptive name, unit, family, and description
172 - Applies scaling using sensor_scale and sensor_precision
173 - Applies value mappings for boolean/enumerated types
174 - Works across vendors (Cisco, Juniper, HPE, Dell, etc.)
175 */
176
177 sensorType := m.Tags["rm:sensor_type"]
178 sensorScale := m.Tags["rm:sensor_scale"]
179 sensorPrecision := m.Tags["rm:sensor_precision"]
180
181 famPrefix := "Hardware/Sensor/"
182 config := map[string]map[string]any{
183 "1": {"name": "unspecified", "family": "Generic", "desc": "Unspecified or vendor-specific sensor"},
184 "2": {"name": "unknown", "family": "Unknown", "desc": "Unknown sensor type"},
185 "3": {"name": "voltage_ac", "unit": "V", "family": "Voltage/AC", "desc": "AC voltage"},
186 "4": {"name": "voltage_dc", "unit": "V", "family": "Voltage/DC", "desc": "DC voltage"},
187 "5": {"name": "current", "unit": "A", "family": "Current", "desc": "Current draw"},
188 "6": {"name": "power", "unit": "W", "family": "Power", "desc": "Power consumption"},
189 "7": {"name": "frequency", "unit": "Hz", "family": "Frequency", "desc": "Frequency"},
190 "8": {"name": "temperature", "unit": "Cel", "family": "Temperature", "desc": "Temperature reading"},
191 "9": {"name": "humidity", "unit": "%", "family": "Humidity", "desc": "Relative humidity"},
192 "10": {"name": "fan_speed", "unit": "{revolution}/min", "family": "FanSpeed", "desc": "Fan rotation speed"},
193 "11": {"name": "airflow", "unit": "m3/min", "family": "Airflow", "desc": "Airflow in cubic meters per minute"},
194 "12": {"name": "sensor_state", "family": "State", "desc": "Boolean sensor state", "mapping": map[int64]string{
195 0: "false", 1: "true", 2: "true",
196 }},
197 "13": {"name": "special_enum", "family": "Enum", "desc": "Vendor-specific enumerated sensor"},
198 "14": {"name": "power_dbm", "unit": "dBm", "family": "Power", "desc": "Power in decibel-milliwatts"},
199 }
200
201 conf, ok := config[sensorType]
202 if !ok {
203 return ""
204 }
205
206 switch m.Name {
207 case "entPhySensorOperStatus":
208 if name, ok := conf["name"].(string); ok {
209 m.Name = m.Name + "_" + name
210 }
211 if family, ok := conf["family"].(string); ok {
212 m.Family = famPrefix + family + "/Status"
213 }
214 m.MultiValue = map[string]int64{
215 "ok": oldmetrix.Bool(m.Value == 1),
216 "unavailable": oldmetrix.Bool(m.Value == 2),
217 "nonoperational": oldmetrix.Bool(m.Value == 3),
218 }
219 case "entPhySensorValue", "entSensorValue":
220 scaleMap := map[string]float64{
221 "1": 1e-24, // yocto (10^-24)
222 "2": 1e-21, // zepto (10^-21)
223 "3": 1e-18, // atto (10^-18)
224 "4": 1e-15, // femto (10^-15)
225 "5": 1e-12, // pico (10^-12)
226 "6": 1e-9, // nano (10^-9)
227 "7": 1e-6, // micro (10^-6)
228 "8": 1e-3, // milli (10^-3)
229 "9": 1, // units (10^0)
230 "10": 1e3, // kilo (10^3)
231 "11": 1e6, // mega (10^6)
232 "12": 1e9, // giga (10^9)
233 "13": 1e12, // tera (10^12)
234 }
235
236 scale := scaleMap[sensorScale]
237 if scale == 0 || scale == 1e-24 {
238 // Workaround for Cisco ASA (MIMIC) temperature bug: treat scale=1 (yocto) as units
239 scale = 1.0
240 }
241
242 precision := 0
243 if p, err := strconv.Atoi(sensorPrecision); err == nil {
244 precision = p
245 }
246
247 if name, ok := conf["name"].(string); ok {
248 m.Name = m.Name + "_" + name
249 }
250 if family, ok := conf["family"].(string); ok {
251 m.Family = famPrefix + family + "/Value"
252 }
253 if desc, ok := conf["desc"].(string); ok {
254 m.Description = desc
255 }
256 if unit, ok := conf["unit"].(string); ok {
257 m.Unit = unit
258 }
259 if mapping, ok := conf["mapping"].(map[int64]string); ok {
260 m.MultiValue = make(map[string]int64)
261 for k, v := range mapping {
262 if _, ok := m.MultiValue[v]; !ok || m.Value == k {
263 m.MultiValue[v] = oldmetrix.Bool(m.Value == k)
264 }
265 }
266 } else {
267 val := float64(m.Value) * scale
268 if precision > 0 {
269 val = val / math.Pow(10, float64(precision))
270 }
271 m.Value = int64(val)
272 }
273 }
274
275 return ""
276 },
277 "transformHrStorage": func(m *Metric) string {
278 // hrStorageType → friendly name/family
279 // https://oidref.com/1.3.6.1.2.1.25.2.1
280 typeMap := map[string]map[string]string{
281 "1.3.6.1.2.1.25.2.1.1": {"key": "other", "family": "System/Memory/Other", "desc": "Other memory"},
282 "1.3.6.1.2.1.25.2.1.2": {"key": "ram", "family": "System/Memory/RAM", "desc": "Physical system memory"},
283 "1.3.6.1.2.1.25.2.1.3": {"key": "virtual_memory", "family": "System/Memory/Swap", "desc": "Virtual memory space"},
284 "1.3.6.1.2.1.25.2.1.4": {"key": "fixed_disk", "family": "System/Storage/Disk", "desc": "Fixed disk storage"},
285 "1.3.6.1.2.1.25.2.1.5": {"key": "removable_disk", "family": "System/Storage/Removable", "desc": "Removable disk storage"},
286 "1.3.6.1.2.1.25.2.1.6": {"key": "floppy", "family": "System/Storage/Removable", "desc": "Floppy disk storage"},
287 "1.3.6.1.2.1.25.2.1.7": {"key": "cdrom", "family": "System/Storage/Removable", "desc": "CD-ROM storage"},
288 "1.3.6.1.2.1.25.2.1.8": {"key": "ram_disk", "family": "System/Storage/RAMDisk", "desc": "RAM-based storage"},
289 "1.3.6.1.2.1.25.2.1.9": {"key": "flash", "family": "System/Storage/Flash", "desc": "Flash-based storage"},
290 "1.3.6.1.2.1.25.2.1.10": {"key": "network_disk", "family": "System/Storage/NetworkDisk", "desc": "Network-mounted storage"},
291 }
292
293 storageTypeOID := m.Tags["rm:storage_type"]
294 allocUnit, _ := strconv.ParseInt(m.Tags["rm:storage_alloc_unit"], 10, 64)
295
296 if storageTypeOID == "" || allocUnit <= 0 || m.Value < 0 {
297 return ""
298 }
299
300 tconf, ok := typeMap[storageTypeOID]
301 if !ok {
302 tconf = map[string]string{"key": "unknown", "family": "System/Storage/Unknown", "desc": "Unknown storage type"}
303 }
304
305 bytesVal := m.Value * allocUnit // to bytes
306 baseKey := tconf["key"]
307 fam := tconf["family"]
308 desc := tconf["desc"]
309
310 switch m.Name {
311 case "hrStorageSize":
312 m.Family = fam + "/Total"
313 m.Description = desc + " - Total"
314 case "hrStorageUsed":
315 m.Family = fam + "/Used"
316 m.Description = desc + " - Used"
317 default:
318 return ""
319 }
320
321 m.Name = m.Name + "_" + baseKey
322 m.Value = bytesVal
323
324 return ""
325 },
326 }
327
328 maps.Copy(fm, extra)
329
330 return fm
331 }
332
333 // textDateLayouts is the set of vendor-friendly date formats accepted by
334 // text_date. The list is intentionally generous: vendors that publish
335 // operational dates through SNMP rarely agree on a single textual format.
336 // Numeric slash-only dates are intentionally excluded because dd/mm/yyyy and
337 // mm/dd/yyyy are ambiguous for values like 01/02/2024.
338 var textDateLayouts = []string{
339 time.RFC3339,
340 "2006-01-02 15:04:05",
341 "2006-01-02",
342 "Mon Jan 2 15:04:05 2006",
343 "Mon Jan 2 2006",
344 "Mon 2 January 2006",
345 "2 January 2006",
346 "January 2 2006",
347 "Jan 2 2006",
348 "Jan 2 2006 15:04:05",
349 "2 Jan 2006",
350 "2 Jan 2006 15:04:05",
351 "02 Jan 2006",
352 "02 Jan 2006 15:04:05",
353 "02Jan2006",
354 "2Jan2006",
355 }
356
357 // ParseTextDate accepts integer- and string-encoded SNMP date shapes (epoch
358 // seconds, milliseconds, decimal no-value sentinels such as 0 and 4294967295,
359 // and the textual layouts above) and returns the equivalent unix timestamp. It
360 // is exported so the value-processor format "text_date" in
361 // ddsnmpcollector/utils.go can apply the same parsing rules.
362 func ParseTextDate(raw string) (int64, bool) {
363 return parseTextDate(raw)
364 }
365
366 // IsTextDateNoValue reports whether raw is a vendor no-timestamp sentinel
367 // accepted by ParseTextDate.
368 func IsTextDateNoValue(raw string) bool {
369 return isTextDateNoValue(raw)
370 }
371
372 func parseTextDate(raw string) (int64, bool) {
373 raw = strings.TrimSpace(raw)
374 if isTextDateNoValue(raw) {
375 return 0, false
376 }
377
378 if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
379 digits := strings.TrimLeft(raw, "+-")
380 switch {
381 case len(digits) >= 12:
382 return n / 1000, true
383 default:
384 return n, true
385 }
386 }
387
388 for _, layout := range textDateLayouts {
389 if t, err := time.Parse(layout, raw); err == nil {
390 return t.Unix(), true
391 }
392 }
393 return 0, false
394 }
395
396 func isTextDateNoValue(raw string) bool {
397 raw = strings.TrimSpace(raw)
398 if raw == "" {
399 return true
400 }
401
402 switch strings.ToLower(raw) {
403 case "0", "none", "n/a", "na", "perpetual", "permanent", "never", "unlimited":
404 return true
405 }
406
407 if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
408 return n <= 0 || n == 4_294_967_295
409 }
410
411 return false
412 }