Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mapping for munin and options to override service type and name #10322

Merged
merged 13 commits into from
Jan 31, 2019
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d

*Metricbeat*

- Replace `namespace` option in munin module with `service.type` and `service.name` to override the ECS fields with the same names. {pull}10322[10322]
- Add checks on valid field names and more strict mapping to munin module. {pull}10322[10322]
- Refactor Prometheus metric mappings {pull}9948[9948]
- Removed Prometheus stats metricset in favor of just using Prometheus collector {pull}9948[9948]

Expand Down
17 changes: 14 additions & 3 deletions metricbeat/docs/fields.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -16986,11 +16986,22 @@ Munin node metrics exporter



[float]
== munin fields
*`munin.metrics.*.*`*::
+
--
type: object

munin.metrics contains metrics exposed by a munin node agent


munin contains metrics exposed by a munin node agent
--

*`munin.metrics.*`*::
+
--
type: object

--


[[exported-fields-mysql]]
Expand Down
12 changes: 10 additions & 2 deletions metricbeat/module/munin/_meta/fields.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@
Munin node metrics exporter
release: beta
fields:
- name: munin.metrics.*.*
type: object
object_type: double
object_type_mapping_type: '*'
description: >
munin.metrics contains metrics exposed by a munin node agent
- name: munin.metrics.*
ruflin marked this conversation as resolved.
Show resolved Hide resolved
type: object
object_type: object
object_type_mapping_type: '*'
- name: munin
type: group
description: >
munin contains metrics exposed by a munin node agent
fields:
2 changes: 1 addition & 1 deletion metricbeat/module/munin/fields.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions metricbeat/module/munin/munin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"net"
"regexp"
"strconv"
"strings"
"time"
Expand All @@ -36,6 +37,12 @@ const (
unknownValue = "U"
)

var (
// Field names must match with this expression
// http://guide.munin-monitoring.org/en/latest/reference/plugin.html#notes-on-fieldnames
ruflin marked this conversation as resolved.
Show resolved Hide resolved
nameRegexp = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
)

// Node connection
type Node struct {
conn net.Conn
Expand Down Expand Up @@ -110,6 +117,11 @@ func (n *Node) Fetch(items ...string) (common.MapStr, error) {
}
value := scanner.Text()

if !nameRegexp.MatchString(name) {
errs = append(errs, fmt.Errorf("field name '%s' doesn't match expected format '%s'", name, nameRegexp.String()))
continue
}

key := fmt.Sprintf("%s.%s", item, name)

if value == unknownValue {
Expand Down
107 changes: 72 additions & 35 deletions metricbeat/module/munin/munin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func TestList(t *testing.T) {
assert.ElementsMatch(t, expected, list)
}

func TestFetch(t *testing.T) {
response := `user.value 4679836
const (
responseCPU = `user.value 4679836
nice.value 59278
system.value 1979168
idle.value 59957502
Expand All @@ -57,43 +57,80 @@ 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
responseUnknown = `some.value U
other.value 42
.
`
n := dummyNode(response)

event, err := n.Fetch("test")

assert.NotNil(t, err)
responseWithWrongFields = `user.value 4679836
nice.value 59278
system.value 1979168
idle.value 59957502
user.1000.value 23456
user.0.value 38284
.
`
)

expected := common.MapStr{
"test": common.MapStr{
"other": float64(42),
func TestFetch(t *testing.T) {
cases := []struct {
title string
response string
expected common.MapStr
errors bool
}{
{
"normal case",
responseCPU,
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),
},
},
false,
},
{
"unknown values",
responseUnknown,
common.MapStr{
"cpu": common.MapStr{
"other": float64(42),
},
},
true,
},
{
"wrong field names",
responseWithWrongFields,
common.MapStr{
"cpu": common.MapStr{
"user": float64(4679836),
"nice": float64(59278),
"system": float64(1979168),
"idle": float64(59957502),
},
},
true,
},
}
assert.Equal(t, expected, event)

for _, c := range cases {
t.Run(c.title, func(t *testing.T) {
n := dummyNode(c.response)
event, err := n.Fetch("cpu", "swap")
assert.Equal(t, c.expected, event)
if c.errors {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
27 changes: 27 additions & 0 deletions metricbeat/module/munin/node/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package node

type Config struct {
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
ServiceName string `config:"service.name"`
ServiceType string `config:"service.type"`
}

var defaultConfig = Config{
ServiceType: "munin",
}
46 changes: 28 additions & 18 deletions metricbeat/module/munin/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,53 +42,63 @@ func init() {
// interface methods except for Fetch.
type MetricSet struct {
mb.BaseMetricSet
namespace string
timeout time.Duration
serviceName string
serviceType 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.Beta("The munin node metricset is beta.")

config := struct {
Namespace string `config:"node.namespace" validate:"required"`
}{}
config := defaultConfig
if err := base.Module().UnpackConfig(&config); err != nil {
return nil, err
}

return &MetricSet{
BaseMetricSet: base,
namespace: config.Namespace,
serviceName: config.ServiceName,
serviceType: config.ServiceType,
timeout: base.Module().Config().Timeout,
}, nil
}

// Fetch method implements the data gathering
func (m *MetricSet) Fetch() (common.MapStr, error) {
func (m *MetricSet) Fetch(r mb.ReporterV2) {
node, err := munin.Connect(m.Host(), m.timeout)
if err != nil {
return nil, err
r.Error(err)
return
}
defer node.Close()

items, err := node.List()
if err != nil {
return nil, err
r.Error(err)
return
}

event, err := node.Fetch(items...)
metrics, err := node.Fetch(items...)
if err != nil {
return nil, err
r.Error(err)
}

// Set dynamic namespace.
_, err = event.Put(mb.NamespaceKey, m.namespace)
if err != nil {
return nil, err
// Even if there was some error, keep sending succesfully collected metrics if any
if len(metrics) == 0 {
return
}

return event, nil

event := mb.Event{
Service: m.serviceType,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service type should not be munin by the service that is monitored. So if apache is monitored with munin, service.type should be apache. Because of this we should also have it set by the user. I wonder if we should make it required for the user to have it set or make it optional?

Copy link
Member

@ruflin ruflin Jan 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a discussion with @exekias and he proposed to have this as general option for all modules instead. Can you remove it from here and potentially open a separate PR with introducing these settings?

Copy link
Member Author

@jsoriano jsoriano Jan 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service type should not be munin by the service that is monitored. So if apache is monitored with munin, service.type should be apache. Because of this we should also have it set by the user. I wonder if we should make it required for the user to have it set or make it optional?

After reading this I think that another option for events could be to use the plugin name as a different field and not for the path, so if we have now an event like this:

{
  "service": {
    "type": "munin",
    "name": "somerole"
  },
  {
    "munin": {
      "metrics": {
         "apache": {
            "accesses": 42,
            "errors": 2
         },
         "cpu": {
            "system": 0.8
         },
      }
    }
  }

Have instead two like this:

{
  "service": {
    "type": "apache",
    "name": "somerole"
  },
  {
    "observer": {
      "type": "munin"
    },
    "munin": {
      "plugin": {
        "name": "apache"
      },
      "metrics": {
        "accesses": 42,
        "errors": 2
      }
    }
  }
{
  "service": {
    "type": "cpu",
    "name": "somerole"
  },
  {
    "observer": {
      "type": "munin"
    },
    "munin": {
      "plugin": {
        "name": "cpu",
      },
      "metrics": {
        "system": 0.8
      }
    }
  }

This would be more aligned with ECS-like metrics.

Copy link
Member Author

@jsoriano jsoriano Jan 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a discussion with @exekias and he proposed to have this as general option for all modules instead. Can you remove it from here and potentially open a separate PR with introducing these settings?

I totally agree with having an option for service.name for all modules. Not so sure for service.type, I think that the type should be set in general by the module (do we want to allow arbitrary service.type for service modules?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • +1 on only allowing to set the service.type for the "input" modules. We should not allow to have it overwritten in other modules
  • It's very interesting that you set the observer above as munin and not metricbeat. @webmat This gets even more interesting now ;-)
  • I like the idea of the plugin field. How easily can this mapping be done? I assume we only have data from 1 plugin at the time?

Copy link
Member Author

@jsoriano jsoriano Jan 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a general option for service.name in other PR.

The setting of service.type and observer.type will also go in another PR, I think there can be some shared code for that in all "input" modules.

The plugin field would be quite easy, the items we are looping on here, should rather be called plugins 🙂. I'll give a try to this here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR to add service.name #10427

RootFields: common.MapStr{
"munin": common.MapStr{
"metrics": metrics,
},
},
}
if m.serviceName != "" {
event.RootFields.Put("service.name", m.serviceName)
}
r.Event(event)
}
10 changes: 7 additions & 3 deletions metricbeat/tests/system/test_munin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_munin_node(self):
"hosts": self.get_hosts(),
"period": "1s",
"extras": {
"node.namespace": namespace,
"service.name": namespace,
},
}])
proc = self.start_beat()
Expand All @@ -31,8 +31,12 @@ def test_munin_node(self):
evt = output[0]
print(evt)

assert evt["munin"][namespace]["cpu"]["user"] > 0
assert evt["service"]["type"] == "munin"
assert evt["service"]["name"] == namespace
assert evt["munin"]["metrics"]["cpu"]["user"] > 0

self.assert_fields_are_documented(evt)

def get_hosts(self):
return [os.getenv('MUNIN_HOST', 'localhost') + ':' +
return [self.compose_hosts()[0] + ':' +
os.getenv('MUNIN_PORT', '4949')]