Add support for dropwizard input format (#2846)

This commit is contained in:
atzoum
2018-01-09 01:11:36 +02:00
committed by Daniel Nelson
parent 5207b32b25
commit 05d691aa81
13 changed files with 1436 additions and 333 deletions

View File

@@ -0,0 +1,253 @@
package dropwizard
import (
"bytes"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal/templating"
"github.com/influxdata/telegraf/metric"
"github.com/tidwall/gjson"
)
// Parser parses json inputs containing dropwizard metrics,
// either top-level or embedded inside a json field.
// This parser is using gjon for retrieving paths within the json file.
type Parser struct {
// an optional json path containing the metric registry object
// if left empty, the whole json object is parsed as a metric registry
MetricRegistryPath string
// an optional json path containing the default time of the metrics
// if left empty, or if cannot be parsed the current processing time is used as the time of the metrics
TimePath string
// time format to use for parsing the time field
// defaults to time.RFC3339
TimeFormat string
// an optional json path pointing to a json object with tag key/value pairs
// takes precedence over TagPathsMap
TagsPath string
// an optional map containing tag names as keys and json paths to retrieve the tag values from as values
// used if TagsPath is empty or doesn't return any tags
TagPathsMap map[string]string
// an optional map of default tags to use for metrics
DefaultTags map[string]string
// templating configuration
Separator string
Templates []string
templateEngine *templating.Engine
}
// Parse parses the input bytes to an array of metrics
func (p *Parser) Parse(buf []byte) ([]telegraf.Metric, error) {
metrics := make([]telegraf.Metric, 0)
metricTime, err := p.parseTime(buf)
if err != nil {
return nil, err
}
dwr, err := p.unmarshalMetrics(buf)
if err != nil {
return nil, err
}
metrics = p.readDWMetrics("counter", dwr["counters"], metrics, metricTime)
metrics = p.readDWMetrics("meter", dwr["meters"], metrics, metricTime)
metrics = p.readDWMetrics("gauge", dwr["gauges"], metrics, metricTime)
metrics = p.readDWMetrics("histogram", dwr["histograms"], metrics, metricTime)
metrics = p.readDWMetrics("timer", dwr["timers"], metrics, metricTime)
jsonTags := p.readTags(buf)
// fill json tags first
if len(jsonTags) > 0 {
for _, m := range metrics {
for k, v := range jsonTags {
// only set the tag if it doesn't already exist:
if !m.HasTag(k) {
m.AddTag(k, v)
}
}
}
}
// fill default tags last
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
}
// InitTemplating initializes the templating support
func (p *Parser) InitTemplating() error {
if len(p.Templates) > 0 {
defaultTemplate, _ := templating.NewDefaultTemplateWithPattern("measurement*")
templateEngine, err := templating.NewEngine(p.Separator, defaultTemplate, p.Templates)
p.templateEngine = templateEngine
return err
}
return nil
}
// ParseLine is not supported by the dropwizard format
func (p *Parser) ParseLine(line string) (telegraf.Metric, error) {
return nil, fmt.Errorf("ParseLine not supported: %s, for data format: dropwizard", line)
}
// SetDefaultTags sets the default tags
func (p *Parser) SetDefaultTags(tags map[string]string) {
p.DefaultTags = tags
}
func (p *Parser) readTags(buf []byte) map[string]string {
if p.TagsPath != "" {
var tagsBytes []byte
tagsResult := gjson.GetBytes(buf, p.TagsPath)
if tagsResult.Index > 0 {
tagsBytes = buf[tagsResult.Index : tagsResult.Index+len(tagsResult.Raw)]
} else {
tagsBytes = []byte(tagsResult.Raw)
}
var tags map[string]string
err := json.Unmarshal(tagsBytes, &tags)
if err != nil {
log.Printf("W! failed to parse tags from JSON path '%s': %s\n", p.TagsPath, err)
} else if len(tags) > 0 {
return tags
}
}
tags := make(map[string]string)
for tagKey, jsonPath := range p.TagPathsMap {
tags[tagKey] = gjson.GetBytes(buf, jsonPath).String()
}
return tags
}
func (p *Parser) parseTime(buf []byte) (time.Time, error) {
if p.TimePath != "" {
timeFormat := p.TimeFormat
if timeFormat == "" {
timeFormat = time.RFC3339
}
timeString := gjson.GetBytes(buf, p.TimePath).String()
if timeString == "" {
err := fmt.Errorf("time not found in JSON path %s", p.TimePath)
return time.Now().UTC(), err
}
t, err := time.Parse(timeFormat, timeString)
if err != nil {
err = fmt.Errorf("time %s cannot be parsed with format %s, %s", timeString, timeFormat, err)
return time.Now().UTC(), err
}
return t.UTC(), nil
}
return time.Now().UTC(), nil
}
func (p *Parser) unmarshalMetrics(buf []byte) (map[string]interface{}, error) {
var registryBytes []byte
if p.MetricRegistryPath != "" {
regResult := gjson.GetBytes(buf, p.MetricRegistryPath)
if regResult.Index > 0 {
registryBytes = buf[regResult.Index : regResult.Index+len(regResult.Raw)]
} else {
registryBytes = []byte(regResult.Raw)
}
if len(registryBytes) == 0 {
err := fmt.Errorf("metric registry not found in JSON path %s", p.MetricRegistryPath)
return nil, err
}
} else {
registryBytes = buf
}
var jsonOut map[string]interface{}
err := json.Unmarshal(registryBytes, &jsonOut)
if err != nil {
err = fmt.Errorf("unable to parse dropwizard metric registry from JSON document, %s", err)
return nil, err
}
return jsonOut, nil
}
func (p *Parser) readDWMetrics(metricType string, dwms interface{}, metrics []telegraf.Metric, time time.Time) []telegraf.Metric {
switch dwmsTyped := dwms.(type) {
case map[string]interface{}:
var metricsBuffer bytes.Buffer
for dwmName, dwmFields := range dwmsTyped {
measurementName := dwmName
tags := make(map[string]string)
fieldPrefix := ""
if p.templateEngine != nil {
measurementName, tags, fieldPrefix, _ = p.templateEngine.Apply(dwmName)
if len(fieldPrefix) > 0 {
fieldPrefix = fmt.Sprintf("%s%s", fieldPrefix, p.Separator)
}
}
tags["metric_type"] = metricType
measurementWithTags := measurementName
for tagName, tagValue := range tags {
tagKeyValue := fmt.Sprintf("%s=%s", tagName, tagValue)
measurementWithTags = fmt.Sprintf("%s,%s", measurementWithTags, tagKeyValue)
}
fields := make([]string, 0)
switch t := dwmFields.(type) {
case map[string]interface{}: // json object
for fieldName, fieldValue := range t {
switch v := fieldValue.(type) {
case float64:
fields = append(fields, fmt.Sprintf("%s%s=%f", fieldPrefix, fieldName, v))
default: // ignore
}
}
default: // ignore
}
metricsBuffer.WriteString(fmt.Sprintf("%s,metric_type=%s ", measurementWithTags, metricType))
metricsBuffer.WriteString(strings.Join(fields, ","))
metricsBuffer.WriteString("\n")
}
newMetrics, err := metric.ParseWithDefaultTime(metricsBuffer.Bytes(), time)
if err != nil {
log.Printf("W! failed to create metric of type '%s': %s\n", metricType, err)
}
return append(metrics, newMetrics...)
default:
return metrics
}
}
func arraymap(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}

View File

@@ -0,0 +1,485 @@
package dropwizard
import (
"testing"
"github.com/influxdata/telegraf"
"fmt"
"time"
"github.com/stretchr/testify/assert"
)
// validEmptyJSON is a valid dropwizard json document, but without any metrics
const validEmptyJSON = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {},
"gauges" : {},
"histograms" : {},
"timers" : {}
}
`
func TestParseValidEmptyJSON(t *testing.T) {
parser := Parser{}
// Most basic vanilla test
metrics, err := parser.Parse([]byte(validEmptyJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 0)
}
// validCounterJSON is a valid dropwizard json document containing one counter
const validCounterJSON = `
{
"version": "3.0.0",
"counters" : {
"measurement" : {
"count" : 1
}
},
"meters" : {},
"gauges" : {},
"histograms" : {},
"timers" : {}
}
`
func TestParseValidCounterJSON(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validCounterJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"count": float64(1),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "counter"}, metrics[0].Tags())
}
// validEmbeddedCounterJSON is a valid json document containing separate fields for dropwizard metrics, tags and time override.
const validEmbeddedCounterJSON = `
{
"time" : "2017-02-22T14:33:03.662+02:00",
"tags" : {
"tag1" : "green",
"tag2" : "yellow"
},
"metrics" : {
"counters" : {
"measurement" : {
"count" : 1
}
},
"meters" : {},
"gauges" : {},
"histograms" : {},
"timers" : {}
}
}
`
func TestParseValidEmbeddedCounterJSON(t *testing.T) {
timeFormat := "2006-01-02T15:04:05Z07:00"
metricTime, _ := time.Parse(timeFormat, "2017-02-22T15:33:03.662+03:00")
parser := Parser{
MetricRegistryPath: "metrics",
TagsPath: "tags",
TimePath: "time",
}
metrics, err := parser.Parse([]byte(validEmbeddedCounterJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"count": float64(1),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "counter", "tag1": "green", "tag2": "yellow"}, metrics[0].Tags())
assert.True(t, metricTime.Equal(metrics[0].Time()), fmt.Sprintf("%s should be equal to %s", metrics[0].Time(), metricTime))
// now test json tags through TagPathsMap
parser2 := Parser{
MetricRegistryPath: "metrics",
TagPathsMap: map[string]string{"tag1": "tags.tag1"},
TimePath: "time",
}
metrics2, err2 := parser2.Parse([]byte(validEmbeddedCounterJSON))
assert.NoError(t, err2)
assert.Equal(t, map[string]string{"metric_type": "counter", "tag1": "green"}, metrics2[0].Tags())
}
// validMeterJSON1 is a valid dropwizard json document containing one meter
const validMeterJSON1 = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {
"measurement1" : {
"count" : 1,
"m15_rate" : 1.0,
"m1_rate" : 1.0,
"m5_rate" : 1.0,
"mean_rate" : 1.0,
"units" : "events/second"
}
},
"gauges" : {},
"histograms" : {},
"timers" : {}
}
`
func TestParseValidMeterJSON1(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validMeterJSON1))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement1", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"count": float64(1),
"m15_rate": float64(1),
"m1_rate": float64(1),
"m5_rate": float64(1),
"mean_rate": float64(1),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "meter"}, metrics[0].Tags())
}
// validMeterJSON2 is a valid dropwizard json document containing one meter with one tag
const validMeterJSON2 = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {
"measurement2,key=value" : {
"count" : 2,
"m15_rate" : 2.0,
"m1_rate" : 2.0,
"m5_rate" : 2.0,
"mean_rate" : 2.0,
"units" : "events/second"
}
},
"gauges" : {},
"histograms" : {},
"timers" : {}
}
`
func TestParseValidMeterJSON2(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validMeterJSON2))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement2", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"count": float64(2),
"m15_rate": float64(2),
"m1_rate": float64(2),
"m5_rate": float64(2),
"mean_rate": float64(2),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "meter", "key": "value"}, metrics[0].Tags())
}
// validGaugeJSON is a valid dropwizard json document containing one gauge
const validGaugeJSON = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {},
"gauges" : {
"measurement" : {
"value" : 0
}
},
"histograms" : {},
"timers" : {}
}
`
func TestParseValidGaugeJSON(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validGaugeJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"value": float64(0),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "gauge"}, metrics[0].Tags())
}
// validHistogramJSON is a valid dropwizard json document containing one histogram
const validHistogramJSON = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {},
"gauges" : {},
"histograms" : {
"measurement" : {
"count" : 1,
"max" : 2,
"mean" : 3,
"min" : 4,
"p50" : 5,
"p75" : 6,
"p95" : 7,
"p98" : 8,
"p99" : 9,
"p999" : 10,
"stddev" : 11
}
},
"timers" : {}
}
`
func TestParseValidHistogramJSON(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validHistogramJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"count": float64(1),
"max": float64(2),
"mean": float64(3),
"min": float64(4),
"p50": float64(5),
"p75": float64(6),
"p95": float64(7),
"p98": float64(8),
"p99": float64(9),
"p999": float64(10),
"stddev": float64(11),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "histogram"}, metrics[0].Tags())
}
// validTimerJSON is a valid dropwizard json document containing one timer
const validTimerJSON = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {},
"gauges" : {},
"histograms" : {},
"timers" : {
"measurement" : {
"count" : 1,
"max" : 2,
"mean" : 3,
"min" : 4,
"p50" : 5,
"p75" : 6,
"p95" : 7,
"p98" : 8,
"p99" : 9,
"p999" : 10,
"stddev" : 11,
"m15_rate" : 12,
"m1_rate" : 13,
"m5_rate" : 14,
"mean_rate" : 15,
"duration_units" : "seconds",
"rate_units" : "calls/second"
}
}
}
`
func TestParseValidTimerJSON(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validTimerJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 1)
assert.Equal(t, "measurement", metrics[0].Name())
assert.Equal(t, map[string]interface{}{
"count": float64(1),
"max": float64(2),
"mean": float64(3),
"min": float64(4),
"p50": float64(5),
"p75": float64(6),
"p95": float64(7),
"p98": float64(8),
"p99": float64(9),
"p999": float64(10),
"stddev": float64(11),
"m15_rate": float64(12),
"m1_rate": float64(13),
"m5_rate": float64(14),
"mean_rate": float64(15),
}, metrics[0].Fields())
assert.Equal(t, map[string]string{"metric_type": "timer"}, metrics[0].Tags())
}
// validAllJSON is a valid dropwizard json document containing one metric of each type
const validAllJSON = `
{
"version": "3.0.0",
"counters" : {
"measurement" : {"count" : 1}
},
"meters" : {
"measurement" : {"count" : 1}
},
"gauges" : {
"measurement" : {"value" : 1}
},
"histograms" : {
"measurement" : {"count" : 1}
},
"timers" : {
"measurement" : {"count" : 1}
}
}
`
func TestParseValidAllJSON(t *testing.T) {
parser := Parser{}
metrics, err := parser.Parse([]byte(validAllJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 5)
}
func TestTagParsingProblems(t *testing.T) {
// giving a wrong path results in empty tags
parser1 := Parser{MetricRegistryPath: "metrics", TagsPath: "tags1"}
metrics1, err1 := parser1.Parse([]byte(validEmbeddedCounterJSON))
assert.NoError(t, err1)
assert.Len(t, metrics1, 1)
assert.Equal(t, map[string]string{"metric_type": "counter"}, metrics1[0].Tags())
// giving a wrong TagsPath falls back to TagPathsMap
parser2 := Parser{
MetricRegistryPath: "metrics",
TagsPath: "tags1",
TagPathsMap: map[string]string{"tag1": "tags.tag1"},
}
metrics2, err2 := parser2.Parse([]byte(validEmbeddedCounterJSON))
assert.NoError(t, err2)
assert.Len(t, metrics2, 1)
assert.Equal(t, map[string]string{"metric_type": "counter", "tag1": "green"}, metrics2[0].Tags())
}
// sampleTemplateJSON is a sample json document containing metrics to be tested against the templating engine.
const sampleTemplateJSON = `
{
"version": "3.0.0",
"counters" : {},
"meters" : {},
"gauges" : {
"vm.memory.heap.committed" : { "value" : 1 },
"vm.memory.heap.init" : { "value" : 2 },
"vm.memory.heap.max" : { "value" : 3 },
"vm.memory.heap.usage" : { "value" : 4 },
"vm.memory.heap.used" : { "value" : 5 },
"vm.memory.non-heap.committed" : { "value" : 6 },
"vm.memory.non-heap.init" : { "value" : 7 },
"vm.memory.non-heap.max" : { "value" : 8 },
"vm.memory.non-heap.usage" : { "value" : 9 },
"vm.memory.non-heap.used" : { "value" : 10 }
},
"histograms" : {
"jenkins.job.building.duration" : {
"count" : 1,
"max" : 2,
"mean" : 3,
"min" : 4,
"p50" : 5,
"p75" : 6,
"p95" : 7,
"p98" : 8,
"p99" : 9,
"p999" : 10,
"stddev" : 11
}
},
"timers" : {}
}
`
func TestParseSampleTemplateJSON(t *testing.T) {
parser := Parser{
Separator: "_",
Templates: []string{
"jenkins.* measurement.metric.metric.field",
"vm.* measurement.measurement.pool.field",
},
}
parser.InitTemplating()
metrics, err := parser.Parse([]byte(sampleTemplateJSON))
assert.NoError(t, err)
assert.Len(t, metrics, 11)
jenkinsMetric := search(metrics, "jenkins", nil, "")
assert.NotNil(t, jenkinsMetric, "the metrics should contain a jenkins measurement")
assert.Equal(t, map[string]interface{}{
"duration_count": float64(1),
"duration_max": float64(2),
"duration_mean": float64(3),
"duration_min": float64(4),
"duration_p50": float64(5),
"duration_p75": float64(6),
"duration_p95": float64(7),
"duration_p98": float64(8),
"duration_p99": float64(9),
"duration_p999": float64(10),
"duration_stddev": float64(11),
}, jenkinsMetric.Fields())
assert.Equal(t, map[string]string{"metric_type": "histogram", "metric": "job_building"}, jenkinsMetric.Tags())
vmMemoryHeapCommitted := search(metrics, "vm_memory", map[string]string{"pool": "heap"}, "committed_value")
assert.NotNil(t, vmMemoryHeapCommitted)
assert.Equal(t, map[string]interface{}{
"committed_value": float64(1),
}, vmMemoryHeapCommitted.Fields())
assert.Equal(t, map[string]string{"metric_type": "gauge", "pool": "heap"}, vmMemoryHeapCommitted.Tags())
vmMemoryNonHeapCommitted := search(metrics, "vm_memory", map[string]string{"pool": "non-heap"}, "committed_value")
assert.NotNil(t, vmMemoryNonHeapCommitted)
assert.Equal(t, map[string]interface{}{
"committed_value": float64(6),
}, vmMemoryNonHeapCommitted.Fields())
assert.Equal(t, map[string]string{"metric_type": "gauge", "pool": "non-heap"}, vmMemoryNonHeapCommitted.Tags())
}
func search(metrics []telegraf.Metric, name string, tags map[string]string, fieldName string) telegraf.Metric {
for _, v := range metrics {
if v.Name() == name && containsAll(v.Tags(), tags) {
if len(fieldName) == 0 {
return v
}
if _, ok := v.Fields()[fieldName]; ok {
return v
}
}
}
return nil
}
func containsAll(t1 map[string]string, t2 map[string]string) bool {
for k, v := range t2 {
if foundValue, ok := t1[k]; !ok || v != foundValue {
return false
}
}
return true
}