github.com/Jeffail/benthos/v3@v3.65.0/lib/metrics/influxdb.go (about)

     1  package metrics
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"net/url"
     8  	"time"
     9  
    10  	"github.com/Jeffail/benthos/v3/internal/docs"
    11  	"github.com/Jeffail/benthos/v3/lib/log"
    12  	btls "github.com/Jeffail/benthos/v3/lib/util/tls"
    13  	client "github.com/influxdata/influxdb1-client/v2"
    14  	"github.com/rcrowley/go-metrics"
    15  )
    16  
    17  func init() {
    18  	Constructors[TypeInfluxDB] = TypeSpec{
    19  		constructor: NewInfluxDB,
    20  		Status:      docs.StatusExperimental,
    21  		Version:     "3.36.0",
    22  		Summary: `
    23  Send metrics to InfluxDB 1.x using the ` + "`/write`" + ` endpoint.`,
    24  		Description: `See https://docs.influxdata.com/influxdb/v1.8/tools/api/#write-http-endpoint for further details on the write API.`,
    25  		FieldSpecs: docs.FieldSpecs{
    26  			docs.FieldCommon("url", "A URL of the format `[https|http|udp]://host:port` to the InfluxDB host."),
    27  			docs.FieldCommon("db", "The name of the database to use."),
    28  			btls.FieldSpec(),
    29  			docs.FieldAdvanced("username", "A username (when applicable)."),
    30  			docs.FieldAdvanced("password", "A password (when applicable)."),
    31  			docs.FieldAdvanced("include", "Optional additional metrics to collect, enabling these metrics may have some performance implications as it acquires a global semaphore and does `stoptheworld()`.").WithChildren(
    32  				docs.FieldCommon("runtime", "A duration string indicating how often to poll and collect runtime metrics. Leave empty to disable this metric", "1m").HasDefault(""),
    33  				docs.FieldCommon("debug_gc", "A duration string indicating how often to poll and collect GC metrics. Leave empty to disable this metric.", "1m").HasDefault(""),
    34  			),
    35  			docs.FieldAdvanced("interval", "A duration string indicating how often metrics should be flushed."),
    36  			docs.FieldAdvanced("ping_interval", "A duration string indicating how often to ping the host."),
    37  			docs.FieldAdvanced("precision", "[ns|us|ms|s] timestamp precision passed to write api."),
    38  			docs.FieldAdvanced("timeout", "How long to wait for response for both ping and writing metrics."),
    39  			docs.FieldString("tags", "Global tags added to each metric.",
    40  				map[string]string{
    41  					"hostname": "localhost",
    42  					"zone":     "danger",
    43  				},
    44  			).Map().Advanced(),
    45  			docs.FieldAdvanced("retention_policy", "Sets the retention policy for each write."),
    46  			docs.FieldAdvanced("write_consistency", "[any|one|quorum|all] sets write consistency when available."),
    47  			pathMappingDocs(true, false),
    48  		},
    49  	}
    50  }
    51  
    52  // InfluxDBConfig is config for the influx metrics type.
    53  type InfluxDBConfig struct {
    54  	URL string `json:"url" yaml:"url"`
    55  	DB  string `json:"db" yaml:"db"`
    56  
    57  	TLS              btls.Config     `json:"tls" yaml:"tls"`
    58  	Interval         string          `json:"interval" yaml:"interval"`
    59  	Password         string          `json:"password" yaml:"password"`
    60  	PingInterval     string          `json:"ping_interval" yaml:"ping_interval"`
    61  	Precision        string          `json:"precision" yaml:"precision"`
    62  	Timeout          string          `json:"timeout" yaml:"timeout"`
    63  	Username         string          `json:"username" yaml:"username"`
    64  	RetentionPolicy  string          `json:"retention_policy" yaml:"retention_policy"`
    65  	WriteConsistency string          `json:"write_consistency" yaml:"write_consistency"`
    66  	Include          InfluxDBInclude `json:"include" yaml:"include"`
    67  
    68  	PathMapping string            `json:"path_mapping" yaml:"path_mapping"`
    69  	Tags        map[string]string `json:"tags" yaml:"tags"`
    70  }
    71  
    72  // InfluxDBInclude contains configuration parameters for optional metrics to
    73  // include.
    74  type InfluxDBInclude struct {
    75  	Runtime string `json:"runtime" yaml:"runtime"`
    76  	DebugGC string `json:"debug_gc" yaml:"debug_gc"`
    77  }
    78  
    79  // NewInfluxDBConfig creates an InfluxDBConfig struct with default values.
    80  func NewInfluxDBConfig() InfluxDBConfig {
    81  	return InfluxDBConfig{
    82  		URL: "",
    83  		DB:  "",
    84  		TLS: btls.NewConfig(),
    85  
    86  		Precision:    "s",
    87  		Interval:     "1m",
    88  		PingInterval: "20s",
    89  		Timeout:      "5s",
    90  	}
    91  }
    92  
    93  // InfluxDB is the stats and client holder
    94  type InfluxDB struct {
    95  	client      client.Client
    96  	batchConfig client.BatchPointsConfig
    97  
    98  	interval     time.Duration
    99  	pingInterval time.Duration
   100  	timeout      time.Duration
   101  
   102  	ctx    context.Context
   103  	cancel func()
   104  
   105  	pathMapping     *pathMapping
   106  	registry        metrics.Registry
   107  	runtimeRegistry metrics.Registry
   108  	config          InfluxDBConfig
   109  	log             log.Modular
   110  }
   111  
   112  // NewInfluxDB creates and returns a new InfluxDB object.
   113  func NewInfluxDB(config Config, opts ...func(Type)) (Type, error) {
   114  	i := &InfluxDB{
   115  		config:          config.InfluxDB,
   116  		registry:        metrics.NewRegistry(),
   117  		runtimeRegistry: metrics.NewRegistry(),
   118  		log:             log.Noop(),
   119  	}
   120  
   121  	i.ctx, i.cancel = context.WithCancel(context.Background())
   122  
   123  	for _, opt := range opts {
   124  		opt(i)
   125  	}
   126  
   127  	if config.InfluxDB.Include.Runtime != "" {
   128  		metrics.RegisterRuntimeMemStats(i.runtimeRegistry)
   129  		interval, err := time.ParseDuration(config.InfluxDB.Include.Runtime)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("failed to parse interval: %s", err)
   132  		}
   133  		go metrics.CaptureRuntimeMemStats(i.runtimeRegistry, interval)
   134  	}
   135  
   136  	if config.InfluxDB.Include.DebugGC != "" {
   137  		metrics.RegisterDebugGCStats(i.runtimeRegistry)
   138  		interval, err := time.ParseDuration(config.InfluxDB.Include.DebugGC)
   139  		if err != nil {
   140  			return nil, fmt.Errorf("failed to parse interval: %s", err)
   141  		}
   142  		go metrics.CaptureDebugGCStats(i.runtimeRegistry, interval)
   143  	}
   144  
   145  	var err error
   146  	if i.pathMapping, err = newPathMapping(config.InfluxDB.PathMapping, i.log); err != nil {
   147  		return nil, fmt.Errorf("failed to init path mapping: %v", err)
   148  	}
   149  
   150  	if i.interval, err = time.ParseDuration(config.InfluxDB.Interval); err != nil {
   151  		return nil, fmt.Errorf("failed to parse interval: %s", err)
   152  	}
   153  
   154  	if i.pingInterval, err = time.ParseDuration(config.InfluxDB.PingInterval); err != nil {
   155  		return nil, fmt.Errorf("failed to parse ping interval: %s", err)
   156  	}
   157  
   158  	if i.timeout, err = time.ParseDuration(config.InfluxDB.Timeout); err != nil {
   159  		return nil, fmt.Errorf("failed to parse timeout interval: %s", err)
   160  	}
   161  
   162  	if err := i.makeClient(); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	i.batchConfig = client.BatchPointsConfig{
   167  		Precision:        config.InfluxDB.Precision,
   168  		Database:         config.InfluxDB.DB,
   169  		RetentionPolicy:  config.InfluxDB.RetentionPolicy,
   170  		WriteConsistency: config.InfluxDB.WriteConsistency,
   171  	}
   172  
   173  	go i.loop()
   174  
   175  	return i, nil
   176  }
   177  
   178  func (i *InfluxDB) toCMName(dotSepName string) (outPath string, labelNames, labelValues []string) {
   179  	return i.pathMapping.mapPathWithTags(dotSepName)
   180  }
   181  
   182  func (i *InfluxDB) makeClient() error {
   183  	var c client.Client
   184  	u, err := url.Parse(i.config.URL)
   185  	if err != nil {
   186  		return fmt.Errorf("problem parsing url: %s", err)
   187  	}
   188  
   189  	if u.Scheme == "https" {
   190  		tlsConfig := &tls.Config{}
   191  		if i.config.TLS.Enabled {
   192  			tlsConfig, err = i.config.TLS.Get()
   193  			if err != nil {
   194  				return err
   195  			}
   196  		}
   197  		c, err = client.NewHTTPClient(client.HTTPConfig{
   198  			Addr:      u.String(),
   199  			TLSConfig: tlsConfig,
   200  			Username:  i.config.Username,
   201  			Password:  i.config.Password,
   202  		})
   203  	} else if u.Scheme == "http" {
   204  		c, err = client.NewHTTPClient(client.HTTPConfig{
   205  			Addr:     u.String(),
   206  			Username: i.config.Username,
   207  			Password: i.config.Password,
   208  		})
   209  	} else if u.Scheme == "udp" {
   210  		c, err = client.NewUDPClient(client.UDPConfig{
   211  			Addr: u.Host,
   212  		})
   213  	} else {
   214  		return fmt.Errorf("protocol needs to be http, https or udp and is %s", u.Scheme)
   215  	}
   216  
   217  	if err == nil {
   218  		i.client = c
   219  	}
   220  	return err
   221  }
   222  
   223  func (i *InfluxDB) loop() {
   224  	ticker := time.NewTicker(i.interval)
   225  	pingTicker := time.NewTicker(i.pingInterval)
   226  	defer ticker.Stop()
   227  	defer pingTicker.Stop()
   228  	for {
   229  		select {
   230  		case <-i.ctx.Done():
   231  			return
   232  		case <-ticker.C:
   233  			if err := i.publishRegistry(); err != nil {
   234  				i.log.Errorf("failed to send metrics data: %s", err)
   235  			}
   236  		case <-pingTicker.C:
   237  			_, _, err := i.client.Ping(i.timeout)
   238  			if err != nil {
   239  				i.log.Warnf("unable to ping influx endpoint: %s", err)
   240  				if err = i.makeClient(); err != nil {
   241  					i.log.Errorf("unable to recreate client: %s", err)
   242  				}
   243  			}
   244  		}
   245  	}
   246  }
   247  
   248  func (i *InfluxDB) publishRegistry() error {
   249  	points, err := client.NewBatchPoints(i.batchConfig)
   250  	if err != nil {
   251  		return fmt.Errorf("problem creating batch points for influx: %s", err)
   252  	}
   253  	now := time.Now()
   254  	all := i.getAllMetrics()
   255  	for k, v := range all {
   256  		name, normalTags := decodeInfluxDBName(k)
   257  		tags := make(map[string]string, len(i.config.Tags)+len(normalTags))
   258  		// apply normal tags
   259  		for k, v := range normalTags {
   260  			tags[k] = v
   261  		}
   262  		// override with any global
   263  		for k, v := range i.config.Tags {
   264  			tags[k] = v
   265  		}
   266  		p, err := client.NewPoint(name, tags, v, now)
   267  		if err != nil {
   268  			i.log.Debugf("problem formatting metrics on %s: %s", name, err)
   269  		} else {
   270  			points.AddPoint(p)
   271  		}
   272  	}
   273  
   274  	return i.client.Write(points)
   275  }
   276  
   277  func getMetricValues(i interface{}) map[string]interface{} {
   278  	var values map[string]interface{}
   279  	switch metric := i.(type) {
   280  	case metrics.Counter:
   281  		values = make(map[string]interface{}, 1)
   282  		values["count"] = metric.Count()
   283  	case metrics.Gauge:
   284  		values = make(map[string]interface{}, 1)
   285  		values["value"] = metric.Value()
   286  	case metrics.GaugeFloat64:
   287  		values = make(map[string]interface{}, 1)
   288  		values["value"] = metric.Value()
   289  	case metrics.Timer:
   290  		values = make(map[string]interface{}, 14)
   291  		t := metric.Snapshot()
   292  		ps := t.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999})
   293  		values["count"] = t.Count()
   294  		values["min"] = t.Min()
   295  		values["max"] = t.Max()
   296  		values["mean"] = t.Mean()
   297  		values["stddev"] = t.StdDev()
   298  		values["p50"] = ps[0]
   299  		values["p75"] = ps[1]
   300  		values["p95"] = ps[2]
   301  		values["p99"] = ps[3]
   302  		values["p999"] = ps[4]
   303  		values["1m.rate"] = t.Rate1()
   304  		values["5m.rate"] = t.Rate5()
   305  		values["15m.rate"] = t.Rate15()
   306  		values["mean.rate"] = t.RateMean()
   307  	case metrics.Histogram:
   308  		values = make(map[string]interface{}, 10)
   309  		t := metric.Snapshot()
   310  		ps := t.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999})
   311  		values["count"] = t.Count()
   312  		values["min"] = t.Min()
   313  		values["max"] = t.Max()
   314  		values["mean"] = t.Mean()
   315  		values["stddev"] = t.StdDev()
   316  		values["p50"] = ps[0]
   317  		values["p75"] = ps[1]
   318  		values["p95"] = ps[2]
   319  		values["p99"] = ps[3]
   320  		values["p999"] = ps[4]
   321  	}
   322  	return values
   323  }
   324  
   325  func (i *InfluxDB) getAllMetrics() map[string]map[string]interface{} {
   326  	data := make(map[string]map[string]interface{})
   327  	i.registry.Each(func(name string, metric interface{}) {
   328  		values := getMetricValues(metric)
   329  		data[name] = values
   330  	})
   331  	i.runtimeRegistry.Each(func(name string, metric interface{}) {
   332  		pathMappedName := i.pathMapping.mapPathNoTags(name)
   333  		values := getMetricValues(metric)
   334  		data[pathMappedName] = values
   335  	})
   336  	return data
   337  }
   338  
   339  // GetCounter returns a stat counter object for a path.
   340  func (i *InfluxDB) GetCounter(path string) StatCounter {
   341  	name, labels, values := i.toCMName(path)
   342  	if name == "" {
   343  		return DudStat{}
   344  	}
   345  	encodedName := encodeInfluxDBName(name, labels, values)
   346  	return i.registry.GetOrRegister(encodedName, func() metrics.Counter {
   347  		return influxDBCounter{
   348  			metrics.NewCounter(),
   349  		}
   350  	}).(influxDBCounter)
   351  }
   352  
   353  // GetCounterVec returns a stat counter object for a path with the labels
   354  func (i *InfluxDB) GetCounterVec(path string, n []string) StatCounterVec {
   355  	name, labels, values := i.toCMName(path)
   356  	if name == "" {
   357  		return fakeCounterVec(func([]string) StatCounter {
   358  			return DudStat{}
   359  		})
   360  	}
   361  	labels = append(labels, n...)
   362  	return &fCounterVec{
   363  		f: func(l []string) StatCounter {
   364  			v := make([]string, 0, len(values)+len(l))
   365  			v = append(v, values...)
   366  			v = append(v, l...)
   367  			encodedName := encodeInfluxDBName(path, labels, v)
   368  			return i.registry.GetOrRegister(encodedName, func() metrics.Counter {
   369  				return influxDBCounter{
   370  					metrics.NewCounter(),
   371  				}
   372  			}).(influxDBCounter)
   373  		},
   374  	}
   375  }
   376  
   377  // GetTimer returns a stat timer object for a path.
   378  func (i *InfluxDB) GetTimer(path string) StatTimer {
   379  	name, labels, values := i.toCMName(path)
   380  	if name == "" {
   381  		return DudStat{}
   382  	}
   383  	encodedName := encodeInfluxDBName(name, labels, values)
   384  	return i.registry.GetOrRegister(encodedName, func() metrics.Timer {
   385  		return influxDBTimer{
   386  			metrics.NewTimer(),
   387  		}
   388  	}).(influxDBTimer)
   389  }
   390  
   391  // GetTimerVec returns a stat timer object for a path with the labels
   392  func (i *InfluxDB) GetTimerVec(path string, n []string) StatTimerVec {
   393  	name, labels, values := i.toCMName(path)
   394  	if name == "" {
   395  		return fakeTimerVec(func([]string) StatTimer {
   396  			return DudStat{}
   397  		})
   398  	}
   399  	labels = append(labels, n...)
   400  	return &fTimerVec{
   401  		f: func(l []string) StatTimer {
   402  			v := make([]string, 0, len(values)+len(l))
   403  			v = append(v, values...)
   404  			v = append(v, l...)
   405  			encodedName := encodeInfluxDBName(name, labels, v)
   406  			return i.registry.GetOrRegister(encodedName, func() metrics.Timer {
   407  				return influxDBTimer{
   408  					metrics.NewTimer(),
   409  				}
   410  			}).(influxDBTimer)
   411  		},
   412  	}
   413  }
   414  
   415  // GetGauge returns a stat gauge object for a path.
   416  func (i *InfluxDB) GetGauge(path string) StatGauge {
   417  	name, labels, values := i.toCMName(path)
   418  	if name == "" {
   419  		return DudStat{}
   420  	}
   421  	encodedName := encodeInfluxDBName(name, labels, values)
   422  	var result = i.registry.GetOrRegister(encodedName, func() metrics.Gauge {
   423  		return influxDBGauge{
   424  			metrics.NewGauge(),
   425  		}
   426  	}).(influxDBGauge)
   427  	return result
   428  }
   429  
   430  // GetGaugeVec returns a stat timer object for a path with the labels
   431  func (i *InfluxDB) GetGaugeVec(path string, n []string) StatGaugeVec {
   432  	name, labels, values := i.toCMName(path)
   433  	if name == "" {
   434  		return fakeGaugeVec(func([]string) StatGauge {
   435  			return DudStat{}
   436  		})
   437  	}
   438  	labels = append(labels, n...)
   439  	return &fGaugeVec{
   440  		f: func(l []string) StatGauge {
   441  			v := make([]string, 0, len(values)+len(l))
   442  			v = append(v, values...)
   443  			v = append(v, l...)
   444  			encodedName := encodeInfluxDBName(name, labels, v)
   445  			return i.registry.GetOrRegister(encodedName, func() metrics.Gauge {
   446  				return influxDBGauge{
   447  					metrics.NewGauge(),
   448  				}
   449  			}).(influxDBGauge)
   450  		},
   451  	}
   452  }
   453  
   454  // SetLogger sets the logger used to print connection errors.
   455  func (i *InfluxDB) SetLogger(log log.Modular) {
   456  	i.log = log
   457  }
   458  
   459  // Close reports metrics one last time and stops the InfluxDB object and closes the underlying client connection
   460  func (i *InfluxDB) Close() error {
   461  	if err := i.publishRegistry(); err != nil {
   462  		i.log.Errorf("failed to send metrics data: %s", err)
   463  	}
   464  	i.client.Close()
   465  	return nil
   466  }