diff --git a/plugins/outputs/wavefront/README.md b/plugins/outputs/wavefront/README.md new file mode 100644 index 0000000000000..3e9788c339c38 --- /dev/null +++ b/plugins/outputs/wavefront/README.md @@ -0,0 +1,117 @@ +# Wavefront Output Plugin + +This plugin writes to a [Wavefront](https://www.wavefront.com) proxy, in Wavefront data format over TCP. + + +## Wavefront Data format + +The expected input for Wavefront is specified in the following way: + +``` + [] = [tagk1=tagv1 ...tagkN=tagvN] +``` + +More information about the Wavefront data format is available [here](https://community.wavefront.com/docs/DOC-1031) + + +By default, to ease Metrics browsing in the Wavefront UI, metrics are grouped by converting any `_` characters to `.` in the final name. +This behavior can be altered by changing the `metric_separator` and/or the `convert_paths` settings. +Most illegal characters in the metric name are automatically converted to `-`. +The `use_regex` setting can be used to ensure all illegal characters are properly handled, but can lead to performance degradation. + +## Configuration: + +```toml +# Configuration for Wavefront output +[[outputs.wavefront]] + ## prefix for metrics keys + prefix = "my.specific.prefix." + + ## DNS name of the wavefront proxy server + host = "wavefront.example.com" + + ## Port that the Wavefront proxy server listens on + port = 2878 + + ## wether to use "value" for name of simple fields + simple_fields = false + + ## character to use between metric and field name. defaults to . (dot) + metric_separator = "." + + ## Convert metric name paths to use metricSeperator character + ## When true (default) will convert all _ (underscore) chartacters in final metric name + convert_paths = true + + ## Use Regex to sanitize metric and tag names from invalid characters + ## Regex is more thorough, but significantly slower + use_regex = false + + ## point tags to use as the source name for Wavefront (if none found, host will be used) + source_tags = ["hostname", "snmp_host", "node_host"] + + ## Print additional debug information requires debug = true at the agent level + debug_all = false +``` + +Parameters: + + Prefix string + Host string + Port int + SimpleFields bool + MetricSeparator string + ConvertPaths bool + UseRegex bool + SourceTags string + DebugAll bool + +* `prefix`: String to use as a prefix for all sent metrics. +* `host`: Name of Wavefront proxy server +* `port`: Port that Wavefront proxy server is configured for `pushListenerPorts` +* `simple_fields`: if false (default) metric field names called `value` are converted to empty strings +* `metric_separator`: character to use to separate metric and field names. (default is `_`) +* `convert_paths`: if true (default) will convert all `_` in metric and field names to `metric_seperator` +* `use_regex`: if true (default is false) will use regex to ensure all illegal characters are converted to `-`. Regex is much slower than the default mode which will catch most illegal characters. Use with caution. +* `source_tags`: ordered list of point tags to use as the source name for Wavefront. Once a match a found that tag is used for that point. If no tags are found the host tag will be used. +* `debug_all`: Will output additional debug information. Requires `debug = true` to be configured at the agent level + + +## + +The Wavefront proxy interface can be simulated with this reader: + +``` +// wavefront_proxy_mock.go +package main + +import ( + "io" + "log" + "net" + "os" +) + +func main() { + l, err := net.Listen("tcp", "localhost:2878") + if err != nil { + log.Fatal(err) + } + defer l.Close() + for { + conn, err := l.Accept() + if err != nil { + log.Fatal(err) + } + go func(c net.Conn) { + defer c.Close() + io.Copy(os.Stdout, c) + }(conn) + } +} + +``` + +## Allowed values for metrics + +Wavefront allows `integers` and `floats` as input values diff --git a/plugins/outputs/wavefront/wavefront.go b/plugins/outputs/wavefront/wavefront.go index 7bd5fab9d507d..1f20619f9c9b1 100644 --- a/plugins/outputs/wavefront/wavefront.go +++ b/plugins/outputs/wavefront/wavefront.go @@ -1,5 +1,3 @@ - - package wavefront import ( @@ -16,15 +14,14 @@ import ( ) type Wavefront struct { + Prefix string Host string Port int - Prefix string SimpleFields bool MetricSeparator string ConvertPaths bool UseRegex bool - - Debug bool + SourceTags []string DebugAll bool } @@ -33,16 +30,18 @@ var sanitizedChars = strings.NewReplacer( "!", "-", "@", "-", "#", "-", "$", "-", "%", "-", "^", "-", "&", "-", "*", "-", "(", "-", ")", "-", "+", "-", "`", "-", "'", "-", "\"", "-", "[", "-", "]", "-", "{", "-", "}", "-", ":", "-", ";", "-", "<", "-", - ">", "-", ",", "-", "?", "-", "\\", "-", "|", "-", " ", "-", + ">", "-", ",", "-", "?", "-", "/", "-", "\\", "-", "|", "-", " ", "-", ) // instead of Replacer which may miss some special characters we can use a regex pattern, but this is significantly slower than Replacer var sanitizedRegex, _ = regexp.Compile("[^a-zA-Z\\d_.-]") +var tagValueReplacer = strings.NewReplacer("\"", "\\\"", "*", "-") + var pathReplacer = strings.NewReplacer("_", "_") var sampleConfig = ` ## prefix for metrics keys - prefix = "my.specific.prefix." + #prefix = "my.specific.prefix." ## DNS name of the wavefront proxy server host = "wavefront.example.com" @@ -51,21 +50,24 @@ var sampleConfig = ` port = 2878 ## wether to use "value" for name of simple fields - simple_fields = false + #simple_fields = false ## character to use between metric and field name. defaults to . (dot) - metric_separator = "." + #metric_separator = "." ## Convert metric name paths to use metricSeperator character - ## When true (edfault) will convert all _ (underscore) chartacters in final metric name - convert_paths = true + ## When true (default) will convert all _ (underscore) chartacters in final metric name + #convert_paths = true ## Use Regex to sanitize metric and tag names from invalid characters ## Regex is more thorough, but significantly slower - use_regex = false + #use_regex = false + + ## point tags to use as the source name for Wavefront (if none found, host will be used) + #source_tags = ["hostname", "snmp_host", "node_host"] - ## Print all Wavefront communication - debug = false + ## Print additional debug information requires debug = true at the agent level + #debug_all = false ` type MetricLine struct { @@ -115,9 +117,7 @@ func (w *Wavefront) Write(metrics []telegraf.Metric) error { for _, m := range metrics { for _, metric := range buildMetrics(m, w) { messageLine := fmt.Sprintf("%s %s %v %s\n", metric.Metric, metric.Value, metric.Timestamp, metric.Tags) - if w.Debug { - log.Printf("DEBUG: output [wavefront] %s", messageLine) - } + log.Printf("D! Output [wavefront] %s", messageLine) _, err := connection.Write([]byte(messageLine)) if err != nil { return fmt.Errorf("Wavefront: TCP writing error %s", err.Error()) @@ -129,34 +129,53 @@ func (w *Wavefront) Write(metrics []telegraf.Metric) error { } func buildTags(mTags map[string]string, w *Wavefront) []string { + sourceTagFound := false + + for _, s := range w.SourceTags { + for k, v := range mTags { + if k == s { + mTags["source"] = v + sourceTagFound = true + delete(mTags, k) + break + } + } + if sourceTagFound { + break + } + } + + if !sourceTagFound { + mTags["source"] = mTags["host"] + } + mTags["telegraf_host"] = mTags["host"] + delete(mTags, "host") + tags := make([]string, len(mTags)) index := 0 for k, v := range mTags { - if k == "host" { - k = "source" - } - if w.UseRegex { - tags[index] = fmt.Sprintf("%s=\"%s\"", sanitizedRegex.ReplaceAllString(k, "-"), sanitizedRegex.ReplaceAllString(v, "-")) + tags[index] = fmt.Sprintf("%s=\"%s\"", sanitizedRegex.ReplaceAllString(k, "-"), tagValueReplacer.Replace(v)) } else { - tags[index] = fmt.Sprintf("%s=\"%s\"", sanitizedChars.Replace(k), sanitizedChars.Replace(v)) + tags[index] = fmt.Sprintf("%s=\"%s\"", sanitizedChars.Replace(k), tagValueReplacer.Replace(v)) } index++ } + sort.Strings(tags) return tags } func buildMetrics(m telegraf.Metric, w *Wavefront) []*MetricLine { if w.DebugAll { - log.Printf("DEBUG: output [wavefront] original name: %s\n", m.Name()) + log.Printf("D! Output [wavefront] original name: %s\n", m.Name()) } ret := []*MetricLine{} for fieldName, value := range m.Fields() { if w.DebugAll { - log.Printf("DEBUG: output [wavefront] original field: %s\n", fieldName) + log.Printf("D! Output [wavefront] original field: %s\n", fieldName) } var name string @@ -182,7 +201,7 @@ func buildMetrics(m telegraf.Metric, w *Wavefront) []*MetricLine { } metricValue, buildError := buildValue(value, metric.Metric) if buildError != nil { - log.Printf("ERROR: output [wavefront] %s\n", buildError.Error()) + log.Printf("E! Output [wavefront] %s\n", buildError.Error()) continue } metric.Value = metricValue @@ -240,4 +259,3 @@ func init() { } }) } - diff --git a/plugins/outputs/wavefront/wavefront_test.go b/plugins/outputs/wavefront/wavefront_test.go index 0c09ccaa75f32..09ac28bfd481d 100644 --- a/plugins/outputs/wavefront/wavefront_test.go +++ b/plugins/outputs/wavefront/wavefront_test.go @@ -18,7 +18,39 @@ func defaultWavefront() *Wavefront { MetricSeparator: ".", ConvertPaths: true, UseRegex: false, - Debug: true, + } +} + +func TestSourceTags(t *testing.T) { + w := defaultWavefront() + w.SourceTags = []string{"snmp_host", "hostagent"} + + var tagtests = []struct { + ptIn map[string]string + outTags []string + }{ + { + map[string]string{"snmp_host": "realHost", "host": "origHost"}, + []string{"source=\"realHost\"", "telegraf_host=\"origHost\""}, + }, + { + map[string]string{"hostagent": "realHost", "host": "origHost"}, + []string{"source=\"realHost\"", "telegraf_host=\"origHost\""}, + }, + { + map[string]string{"hostagent": "abc", "snmp_host": "realHost", "host": "origHost"}, + []string{"hostagent=\"abc\"", "source=\"realHost\"", "telegraf_host=\"origHost\""}, + }, + { + map[string]string{"something": "abc", "host": "realHost"}, + []string{"something=\"abc\"", "source=\"realHost\"", "telegraf_host=\"realHost\""}, + }, + } + for _, tt := range tagtests { + tags := buildTags(tt.ptIn, w) + if !reflect.DeepEqual(tags, tt.outTags) { + t.Errorf("\nexpected\t%+v\nreceived\t%+v\n", tt.outTags, tags) + } } } @@ -38,9 +70,9 @@ func TestBuildMetricsNoSimpleFields(t *testing.T) { ) var metricTests = []struct { - metric telegraf.Metric + metric telegraf.Metric metricLines []MetricLine - } { + }{ { testutil.TestMetric(float64(1.0), "testing_just*a%metric:float"), []MetricLine{{Metric: w.Prefix + "testing.just-a-metric-float", Value: "1.000000"}}, @@ -78,9 +110,9 @@ func TestBuildMetricsWithSimpleFields(t *testing.T) { ) var metricTests = []struct { - metric telegraf.Metric + metric telegraf.Metric metricLines []MetricLine - } { + }{ { testutil.TestMetric(float64(1.0), "testing_just*a%metric:float"), []MetricLine{{Metric: w.Prefix + "testing.just-a-metric-float.value", Value: "1.000000"}}, @@ -111,28 +143,24 @@ func TestBuildTags(t *testing.T) { outTags []string }{ { - map[string]string{"one": "two", "three": "four"}, - []string{"one=\"two\"", "three=\"four\""}, + map[string]string{"one": "two", "three": "four", "host": "testHost"}, + []string{"one=\"two\"", "source=\"testHost\"", "telegraf_host=\"testHost\"", "three=\"four\""}, }, { - map[string]string{"aaa": "bbb"}, - []string{"aaa=\"bbb\""}, + map[string]string{"aaa": "bbb", "host": "testHost"}, + []string{"aaa=\"bbb\"", "source=\"testHost\"", "telegraf_host=\"testHost\""}, }, { - map[string]string{"bbb": "789", "aaa": "123"}, - []string{"aaa=\"123\"", "bbb=\"789\""}, + map[string]string{"bbb": "789", "aaa": "123", "host": "testHost"}, + []string{"aaa=\"123\"", "bbb=\"789\"", "source=\"testHost\"", "telegraf_host=\"testHost\""}, }, { map[string]string{"host": "aaa", "dc": "bbb"}, - []string{"dc=\"bbb\"", "source=\"aaa\""}, + []string{"dc=\"bbb\"", "source=\"aaa\"", "telegraf_host=\"aaa\""}, }, { - map[string]string{"Sp%ci@l Chars": "\"g$t repl#ced"}, - []string{"Sp-ci-l-Chars=\"-g-t-repl-ced\""}, - }, - { - map[string]string{}, - []string{}, + map[string]string{"Sp%ci@l Chars": "\"g*t repl#ced", "host": "testHost"}, + []string{"Sp-ci-l-Chars=\"\\\"g-t repl#ced\"", "source=\"testHost\"", "telegraf_host=\"testHost\""}, }, } for _, tt := range tagtests { @@ -179,4 +207,5 @@ func TestBuildTags(t *testing.T) { // err = w.Write(metrics) // require.NoError(t, err) -// } \ No newline at end of file +// } + diff --git a/telegraf.conf b/telegraf.conf index 56f7448aec827..c3d29ae5261e0 100644 --- a/telegraf.conf +++ b/telegraf.conf @@ -48,7 +48,7 @@ ## Default flushing interval for all outputs. You shouldn't set this below ## interval. Maximum flush_interval will be flush_interval + flush_jitter - flush_interval = "10s" + flush_interval = "60s" ## Jitter the flush interval by a random amount. This is primarily to avoid ## large write spikes for users running a large number of telegraf instances. ## ie, a jitter of 5s and interval 10s means flushes will happen every 10-15s @@ -77,55 +77,37 @@ # # Configuration for Wavefront proxy to send metrics to [[outputs.wavefront]] -# prefix = "telegraf." - host = "192.168.99.100" + ## prefix for metrics keys + #prefix = "my.specific.prefix." + + ## DNS name of the wavefront proxy server + host = "localhost" + + ## Port that the Wavefront proxy server listens on port = 2878 - metric_separator = "." - convert_paths = true - use_regex = false + + ## wether to use "value" for name of simple fields + #simple_fields = false + + ## character to use between metric and field name. defaults to . (dot) + #metric_separator = "." + + ## Convert metric name paths to use metricSeperator character + ## When true (default) will convert all _ (underscore) chartacters in final metric name + #convert_paths = true + + ## Use Regex to sanitize metric and tag names from invalid characters + ## Regex is more thorough, but significantly slower + #use_regex = false + + ## point tags to use as the source name for Wavefront (if none found, host will be used) + #source_tags = ["hostname", "snmp_host", "node_host"] + ############################################################################### # INPUT PLUGINS # ############################################################################### -# Statsd Server -#[[inputs.statsd]] - ## Address and port to host UDP listener on - #service_address = ":8125" - ## Delete gauges every interval (default=false) - #delete_gauges = false - ## Delete counters every interval (default=false) - #delete_counters = false - ## Delete sets every interval (default=false) - #delete_sets = false - ## Delete timings & histograms every interval (default=true) - #delete_timings = true - ## Percentiles to calculate for timing & histogram stats - #percentiles = [90] - - ## separator to use between elements of a statsd metric - #metric_separator = "_" - - ## Parses tags in the datadog statsd format - ## http://docs.datadoghq.com/guides/dogstatsd/ - #parse_data_dog_tags = false - - ## Statsd data translation templates, more info can be read here: - ## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md#graphite - # templates = [ - # "cpu.* measurement*" - # ] - - ## Number of UDP messages allowed to queue up, once filled, - ## the statsd server will start dropping packets - #allowed_pending_messages = 10000 - - ## Number of timing/histogram values to track per-measurement in the - ## calculation of percentiles. Raising this limit increases the accuracy - ## of percentiles but also increases the memory usage and cpu time. - #percentile_limit = 1000 - - # Read metrics about cpu usage [[inputs.cpu]] ## Whether to report per-cpu stats or not