Add collectd parser (#2654)

This commit is contained in:
Daniel Nelson
2017-04-12 10:41:26 -07:00
committed by GitHub
parent 0193cbee51
commit 2c98e5ae66
12 changed files with 592 additions and 4 deletions

View File

@@ -0,0 +1,165 @@
package collectd
import (
"errors"
"fmt"
"log"
"os"
"collectd.org/api"
"collectd.org/network"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric"
)
const (
DefaultAuthFile = "/etc/collectd/auth_file"
)
type CollectdParser struct {
// DefaultTags will be added to every parsed metric
DefaultTags map[string]string
popts network.ParseOpts
}
func (p *CollectdParser) SetParseOpts(popts *network.ParseOpts) {
p.popts = *popts
}
func NewCollectdParser(
authFile string,
securityLevel string,
typesDB []string,
) (*CollectdParser, error) {
popts := network.ParseOpts{}
switch securityLevel {
case "none":
popts.SecurityLevel = network.None
case "sign":
popts.SecurityLevel = network.Sign
case "encrypt":
popts.SecurityLevel = network.Encrypt
default:
popts.SecurityLevel = network.None
}
if authFile == "" {
authFile = DefaultAuthFile
}
popts.PasswordLookup = network.NewAuthFile(authFile)
for _, path := range typesDB {
db, err := LoadTypesDB(path)
if err != nil {
return nil, err
}
if popts.TypesDB != nil {
popts.TypesDB.Merge(db)
} else {
popts.TypesDB = db
}
}
parser := CollectdParser{popts: popts}
return &parser, nil
}
func (p *CollectdParser) Parse(buf []byte) ([]telegraf.Metric, error) {
valueLists, err := network.Parse(buf, p.popts)
if err != nil {
return nil, fmt.Errorf("Collectd parser error: %s", err)
}
metrics := []telegraf.Metric{}
for _, valueList := range valueLists {
metrics = append(metrics, UnmarshalValueList(valueList)...)
}
if len(p.DefaultTags) > 0 {
for _, m := range metrics {
for k, v := range p.DefaultTags {
// only set the default tag if it doesn't already exist:
if !m.HasTag(k) {
m.AddTag(k, v)
}
}
}
}
return metrics, nil
}
func (p *CollectdParser) ParseLine(line string) (telegraf.Metric, error) {
metrics, err := p.Parse([]byte(line))
if err != nil {
return nil, err
}
if len(metrics) != 1 {
return nil, errors.New("Line contains multiple metrics")
}
return metrics[0], nil
}
func (p *CollectdParser) SetDefaultTags(tags map[string]string) {
p.DefaultTags = tags
}
// UnmarshalValueList translates a ValueList into a Telegraf metric.
func UnmarshalValueList(vl *api.ValueList) []telegraf.Metric {
timestamp := vl.Time.UTC()
var metrics []telegraf.Metric
for i := range vl.Values {
var name string
name = fmt.Sprintf("%s_%s", vl.Identifier.Plugin, vl.DSName(i))
tags := make(map[string]string)
fields := make(map[string]interface{})
// Convert interface back to actual type, then to float64
switch value := vl.Values[i].(type) {
case api.Gauge:
fields["value"] = float64(value)
case api.Derive:
fields["value"] = float64(value)
case api.Counter:
fields["value"] = float64(value)
}
if vl.Identifier.Host != "" {
tags["host"] = vl.Identifier.Host
}
if vl.Identifier.PluginInstance != "" {
tags["instance"] = vl.Identifier.PluginInstance
}
if vl.Identifier.Type != "" {
tags["type"] = vl.Identifier.Type
}
if vl.Identifier.TypeInstance != "" {
tags["type_instance"] = vl.Identifier.TypeInstance
}
// Drop invalid points
m, err := metric.New(name, tags, fields, timestamp)
if err != nil {
log.Printf("E! Dropping metric %v: %v", name, err)
continue
}
metrics = append(metrics, m)
}
return metrics
}
func LoadTypesDB(path string) (*api.TypesDB, error) {
reader, err := os.Open(path)
if err != nil {
return nil, err
}
return api.NewTypesDB(reader)
}

View File

@@ -0,0 +1,298 @@
package collectd
import (
"context"
"testing"
"collectd.org/api"
"collectd.org/network"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
)
type AuthMap struct {
Passwd map[string]string
}
func (p *AuthMap) Password(user string) (string, error) {
return p.Passwd[user], nil
}
type metricData struct {
name string
tags map[string]string
fields map[string]interface{}
}
type testCase struct {
vl []api.ValueList
expected []metricData
}
var singleMetric = testCase{
[]api.ValueList{
api.ValueList{
Identifier: api.Identifier{
Host: "xyzzy",
Plugin: "cpu",
PluginInstance: "1",
Type: "cpu",
TypeInstance: "user",
},
Values: []api.Value{
api.Counter(42),
},
DSNames: []string(nil),
},
},
[]metricData{
metricData{
"cpu_value",
map[string]string{
"type_instance": "user",
"host": "xyzzy",
"instance": "1",
"type": "cpu",
},
map[string]interface{}{
"value": float64(42),
},
},
},
}
var multiMetric = testCase{
[]api.ValueList{
api.ValueList{
Identifier: api.Identifier{
Host: "xyzzy",
Plugin: "cpu",
PluginInstance: "0",
Type: "cpu",
TypeInstance: "user",
},
Values: []api.Value{
api.Derive(42),
api.Gauge(42),
},
DSNames: []string(nil),
},
},
[]metricData{
metricData{
"cpu_0",
map[string]string{
"type_instance": "user",
"host": "xyzzy",
"instance": "0",
"type": "cpu",
},
map[string]interface{}{
"value": float64(42),
},
},
metricData{
"cpu_1",
map[string]string{
"type_instance": "user",
"host": "xyzzy",
"instance": "0",
"type": "cpu",
},
map[string]interface{}{
"value": float64(42),
},
},
},
}
func TestNewCollectdParser(t *testing.T) {
parser, err := NewCollectdParser("", "", []string{})
require.Nil(t, err)
require.Equal(t, parser.popts.SecurityLevel, network.None)
require.NotNil(t, parser.popts.PasswordLookup)
require.Nil(t, parser.popts.TypesDB)
}
func TestParse(t *testing.T) {
cases := []testCase{singleMetric, multiMetric}
for _, tc := range cases {
buf, err := writeValueList(tc.vl)
require.Nil(t, err)
bytes, err := buf.Bytes()
require.Nil(t, err)
parser := &CollectdParser{}
require.Nil(t, err)
metrics, err := parser.Parse(bytes)
require.Nil(t, err)
assertEqualMetrics(t, tc.expected, metrics)
}
}
func TestParse_DefaultTags(t *testing.T) {
buf, err := writeValueList(singleMetric.vl)
require.Nil(t, err)
bytes, err := buf.Bytes()
require.Nil(t, err)
parser := &CollectdParser{}
parser.SetDefaultTags(map[string]string{
"foo": "bar",
})
require.Nil(t, err)
metrics, err := parser.Parse(bytes)
require.Nil(t, err)
require.Equal(t, "bar", metrics[0].Tags()["foo"])
}
func TestParse_SignSecurityLevel(t *testing.T) {
parser := &CollectdParser{}
popts := &network.ParseOpts{
SecurityLevel: network.Sign,
PasswordLookup: &AuthMap{
map[string]string{
"user0": "bar",
},
},
}
parser.SetParseOpts(popts)
// Signed data
buf, err := writeValueList(singleMetric.vl)
require.Nil(t, err)
buf.Sign("user0", "bar")
bytes, err := buf.Bytes()
require.Nil(t, err)
metrics, err := parser.Parse(bytes)
require.Nil(t, err)
assertEqualMetrics(t, singleMetric.expected, metrics)
// Encrypted data
buf, err = writeValueList(singleMetric.vl)
require.Nil(t, err)
buf.Encrypt("user0", "bar")
bytes, err = buf.Bytes()
require.Nil(t, err)
metrics, err = parser.Parse(bytes)
require.Nil(t, err)
assertEqualMetrics(t, singleMetric.expected, metrics)
// Plain text data skipped
buf, err = writeValueList(singleMetric.vl)
require.Nil(t, err)
bytes, err = buf.Bytes()
require.Nil(t, err)
metrics, err = parser.Parse(bytes)
require.Nil(t, err)
require.Equal(t, []telegraf.Metric{}, metrics)
// Wrong password error
buf, err = writeValueList(singleMetric.vl)
require.Nil(t, err)
buf.Sign("x", "y")
bytes, err = buf.Bytes()
require.Nil(t, err)
metrics, err = parser.Parse(bytes)
require.NotNil(t, err)
}
func TestParse_EncryptSecurityLevel(t *testing.T) {
parser := &CollectdParser{}
popts := &network.ParseOpts{
SecurityLevel: network.Encrypt,
PasswordLookup: &AuthMap{
map[string]string{
"user0": "bar",
},
},
}
parser.SetParseOpts(popts)
// Signed data skipped
buf, err := writeValueList(singleMetric.vl)
require.Nil(t, err)
buf.Sign("user0", "bar")
bytes, err := buf.Bytes()
require.Nil(t, err)
metrics, err := parser.Parse(bytes)
require.Nil(t, err)
require.Equal(t, []telegraf.Metric{}, metrics)
// Encrypted data
buf, err = writeValueList(singleMetric.vl)
require.Nil(t, err)
buf.Encrypt("user0", "bar")
bytes, err = buf.Bytes()
require.Nil(t, err)
metrics, err = parser.Parse(bytes)
require.Nil(t, err)
assertEqualMetrics(t, singleMetric.expected, metrics)
// Plain text data skipped
buf, err = writeValueList(singleMetric.vl)
require.Nil(t, err)
bytes, err = buf.Bytes()
require.Nil(t, err)
metrics, err = parser.Parse(bytes)
require.Nil(t, err)
require.Equal(t, []telegraf.Metric{}, metrics)
// Wrong password error
buf, err = writeValueList(singleMetric.vl)
require.Nil(t, err)
buf.Sign("x", "y")
bytes, err = buf.Bytes()
require.Nil(t, err)
metrics, err = parser.Parse(bytes)
require.NotNil(t, err)
}
func TestParseLine(t *testing.T) {
buf, err := writeValueList(singleMetric.vl)
require.Nil(t, err)
bytes, err := buf.Bytes()
require.Nil(t, err)
parser, err := NewCollectdParser("", "", []string{})
require.Nil(t, err)
metric, err := parser.ParseLine(string(bytes))
require.Nil(t, err)
assertEqualMetrics(t, singleMetric.expected, []telegraf.Metric{metric})
}
func writeValueList(valueLists []api.ValueList) (*network.Buffer, error) {
buffer := network.NewBuffer(0)
ctx := context.Background()
for _, vl := range valueLists {
err := buffer.Write(ctx, &vl)
if err != nil {
return nil, err
}
}
return buffer, nil
}
func assertEqualMetrics(t *testing.T, expected []metricData, received []telegraf.Metric) {
require.Equal(t, len(expected), len(received))
for i, m := range received {
require.Equal(t, expected[i].name, m.Name())
require.Equal(t, expected[i].tags, m.Tags())
require.Equal(t, expected[i].fields, m.Fields())
}
}