Add new line protocol parser and serializer, influxdb output (#3924)
This commit is contained in:
@@ -1,29 +1,37 @@
|
||||
package influxdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/internal"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
|
||||
"github.com/influxdata/telegraf/plugins/outputs/influxdb/client"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/influx"
|
||||
)
|
||||
|
||||
var (
|
||||
// Quote Ident replacer.
|
||||
qiReplacer = strings.NewReplacer("\n", `\n`, `\`, `\\`, `"`, `\"`)
|
||||
defaultURL = "http://localhost:8086"
|
||||
|
||||
ErrMissingURL = errors.New("missing URL")
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Write(context.Context, []telegraf.Metric) error
|
||||
CreateDatabase(ctx context.Context) error
|
||||
|
||||
URL() string
|
||||
Database() string
|
||||
}
|
||||
|
||||
// InfluxDB struct is the primary data structure for the plugin
|
||||
type InfluxDB struct {
|
||||
// URL is only for backwards compatibility
|
||||
URL string
|
||||
URL string // url deprecated in 0.1.9; use urls
|
||||
URLs []string `toml:"urls"`
|
||||
Username string
|
||||
Password string
|
||||
@@ -46,36 +54,45 @@ type InfluxDB struct {
|
||||
// Use SSL but skip chain & host verification
|
||||
InsecureSkipVerify bool
|
||||
|
||||
// Precision is only here for legacy support. It will be ignored.
|
||||
Precision string
|
||||
Precision string // precision deprecated in 1.0; value is ignored
|
||||
|
||||
clients []client.Client
|
||||
clients []Client
|
||||
|
||||
CreateHTTPClientF func(config *HTTPConfig) (Client, error)
|
||||
CreateUDPClientF func(config *UDPConfig) (Client, error)
|
||||
|
||||
serializer *influx.Serializer
|
||||
}
|
||||
|
||||
var sampleConfig = `
|
||||
## The full HTTP or UDP URL for your InfluxDB instance.
|
||||
##
|
||||
## Multiple urls can be specified as part of the same cluster,
|
||||
## this means that only ONE of the urls will be written to each interval.
|
||||
# urls = ["udp://127.0.0.1:8089"] # UDP endpoint example
|
||||
urls = ["http://127.0.0.1:8086"] # required
|
||||
## The target database for metrics (telegraf will create it if not exists).
|
||||
database = "telegraf" # required
|
||||
## Multiple URLs can be specified for a single cluster, only ONE of the
|
||||
## urls will be written to each interval.
|
||||
# urls = ["udp://127.0.0.1:8089"]
|
||||
# urls = ["http://127.0.0.1:8086"]
|
||||
|
||||
## The target database for metrics; will be created as needed.
|
||||
# database = "telegraf"
|
||||
|
||||
## Name of existing retention policy to write to. Empty string writes to
|
||||
## the default retention policy.
|
||||
retention_policy = ""
|
||||
## Write consistency (clusters only), can be: "any", "one", "quorum", "all"
|
||||
write_consistency = "any"
|
||||
# retention_policy = ""
|
||||
|
||||
## Write timeout (for the InfluxDB client), formatted as a string.
|
||||
## If not provided, will default to 5s. 0s means no timeout (not recommended).
|
||||
timeout = "5s"
|
||||
## Write consistency (clusters only), can be: "any", "one", "quorum", "all"
|
||||
# write_consistency = "any"
|
||||
|
||||
## Timeout for HTTP messages.
|
||||
# timeout = "5s"
|
||||
|
||||
## HTTP Basic Auth
|
||||
# username = "telegraf"
|
||||
# password = "metricsmetricsmetricsmetrics"
|
||||
## Set the user agent for HTTP POSTs (can be useful for log differentiation)
|
||||
|
||||
## HTTP User-Agent
|
||||
# user_agent = "telegraf"
|
||||
## Set UDP payload size, defaults to InfluxDB UDP Client default (512 bytes)
|
||||
|
||||
## UDP payload size is the maximum packet size to send.
|
||||
# udp_payload = 512
|
||||
|
||||
## Optional SSL Config
|
||||
@@ -85,170 +102,181 @@ var sampleConfig = `
|
||||
## Use SSL but skip chain & host verification
|
||||
# insecure_skip_verify = false
|
||||
|
||||
## HTTP Proxy Config
|
||||
## HTTP Proxy override, if unset values the standard proxy environment
|
||||
## variables are consulted to determine which proxy, if any, should be used.
|
||||
# http_proxy = "http://corporate.proxy:3128"
|
||||
|
||||
## Optional HTTP headers
|
||||
## Additional HTTP headers
|
||||
# http_headers = {"X-Special-Header" = "Special-Value"}
|
||||
|
||||
## Compress each HTTP request payload using GZIP.
|
||||
# content_encoding = "gzip"
|
||||
## HTTP Content-Encoding for write request body, can be set to "gzip" to
|
||||
## compress body or "identity" to apply no encoding.
|
||||
# content_encoding = "identity"
|
||||
`
|
||||
|
||||
// Connect initiates the primary connection to the range of provided URLs
|
||||
func (i *InfluxDB) Connect() error {
|
||||
var urls []string
|
||||
urls = append(urls, i.URLs...)
|
||||
ctx := context.Background()
|
||||
|
||||
// Backward-compatibility with single Influx URL config files
|
||||
// This could eventually be removed in favor of specifying the urls as a list
|
||||
urls := make([]string, 0, len(i.URLs))
|
||||
urls = append(urls, i.URLs...)
|
||||
if i.URL != "" {
|
||||
urls = append(urls, i.URL)
|
||||
}
|
||||
|
||||
tlsConfig, err := internal.GetTLSConfig(
|
||||
i.SSLCert, i.SSLKey, i.SSLCA, i.InsecureSkipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
if len(urls) == 0 {
|
||||
urls = append(urls, defaultURL)
|
||||
}
|
||||
|
||||
i.serializer = influx.NewSerializer()
|
||||
|
||||
for _, u := range urls {
|
||||
switch {
|
||||
case strings.HasPrefix(u, "udp"):
|
||||
config := client.UDPConfig{
|
||||
URL: u,
|
||||
PayloadSize: i.UDPPayload,
|
||||
}
|
||||
c, err := client.NewUDP(config)
|
||||
u, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing url [%s]: %v", u, err)
|
||||
}
|
||||
|
||||
var proxy *url.URL
|
||||
if len(i.HTTPProxy) > 0 {
|
||||
proxy, err = url.Parse(i.HTTPProxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating UDP Client [%s]: %s", u, err)
|
||||
return fmt.Errorf("error parsing proxy_url [%s]: %v", proxy, err)
|
||||
}
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "udp", "udp4", "udp6":
|
||||
c, err := i.udpClient(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.clients = append(i.clients, c)
|
||||
case "http", "https":
|
||||
c, err := i.httpClient(ctx, u, proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.clients = append(i.clients, c)
|
||||
default:
|
||||
// If URL doesn't start with "udp", assume HTTP client
|
||||
config := client.HTTPConfig{
|
||||
URL: u,
|
||||
Timeout: i.Timeout.Duration,
|
||||
TLSConfig: tlsConfig,
|
||||
UserAgent: i.UserAgent,
|
||||
Username: i.Username,
|
||||
Password: i.Password,
|
||||
HTTPProxy: i.HTTPProxy,
|
||||
HTTPHeaders: client.HTTPHeaders{},
|
||||
ContentEncoding: i.ContentEncoding,
|
||||
}
|
||||
for header, value := range i.HTTPHeaders {
|
||||
config.HTTPHeaders[header] = value
|
||||
}
|
||||
wp := client.WriteParams{
|
||||
Database: i.Database,
|
||||
RetentionPolicy: i.RetentionPolicy,
|
||||
Consistency: i.WriteConsistency,
|
||||
}
|
||||
c, err := client.NewHTTP(config, wp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating HTTP Client [%s]: %s", u, err)
|
||||
}
|
||||
i.clients = append(i.clients, c)
|
||||
|
||||
err = c.Query(fmt.Sprintf(`CREATE DATABASE "%s"`, qiReplacer.Replace(i.Database)))
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "Status Code [403]") {
|
||||
log.Println("I! Database creation failed: " + err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unsupported scheme [%s]: %q", u, u.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close will terminate the session to the backend, returning error if an issue arises
|
||||
func (i *InfluxDB) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SampleConfig returns the formatted sample configuration for the plugin
|
||||
func (i *InfluxDB) Description() string {
|
||||
return "Configuration for sending metrics to InfluxDB"
|
||||
}
|
||||
|
||||
func (i *InfluxDB) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
// Description returns the human-readable function definition of the plugin
|
||||
func (i *InfluxDB) Description() string {
|
||||
return "Configuration for influxdb server to send metrics to"
|
||||
}
|
||||
|
||||
// Write will choose a random server in the cluster to write to until a successful write
|
||||
// occurs, logging each unsuccessful. If all servers fail, return error.
|
||||
// Write sends metrics to one of the configured servers, logging each
|
||||
// unsuccessful. If all servers fail, return an error.
|
||||
func (i *InfluxDB) Write(metrics []telegraf.Metric) error {
|
||||
r := metric.NewReader(metrics)
|
||||
|
||||
// This will get set to nil if a successful write occurs
|
||||
err := fmt.Errorf("Could not write to any InfluxDB server in cluster")
|
||||
ctx := context.Background()
|
||||
|
||||
var err error
|
||||
p := rand.Perm(len(i.clients))
|
||||
for _, n := range p {
|
||||
if e := i.clients[n].WriteStream(r); e != nil {
|
||||
// If the database was not found, try to recreate it:
|
||||
if strings.Contains(e.Error(), "database not found") {
|
||||
errc := i.clients[n].Query(fmt.Sprintf(`CREATE DATABASE "%s"`, qiReplacer.Replace(i.Database)))
|
||||
if errc != nil {
|
||||
log.Printf("E! Error: Database %s not found and failed to recreate\n",
|
||||
i.Database)
|
||||
client := i.clients[n]
|
||||
err = client.Write(ctx, metrics)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch apiError := err.(type) {
|
||||
case APIError:
|
||||
if apiError.Type == DatabaseNotFound {
|
||||
err := client.CreateDatabase(ctx)
|
||||
if err != nil {
|
||||
log.Printf("E! [outputs.influxdb] when writing to [%s]: database %q not found and failed to recreate",
|
||||
client.URL(), client.Database())
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(e.Error(), "field type conflict") {
|
||||
log.Printf("E! Field type conflict, dropping conflicted points: %s", e)
|
||||
// setting err to nil, otherwise we will keep retrying and points
|
||||
// w/ conflicting types will get stuck in the buffer forever.
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(e.Error(), "points beyond retention policy") {
|
||||
log.Printf("W! Points beyond retention policy: %s", e)
|
||||
// This error is indicates the point is older than the
|
||||
// retention policy permits, and is probably not a cause for
|
||||
// concern. Retrying will not help unless the retention
|
||||
// policy is modified.
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(e.Error(), "unable to parse") {
|
||||
log.Printf("E! Parse error; dropping points: %s", e)
|
||||
// This error indicates a bug in Telegraf or InfluxDB parsing
|
||||
// of line protocol. Retries will not be successful.
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(e.Error(), "hinted handoff queue not empty") {
|
||||
// This is an informational message
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
// Log write failure
|
||||
log.Printf("E! InfluxDB Output Error: %s", e)
|
||||
} else {
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("E! [outputs.influxdb]: when writing to [%s]: %v", client.URL(), err)
|
||||
}
|
||||
|
||||
return err
|
||||
return errors.New("could not write any address")
|
||||
}
|
||||
|
||||
func newInflux() *InfluxDB {
|
||||
return &InfluxDB{
|
||||
Timeout: internal.Duration{Duration: time.Second * 5},
|
||||
func (i *InfluxDB) udpClient(url *url.URL) (Client, error) {
|
||||
config := &UDPConfig{
|
||||
URL: url,
|
||||
MaxPayloadSize: i.UDPPayload,
|
||||
Serializer: i.serializer,
|
||||
}
|
||||
|
||||
c, err := i.CreateUDPClientF(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating UDP client [%s]: %v", url, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (i *InfluxDB) httpClient(ctx context.Context, url *url.URL, proxy *url.URL) (Client, error) {
|
||||
tlsConfig, err := internal.GetTLSConfig(
|
||||
i.SSLCert, i.SSLKey, i.SSLCA, i.InsecureSkipVerify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &HTTPConfig{
|
||||
URL: url,
|
||||
Timeout: i.Timeout.Duration,
|
||||
TLSConfig: tlsConfig,
|
||||
UserAgent: i.UserAgent,
|
||||
Username: i.Username,
|
||||
Password: i.Password,
|
||||
Proxy: proxy,
|
||||
ContentEncoding: i.ContentEncoding,
|
||||
Headers: i.HTTPHeaders,
|
||||
Database: i.Database,
|
||||
RetentionPolicy: i.RetentionPolicy,
|
||||
Consistency: i.WriteConsistency,
|
||||
Serializer: i.serializer,
|
||||
}
|
||||
|
||||
c, err := i.CreateHTTPClientF(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating HTTP client [%s]: %v", url, err)
|
||||
}
|
||||
|
||||
err = c.CreateDatabase(ctx)
|
||||
if err != nil {
|
||||
if err, ok := err.(APIError); ok {
|
||||
if err.StatusCode == 503 {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("W! [outputs.influxdb] when writing to [%s]: database %q creation failed: %v",
|
||||
c.URL(), c.Database(), err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("influxdb", func() telegraf.Output { return newInflux() })
|
||||
outputs.Add("influxdb", func() telegraf.Output {
|
||||
return &InfluxDB{
|
||||
Timeout: internal.Duration{Duration: time.Second * 5},
|
||||
CreateHTTPClientF: func(config *HTTPConfig) (Client, error) {
|
||||
return NewHTTPClient(config)
|
||||
},
|
||||
CreateUDPClientF: func(config *UDPConfig) (Client, error) {
|
||||
return NewUDPClient(config)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user