From f454ca7c8a604b132c77d0ce3f920fd5048104af Mon Sep 17 00:00:00 2001 From: Henry Hu Date: Mon, 1 Feb 2016 11:43:38 +0800 Subject: [PATCH] Add Graphite line protocol listen, command and tail plugin support... --- Godeps | 1 + Godeps_windows | 1 + README.md | 3 + plugins/inputs/all/all.go | 3 + plugins/inputs/execline/README.md | 151 +++++++++++ plugins/inputs/execline/config.go | 172 ++++++++++++ plugins/inputs/execline/errors.go | 14 + plugins/inputs/execline/execline.go | 179 +++++++++++++ plugins/inputs/execline/parser.go | 392 ++++++++++++++++++++++++++++ plugins/inputs/graphite/README.md | 160 ++++++++++++ plugins/inputs/graphite/config.go | 205 +++++++++++++++ plugins/inputs/graphite/errors.go | 14 + plugins/inputs/graphite/graphite.go | 315 ++++++++++++++++++++++ plugins/inputs/graphite/parser.go | 392 ++++++++++++++++++++++++++++ plugins/inputs/tail/README.md | 159 +++++++++++ plugins/inputs/tail/config.go | 172 ++++++++++++ plugins/inputs/tail/errors.go | 14 + plugins/inputs/tail/parser.go | 392 ++++++++++++++++++++++++++++ plugins/inputs/tail/tail.go | 186 +++++++++++++ 19 files changed, 2925 insertions(+) create mode 100644 plugins/inputs/execline/README.md create mode 100644 plugins/inputs/execline/config.go create mode 100644 plugins/inputs/execline/errors.go create mode 100644 plugins/inputs/execline/execline.go create mode 100644 plugins/inputs/execline/parser.go create mode 100644 plugins/inputs/graphite/README.md create mode 100644 plugins/inputs/graphite/config.go create mode 100644 plugins/inputs/graphite/errors.go create mode 100644 plugins/inputs/graphite/graphite.go create mode 100644 plugins/inputs/graphite/parser.go create mode 100644 plugins/inputs/tail/README.md create mode 100644 plugins/inputs/tail/config.go create mode 100644 plugins/inputs/tail/errors.go create mode 100644 plugins/inputs/tail/parser.go create mode 100644 plugins/inputs/tail/tail.go diff --git a/Godeps b/Godeps index 3393a1cee..3967c32b8 100644 --- a/Godeps +++ b/Godeps @@ -57,3 +57,4 @@ gopkg.in/dancannon/gorethink.v1 6f088135ff288deb9d5546f4c71919207f891a70 gopkg.in/fatih/pool.v2 cba550ebf9bce999a02e963296d4bc7a486cb715 gopkg.in/mgo.v2 03c9f3ee4c14c8e51ee521a6a7d0425658dd6f64 gopkg.in/yaml.v2 f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 +github.com/hpcloud/tail 1a0242e795eeefe54261ff308dc685f7d29cc58c \ No newline at end of file diff --git a/Godeps_windows b/Godeps_windows index 8f147ed87..0a661a901 100644 --- a/Godeps_windows +++ b/Godeps_windows @@ -61,3 +61,4 @@ gopkg.in/dancannon/gorethink.v1 6f088135ff288deb9d5546f4c71919207f891a70 gopkg.in/fatih/pool.v2 cba550ebf9bce999a02e963296d4bc7a486cb715 gopkg.in/mgo.v2 03c9f3ee4c14c8e51ee521a6a7d0425658dd6f64 gopkg.in/yaml.v2 f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 +github.com/hpcloud/tail 1a0242e795eeefe54261ff308dc685f7d29cc58c \ No newline at end of file diff --git a/README.md b/README.md index 506ac301b..0acd8d9e5 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,9 @@ Currently implemented sources: * docker * elasticsearch * exec (generic JSON-emitting executable plugin) +* execline (generic Graphite-line-protocol-emitting executable plugin) +* graphite (Graphite line protocol listen service) +* tail (Plugin to tail the files to process graphite line protocol contents) * haproxy * httpjson (generic JSON-emitting http service plugin) * influxdb diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 52ab428f8..bc3d6f311 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -8,7 +8,9 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/docker" _ "github.com/influxdata/telegraf/plugins/inputs/elasticsearch" _ "github.com/influxdata/telegraf/plugins/inputs/exec" + _ "github.com/influxdata/telegraf/plugins/inputs/execline" _ "github.com/influxdata/telegraf/plugins/inputs/github_webhooks" + _ "github.com/influxdata/telegraf/plugins/inputs/graphite" _ "github.com/influxdata/telegraf/plugins/inputs/haproxy" _ "github.com/influxdata/telegraf/plugins/inputs/httpjson" _ "github.com/influxdata/telegraf/plugins/inputs/influxdb" @@ -38,6 +40,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/sqlserver" _ "github.com/influxdata/telegraf/plugins/inputs/statsd" _ "github.com/influxdata/telegraf/plugins/inputs/system" + _ "github.com/influxdata/telegraf/plugins/inputs/tail" _ "github.com/influxdata/telegraf/plugins/inputs/trig" _ "github.com/influxdata/telegraf/plugins/inputs/twemproxy" _ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters" diff --git a/plugins/inputs/execline/README.md b/plugins/inputs/execline/README.md new file mode 100644 index 000000000..d73520e11 --- /dev/null +++ b/plugins/inputs/execline/README.md @@ -0,0 +1,151 @@ +# ExecLine Plugin +The exec plugin can execute arbitrary commands which output graphite line protocol. + +## Parsing Metrics + +The graphite plugin allows measurements to be saved using the graphite line protocol. By default, enabling the graphite plugin will allow you to collect metrics and store them using the metric name as the measurement. If you send a metric named `servers.localhost.cpu.loadavg.10`, it will store the full metric name as the measurement with no extracted tags. + +While this default setup works, it is not the ideal way to store measurements in InfluxDB since it does not take advantage of tags. It also will not perform optimally with a large dataset sizes since queries will be forced to use regexes which is known to not scale well. + +To extract tags from metrics, one or more templates must be configured to parse metrics into tags and measurements. + +## Templates + +Templates allow matching parts of a metric name to be used as tag keys in the stored metric. They have a similar format to graphite metric names. The values in between the separators are used as the tag keys. The location of the tag key that matches the same position as the graphite metric section is used as the value. If there is no value, the graphite portion is skipped. + +The special value _measurement_ is used to define the measurement name. It can have a trailing `*` to indicate that the remainder of the metric should be used. If a _measurement_ is not specified, the full metric name is used. + +### Basic Matching + +`servers.localhost.cpu.loadavg.10` +* Template: `.host.resource.measurement*` +* Output: _measurement_ =`loadavg.10` _tags_ =`host=localhost resource=cpu` + +### Multiple Measurement Matching + +The _measurement_ can be specified multiple times in a template to provide more control over the measurement name. Multiple values +will be joined together using the _Separator_ config variable. By default, this value is `.`. + +`servers.localhost.cpu.cpu0.user` +* Template: `.host.measurement.cpu.measurement` +* Output: _measurement_ = `cpu.user` _tags_ = `host=localhost cpu=cpu0` + +Since '.' requires queries on measurements to be double-quoted, you may want to set this to `_` to simplify querying parsed metrics. + +`servers.localhost.cpu.cpu0.user` +* Separator: `_` +* Template: `.host.measurement.cpu.measurement` +* Output: _measurement_ = `cpu_user` _tags_ = `host=localhost cpu=cpu0` + +### Adding Tags + +Additional tags can be added to a metric that don't exist on the received metric. You can add additional tags by specifying them after the pattern. Tags have the same format as the line protocol. Multiple tags are separated by commas. + +`servers.localhost.cpu.loadavg.10` +* Template: `.host.resource.measurement* region=us-west,zone=1a` +* Output: _measurement_ = `loadavg.10` _tags_ = `host=localhost resource=cpu region=us-west zone=1a` + +### Fields + +A field key can be specified by using the keyword _field_. By default if no _field_ keyword is specified then the metric will be written to a field named _value_. + +When using the current default engine _BZ1_, it's recommended to use a single field per value for performance reasons. + +When using the _TSM1_ engine it's possible to amend measurement metrics with additional fields, e.g: + +Input: +``` +sensu.metric.net.server0.eth0.rx_packets 461295119435 1444234982 +sensu.metric.net.server0.eth0.tx_bytes 1093086493388480 1444234982 +sensu.metric.net.server0.eth0.rx_bytes 1015633926034834 1444234982 +sensu.metric.net.server0.eth0.tx_errors 0 1444234982 +sensu.metric.net.server0.eth0.rx_errors 0 1444234982 +sensu.metric.net.server0.eth0.tx_dropped 0 1444234982 +sensu.metric.net.server0.eth0.rx_dropped 0 1444234982 +``` + +With template: +``` +sensu.metric.* ..measurement.host.interface.field +``` + +Becomes database entry: +``` +> select * from net +name: net +--------- +time host interface rx_bytes rx_dropped rx_errors rx_packets tx_bytes tx_dropped tx_errors +1444234982000000000 server0 eth0 1.015633926034834e+15 0 0 4.61295119435e+11 1.09308649338848e+15 0 0 +``` + +## Multiple Templates + +One template may not match all metrics. For example, using multiple plugins with diamond will produce metrics in different formats. If you need to use multiple templates, you'll need to define a prefix filter that must match before the template can be applied. + +### Filters + +Filters have a similar format to templates but work more like wildcard expressions. When multiple filters would match a metric, the more specific one is chosen. Filters are configured by adding them before the template. + +For example, + +``` +servers.localhost.cpu.loadavg.10 +servers.host123.elasticsearch.cache_hits 100 +servers.host456.mysql.tx_count 10 +servers.host789.prod.mysql.tx_count 10 +``` +* `servers.*` would match all values +* `servers.*.mysql` would match `servers.host456.mysql.tx_count 10` +* `servers.localhost.*` would match `servers.localhost.cpu.loadavg` +* `servers.*.*.mysql` would match `servers.host789.prod.mysql.tx_count 10` + +## Default Templates + +If no template filters are defined or you want to just have one basic template, you can define a default template. This template will apply to any metric that has not already matched a filter. + +``` +dev.http.requests.200 +prod.myapp.errors.count +dev.db.queries.count +``` + +* `env.app.measurement*` would create + * _measurement_=`requests.200` _tags_=`env=dev,app=http` + * _measurement_= `errors.count` _tags_=`env=prod,app=myapp` + * _measurement_=`queries.count` _tags_=`env=dev,app=db` + +## Global Tags + +If you need to add the same set of tags to all metrics, you can define them globally at the plugin level and not within each template description. + + +# Minimal Config + +``` + # NOTE This execline plugin only reads numerical measurements output by commands, + # strings and booleans ill be ignored. + commands = ["/tmp/test.sh","/tmp/test2.sh"] # the bind address + + ### If matching multiple measurement files, this string will be used to join the matched values. + separator = "." + + ### Default tags that will be added to all metrics. These can be overridden at the template level + ### or by tags extracted from metric + tags = ["region=north-china", "zone=1c"] + + ### Each template line requires a template pattern. It can have an optional + ### filter before the template and separated by spaces. It can also have optional extra + ### tags following the template. Multiple tags should be separated by commas and no spaces + ### similar to the line protocol format. The can be only one default template. + ### Templates support below format: + ### filter + template + ### filter + template + extra tag + ### filter + template with field key + ### default template. Ignore the first graphite component "servers" + templates = [ + "*.app env.service.resource.measurement", + "stats.* .host.measurement* region=us-west,agent=sensu", + "stats2.* .host.measurement.field", + "measurement*" + ] +``` diff --git a/plugins/inputs/execline/config.go b/plugins/inputs/execline/config.go new file mode 100644 index 000000000..424b9e47d --- /dev/null +++ b/plugins/inputs/execline/config.go @@ -0,0 +1,172 @@ +package execline + +import ( + "fmt" + "strings" + + "github.com/influxdata/influxdb/models" +) + +const ( + // DefaultSeparator is the default join character to use when joining multiple + // measurment parts in a template. + DefaultSeparator = "." +) + +// Config represents the configuration for Graphite endpoints. +type Config struct { + Commands []string + Separator string + Tags []string + Templates []string +} + +// WithDefaults takes the given config and returns a new config with any required +// default values set. +func (c *Config) WithDefaults() *Config { + d := *c + if d.Separator == "" { + d.Separator = DefaultSeparator + } + return &d +} + +// DefaultTags returns the config's tags. +func (c *Config) DefaultTags() models.Tags { + tags := models.Tags{} + for _, t := range c.Tags { + parts := strings.Split(t, "=") + tags[parts[0]] = parts[1] + } + return tags +} + +// Validate validates the config's templates and tags. +func (c *Config) Validate() error { + if err := c.validateTemplates(); err != nil { + return err + } + + if err := c.validateTags(); err != nil { + return err + } + + return nil +} + +func (c *Config) validateTemplates() error { + // map to keep track of filters we see + filters := map[string]struct{}{} + + for i, t := range c.Templates { + parts := strings.Fields(t) + // Ensure template string is non-empty + if len(parts) == 0 { + return fmt.Errorf("missing template at position: %d", i) + } + if len(parts) == 1 && parts[0] == "" { + return fmt.Errorf("missing template at position: %d", i) + } + + if len(parts) > 3 { + return fmt.Errorf("invalid template format: '%s'", t) + } + + template := t + filter := "" + tags := "" + if len(parts) >= 2 { + // We could have