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 [![Build Status](https://travis-ci.org/digitalocean/rsyslog_exporter.svg?branch=master)](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
+}