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 }