diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..daf913b1b347aae6de6f48d599bc89ef8c8693d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..ff32db4000b3206cf570815082ed66386592cde2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: go + +go: + - 1.4 + +install: + - go get github.com/taotetek/rsyslog_exporter + +script: + - go test -v ./... diff --git a/README.md b/README.md index dd869b3a80c2b4df6e6f80021dcdf735b43f3a17..3bec7d408601a7537dc72c1ae5ddc4da1d611458 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ -# rsyslog_exporter +# rsyslog_exporter [](https://travis-ci.org/digitalocean/rsyslog_exporter) +A [prometheus](http://prometheus.io/) exporter for [rsyslog](http://rsyslog.com). It accepts rsyslog [impstats](http://www.rsyslog.com/doc/master/configuration/modules/impstats.html) metrics in JSON format over stdin via the rsyslog [omprog](http://www.rsyslog.com/doc/v8-stable/configuration/modules/omprog.html) plugin and transforms and exposes them for consumption by Prometheus. + +## Rsyslog Configuration +Configure rsyslog to push JSON formatted stats via omprog: +``` +module( + load="impstats" + interval="10" + format="json" + resetCounters="off" + ruleset="process_stats" +) + +ruleset(name="process_stats") { + action( + type="omprog" + name="to_exporter" + binary="/usr/local/bin/rsyslog_exporter" + ) +} +``` diff --git a/actions.go b/actions.go new file mode 100644 index 0000000000000000000000000000000000000000..a859f16156c6cd12fe4e4cf3c6e1f66d2f2830d6 --- /dev/null +++ b/actions.go @@ -0,0 +1,67 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +type action struct { + Name string `json:"name"` + Processed int64 `json:"processed"` + Failed int64 `json:"failed"` + Suspended int64 `json:"suspended"` + SuspendedDuration int64 `json:"suspended.duration"` + Resumed int64 `json:"resumed"` +} + +func newActionFromJSON(b []byte) *action { + dec := json.NewDecoder(bytes.NewReader(b)) + var pstat action + dec.Decode(&pstat) + pstat.Name = strings.ToLower(pstat.Name) + pstat.Name = strings.Replace(pstat.Name, " ", "_", -1) + return &pstat +} + +func (a *action) toPoints() []*point { + points := make([]*point, 5) + + points[0] = &point{ + Name: fmt.Sprintf("%s_processed", a.Name), + Type: counter, + Value: a.Processed, + Description: "messages processed", + } + + points[1] = &point{ + Name: fmt.Sprintf("%s_failed", a.Name), + Type: counter, + Value: a.Failed, + Description: "messages failed", + } + + points[2] = &point{ + Name: fmt.Sprintf("%s_suspended", a.Name), + Type: counter, + Value: a.Suspended, + Description: "times suspended", + } + + points[3] = &point{ + Name: fmt.Sprintf("%s_suspended_duration", a.Name), + Type: counter, + Value: a.SuspendedDuration, + Description: "time spent suspended", + } + + points[4] = &point{ + Name: fmt.Sprintf("%s_resumed", a.Name), + Type: counter, + Value: a.Resumed, + Description: "times resumed", + } + + return points +} diff --git a/actions_test.go b/actions_test.go new file mode 100644 index 0000000000000000000000000000000000000000..30ecc88391a10d5de6186e7812a73288ea8a885f --- /dev/null +++ b/actions_test.go @@ -0,0 +1,110 @@ +package main + +import "testing" + +var ( + actionLog = `{"name":"test_action","processed":100000,"failed":2,"suspended":1,"suspended.duration":1000,"resumed":1}` +) + +func TestNewActionFromJSON(t *testing.T) { + logType := getStatType(actionLog) + if logType != rsyslogAction { + t.Errorf("detected pstat type should be %d but is %d", rsyslogAction, logType) + } + + pstat := newActionFromJSON([]byte(actionLog)) + + if want, got := "test_action", pstat.Name; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + if want, got := int64(100000), pstat.Processed; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := int64(2), pstat.Failed; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := int64(1), pstat.Suspended; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := int64(1000), pstat.SuspendedDuration; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := int64(1), pstat.Resumed; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } +} + +func TestActionToPoints(t *testing.T) { + pstat := newActionFromJSON([]byte(actionLog)) + points := pstat.toPoints() + + point := points[0] + if want, got := "test_action_processed", point.Name; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + if want, got := int64(100000), point.Value; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + point = points[1] + if want, got := "test_action_failed", point.Name; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + if want, got := int64(2), point.Value; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + point = points[2] + if want, got := "test_action_suspended", point.Name; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + if want, got := int64(1), point.Value; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + point = points[3] + if want, got := "test_action_suspended_duration", point.Name; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + if want, got := int64(1000), point.Value; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + point = points[4] + if want, got := "test_action_resumed", point.Name; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + if want, got := int64(1), point.Value; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("wanted '%d', got '%d'", want, got) + } +} diff --git a/exporter.go b/exporter.go new file mode 100644 index 0000000000000000000000000000000000000000..977d28bc86937c457e312e484e470fc11e122117 --- /dev/null +++ b/exporter.go @@ -0,0 +1,147 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +type rsyslogType int + +const ( + rsyslogUnknown rsyslogType = iota + rsyslogAction + rsyslogInput + rsyslogQueue + rsyslogResource +) + +type rsyslogExporter struct { + debug bool + started bool + logfile *os.File + scanner *bufio.Scanner + pointStore +} + +func newRsyslogExporter(logPath string) (*rsyslogExporter, error) { + debug := false + if len(logPath) > 0 { + debug = true + } + + e := &rsyslogExporter{ + debug: debug, + scanner: bufio.NewScanner(os.Stdin), + pointStore: pointStore{ + pointMap: make(map[string]*point), + lock: &sync.RWMutex{}, + }, + } + + if e.debug { + var err error + e.logfile, err = os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return e, fmt.Errorf("could not open log file") + } + log.SetOutput(e.logfile) + log.Println("starting") + } + + return e, nil +} + +func (re *rsyslogExporter) handleStatLine(line string) { + pstatType := getStatType(re.scanner.Text()) + + switch pstatType { + case rsyslogAction: + a := newActionFromJSON(re.scanner.Bytes()) + for _, p := range a.toPoints() { + re.add(p) + } + + case rsyslogInput: + i := newInputFromJSON(re.scanner.Bytes()) + for _, p := range i.toPoints() { + re.add(p) + } + + case rsyslogQueue: + q := newQueueFromJSON(re.scanner.Bytes()) + for _, p := range q.toPoints() { + re.add(p) + } + + case rsyslogResource: + r := newResourceFromJSON(re.scanner.Bytes()) + for _, p := range r.toPoints() { + re.add(p) + } + + default: + } +} + +// Describe sends the description of currently known metrics collected +// by this Collector to the provided channel. Note that this implementation +// does not necessarily send the "super-set of all possible descriptors" as +// defined by the Collector interface spec, depending on the timing of when +// it is called. The rsyslog exporter does not know all possible metrics +// it will export until the first full batch of rsyslog impstats messages +// are received via stdin. This is ok for now. +func (re *rsyslogExporter) Describe(ch chan<- *prometheus.Desc) { + ch <- prometheus.NewDesc( + prometheus.BuildFQName("", "rsyslog", "scrapes"), + "times exporter has been scraped", + nil, nil, + ) + + keys := re.keys() + + for _, k := range keys { + p, err := re.get(k) + if err != nil { + ch <- p.promDescription() + } + } +} + +// Collect is called by Prometheus when collecting metrics. +func (re *rsyslogExporter) Collect(ch chan<- prometheus.Metric) { + if re.debug { + log.Print("Collect waiting for lock") + } + + keys := re.keys() + + for _, k := range keys { + p, err := re.get(k) + if err != nil { + continue + } + + metric := prometheus.MustNewConstMetric( + p.promDescription(), + p.promType(), + p.promValue(), + ) + + ch <- metric + } +} + +func (re *rsyslogExporter) run() { + for re.scanner.Scan() { + if strings.Contains(re.scanner.Text(), "EOF") { + os.Exit(0) + } + re.handleStatLine(re.scanner.Text()) + } +} diff --git a/inputs.go b/inputs.go new file mode 100644 index 0000000000000000000000000000000000000000..9db800d634b8623965d1014f7758c295cc2a4263 --- /dev/null +++ b/inputs.go @@ -0,0 +1,32 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type input struct { + Name string `json:"name"` + Submitted int64 `json:"submitted"` +} + +func newInputFromJSON(b []byte) *input { + dec := json.NewDecoder(bytes.NewReader(b)) + var pstat input + dec.Decode(&pstat) + return &pstat +} + +func (i *input) toPoints() []*point { + points := make([]*point, 1) + + points[0] = &point{ + Name: fmt.Sprintf("%s_submitted", i.Name), + Type: counter, + Value: i.Submitted, + Description: "messages submitted", + } + + return points +} diff --git a/inputs_test.go b/inputs_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6d730ad76a3655dcd702749c63ac88078b27fb9e --- /dev/null +++ b/inputs_test.go @@ -0,0 +1,38 @@ +package main + +import "testing" + +var ( + inputLog = `{"name":"test_input", "origin":"imuxsock", "submitted":1000}` +) + +func TestgetInput(t *testing.T) { + logType := getStatType(inputLog) + if logType != rsyslogInput { + t.Errorf("detected pstat type should be %d but is %d", rsyslogInput, logType) + } + + pstat := newInputFromJSON([]byte(inputLog)) + + if want, got := "test_input", pstat.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(1000), pstat.Submitted; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} + +func TestInputtoPoints(t *testing.T) { + pstat := newInputFromJSON([]byte(inputLog)) + points := pstat.toPoints() + + point := points[0] + if want, got := "test_input_submitted", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(1000), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..d5b6409a45b86ebfb40fc1f43c4ac8a18f7c8f56 --- /dev/null +++ b/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "flag" + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + logPath = flag.String("logpath", "", "Log file to write to for debugging purposes") + listenAddress = flag.String("web.listen-address", ":9104", "Address to listen on for web interface and telemetry.") + metricPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") +) + +func main() { + flag.Parse() + exporter, err := newRsyslogExporter(*logPath) + if err != nil { + log.Fatal(err) + } + + go func() { + exporter.run() + }() + + prometheus.MustRegister(exporter) + http.Handle(*metricPath, prometheus.Handler()) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`<html> +<head><title>Rsyslog exporter</title></head> +<body> +<h1>Rsyslog exporter</h1> +<p><a href='` + *metricPath + `'>Metrics</a></p> +</body> +</html> +`)) + }) + + err = http.ListenAndServe(*listenAddress, nil) + if err != nil { + panic(err) + } +} diff --git a/point.go b/point.go new file mode 100644 index 0000000000000000000000000000000000000000..5e6b16e93dd4fde4c72b5d04ecc907ce86d6742c --- /dev/null +++ b/point.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + + "github.com/prometheus/client_golang/prometheus" +) + +type pointType int + +const ( + counter pointType = iota + gauge +) + +type point struct { + Name string + Description string + Type pointType + Value int64 +} + +func (s *point) add(newPoint *point) error { + switch s.Type { + case gauge: + if newPoint.Type != gauge { + return fmt.Errorf("incompatible point type") + } + s.Value = newPoint.Value + case counter: + if newPoint.Type != counter { + return fmt.Errorf("incompatible point type") + } + s.Value = s.Value + newPoint.Value + } + return nil +} + +func (p *point) promDescription() *prometheus.Desc { + return prometheus.NewDesc( + prometheus.BuildFQName("", "rsyslog", p.Name), + p.Description, + nil, nil, + ) +} + +func (p *point) promType() prometheus.ValueType { + if p.Type == counter { + return prometheus.CounterValue + } + return prometheus.GaugeValue +} + +func (p *point) promValue() float64 { + return float64(p.Value) +} diff --git a/point_test.go b/point_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d29eb0cd7beb3bb2f51430b2eea4ea18946bbdd0 --- /dev/null +++ b/point_test.go @@ -0,0 +1,71 @@ +package main + +import "testing" + +func TestaddCounter(t *testing.T) { + s1 := &point{ + Name: "my counter", + Type: counter, + Value: int64(10), + } + + s2 := &point{ + Name: "my counter", + Type: counter, + Value: int64(5), + } + + err := s1.add(s2) + if err != nil { + t.Error(err) + } + + if expect := int64(15); s1.Value != expect { + t.Errorf("expected '%d', got '%d'", expect, s1.Value) + } + + s3 := &point{ + Name: "my gauge", + Type: gauge, + Value: int64(10), + } + + err = s1.add(s3) + if err == nil { + t.Errorf("incompatible point types should raise error") + } +} + +func TestaddGauge(t *testing.T) { + s1 := &point{ + Name: "my gauge", + Type: gauge, + Value: int64(10), + } + + s2 := &point{ + Name: "my gauge", + Type: gauge, + Value: int64(5), + } + + err := s1.add(s2) + if err != nil { + t.Error(err) + } + + if want, got := int64(5), s1.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + s3 := &point{ + Name: "my counter", + Type: counter, + Value: int64(10), + } + + err = s1.add(s3) + if err == nil { + t.Errorf("incompatible point types should raise error") + } +} diff --git a/pointstore.go b/pointstore.go new file mode 100644 index 0000000000000000000000000000000000000000..132bfa0430fb8d0d7d2abc5a0b32b437591e398d --- /dev/null +++ b/pointstore.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "sort" + "sync" +) + +type pointStore struct { + pointMap map[string]*point + lock *sync.RWMutex +} + +func newPointStore() *pointStore { + return &pointStore{ + pointMap: make(map[string]*point), + lock: &sync.RWMutex{}, + } +} + +func (ps *pointStore) keys() []string { + ps.lock.Lock() + keys := make([]string, 0) + for k, _ := range ps.pointMap { + keys = append(keys, k) + } + sort.Strings(keys) + ps.lock.Unlock() + return keys +} + +func (ps *pointStore) add(p *point) error { + var err error + ps.lock.Lock() + if _, ok := ps.pointMap[p.Name]; ok { + err = ps.pointMap[p.Name].add(p) + } else { + ps.pointMap[p.Name] = p + } + ps.lock.Unlock() + return err +} + +func (ps *pointStore) get(name string) (*point, error) { + ps.lock.Lock() + if p, ok := ps.pointMap[name]; ok { + ps.lock.Unlock() + return p, nil + } + return &point{}, fmt.Errorf("point does not exist") +} diff --git a/pointstore_test.go b/pointstore_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b8720bd1f55dc5568fee28e45410207b0558f0ee --- /dev/null +++ b/pointstore_test.go @@ -0,0 +1,87 @@ +package main + +import "testing" + +func TestPointStore(t *testing.T) { + ps := newPointStore() + + s1 := &point{ + Name: "my counter", + Type: counter, + Value: int64(10), + } + + s2 := &point{ + Name: "my counter", + Type: counter, + Value: int64(5), + } + + err := ps.add(s1) + if err != nil { + t.Error(err) + } + + got, err := ps.get(s1.Name) + if err != nil { + t.Error(err) + } + + if want, got := int64(10), got.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + err = ps.add(s2) + if err != nil { + t.Error(err) + } + + got, err = ps.get(s2.Name) + if err != nil { + t.Error(err) + } + + if want, got := int64(15), got.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + s3 := &point{ + Name: "my gauge", + Type: gauge, + Value: int64(20), + } + + err = ps.add(s3) + if err != nil { + t.Error(err) + } + + got, err = ps.get(s3.Name) + if err != nil { + t.Error(err) + } + + if want, got := int64(20), got.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + s4 := &point{ + Name: "my gauge", + Type: gauge, + Value: int64(15), + } + + err = ps.add(s4) + if err != nil { + t.Error(err) + } + + got, err = ps.get(s4.Name) + if err != nil { + t.Error(err) + } + + if want, got := int64(15), got.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} diff --git a/queues.go b/queues.go new file mode 100644 index 0000000000000000000000000000000000000000..f2a013140269f44dd43b49121b0fbfe6a3fc246c --- /dev/null +++ b/queues.go @@ -0,0 +1,75 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +type queue struct { + Name string `json:"name"` + Size int64 `json:"size"` + Enqueued int64 `json:"enqueued"` + Full int64 `json:"full"` + DiscardedFull int64 `json:"discarded.full"` + DiscardedNf int64 `json:"discarded.nf"` + MaxQsize int64 `json:"maxqsize"` +} + +func newQueueFromJSON(b []byte) *queue { + dec := json.NewDecoder(bytes.NewReader(b)) + var pstat queue + dec.Decode(&pstat) + pstat.Name = strings.ToLower(pstat.Name) + pstat.Name = strings.Replace(pstat.Name, " ", "_", -1) + return &pstat +} + +func (q *queue) toPoints() []*point { + points := make([]*point, 6) + + points[0] = &point{ + Name: fmt.Sprintf("%s_size", q.Name), + Type: gauge, + Value: q.Size, + Description: "messages currently in queue", + } + + points[1] = &point{ + Name: fmt.Sprintf("%s_enqueued", q.Name), + Type: counter, + Value: q.Enqueued, + Description: "total messages enqueued", + } + + points[2] = &point{ + Name: fmt.Sprintf("%s_full", q.Name), + Type: counter, + Value: q.Full, + Description: "times queue was full", + } + + points[3] = &point{ + Name: fmt.Sprintf("%s_discarded_full", q.Name), + Type: counter, + Value: q.DiscardedFull, + Description: "messages discarded due to queue being full", + } + + points[4] = &point{ + Name: fmt.Sprintf("%s_discarded_not_full", q.Name), + Type: counter, + Value: q.DiscardedNf, + Description: "messages discarded when queue not full", + } + + points[5] = &point{ + Name: fmt.Sprintf("%s_max_queue_size", q.Name), + Type: gauge, + Value: q.MaxQsize, + Description: "maximum size queue has reached", + } + + return points +} diff --git a/queues_test.go b/queues_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c225e9dfbfea5506c5ae1424cb8ae12db84b59e1 --- /dev/null +++ b/queues_test.go @@ -0,0 +1,126 @@ +package main + +import "testing" + +var ( + queueLog = `{"name":"main Q","size":10,"enqueued":20,"full":30,"discarded.full":40,"discarded.nf":50,"maxqsize":60}` +) + +func TestNewQueueFromJSON(t *testing.T) { + logType := getStatType(queueLog) + if logType != rsyslogQueue { + t.Errorf("detected pstat type should be %d but is %d", rsyslogQueue, logType) + } + + pstat := newQueueFromJSON([]byte(queueLog)) + + if want, got := "main_q", pstat.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(10), pstat.Size; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(20), pstat.Enqueued; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(30), pstat.Full; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(40), pstat.DiscardedFull; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(50), pstat.DiscardedNf; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(60), pstat.MaxQsize; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} + +func TestQueueToPoints(t *testing.T) { + pstat := newQueueFromJSON([]byte(queueLog)) + points := pstat.toPoints() + + point := points[0] + if want, got := "main_q_size", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(10), point.Value; want != got { + } + + if want, got := gauge, point.Type; want != got { + } + + point = points[1] + if want, got := "main_q_enqueued", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(20), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[2] + if want, got := "main_q_full", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(30), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[3] + if want, got := "main_q_discarded_full", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(40), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[4] + if want, got := "main_q_discarded_not_full", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(50), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[5] + if want, got := "main_q_max_queue_size", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(60), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := gauge, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + +} diff --git a/resources.go b/resources.go new file mode 100644 index 0000000000000000000000000000000000000000..6e463f920f223ba101f85e9261a8febc59bd1ba2 --- /dev/null +++ b/resources.go @@ -0,0 +1,96 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type resource struct { + Name string `json:"name"` + Utime int64 `json:"utime"` + Stime int64 `json:"stime"` + Maxrss int64 `json:"maxrss"` + Minflt int64 `json:"minflt"` + Majflt int64 `json:"majflt"` + Inblock int64 `json:"inblock"` + Outblock int64 `json:"oublock"` + Nvcsw int64 `json:"nvcsw"` + Nivcsw int64 `json:"nivcsw"` +} + +func newResourceFromJSON(b []byte) *resource { + dec := json.NewDecoder(bytes.NewReader(b)) + var pstat resource + dec.Decode(&pstat) + return &pstat +} + +func (r *resource) toPoints() []*point { + points := make([]*point, 9) + + points[0] = &point{ + Name: fmt.Sprintf("%s_utime", r.Name), + Type: counter, + Value: r.Utime, + Description: "user time used in microseconds", + } + + points[1] = &point{ + Name: fmt.Sprintf("%s_stime", r.Name), + Type: counter, + Value: r.Stime, + Description: "system time used in microsends", + } + + points[2] = &point{ + Name: fmt.Sprintf("%s_maxrss", r.Name), + Type: gauge, + Value: r.Maxrss, + Description: "maximum resident set size", + } + + points[3] = &point{ + Name: fmt.Sprintf("%s_minflt", r.Name), + Type: counter, + Value: r.Minflt, + Description: "total minor faults", + } + + points[4] = &point{ + Name: fmt.Sprintf("%s_majflt", r.Name), + Type: counter, + Value: r.Majflt, + Description: "total major faults", + } + + points[5] = &point{ + Name: fmt.Sprintf("%s_inblock", r.Name), + Type: counter, + Value: r.Inblock, + Description: "filesystem input operations", + } + + points[6] = &point{ + Name: fmt.Sprintf("%s_oublock", r.Name), + Type: counter, + Value: r.Outblock, + Description: "filesystem output operations", + } + + points[7] = &point{ + Name: fmt.Sprintf("%s_nvcsw", r.Name), + Type: counter, + Value: r.Nvcsw, + Description: "voluntary context switches", + } + + points[8] = &point{ + Name: fmt.Sprintf("%s_nivcsw", r.Name), + Type: counter, + Value: r.Nivcsw, + Description: "involuntary context switches", + } + + return points +} diff --git a/resources_test.go b/resources_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bce1c6ca8a9fcea1f5f1ebcca833bc50f254b82f --- /dev/null +++ b/resources_test.go @@ -0,0 +1,178 @@ +package main + +import "testing" + +var ( + resourceLog = `{"name":"resource-usage","utime":10,"stime":20,"maxrss":30,"minflt":40,"majflt":50,"inblock":60,"oublock":70,"nvcsw":80,"nivcsw":90}` +) + +func TestNewResourceFromJSON(t *testing.T) { + logType := getStatType(resourceLog) + if logType != rsyslogResource { + t.Errorf("detected pstat type should be %d but is %d", rsyslogResource, logType) + } + + pstat := newResourceFromJSON([]byte(resourceLog)) + + if want, got := "resource-usage", pstat.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(10), pstat.Utime; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(20), pstat.Stime; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(30), pstat.Maxrss; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(40), pstat.Minflt; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(50), pstat.Majflt; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(60), pstat.Inblock; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(70), pstat.Outblock; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(80), pstat.Nvcsw; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(90), pstat.Nivcsw; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} + +func TestResourceToPoints(t *testing.T) { + pstat := newResourceFromJSON([]byte(resourceLog)) + points := pstat.toPoints() + + point := points[0] + if want, got := "resource-usage_utime", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(10), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[1] + if want, got := "resource-usage_stime", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(20), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[2] + if want, got := "resource-usage_maxrss", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(30), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := gauge, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[3] + if want, got := "resource-usage_minflt", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(40), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[4] + if want, got := "resource-usage_majflt", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(50), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[5] + if want, got := "resource-usage_inblock", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(60), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[6] + if want, got := "resource-usage_oublock", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(70), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[7] + if want, got := "resource-usage_nvcsw", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(80), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + point = points[8] + if want, got := "resource-usage_nivcsw", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(90), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := counter, point.Type; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..fccc1e4ec2749e442eab668510c1ade61a4a4f2d --- /dev/null +++ b/utils.go @@ -0,0 +1,16 @@ +package main + +import "strings" + +func getStatType(line string) rsyslogType { + if strings.Contains(line, "processed") { + return rsyslogAction + } else if strings.Contains(line, "submitted") { + return rsyslogInput + } else if strings.Contains(line, "enqueued") { + return rsyslogQueue + } else if strings.Contains(line, "utime") { + return rsyslogResource + } + return rsyslogUnknown +}