master
go 253 lines 4.81 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package output
4
5 import (
6 "math"
7 "strconv"
8 "strings"
9 "unicode"
10 )
11
12 // ParsedOutput represents the structured form of a Nagios plugin output.
13 type ParsedOutput struct {
14 statusLine string
15 longOutput string
16 Perfdata []PerfDatum
17 }
18
19 // PerfDatum represents a single perfdata entry.
20 type PerfDatum struct {
21 Label string
22 Unit string
23 Value float64
24 Warn *ThresholdRange
25 Crit *ThresholdRange
26 }
27
28 // ThresholdRange captures the Nagios range grammar semantics.
29 type ThresholdRange struct {
30 Inclusive bool
31 Low *float64
32 High *float64
33 }
34
35 // Parse converts raw plugin output into status, long output, and perfdata sections.
36 func Parse(raw []byte) ParsedOutput {
37 text := strings.ReplaceAll(string(raw), "\r\n", "\n")
38 text = strings.TrimSpace(text)
39 if text == "" {
40 return ParsedOutput{}
41 }
42
43 lines := strings.Split(text, "\n")
44 var perfSections []string
45
46 firstLine := lines[0]
47 if idx := strings.Index(firstLine, "|"); idx >= 0 {
48 perfSections = append(perfSections, firstLine[idx+1:])
49 firstLine = firstLine[:idx]
50 }
51
52 status := strings.TrimSpace(firstLine)
53
54 var longLines []string
55 if len(lines) > 1 {
56 for _, line := range lines[1:] {
57 trimmed := line
58 if idx := strings.Index(trimmed, "|"); idx >= 0 {
59 perfSections = append(perfSections, trimmed[idx+1:])
60 trimmed = trimmed[:idx]
61 }
62 longLines = append(longLines, strings.TrimRightFunc(trimmed, unicode.IsSpace))
63 }
64 }
65
66 perfTokens := tokenizePerfdata(strings.Join(perfSections, " "))
67
68 var perfdata []PerfDatum
69 for _, token := range perfTokens {
70 if datum, ok := parsePerfToken(token); ok {
71 perfdata = append(perfdata, datum)
72 }
73 }
74
75 longOutput := strings.Join(longLines, "\n")
76 longOutput = strings.TrimRightFunc(longOutput, unicode.IsSpace)
77
78 return ParsedOutput{
79 statusLine: status,
80 longOutput: longOutput,
81 Perfdata: perfdata,
82 }
83 }
84
85 func (p ParsedOutput) StatusLine() string { return p.statusLine }
86
87 func (p ParsedOutput) LongOutput() string { return p.longOutput }
88
89 func tokenizePerfdata(s string) []string {
90 var tokens []string
91 var cur strings.Builder
92 var quote rune
93
94 flush := func() {
95 if cur.Len() > 0 {
96 tokens = append(tokens, cur.String())
97 cur.Reset()
98 }
99 }
100
101 for _, r := range s {
102 switch {
103 case quote != 0:
104 if r == quote {
105 quote = 0
106 } else {
107 cur.WriteRune(r)
108 }
109 case r == '\'' || r == '"':
110 quote = r
111 case unicode.IsSpace(r):
112 flush()
113 default:
114 cur.WriteRune(r)
115 }
116 }
117 flush()
118
119 return tokens
120 }
121
122 func parsePerfToken(token string) (PerfDatum, bool) {
123 parts := strings.SplitN(token, "=", 2)
124 if len(parts) != 2 {
125 return PerfDatum{}, false
126 }
127
128 label := strings.Trim(parts[0], "\"'")
129 if label == "" {
130 return PerfDatum{}, false
131 }
132
133 fields := strings.SplitN(parts[1], ";", 5)
134 valueStr := fields[0]
135 warnStr, critStr := getField(fields, 1), getField(fields, 2)
136
137 val, unit, ok := parseValueUnit(valueStr)
138 if !ok {
139 return PerfDatum{}, false
140 }
141
142 datum := PerfDatum{
143 Label: label,
144 Unit: unit,
145 Value: val,
146 Warn: parseRange(warnStr),
147 Crit: parseRange(critStr),
148 }
149
150 return datum, true
151 }
152
153 func getField(fields []string, idx int) string {
154 if idx >= len(fields) {
155 return ""
156 }
157 return fields[idx]
158 }
159
160 func parseValueUnit(value string) (float64, string, bool) {
161 if value == "" {
162 return 0, "", false
163 }
164 i := 0
165 for i < len(value) {
166 if isNumberChar(value[i]) {
167 i++
168 continue
169 }
170 break
171 }
172 numStr := value[:i]
173 unit := value[i:]
174 if numStr == "" {
175 return 0, unit, false
176 }
177 v, err := strconv.ParseFloat(numStr, 64)
178 if err != nil {
179 return 0, unit, false
180 }
181 return v, unit, true
182 }
183
184 func parseRange(val string) *ThresholdRange {
185 s := strings.TrimSpace(val)
186 if s == "" || strings.EqualFold(s, "u") {
187 return nil
188 }
189 rng := &ThresholdRange{}
190 if strings.HasPrefix(s, "@") {
191 rng.Inclusive = true
192 s = strings.TrimSpace(s[1:])
193 }
194 if s == "" {
195 return nil
196 }
197 if strings.Contains(s, ":") {
198 parts := strings.SplitN(s, ":", 2)
199 start := strings.TrimSpace(parts[0])
200 end := strings.TrimSpace(parts[1])
201 if start == "" {
202 zero := 0.0
203 rng.Low = &zero
204 } else if start != "~" {
205 if v, ok := parseRangeNumber(start); ok {
206 rng.Low = &v
207 } else {
208 return nil
209 }
210 }
211 if end != "" && end != "~" {
212 if v, ok := parseRangeNumber(end); ok {
213 rng.High = &v
214 } else {
215 return nil
216 }
217 }
218 } else {
219 v, ok := parseRangeNumber(s)
220 if !ok {
221 return nil
222 }
223 zero := 0.0
224 rng.Low = &zero
225 rng.High = &v
226 }
227 return rng
228 }
229
230 func parseRangeNumber(val string) (float64, bool) {
231 if val == "" {
232 return 0, false
233 }
234 switch strings.ToLower(val) {
235 case "inf", "+inf", "infinity":
236 return math.Inf(1), true
237 case "-inf":
238 return math.Inf(-1), true
239 }
240 v, err := strconv.ParseFloat(val, 64)
241 if err != nil {
242 return 0, false
243 }
244 return v, true
245 }
246
247 func isNumberChar(b byte) bool {
248 switch b {
249 case '+', '-', '.', 'e', 'E':
250 return true
251 }
252 return b >= '0' && b <= '9'
253 }