From af03473bdbc91eaabe6e695672c5110edcdb5220 Mon Sep 17 00:00:00 2001 From: Jaime Soriano Pastor Date: Fri, 16 Mar 2018 09:43:21 -0700 Subject: [PATCH] Metricbeat: Add munin module (#6473) Add munin module, with node metricset that obtain metrics from a running munin node using the same protocol used by munin masters. --- CHANGELOG.asciidoc | 1 + metricbeat/docker-compose.yml | 4 + metricbeat/docs/fields.asciidoc | 16 +++ metricbeat/docs/modules/munin.asciidoc | 41 +++++++ metricbeat/docs/modules/munin/node.asciidoc | 17 +++ metricbeat/docs/modules_list.asciidoc | 3 + metricbeat/include/list.go | 2 + metricbeat/metricbeat.reference.yml | 8 ++ metricbeat/module/munin/_meta/Dockerfile | 13 ++ metricbeat/module/munin/_meta/config.yml | 6 + metricbeat/module/munin/_meta/docs.asciidoc | 4 + metricbeat/module/munin/_meta/env | 2 + metricbeat/module/munin/_meta/fields.yml | 13 ++ metricbeat/module/munin/_meta/munin-node.conf | 15 +++ metricbeat/module/munin/doc.go | 2 + metricbeat/module/munin/munin.go | 115 ++++++++++++++++++ metricbeat/module/munin/munin_test.go | 82 +++++++++++++ .../module/munin/node/_meta/docs.asciidoc | 58 +++++++++ metricbeat/module/munin/node/_meta/fields.yml | 1 + metricbeat/module/munin/node/node.go | 75 ++++++++++++ metricbeat/modules.d/munin.yml.disabled | 6 + metricbeat/tests/system/test_munin.py | 38 ++++++ 22 files changed, 522 insertions(+) create mode 100644 metricbeat/docs/modules/munin.asciidoc create mode 100644 metricbeat/docs/modules/munin/node.asciidoc create mode 100644 metricbeat/module/munin/_meta/Dockerfile create mode 100644 metricbeat/module/munin/_meta/config.yml create mode 100644 metricbeat/module/munin/_meta/docs.asciidoc create mode 100644 metricbeat/module/munin/_meta/env create mode 100644 metricbeat/module/munin/_meta/fields.yml create mode 100644 metricbeat/module/munin/_meta/munin-node.conf create mode 100644 metricbeat/module/munin/doc.go create mode 100644 metricbeat/module/munin/munin.go create mode 100644 metricbeat/module/munin/munin_test.go create mode 100644 metricbeat/module/munin/node/_meta/docs.asciidoc create mode 100644 metricbeat/module/munin/node/_meta/fields.yml create mode 100644 metricbeat/module/munin/node/node.go create mode 100644 metricbeat/modules.d/munin.yml.disabled create mode 100644 metricbeat/tests/system/test_munin.py diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 949141335bfe..cfedcdfc4e38 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -251,6 +251,7 @@ https://github.com/elastic/beats/compare/v6.0.0-beta2...master[Check the HEAD di - Making the jolokia/jmx module GA. {pull}6143[6143] - Making the MongoDB module GA. {pull}6554[6554] - Allow to disable labels `dedot` in Docker module, in favor of a safe way to keep dots. {pull}6490[6490] +- Add experimental module to collect metrics from munin nodes. {pull}6517[6517] *Packetbeat* diff --git a/metricbeat/docker-compose.yml b/metricbeat/docker-compose.yml index 04d92909f5f2..79b46569746f 100644 --- a/metricbeat/docker-compose.yml +++ b/metricbeat/docker-compose.yml @@ -27,6 +27,7 @@ services: - ./module/logstash/_meta/env - ./module/memcached/_meta/env - ./module/mongodb/_meta/env + - ./module/munin/_meta/env - ./module/mysql/_meta/env - ./module/nginx/_meta/env - ./module/php_fpm/_meta/env @@ -107,6 +108,9 @@ services: mongodb: build: ./module/mongodb/_meta + munin: + build: ./module/munin/_meta + mysql: build: ./module/mysql/_meta diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 87360d0316eb..0fc569f3565a 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -37,6 +37,7 @@ grouped in the following categories: * <> * <> * <> +* <> * <> * <> * <> @@ -7656,6 +7657,21 @@ type: long Number of sync operations. +[[exported-fields-munin]] +== Munin fields + +experimental[] +Munin node metrics exporter + + + +[float] +== munin fields + +munin contains metrics exposed by a munin node agent + + + [[exported-fields-mysql]] == MySQL fields diff --git a/metricbeat/docs/modules/munin.asciidoc b/metricbeat/docs/modules/munin.asciidoc new file mode 100644 index 000000000000..854664fa35c8 --- /dev/null +++ b/metricbeat/docs/modules/munin.asciidoc @@ -0,0 +1,41 @@ +//// +This file is generated! See scripts/docs_collector.py +//// + +[[metricbeat-module-munin]] +== Munin module + +experimental[] + +== munin module + +This is the munin module. + + + +[float] +=== Example configuration + +The Munin module supports the standard configuration options that are described +in <>. Here is an example configuration: + +[source,yaml] +---- +metricbeat.modules: +- module: munin + metricsets: ["node"] + enabled: false + period: 10s + hosts: ["localhost:4949"] + node.namespace: node +---- + +[float] +=== Metricsets + +The following metricsets are available: + +* <> + +include::munin/node.asciidoc[] + diff --git a/metricbeat/docs/modules/munin/node.asciidoc b/metricbeat/docs/modules/munin/node.asciidoc new file mode 100644 index 000000000000..9e40635bd3f3 --- /dev/null +++ b/metricbeat/docs/modules/munin/node.asciidoc @@ -0,0 +1,17 @@ +//// +This file is generated! See scripts/docs_collector.py +//// + +[[metricbeat-metricset-munin-node]] +=== Munin node metricset + +experimental[] + +include::../../../module/munin/node/_meta/docs.asciidoc[] + + +==== Fields + +For a description of each field in the metricset, see the +<> section. + diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index a5685a90ba8f..02f3ed45fb1d 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -79,6 +79,8 @@ This file is generated! See scripts/docs_collector.py .3+| |<> |<> |<> +|<> experimental[] | +.1+| |<> experimental[] |<> | .1+| |<> |<> | @@ -147,6 +149,7 @@ include::modules/kubernetes.asciidoc[] include::modules/logstash.asciidoc[] include::modules/memcached.asciidoc[] include::modules/mongodb.asciidoc[] +include::modules/munin.asciidoc[] include::modules/mysql.asciidoc[] include::modules/nginx.asciidoc[] include::modules/php_fpm.asciidoc[] diff --git a/metricbeat/include/list.go b/metricbeat/include/list.go index f2163860da20..6c84426c7aad 100644 --- a/metricbeat/include/list.go +++ b/metricbeat/include/list.go @@ -83,6 +83,8 @@ import ( _ "github.com/elastic/beats/metricbeat/module/mongodb/collstats" _ "github.com/elastic/beats/metricbeat/module/mongodb/dbstats" _ "github.com/elastic/beats/metricbeat/module/mongodb/status" + _ "github.com/elastic/beats/metricbeat/module/munin" + _ "github.com/elastic/beats/metricbeat/module/munin/node" _ "github.com/elastic/beats/metricbeat/module/mysql" _ "github.com/elastic/beats/metricbeat/module/mysql/status" _ "github.com/elastic/beats/metricbeat/module/nginx" diff --git a/metricbeat/metricbeat.reference.yml b/metricbeat/metricbeat.reference.yml index f035e3d636e6..e2fea98ae6e6 100644 --- a/metricbeat/metricbeat.reference.yml +++ b/metricbeat/metricbeat.reference.yml @@ -349,6 +349,14 @@ metricbeat.modules: # Password to use when connecting to MongoDB. Empty by default. #password: pass +#-------------------------------- Munin Module ------------------------------- +- module: munin + metricsets: ["node"] + enabled: false + period: 10s + hosts: ["localhost:4949"] + node.namespace: node + #-------------------------------- MySQL Module ------------------------------- - module: mysql metricsets: ["status"] diff --git a/metricbeat/module/munin/_meta/Dockerfile b/metricbeat/module/munin/_meta/Dockerfile new file mode 100644 index 000000000000..bae8d6112a61 --- /dev/null +++ b/metricbeat/module/munin/_meta/Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:16.04 + +RUN apt-get update && \ + apt-get install -y munin-node netcat && \ + apt-get clean && rm rm -rf /var/lib/apt/lists/* + +EXPOSE 4949 + +COPY munin-node.conf /etc/munin/munin-node.conf + +HEALTHCHECK --interval=1s --retries=90 CMD nc -z 127.0.0.1 4949 + +CMD munin-node diff --git a/metricbeat/module/munin/_meta/config.yml b/metricbeat/module/munin/_meta/config.yml new file mode 100644 index 000000000000..b752d1e2fdc7 --- /dev/null +++ b/metricbeat/module/munin/_meta/config.yml @@ -0,0 +1,6 @@ +- module: munin + metricsets: ["node"] + enabled: false + period: 10s + hosts: ["localhost:4949"] + node.namespace: node diff --git a/metricbeat/module/munin/_meta/docs.asciidoc b/metricbeat/module/munin/_meta/docs.asciidoc new file mode 100644 index 000000000000..deb2472a528e --- /dev/null +++ b/metricbeat/module/munin/_meta/docs.asciidoc @@ -0,0 +1,4 @@ +== munin module + +This is the munin module. + diff --git a/metricbeat/module/munin/_meta/env b/metricbeat/module/munin/_meta/env new file mode 100644 index 000000000000..b81c5ee86790 --- /dev/null +++ b/metricbeat/module/munin/_meta/env @@ -0,0 +1,2 @@ +MUNIN_HOST=munin +MUNIN_PORT=4949 diff --git a/metricbeat/module/munin/_meta/fields.yml b/metricbeat/module/munin/_meta/fields.yml new file mode 100644 index 000000000000..35912fe1a718 --- /dev/null +++ b/metricbeat/module/munin/_meta/fields.yml @@ -0,0 +1,13 @@ +- key: munin + title: "Munin" + description: > + experimental[] + + Munin node metrics exporter + release: experimental + fields: + - name: munin + type: group + description: > + munin contains metrics exposed by a munin node agent + fields: diff --git a/metricbeat/module/munin/_meta/munin-node.conf b/metricbeat/module/munin/_meta/munin-node.conf new file mode 100644 index 000000000000..17c6c319a464 --- /dev/null +++ b/metricbeat/module/munin/_meta/munin-node.conf @@ -0,0 +1,15 @@ +setsid 0 + +ignore_file [\#~]$ +ignore_file DEADJOE$ +ignore_file \.bak$ +ignore_file %$ +ignore_file \.dpkg-(tmp|new|old|dist)$ +ignore_file \.rpm(save|new)$ +ignore_file \.pod$ + +allow .* + +host 0.0.0.0 + +port 4949 diff --git a/metricbeat/module/munin/doc.go b/metricbeat/module/munin/doc.go new file mode 100644 index 000000000000..4b99318663f2 --- /dev/null +++ b/metricbeat/module/munin/doc.go @@ -0,0 +1,2 @@ +// Package munin is a Metricbeat module that contains MetricSets. +package munin diff --git a/metricbeat/module/munin/munin.go b/metricbeat/module/munin/munin.go new file mode 100644 index 000000000000..ca93abfcaf40 --- /dev/null +++ b/metricbeat/module/munin/munin.go @@ -0,0 +1,115 @@ +package munin + +import ( + "bufio" + "fmt" + "io" + "net" + "strconv" + "strings" + "time" + + "github.com/joeshaw/multierror" + "github.com/pkg/errors" + + "github.com/elastic/beats/libbeat/common" +) + +const ( + unknownValue = "U" +) + +// Node connection +type Node struct { + conn net.Conn + + writer io.Writer + reader *bufio.Reader +} + +// Connect with a munin node +func Connect(address string, timeout time.Duration) (*Node, error) { + conn, err := net.DialTimeout("tcp", address, timeout) + if err != nil { + return nil, err + } + n := &Node{conn: conn, + writer: conn, + reader: bufio.NewReader(conn), + } + // Cosume and ignore first line returned by munin, it is a comment + // about the node + scanner := bufio.NewScanner(n.reader) + scanner.Scan() + return n, scanner.Err() +} + +// Close node connection relasing its resources +func (n *Node) Close() error { + return n.conn.Close() +} + +// List of items exposed by the node +func (n *Node) List() ([]string, error) { + _, err := io.WriteString(n.writer, "list\n") + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(n.reader) + scanner.Scan() + return strings.Fields(scanner.Text()), scanner.Err() +} + +// Fetch metrics from munin node +func (n *Node) Fetch(items ...string) (common.MapStr, error) { + var errs multierror.Errors + event := common.MapStr{} + + for _, item := range items { + _, err := io.WriteString(n.writer, "fetch "+item+"\n") + if err != nil { + errs = append(errs, err) + continue + } + + scanner := bufio.NewScanner(n.reader) + scanner.Split(bufio.ScanWords) + for scanner.Scan() { + name := strings.TrimSpace(scanner.Text()) + + // Munin delimites metrics with a dot + if name == "." { + break + } + + name = strings.TrimSuffix(name, ".value") + + if !scanner.Scan() { + if scanner.Err() == nil { + errs = append(errs, errors.New("unexpected EOF when expecting value")) + } + break + } + value := scanner.Text() + + key := fmt.Sprintf("%s.%s", item, name) + + if value == unknownValue { + errs = append(errs, errors.Errorf("unknown value for %s", key)) + continue + } + if f, err := strconv.ParseFloat(value, 64); err == nil { + event.Put(key, f) + continue + } + event.Put(key, value) + } + + if scanner.Err() != nil { + errs = append(errs, scanner.Err()) + } + } + + return event, errs.Err() +} diff --git a/metricbeat/module/munin/munin_test.go b/metricbeat/module/munin/munin_test.go new file mode 100644 index 000000000000..adf9c8d6d723 --- /dev/null +++ b/metricbeat/module/munin/munin_test.go @@ -0,0 +1,82 @@ +package munin + +import ( + "bufio" + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/beats/libbeat/common" +) + +func dummyNode(response string) *Node { + return &Node{ + writer: &bytes.Buffer{}, + reader: bufio.NewReader(bytes.NewBuffer([]byte(response))), + } +} + +func TestList(t *testing.T) { + n := dummyNode("cpu df uptime\n") + + list, err := n.List() + + assert.Nil(t, err) + + expected := []string{"cpu", "df", "uptime"} + assert.ElementsMatch(t, expected, list) +} + +func TestFetch(t *testing.T) { + response := `user.value 4679836 +nice.value 59278 +system.value 1979168 +idle.value 59957502 +iowait.value 705373 +irq.value 76 +softirq.value 36404 +steal.value 0 +guest.value 0 +. +` + n := dummyNode(response) + + event, err := n.Fetch("cpu", "swap") + + assert.Nil(t, err) + + expected := common.MapStr{ + "cpu": common.MapStr{ + "user": float64(4679836), + "nice": float64(59278), + "system": float64(1979168), + "idle": float64(59957502), + "iowait": float64(705373), + "irq": float64(76), + "softirq": float64(36404), + "steal": float64(0), + "guest": float64(0), + }, + } + assert.Equal(t, expected, event) +} + +func TestFetchUnknown(t *testing.T) { + response := `some.value U +other.value 42 +. +` + n := dummyNode(response) + + event, err := n.Fetch("test") + + assert.NotNil(t, err) + + expected := common.MapStr{ + "test": common.MapStr{ + "other": float64(42), + }, + } + assert.Equal(t, expected, event) +} diff --git a/metricbeat/module/munin/node/_meta/docs.asciidoc b/metricbeat/module/munin/node/_meta/docs.asciidoc new file mode 100644 index 000000000000..a20a538c7462 --- /dev/null +++ b/metricbeat/module/munin/node/_meta/docs.asciidoc @@ -0,0 +1,58 @@ +=== munin node MetricSet + +This is the node metricset of the module munin. + +[float] +=== Features and configuration + +The node metricset of the munin module collects metrics from a munin node agent +and sends them as events to Elastic. + +[source,yaml] +--- +- module: munin + metricsets: ["node"] + hosts: ["localhost:4949"] + node.namespace: node +--- + +All metrics exposed by a single munin node will be sent in a single event, +grouped by munin items, e.g: + +[source,json] +--- +"munin": { + "node": { + "swap": { + "swap_in": 198609, + "swap_out": 612629 + }, + "cpu": { + "softirq": 680, + "guest": 0, + "user": 158212, + "iowait": 71095, + "irq": 1, + "system": 35906, + "idle": 1185709, + "steal": 0, + "nice": 1633 + } + } +} +--- + +In principle this module can be used to collect metrics from any agent that +implements the munin node protocol (http://guide.munin-monitoring.org/en/latest/master/network-protocol.html). + +[float] +=== Limitations +Currently this module only collects metrics using the basic protocol. It doesn't +support capabilities or automatic dashboards generation based on munin +configuration. + +[float] +=== Exposed fields, dashboards, indexes, etc. +Munin supports a great variety of plugins each of them can be used to obtain different +sets of metrics. Metricbeat cannot know the metrics exposed beforehand, so no field +description or dashboard is generated automatically. diff --git a/metricbeat/module/munin/node/_meta/fields.yml b/metricbeat/module/munin/node/_meta/fields.yml new file mode 100644 index 000000000000..dd5e036bb81e --- /dev/null +++ b/metricbeat/module/munin/node/_meta/fields.yml @@ -0,0 +1 @@ +- release: experimental diff --git a/metricbeat/module/munin/node/node.go b/metricbeat/module/munin/node/node.go new file mode 100644 index 000000000000..27d406ca9c33 --- /dev/null +++ b/metricbeat/module/munin/node/node.go @@ -0,0 +1,75 @@ +package node + +import ( + "time" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/common/cfgwarn" + "github.com/elastic/beats/metricbeat/mb" + "github.com/elastic/beats/metricbeat/module/munin" +) + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet("munin", "node", New) +} + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + namespace string + timeout time.Duration +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Experimental("The munin node metricset is experimental.") + + config := struct { + Namespace string `config:"node.namespace" validate:"required"` + }{} + if err := base.Module().UnpackConfig(&config); err != nil { + return nil, err + } + + return &MetricSet{ + BaseMetricSet: base, + namespace: config.Namespace, + timeout: base.Module().Config().Timeout, + }, nil +} + +// Fetch method implements the data gathering +func (m *MetricSet) Fetch() (common.MapStr, error) { + node, err := munin.Connect(m.Host(), m.timeout) + if err != nil { + return nil, err + } + defer node.Close() + + items, err := node.List() + if err != nil { + return nil, err + } + + event, err := node.Fetch(items...) + if err != nil { + return nil, err + } + + // Set dynamic namespace. + _, err = event.Put(mb.NamespaceKey, m.namespace) + if err != nil { + return nil, err + } + + return event, nil + +} diff --git a/metricbeat/modules.d/munin.yml.disabled b/metricbeat/modules.d/munin.yml.disabled new file mode 100644 index 000000000000..b752d1e2fdc7 --- /dev/null +++ b/metricbeat/modules.d/munin.yml.disabled @@ -0,0 +1,6 @@ +- module: munin + metricsets: ["node"] + enabled: false + period: 10s + hosts: ["localhost:4949"] + node.namespace: node diff --git a/metricbeat/tests/system/test_munin.py b/metricbeat/tests/system/test_munin.py new file mode 100644 index 000000000000..e1618202fa49 --- /dev/null +++ b/metricbeat/tests/system/test_munin.py @@ -0,0 +1,38 @@ +import os +import metricbeat +import unittest +from nose.plugins.attrib import attr + + +class Test(metricbeat.BaseTest): + + COMPOSE_SERVICES = ['munin'] + + @unittest.skipUnless(metricbeat.INTEGRATION_TESTS, "integration test") + def test_munin_node(self): + namespace = "node_test" + + self.render_config_template(modules=[{ + "name": "munin", + "metricsets": ["node"], + "hosts": self.get_hosts(), + "period": "1s", + "extras": { + "node.namespace": namespace, + }, + }]) + proc = self.start_beat() + self.wait_until(lambda: self.output_lines() > 0, max_timeout=20) + proc.check_kill_and_wait() + self.assert_no_logged_warnings() + + output = self.read_output_json() + self.assertTrue(len(output) >= 1) + evt = output[0] + print(evt) + + assert evt["munin"][namespace]["cpu"]["user"] > 0 + + def get_hosts(self): + return [os.getenv('MUNIN_HOST', 'localhost') + ':' + + os.getenv('MUNIN_PORT', '4949')]