diff --git a/plugins/parsers/nagios/parser.go b/plugins/parsers/nagios/parser.go index 4d5f7f008..858f5082c 100644 --- a/plugins/parsers/nagios/parser.go +++ b/plugins/parsers/nagios/parser.go @@ -1,6 +1,8 @@ package nagios import ( + "errors" + "log" "regexp" "strconv" "strings" @@ -17,8 +19,10 @@ type NagiosParser struct { // Got from Alignak // https://github.com/Alignak-monitoring/alignak/blob/develop/alignak/misc/perfdata.py -var perfSplitRegExp, _ = regexp.Compile(`([^=]+=\S+)`) -var nagiosRegExp, _ = regexp.Compile(`^([^=]+)=([\d\.\-\+eE]+)([\w\/%]*);?([\d\.\-\+eE:~@]+)?;?([\d\.\-\+eE:~@]+)?;?([\d\.\-\+eE]+)?;?([\d\.\-\+eE]+)?;?\s*`) +var ( + perfSplitRegExp = regexp.MustCompile(`([^=]+=\S+)`) + nagiosRegExp = regexp.MustCompile(`^([^=]+)=([\d\.\-\+eE]+)([\w\/%]*);?([\d\.\-\+eE:~@]+)?;?([\d\.\-\+eE:~@]+)?;?([\d\.\-\+eE]+)?;?([\d\.\-\+eE]+)?;?\s*`) +) func (p *NagiosParser) ParseLine(line string) (telegraf.Metric, error) { metrics, err := p.Parse([]byte(line)) @@ -29,88 +33,99 @@ func (p *NagiosParser) SetDefaultTags(tags map[string]string) { p.DefaultTags = tags } -//> rta,host=absol,unit=ms critical=6000,min=0,value=0.332,warning=4000 1456374625003628099 -//> pl,host=absol,unit=% critical=90,min=0,value=0,warning=80 1456374625003693967 - func (p *NagiosParser) Parse(buf []byte) ([]telegraf.Metric, error) { metrics := make([]telegraf.Metric, 0) - // Convert to string - out := string(buf) - // Prepare output for splitting - // Delete escaped pipes - out = strings.Replace(out, `\|`, "___PROTECT_PIPE___", -1) - // Split lines and get the first one - lines := strings.Split(out, "\n") - // Split output and perfdatas - data_splitted := strings.Split(lines[0], "|") - if len(data_splitted) <= 1 { - // No pipe == no perf data - return nil, nil + lines := strings.Split(strings.TrimSpace(string(buf)), "\n") + + for _, line := range lines { + data_splitted := strings.Split(line, "|") + + if len(data_splitted) != 2 { + // got human readable output only or bad line + continue + } + m, err := parsePerfData(data_splitted[1]) + if err != nil { + log.Printf("E! [parser.nagios] failed to parse performance data: %s\n", err.Error()) + continue + } + metrics = append(metrics, m...) } - // Get perfdatas - perfdatas := data_splitted[1] - // Add escaped pipes - perfdatas = strings.Replace(perfdatas, "___PROTECT_PIPE___", `\|`, -1) - // Split perfs - unParsedPerfs := perfSplitRegExp.FindAllSubmatch([]byte(perfdatas), -1) - // Iterate on all perfs - for _, unParsedPerfs := range unParsedPerfs { - // Get metrics - // Trim perf - trimedPerf := strings.Trim(string(unParsedPerfs[0]), " ") - // Parse perf - perf := nagiosRegExp.FindAllSubmatch([]byte(trimedPerf), -1) - // Bad string - if len(perf) == 0 { + return metrics, nil +} + +func parsePerfData(perfdatas string) ([]telegraf.Metric, error) { + metrics := make([]telegraf.Metric, 0) + + for _, unParsedPerf := range perfSplitRegExp.FindAllString(perfdatas, -1) { + trimedPerf := strings.TrimSpace(unParsedPerf) + perf := nagiosRegExp.FindStringSubmatch(trimedPerf) + + // verify at least `'label'=value[UOM];` existed + if len(perf) < 3 { continue } - if len(perf[0]) <= 2 { + if perf[1] == "" || perf[2] == "" { continue } - if perf[0][1] == nil || perf[0][2] == nil { - continue - } - fieldName := string(perf[0][1]) - tags := make(map[string]string) - if perf[0][3] != nil { - str := string(perf[0][3]) + + fieldName := strings.Trim(perf[1], "'") + tags := map[string]string{"perfdata": fieldName} + if perf[3] != "" { + str := string(perf[3]) if str != "" { tags["unit"] = str } } + fields := make(map[string]interface{}) - f, err := strconv.ParseFloat(string(perf[0][2]), 64) + if perf[2] == "U" { + return nil, errors.New("Value undetermined") + } + + f, err := strconv.ParseFloat(string(perf[2]), 64) if err == nil { fields["value"] = f } - // TODO should we set empty field - // if metric if there is no data ? - if perf[0][4] != nil { - f, err := strconv.ParseFloat(string(perf[0][4]), 64) + if perf[4] != "" { + low, high, err := parseThreshold(perf[4]) if err == nil { - fields["warning"] = f + if strings.Contains(perf[4], "@") { + fields["warning_le"] = low + fields["warning_ge"] = high + } else { + fields["warning_lt"] = low + fields["warning_gt"] = high + } } } - if perf[0][5] != nil { - f, err := strconv.ParseFloat(string(perf[0][5]), 64) + if perf[5] != "" { + low, high, err := parseThreshold(perf[5]) if err == nil { - fields["critical"] = f + if strings.Contains(perf[5], "@") { + fields["critical_le"] = low + fields["critical_ge"] = high + } else { + fields["critical_lt"] = low + fields["critical_gt"] = high + } } } - if perf[0][6] != nil { - f, err := strconv.ParseFloat(string(perf[0][6]), 64) + if perf[6] != "" { + f, err := strconv.ParseFloat(perf[6], 64) if err == nil { fields["min"] = f } } - if perf[0][7] != nil { - f, err := strconv.ParseFloat(string(perf[0][7]), 64) + if perf[7] != "" { + f, err := strconv.ParseFloat(perf[7], 64) if err == nil { fields["max"] = f } } + // Create metric - metric, err := metric.New(fieldName, tags, fields, time.Now().UTC()) + metric, err := metric.New("nagios", tags, fields, time.Now().UTC()) if err != nil { return nil, err } @@ -120,3 +135,47 @@ func (p *NagiosParser) Parse(buf []byte) ([]telegraf.Metric, error) { return metrics, nil } + +// from math +const ( + MaxFloat64 = 1.797693134862315708145274237317043567981e+308 // 2**1023 * (2**53 - 1) / 2**52 + MinFloat64 = 4.940656458412465441765687928682213723651e-324 // 1 / 2**(1023 - 1 + 52) +) + +var ErrBadThresholdFormat = errors.New("Bad threshold format") + +// Handles all cases from https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT +func parseThreshold(threshold string) (min float64, max float64, err error) { + thresh := strings.Split(threshold, ":") + switch len(thresh) { + case 1: + max, err = strconv.ParseFloat(string(thresh[0]), 64) + if err != nil { + return 0, 0, ErrBadThresholdFormat + } + + return 0, max, nil + case 2: + if thresh[0] == "~" { + min = MinFloat64 + } else { + min, err = strconv.ParseFloat(string(thresh[0]), 64) + if err != nil { + min = 0 + } + } + + if thresh[1] == "" { + max = MaxFloat64 + } else { + max, err = strconv.ParseFloat(string(thresh[1]), 64) + if err != nil { + return 0, 0, ErrBadThresholdFormat + } + } + default: + return 0, 0, ErrBadThresholdFormat + } + + return +} diff --git a/plugins/parsers/nagios/parser_test.go b/plugins/parsers/nagios/parser_test.go index b1e3d6fdd..a4da30030 100644 --- a/plugins/parsers/nagios/parser_test.go +++ b/plugins/parsers/nagios/parser_test.go @@ -13,6 +13,7 @@ with three lines ` const validOutput2 = "TCP OK - 0.008 second response time on port 80|time=0.008457s;;;0.000000;10.000000" const validOutput3 = "TCP OK - 0.008 second response time on port 80|time=0.008457" +const validOutput4 = "OK: Load average: 0.00, 0.01, 0.05 | 'load1'=0.00;~:4;@0:6;0; 'load5'=0.01;3;0:5;0; 'load15'=0.05;0:2;0:4;0;" const invalidOutput3 = "PING OK - Packet loss = 0%, RTA = 0.30 ms" const invalidOutput4 = "PING OK - Packet loss = 0%, RTA = 0.30 ms| =3;;;; dgasdg =;;;; sff=;;;;" @@ -24,50 +25,71 @@ func TestParseValidOutput(t *testing.T) { // Output1 metrics, err := parser.Parse([]byte(validOutput1)) require.NoError(t, err) - assert.Len(t, metrics, 2) + require.Len(t, metrics, 2) // rta - assert.Equal(t, "rta", metrics[0].Name()) + assert.Equal(t, "rta", metrics[0].Tags()["perfdata"]) assert.Equal(t, map[string]interface{}{ - "value": float64(0.298), - "warning": float64(4000), - "critical": float64(6000), - "min": float64(0), + "value": float64(0.298), + "warning_lt": float64(0), + "warning_gt": float64(4000), + "critical_lt": float64(0), + "critical_gt": float64(6000), + "min": float64(0), }, metrics[0].Fields()) - assert.Equal(t, map[string]string{"unit": "ms"}, metrics[0].Tags()) + assert.Equal(t, map[string]string{"unit": "ms", "perfdata": "rta"}, metrics[0].Tags()) // pl - assert.Equal(t, "pl", metrics[1].Name()) + assert.Equal(t, "pl", metrics[1].Tags()["perfdata"]) assert.Equal(t, map[string]interface{}{ - "value": float64(0), - "warning": float64(80), - "critical": float64(90), - "min": float64(0), - "max": float64(100), + "value": float64(0), + "warning_lt": float64(0), + "warning_gt": float64(80), + "critical_lt": float64(0), + "critical_gt": float64(90), + "min": float64(0), + "max": float64(100), }, metrics[1].Fields()) - assert.Equal(t, map[string]string{"unit": "%"}, metrics[1].Tags()) + assert.Equal(t, map[string]string{"unit": "%", "perfdata": "pl"}, metrics[1].Tags()) // Output2 metrics, err = parser.Parse([]byte(validOutput2)) require.NoError(t, err) - assert.Len(t, metrics, 1) + require.Len(t, metrics, 1) // time - assert.Equal(t, "time", metrics[0].Name()) + assert.Equal(t, "time", metrics[0].Tags()["perfdata"]) assert.Equal(t, map[string]interface{}{ "value": float64(0.008457), "min": float64(0), "max": float64(10), }, metrics[0].Fields()) - assert.Equal(t, map[string]string{"unit": "s"}, metrics[0].Tags()) + assert.Equal(t, map[string]string{"unit": "s", "perfdata": "time"}, metrics[0].Tags()) // Output3 metrics, err = parser.Parse([]byte(validOutput3)) require.NoError(t, err) - assert.Len(t, metrics, 1) + require.Len(t, metrics, 1) // time - assert.Equal(t, "time", metrics[0].Name()) + assert.Equal(t, "time", metrics[0].Tags()["perfdata"]) assert.Equal(t, map[string]interface{}{ "value": float64(0.008457), }, metrics[0].Fields()) - assert.Equal(t, map[string]string{}, metrics[0].Tags()) + assert.Equal(t, map[string]string{"perfdata": "time"}, metrics[0].Tags()) + + // Output4 + metrics, err = parser.Parse([]byte(validOutput4)) + require.NoError(t, err) + require.Len(t, metrics, 3) + // load + // const validOutput4 = "OK: Load average: 0.00, 0.01, 0.05 | 'load1'=0.00;0:4;0:6;0; 'load5'=0.01;0:3;0:5;0; 'load15'=0.05;0:2;0:4;0;" + assert.Equal(t, map[string]interface{}{ + "value": float64(0.00), + "warning_lt": MinFloat64, + "warning_gt": float64(4), + "critical_le": float64(0), + "critical_ge": float64(6), + "min": float64(0), + }, metrics[0].Fields()) + + assert.Equal(t, map[string]string{"perfdata": "load1"}, metrics[0].Tags()) } func TestParseInvalidOutput(t *testing.T) { @@ -78,11 +100,64 @@ func TestParseInvalidOutput(t *testing.T) { // invalidOutput3 metrics, err := parser.Parse([]byte(invalidOutput3)) require.NoError(t, err) - assert.Len(t, metrics, 0) + require.Len(t, metrics, 0) // invalidOutput4 metrics, err = parser.Parse([]byte(invalidOutput4)) require.NoError(t, err) - assert.Len(t, metrics, 0) + require.Len(t, metrics, 0) } + +func TestParseThreshold(t *testing.T) { + tests := []struct { + input string + eMin float64 + eMax float64 + eErr error + }{ + { + input: "10", + eMin: 0, + eMax: 10, + eErr: nil, + }, + { + input: "10:", + eMin: 10, + eMax: MaxFloat64, + eErr: nil, + }, + { + input: "~:10", + eMin: MinFloat64, + eMax: 10, + eErr: nil, + }, + { + input: "10:20", + eMin: 10, + eMax: 20, + eErr: nil, + }, + { + input: "10:20", + eMin: 10, + eMax: 20, + eErr: nil, + }, + { + input: "10:20:30", + eMin: 0, + eMax: 0, + eErr: ErrBadThresholdFormat, + }, + } + + for i := range tests { + min, max, err := parseThreshold(tests[i].input) + require.Equal(t, tests[i].eMin, min) + require.Equal(t, tests[i].eMax, max) + require.Equal(t, tests[i].eErr, err) + } +}