diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000000000000000000000000000000000..63c6cfc8a5da15498d63815b721ee09beca0eab6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: go get -v -t -d ./... + + - name: Build + run: go build -v . + + - name: Test + run: go test -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..2219910ae91aacb31f8f9fb9e851495e1f5731f5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: release + +on: + release: + types: + - created + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Unshallow + run: git fetch --prune --unshallow + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: 1.13.x + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v1 + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 9e60eef838ee79cd6ce03d70ae7e461d1ee9864a..7f0e8dd45f2ae60c5dea4970343fdc20262daf01 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ _testmain.go *.test *.prof rsyslog_exporter + +dist diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000000000000000000000000000000000000..d87e2a279ab179eb723486f89d71c6f1bf79862f --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,46 @@ +env: + - GO111MODULE=on + - GOPROXY=https://gocenter.io +before: + hooks: + - go mod download +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm + - arm64 +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - Merge pull request + - Merge branch +archives: + - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + replacements: + linux: Linux + amd64: x86_64 +nfpms: + - package_name: "rsyslog-exporter" + homepage: https://github.com/aleroyer/rsyslog_exporter + description: rsyslog-exporter for prometheus + maintainer: Antoine Leroyer <aleroyer@deezer.com> + license: Apache 2.0 + bindir: /usr/bin + release: 1 + formats: + - deb + - rpm + overrides: + deb: + file_name_template: '{{ replace .ProjectName "_" "-" }}_{{ .Version }}-{{ .Release }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + rpm: + file_name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Release }}.{{ .Arch }}" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c095a514d787a3059cb334f3219fda1bd1d22bd4..0000000000000000000000000000000000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: go - -go: - - 1.4 - -install: - - go get github.com/digitalocean/rsyslog_exporter - -script: - - make test diff --git a/README.md b/README.md index 7a7d5863c040742be5c0f28eb5d73f88fd388ace..73357f3cd75ca3d993fa5616a444157aeb2e40c2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ A [prometheus](http://prometheus.io/) exporter for [rsyslog](http://rsyslog.com) ## Rsyslog Configuration Configure rsyslog to push JSON formatted stats via omprog: ``` +module(load="omprog") + module( load="impstats" interval="10" @@ -17,15 +19,27 @@ ruleset(name="process_stats") { action( type="omprog" name="to_exporter" - binary="/usr/local/bin/rsyslog_exporter" + binary="/usr/local/bin/rsyslog_exporter [--tls.server-crt=/path/to/tls.crt --tls.server-key=/path/to/tls.key]" ) } ``` The exporter itself logs back via syslog, this cannot be configured at the moment. +## Command Line Switches +* `web.listen-address` - default `:9104` - port to listen to (NOTE: the leading + `:` is required for `http.ListenAndServe`) +* `web.telemetry-path` - default `/metrics` - path from which to serve Prometheus metrics +* `tls.server-crt` - default `""` - PEM encoded file containing the server certificate and + the CA certificate for use with `http.ListenAndServeTLS` +* `tls.server-key` - default `""` - PEM encoded file containing the unencrypted + server key for use with `tls.server-crt` + +If you want the exporter to listen for TLS (`https`) you must specify both +`tls.server-crt` and `tls.server-key`. + ## Provided Metrics -The following metrics provided by the rsyslog impstats module are tracked by rsyslog_exporter: +The following metrics provided by the rsyslog [impstats](https://www.rsyslog.com/doc/master/configuration/modules/impstats.html) module are tracked by rsyslog_exporter: ### Actions Action objects describe what is to be done with a message, and are implemented via output modules. @@ -68,4 +82,33 @@ Rsyslog tracks how it uses system resources and provides the following metrics: * nvcsw - number of voluntary context switches * nivcsw - number of involuntary context switches +### Dynafile Cache +The [omfile](https://www.rsyslog.com/rsyslog-statistic-counter-plugin-omfile/) module can generate +file names from a template. A cache of recent filehandles can be maintained, whose sizing can +impact performance considerably. The module provides the following metrics: + +* requests - number of requests made to obtain a dynafile +* level0 - number of requests for the current active file +* missed - number of cache misses +* evicted - number of times a file needed to be evicted from cache +* maxused - maximum number of cache entries ever used +* closetimeouts - number of times a file was closed due to timeout settings + +### Dynamic Stats +Rsyslog allows the user to define their own stats namespaces and increment counters within these +buckets using Rainerscript function calls. + +These are exported as counters with the metric name identifying the bucket, and a label value +matching the name of the counter (the label name will always be "counter"). As well as custom +metrics, a "global" dynstats namespace is also published with some additional bookeeping counters. + +See the [dyn_stats](https://www.rsyslog.com/doc/master/configuration/dyn_stats.html) +documentation for more information. + +### IMUDP Workerthread stats +The [imudp](https://www.rsyslog.com/rsyslog-statistic-counter-plugin-imudp/) module can be configured +to run on multiple worker threads and the following metrics are returned: +* input_called_recvmmsg - Number of recvmmsg called +* input_called_recvmsg -Number of recvmmsg called +* input_received - Messages received diff --git a/dyn_stat_test.go b/dyn_stat_test.go index 8d7ff843aeab873032c718105b8a4725a2bfbebf..d96bea5255da2b907196a9297e15b55a3eb24ca4 100644 --- a/dyn_stat_test.go +++ b/dyn_stat_test.go @@ -79,7 +79,7 @@ func TestDynStatToPoints(t *testing.T) { } seen := map[string]bool{} - for name, _ := range wants { + for name := range wants { seen[name] = false } diff --git a/dynafile_cache.go b/dynafile_cache.go new file mode 100644 index 0000000000000000000000000000000000000000..f0e1f25837b697c1c899845e81ca56299221a6df --- /dev/null +++ b/dynafile_cache.go @@ -0,0 +1,83 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" +) + +type dfcStat struct { + Name string `json:"name"` + Origin string `json:"origin"` + Requests int64 `json:"requests"` + Level0 int64 `json:"level0"` + Missed int64 `json:"missed"` + Evicted int64 `json:"evicted"` + MaxUsed int64 `json:"maxused"` + CloseTimeouts int64 `json:"closetimeouts"` +} + +func newDynafileCacheFromJSON(b []byte) (*dfcStat, error) { + var pstat dfcStat + err := json.Unmarshal(b, &pstat) + if err != nil { + return nil, fmt.Errorf("error decoding dynafile cache stat `%v`: %v", string(b), err) + } + pstat.Name = strings.TrimPrefix(pstat.Name, "dynafile cache ") + return &pstat, nil +} + +func (d *dfcStat) toPoints() []*point { + points := make([]*point, 6) + + points[0] = &point{ + Name: "dynafile_cache_requests", + Type: counter, + Value: d.Requests, + Description: "number of requests made to obtain a dynafile", + LabelName: "cache", + LabelValue: d.Name, + } + points[1] = &point{ + Name: "dynafile_cache_level0", + Type: counter, + Value: d.Level0, + Description: "number of requests for the current active file", + LabelName: "cache", + LabelValue: d.Name, + } + points[2] = &point{ + Name: "dynafile_cache_missed", + Type: counter, + Value: d.Missed, + Description: "number of cache misses", + LabelName: "cache", + LabelValue: d.Name, + } + points[3] = &point{ + Name: "dynafile_cache_evicted", + Type: counter, + Value: d.Evicted, + Description: "number of times a file needed to be evicted from cache", + LabelName: "cache", + LabelValue: d.Name, + } + points[4] = &point{ + Name: "dynafile_cache_maxused", + Type: counter, + Value: d.MaxUsed, + Description: "maximum number of cache entries ever used", + LabelName: "cache", + LabelValue: d.Name, + } + points[5] = &point{ + Name: "dynafile_cache_closetimeouts", + Type: counter, + Value: d.CloseTimeouts, + Description: "number of times a file was closed due to timeout settings", + LabelName: "cache", + LabelValue: d.Name, + } + + return points +} diff --git a/dynafile_cache_test.go b/dynafile_cache_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a7cd762ac90e53a27e0f60176742a0f075856a6f --- /dev/null +++ b/dynafile_cache_test.go @@ -0,0 +1,139 @@ +package main + +import ( + "reflect" + "testing" +) + +var ( + dynafileCacheLog = []byte(`{ "name": "dynafile cache cluster", "origin": "omfile", "requests": 1783254, "level0": 1470906, "missed": 2625, "evicted": 2525, "maxused": 100, "closetimeouts": 10 }`) +) + +func TestNewDynafileCacheFromJSON(t *testing.T) { + logType := getStatType(dynafileCacheLog) + if logType != rsyslogDynafileCache { + t.Errorf("detected pstat type should be %d but is %d", rsyslogDynafileCache, logType) + } + + pstat, err := newDynafileCacheFromJSON([]byte(dynafileCacheLog)) + if err != nil { + t.Fatalf("expected parsing dynafile cache stat not to fail, got: %v", err) + } + + if want, got := "cluster", pstat.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(1783254), pstat.Requests; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(1470906), pstat.Level0; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(2625), pstat.Missed; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(2525), pstat.Evicted; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(100), pstat.MaxUsed; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(10), pstat.CloseTimeouts; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} + +func TestDynafileCacheToPoints(t *testing.T) { + + wants := map[string]point{ + "dynafile_cache_requests": point{ + Name: "dynafile_cache_requests", + Type: counter, + Value: 1783254, + Description: "number of requests made to obtain a dynafile", + LabelName: "cache", + LabelValue: "cluster", + }, + "dynafile_cache_level0": point{ + Name: "dynafile_cache_level0", + Type: counter, + Value: 1470906, + Description: "number of requests for the current active file", + LabelName: "cache", + + LabelValue: "cluster", + }, + "dynafile_cache_missed": point{ + Name: "dynafile_cache_missed", + Type: counter, + Value: 2625, + Description: "number of cache misses", + LabelName: "cache", + LabelValue: "cluster", + }, + "dynafile_cache_evicted": point{ + Name: "dynafile_cache_evicted", + Type: counter, + Value: 2525, + Description: "number of times a file needed to be evicted from cache", + LabelName: "cache", + LabelValue: "cluster", + }, + "dynafile_cache_maxused": point{ + Name: "dynafile_cache_maxused", + Type: counter, + Value: 100, + Description: "maximum number of cache entries ever used", + LabelName: "cache", + LabelValue: "cluster", + }, + "dynafile_cache_closetimeouts": point{ + Name: "dynafile_cache_closetimeouts", + Type: counter, + Value: 10, + Description: "number of times a file was closed due to timeout settings", + LabelName: "cache", + LabelValue: "cluster", + }, + } + + seen := map[string]bool{} + for name := range wants { + seen[name] = false + } + + pstat, err := newDynafileCacheFromJSON(dynafileCacheLog) + if err != nil { + t.Fatalf("expected parsing dynafile cache stat not to fail, got: %v", err) + } + + points := pstat.toPoints() + for _, got := range points { + want, ok := wants[got.Name] + 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[got.Name] { + t.Errorf("point seen multiple times: %+v", got) + } + seen[got.Name] = 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 746c8c65580f4b1f4e0ad06ea673503b8a49e513..5ee725c340abf07ed548ddd485b66d81e74d6be9 100644 --- a/exporter.go +++ b/exporter.go @@ -20,6 +20,8 @@ const ( rsyslogQueue rsyslogResource rsyslogDynStat + rsyslogDynafileCache + rsyslogInputIMDUP ) type rsyslogExporter struct { @@ -68,6 +70,15 @@ func (re *rsyslogExporter) handleStatLine(rawbuf []byte) error { re.set(p) } + case rsyslogInputIMDUP: + u, err := newInputIMUDPFromJSON(buf) + if err != nil { + return err + } + for _, p := range u.toPoints() { + re.set(p) + } + case rsyslogQueue: q, err := newQueueFromJSON(buf) if err != nil { @@ -93,6 +104,14 @@ func (re *rsyslogExporter) handleStatLine(rawbuf []byte) error { for _, p := range s.toPoints() { re.set(p) } + case rsyslogDynafileCache: + d, err := newDynafileCacheFromJSON(buf) + if err != nil { + return err + } + for _, p := range d.toPoints() { + re.set(p) + } default: return fmt.Errorf("unknown pstat type: %v", pstatType) diff --git a/exporter_test.go b/exporter_test.go index df2326e6102bbf541aa8c9afc389793df6b4d7ab..0afded362deec3e6b39c1eaa0fc882efed47a0e6 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -230,6 +230,34 @@ func TestHandleLineWithGlobal(t *testing.T) { testHelper(t, log, tests) } +func TestHandleLineWithDynafileCache(t *testing.T) { + tests := []*testUnit{ + &testUnit{ + Name: "dynafile_cache_requests", + Val: 412044, + LabelValue: "cluster", + }, + &testUnit{ + Name: "dynafile_cache_level0", + Val: 294002, + LabelValue: "cluster", + }, + &testUnit{ + Name: "dynafile_cache_missed", + Val: 210, + LabelValue: "cluster", + }, + &testUnit{ + Name: "dynafile_cache_evicted", + Val: 14, + LabelValue: "cluster", + }, + } + + dynafileCacheLog := []byte(`2019-07-03T17:04:01.312432+00:00 some-node.example.org rsyslogd-pstats: { "name": "dynafile cache cluster", "origin": "omfile", "requests": 412044, "level0": 294002, "missed": 210, "evicted": 14, "maxused": 100, "closetimeouts": 0 }`) + testHelper(t, dynafileCacheLog, 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/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..60075859a0a7ff0b589de13d205102004c349846 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/aleroyer/rsyslog_exporter + +go 1.13 + +require github.com/prometheus/client_golang v1.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..b8fb9d079d3f8a2b5aae1899d86e92d111983283 --- /dev/null +++ b/go.sum @@ -0,0 +1,67 @@ +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/input_imudp.go b/input_imudp.go new file mode 100644 index 0000000000000000000000000000000000000000..13b5ec0a3eeb3c33aa941ef80822764b33ce6e78 --- /dev/null +++ b/input_imudp.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +type inputIMUDP struct { + Name string `json:"name"` + Recvmmsg int64 `json:"called.recvmmsg"` + Recvmsg int64 `json:"called.recvmsg"` + Received int64 `json:"msgs.received"` +} + +func newInputIMUDPFromJSON(b []byte) (*inputIMUDP, error) { + var pstat inputIMUDP + err := json.Unmarshal(b, &pstat) + if err != nil { + return nil, fmt.Errorf("error decoding input stat `%v`: %v", string(b), err) + } + return &pstat, nil +} + +func (i *inputIMUDP) toPoints() []*point { + points := make([]*point, 3) + + points[0] = &point{ + Name: "input_called_recvmmsg", + Type: counter, + Value: i.Recvmmsg, + Description: "Number of recvmmsg called", + LabelName: "worker", + LabelValue: i.Name, + } + points[1] = &point{ + Name: "input_called_recvmsg", + Type: counter, + Value: i.Recvmsg, + Description: "Number of recvmmsg called", + LabelName: "worker", + LabelValue: i.Name, + } + + points[2] = &point{ + Name: "input_received", + Type: counter, + Value: i.Received, + Description: "messages received", + LabelName: "worker", + LabelValue: i.Name, + } + + return points +} diff --git a/input_imudp_test.go b/input_imudp_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8c98b6301a60cb459ca8142ac83d2d58139afd06 --- /dev/null +++ b/input_imudp_test.go @@ -0,0 +1,83 @@ +package main + +import "testing" + +var ( + inputIMUDPLog = []byte(`{ "name": "test_input_imudp", "origin": "imudp", "called.recvmmsg":1000, "called.recvmsg":2000, "msgs.received":500}`) +) + +func TestgetInputIMUDP(t *testing.T) { + logType := getStatType(inputIMUDPLog) + if logType != rsyslogInputIMDUP { + t.Errorf("detected pstat type should be %d but is %d", rsyslogInputIMDUP, logType) + } + + pstat, err := newInputIMUDPFromJSON([]byte(inputLog)) + if err != nil { + t.Fatalf("expected parsing input stat not to fail, got: %v", err) + } + + if want, got := "test_input_imudp", pstat.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(1000), pstat.Recvmsg; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(2000), pstat.Recvmmsg; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := int64(500), pstat.Received; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } +} + +func TestInputIMUDPtoPoints(t *testing.T) { + pstat, err := newInputIMUDPFromJSON([]byte(inputIMUDPLog)) + if err != nil { + t.Fatalf("expected parsing input stat not to fail, got: %v", err) + } + + points := pstat.toPoints() + + point := points[0] + if want, got := "input_called_recvmmsg", 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) + } + + if want, got := "test_input_imudp", point.LabelValue; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + point = points[1] + if want, got := "input_called_recvmsg", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(2000), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := "test_input_imudp", point.LabelValue; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } + + point = points[2] + if want, got := "input_received", point.Name; want != got { + t.Errorf("want '%s', got '%s'", want, got) + } + + if want, got := int64(500), point.Value; want != got { + t.Errorf("want '%d', got '%d'", want, got) + } + + if want, got := "test_input_imudp", point.LabelValue; want != got { + t.Errorf("wanted '%s', got '%s'", want, got) + } +} diff --git a/main.go b/main.go index 029f26aec1dbeafb1c7e3dd07ff040a4f485d72d..90ef19f31fc95a93f21a1ba16fcd5ab4a03e9598 100644 --- a/main.go +++ b/main.go @@ -9,11 +9,14 @@ import ( "os/signal" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( 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.") + certPath = flag.String("tls.server-crt", "", "Path to PEM encoded file containing TLS server cert.") + keyPath = flag.String("tls.server-key", "", "Path to PEM encoded file containing TLS server key (unencyrpted).") ) func main() { @@ -38,7 +41,7 @@ func main() { }() prometheus.MustRegister(exporter) - http.Handle(*metricPath, prometheus.Handler()) + http.Handle(*metricPath, promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`<html> <head><title>Rsyslog exporter</title></head> @@ -50,6 +53,13 @@ func main() { `)) }) - log.Printf("Listening on %s", *listenAddress) - log.Fatal(http.ListenAndServe(*listenAddress, nil)) + if *certPath == "" && *keyPath == "" { + log.Printf("Listening on %s", *listenAddress) + log.Fatal(http.ListenAndServe(*listenAddress, nil)) + } else if *certPath == "" || *keyPath == "" { + log.Fatal("Both tls.server-crt and tls.server-key must be specified") + } else { + log.Printf("Listening for TLS on %s", *listenAddress) + log.Fatal(http.ListenAndServeTLS(*listenAddress, *certPath, *keyPath, nil)) + } } diff --git a/point.go b/point.go index 499cfc2788014f1b85fcf4d166fbcc56e41c7f89..1f441182587186f24017591c90c506bc963fb67b 100644 --- a/point.go +++ b/point.go @@ -1,7 +1,6 @@ package main import ( - "errors" "fmt" "github.com/prometheus/client_golang/prometheus" @@ -14,11 +13,6 @@ const ( gauge ) -var ( - ErrIncompatiblePointType = errors.New("incompatible point type") - ErrUnknownPointType = errors.New("unknown point type") -) - type point struct { Name string Description string diff --git a/pointstore.go b/pointstore.go index 3da93dfeb75e5cff6318706c4b10a78677ce57c1..632a46bb10945c17cb49c3019c12307f1161c523 100644 --- a/pointstore.go +++ b/pointstore.go @@ -7,7 +7,7 @@ import ( ) var ( - ErrPointNotFound = errors.New("point does not exist") + errPointNotFound = errors.New("point does not exist") ) type pointStore struct { @@ -25,7 +25,7 @@ func newPointStore() *pointStore { func (ps *pointStore) keys() []string { ps.lock.Lock() keys := make([]string, 0) - for k, _ := range ps.pointMap { + for k := range ps.pointMap { keys = append(keys, k) } sort.Strings(keys) @@ -48,5 +48,5 @@ func (ps *pointStore) get(name string) (*point, error) { return p, nil } ps.lock.Unlock() - return &point{}, ErrPointNotFound + return &point{}, errPointNotFound } diff --git a/pointstore_test.go b/pointstore_test.go index ad7afcbff5807eebc69bbeda6d8dbde3cbafba3e..39f09190f56c5d6e77421ca38face5fe7a1ed55f 100644 --- a/pointstore_test.go +++ b/pointstore_test.go @@ -86,7 +86,7 @@ func TestPointStore(t *testing.T) { } _, err = ps.get("no point") - if err != ErrPointNotFound { + if err != errPointNotFound { t.Error("getting non existent point should raise error") } } diff --git a/utils.go b/utils.go index 5130060c62c30bb4a93942959a7bf15bc1792270..1cd38e8fca5ed31a470412ce896505c99d4dbf70 100644 --- a/utils.go +++ b/utils.go @@ -8,12 +8,16 @@ func getStatType(buf []byte) rsyslogType { return rsyslogAction } else if strings.Contains(line, "submitted") { return rsyslogInput + } else if strings.Contains(line, "called.recvmmsg") { + return rsyslogInputIMDUP } else if strings.Contains(line, "enqueued") { return rsyslogQueue } else if strings.Contains(line, "utime") { return rsyslogResource } else if strings.Contains(line, "dynstats") { return rsyslogDynStat + } else if strings.Contains(line, "dynafile cache") { + return rsyslogDynafileCache } return rsyslogUnknown }