diff --git a/dyn_stat.go b/dyn_stat.go new file mode 100644 index 0000000000000000000000000000000000000000..f41fbcd19fb05cd6740d5a9aea615bf5f65871bd --- /dev/null +++ b/dyn_stat.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +type dynStat struct { + Name string `json:"name"` + Origin string `json:"origin"` + Values map[string]int64 `json:"values"` +} + +func newDynStatFromJSON(b []byte) (*dynStat, error) { + var pstat dynStat + err := json.Unmarshal(b, &pstat) + if err != nil { + return nil, fmt.Errorf("error decoding values stat `%v`: %v", string(b), err) + } + return &pstat, nil +} + +func (i *dynStat) toPoints() []*point { + points := make([]*point, 0, len(i.Values)) + + for name, value := range i.Values { + points = append(points, &point{ + Name: fmt.Sprintf("dynstat_%s", i.Name), + Type: counter, + Value: value, + Description: fmt.Sprintf("dynamic statistic bucket %s", i.Name), + LabelName: "counter", + LabelValue: name, + }) + } + + return points +} diff --git a/dyn_stat_test.go b/dyn_stat_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8d7ff843aeab873032c718105b8a4725a2bfbebf --- /dev/null +++ b/dyn_stat_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestGetDynStat(t *testing.T) { + log := []byte(`{ "name": "global", "origin": "dynstats", "values": { "msg_per_host.ops_overflow": 1, "msg_per_host.new_metric_add": 3, "msg_per_host.no_metric": 0, "msg_per_host.metrics_purged": 0, "msg_per_host.ops_ignored": 0 } }`) + values := map[string]int64{ + "msg_per_host.ops_overflow": 1, + "msg_per_host.new_metric_add": 3, + "msg_per_host.no_metric": 0, + "msg_per_host.metrics_purged": 0, + "msg_per_host.ops_ignored": 0, + } + + if want, got := rsyslogDynStat, getStatType(log); want != got { + t.Errorf("detected pstat type should be %d but is %d", want, got) + } + + pstat, err := newDynStatFromJSON(log) + if err != nil { + t.Fatalf("expected parsing dynamic stat not to fail, got: %v", err) + } + + if want, got := "global", pstat.Name; want != got { + t.Errorf("invalid name, want '%s', got '%s'", want, got) + } + + if want, got := values, pstat.Values; !reflect.DeepEqual(want, got) { + t.Errorf("unexpected values, want: %+v got: %+v", want, got) + } +} + +func TestDynStatToPoints(t *testing.T) { + log := []byte(`{ "name": "global", "origin": "dynstats", "values": { "msg_per_host.ops_overflow": 1, "msg_per_host.new_metric_add": 3, "msg_per_host.no_metric": 0, "msg_per_host.metrics_purged": 0, "msg_per_host.ops_ignored": 0 } }`) + wants := map[string]point{ + "msg_per_host.ops_overflow": point{ + Name: "dynstat_global", + Type: counter, + Value: 1, + Description: "dynamic statistic bucket global", + LabelName: "counter", + LabelValue: "msg_per_host.ops_overflow", + }, + "msg_per_host.new_metric_add": point{ + Name: "dynstat_global", + Type: counter, + Value: 3, + Description: "dynamic statistic bucket global", + LabelName: "counter", + LabelValue: "msg_per_host.new_metric_add", + }, + "msg_per_host.no_metric": point{ + Name: "dynstat_global", + Type: counter, + Value: 0, + Description: "dynamic statistic bucket global", + LabelName: "counter", + LabelValue: "msg_per_host.no_metric", + }, + "msg_per_host.metrics_purged": point{ + Name: "dynstat_global", + Type: counter, + Value: 0, + Description: "dynamic statistic bucket global", + LabelName: "counter", + LabelValue: "msg_per_host.metrics_purged", + }, + "msg_per_host.ops_ignored": point{ + Name: "dynstat_global", + Type: counter, + Value: 0, + Description: "dynamic statistic bucket global", + LabelName: "counter", + LabelValue: "msg_per_host.ops_ignored", + }, + } + + seen := map[string]bool{} + for name, _ := range wants { + seen[name] = false + } + + pstat, err := newDynStatFromJSON(log) + if err != nil { + t.Fatalf("expected parsing dyn stat not to fail, got: %v", err) + } + + points := pstat.toPoints() + for _, got := range points { + key := got.LabelValue + want, ok := wants[key] + if !ok { + t.Errorf("unexpected point, got: %+v", got) + continue + } + + if !reflect.DeepEqual(want, *got) { + t.Errorf("expected point to be %+v, got %+v", want, got) + } + + if seen[key] { + t.Errorf("point seen multiple times: %+v", got) + } + seen[key] = true + } + + for name, ok := range seen { + if !ok { + t.Errorf("expected to see point with key %s, but did not", name) + } + } +} diff --git a/exporter.go b/exporter.go index 92acdc27644cd013754b49b36c1fe17eb8a43ff9..746c8c65580f4b1f4e0ad06ea673503b8a49e513 100644 --- a/exporter.go +++ b/exporter.go @@ -19,6 +19,7 @@ const ( rsyslogInput rsyslogQueue rsyslogResource + rsyslogDynStat ) type rsyslogExporter struct { @@ -84,6 +85,14 @@ func (re *rsyslogExporter) handleStatLine(rawbuf []byte) error { for _, p := range r.toPoints() { re.set(p) } + case rsyslogDynStat: + s, err := newDynStatFromJSON(buf) + if err != nil { + return err + } + for _, p := range s.toPoints() { + re.set(p) + } default: return fmt.Errorf("unknown pstat type: %v", pstatType) diff --git a/exporter_test.go b/exporter_test.go index 99fedfc0238e1e74960d564b95f10b5fee6efd79..df2326e6102bbf541aa8c9afc389793df6b4d7ab 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -196,6 +196,40 @@ func TestHandleLineWithQueue(t *testing.T) { testHelper(t, queueLog, tests) } +func TestHandleLineWithGlobal(t *testing.T) { + tests := []*testUnit{ + &testUnit{ + Name: "dynstat_global", + Val: 1, + LabelValue: "msg_per_host.ops_overflow", + }, + &testUnit{ + Name: "dynstat_global", + Val: 3, + LabelValue: "msg_per_host.new_metric_add", + }, + &testUnit{ + Name: "dynstat_global", + Val: 0, + LabelValue: "msg_per_host.no_metric", + }, + &testUnit{ + Name: "dynstat_global", + Val: 0, + LabelValue: "msg_per_host.metrics_purged", + }, + &testUnit{ + Name: "dynstat_global", + Val: 0, + LabelValue: "msg_per_host.ops_ignored", + }, + } + + log := []byte(`2018-01-18T09:39:12.763025+00:00 some-node.example.org rsyslogd-pstats: { "name": "global", "origin": "dynstats", "values": { "msg_per_host.ops_overflow": 1, "msg_per_host.new_metric_add": 3, "msg_per_host.no_metric": 0, "msg_per_host.metrics_purged": 0, "msg_per_host.ops_ignored": 0 } }`) + + testHelper(t, log, tests) +} + func TestHandleUnknown(t *testing.T) { unknownLog := []byte(`2017-08-30T08:10:04.786350+00:00 some-node.example.org rsyslogd-pstats: {"a":"b"}`) diff --git a/utils.go b/utils.go index 3180be8bbf658bbc4ef8623132e8b682b6ddac25..5130060c62c30bb4a93942959a7bf15bc1792270 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,8 @@ func getStatType(buf []byte) rsyslogType { return rsyslogQueue } else if strings.Contains(line, "utime") { return rsyslogResource + } else if strings.Contains(line, "dynstats") { + return rsyslogDynStat } return rsyslogUnknown }