Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
51
plugins/outputs/instrumental/README.md
Normal file
51
plugins/outputs/instrumental/README.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Instrumental Output Plugin
|
||||
|
||||
This plugin writes metrics to the [Instrumental Collector API][instrumental]
|
||||
and requires a project-specific API token.
|
||||
|
||||
Instrumental accepts stats in a format very close to Graphite, with the only
|
||||
difference being that the type of stat (gauge, increment) is the first token,
|
||||
separated from the metric itself by whitespace. The `increment` type is only
|
||||
used if the metric comes in as a counter via the [statsd input plugin][statsd].
|
||||
|
||||
⭐ Telegraf v0.13.1
|
||||
🏷️ applications
|
||||
💻 all
|
||||
|
||||
[instrumental]: https://instrumentalapp.com/docs/tcp-collector
|
||||
[statsd]: /plugins/inputs/statsd/README.md
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Secret-store support
|
||||
|
||||
This plugin supports secrets from secret-stores for the `api_token` option.
|
||||
See the [secret-store documentation][SECRETSTORE] for more details on how
|
||||
to use them.
|
||||
|
||||
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Configuration for sending metrics to an Instrumental project
|
||||
[[outputs.instrumental]]
|
||||
## Project API Token (required)
|
||||
api_token = "API Token" # required
|
||||
## Prefix the metrics with a given name
|
||||
prefix = ""
|
||||
## Stats output template (Graphite formatting)
|
||||
## see https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md#graphite
|
||||
template = "host.tags.measurement.field"
|
||||
## Timeout in seconds to connect
|
||||
timeout = "2s"
|
||||
## Debug true - Print communication to Instrumental
|
||||
debug = false
|
||||
```
|
212
plugins/outputs/instrumental/instrumental.go
Normal file
212
plugins/outputs/instrumental/instrumental.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package instrumental
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
"github.com/influxdata/telegraf/plugins/serializers/graphite"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
var (
|
||||
ValueIncludesBadChar = regexp.MustCompile("[^[:digit:].]")
|
||||
MetricNameReplacer = regexp.MustCompile("[^-[:alnum:]_.]+")
|
||||
)
|
||||
|
||||
type Instrumental struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
APIToken config.Secret `toml:"api_token"`
|
||||
Prefix string `toml:"prefix"`
|
||||
DataFormat string `toml:"data_format"`
|
||||
Template string `toml:"template"`
|
||||
Templates []string `toml:"templates"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
Debug bool `toml:"debug"`
|
||||
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
|
||||
conn net.Conn
|
||||
serializer *graphite.GraphiteSerializer
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultHost = "collector.instrumentalapp.com"
|
||||
DefaultPort = 8000
|
||||
HelloMessage = "hello version go/telegraf/1.1\n"
|
||||
AuthFormat = "authenticate %s\n"
|
||||
HandshakeFormat = HelloMessage + AuthFormat
|
||||
)
|
||||
|
||||
func (*Instrumental) SampleConfig() string {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (i *Instrumental) Init() error {
|
||||
s := &graphite.GraphiteSerializer{
|
||||
Prefix: i.Prefix,
|
||||
Template: i.Template,
|
||||
TagSanitizeMode: "strict",
|
||||
Separator: ".",
|
||||
Templates: i.Templates,
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
i.serializer = s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Instrumental) Connect() error {
|
||||
addr := fmt.Sprintf("%s:%d", i.Host, i.Port)
|
||||
connection, err := net.DialTimeout("tcp", addr, time.Duration(i.Timeout))
|
||||
|
||||
if err != nil {
|
||||
i.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
err = i.authenticate(connection)
|
||||
if err != nil {
|
||||
i.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Instrumental) Close() error {
|
||||
err := i.conn.Close()
|
||||
i.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *Instrumental) Write(metrics []telegraf.Metric) error {
|
||||
if i.conn == nil {
|
||||
err := i.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to (re)connect to Instrumental. Error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var points []string
|
||||
var metricType string
|
||||
|
||||
for _, m := range metrics {
|
||||
// Pull the metric_type out of the metric's tags. We don't want the type
|
||||
// to show up with the other tags pulled from the system, as they go in the
|
||||
// beginning of the line instead.
|
||||
// e.g we want:
|
||||
//
|
||||
// increment some_prefix.host.tag1.tag2.tag3.field value timestamp
|
||||
//
|
||||
// vs
|
||||
//
|
||||
// increment some_prefix.host.tag1.tag2.tag3.counter.field value timestamp
|
||||
//
|
||||
metricType = m.Tags()["metric_type"]
|
||||
m.RemoveTag("metric_type")
|
||||
|
||||
buf, err := i.serializer.Serialize(m)
|
||||
if err != nil {
|
||||
i.Log.Debugf("Could not serialize metric: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch metricType {
|
||||
case "counter":
|
||||
fallthrough
|
||||
case "histogram":
|
||||
metricType = "increment"
|
||||
default:
|
||||
metricType = "gauge"
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(buf)
|
||||
for {
|
||||
line, err := buffer.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
stat := string(line)
|
||||
|
||||
// decompose "metric.name value time"
|
||||
splitStat := strings.SplitN(stat, " ", 3)
|
||||
name := splitStat[0]
|
||||
value := splitStat[1]
|
||||
timestamp := splitStat[2]
|
||||
|
||||
// replace invalid components of metric name with underscore
|
||||
cleanMetric := MetricNameReplacer.ReplaceAllString(name, "_")
|
||||
|
||||
if !ValueIncludesBadChar.MatchString(value) {
|
||||
points = append(points, fmt.Sprintf("%s %s %s %s", metricType, cleanMetric, value, timestamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allPoints := strings.Join(points, "")
|
||||
if _, err := fmt.Fprint(i.conn, allPoints); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
_ = i.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// force the connection closed after sending data
|
||||
// to deal with various disconnection scenarios and eschew holding
|
||||
// open idle connections en masse
|
||||
_ = i.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Instrumental) authenticate(conn net.Conn) error {
|
||||
token, err := i.APIToken.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting token failed: %w", err)
|
||||
}
|
||||
defer token.Destroy()
|
||||
|
||||
if _, err := fmt.Fprintf(conn, HandshakeFormat, token.TemporaryString()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The response here will either be two "ok"s or an error message.
|
||||
responses := make([]byte, 512)
|
||||
if _, err = conn.Read(responses); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(responses)[:6] != "ok\nok\n" {
|
||||
return fmt.Errorf("authentication failed: %s", responses)
|
||||
}
|
||||
|
||||
i.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("instrumental", func() telegraf.Output {
|
||||
return &Instrumental{
|
||||
Host: DefaultHost,
|
||||
Port: DefaultPort,
|
||||
Template: graphite.DefaultTemplate,
|
||||
}
|
||||
})
|
||||
}
|
243
plugins/outputs/instrumental/instrumental_test.go
Normal file
243
plugins/outputs/instrumental/instrumental_test.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package instrumental
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
)
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
port := TCPServer(t, &wg)
|
||||
|
||||
i := Instrumental{
|
||||
Host: "127.0.0.1",
|
||||
Port: port,
|
||||
APIToken: config.NewSecret([]byte("abc123token")),
|
||||
Prefix: "my.prefix",
|
||||
}
|
||||
require.NoError(t, i.Init())
|
||||
|
||||
// Default to gauge
|
||||
m1 := metric.New(
|
||||
"mymeasurement",
|
||||
map[string]string{"host": "192.168.0.1"},
|
||||
map[string]interface{}{"myfield": float64(3.14)},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
m2 := metric.New(
|
||||
"mymeasurement",
|
||||
map[string]string{"host": "192.168.0.1", "metric_type": "set"},
|
||||
map[string]interface{}{"value": float64(3.14)},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
metrics := []telegraf.Metric{m1, m2}
|
||||
err := i.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Counter and Histogram are increments
|
||||
m3 := metric.New(
|
||||
"my_histogram",
|
||||
map[string]string{"host": "192.168.0.1", "metric_type": "histogram"},
|
||||
map[string]interface{}{"value": float64(3.14)},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
// We will modify metric names that won't be accepted by Instrumental
|
||||
m4 := metric.New(
|
||||
"bad_metric_name",
|
||||
map[string]string{"host": "192.168.0.1:8888::123", "metric_type": "counter"},
|
||||
map[string]interface{}{"value": 1},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
// We will drop metric values that won't be accepted by Instrumental
|
||||
m5 := metric.New(
|
||||
"bad_values",
|
||||
map[string]string{"host": "192.168.0.1", "metric_type": "counter"},
|
||||
map[string]interface{}{"value": "\" 3:30\""},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
m6 := metric.New(
|
||||
"my_counter",
|
||||
map[string]string{"host": "192.168.0.1", "metric_type": "counter"},
|
||||
map[string]interface{}{"value": float64(3.14)},
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
metrics = []telegraf.Metric{m3, m4, m5, m6}
|
||||
err = i.Write(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TCPServer(t *testing.T, wg *sync.WaitGroup) int {
|
||||
tcpServer, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer tcpServer.Close()
|
||||
|
||||
conn, err := tcpServer.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(1 * time.Second))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
tp := textproto.NewReader(reader)
|
||||
|
||||
helloExpected := "hello version go/telegraf/1.1"
|
||||
hello, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if hello != helloExpected {
|
||||
t.Errorf("expected %q, got %q", helloExpected, hello)
|
||||
return
|
||||
}
|
||||
|
||||
authExpected := "authenticate abc123token"
|
||||
auth, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if auth != authExpected {
|
||||
t.Errorf("expected %q, got %q", authExpected, auth)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write([]byte("ok\nok\n"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
data1Expected := "gauge my.prefix.192_168_0_1.mymeasurement.myfield 3.14 1289430000"
|
||||
data1, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if data1 != data1Expected {
|
||||
t.Errorf("expected %q, got %q", data1Expected, data1)
|
||||
return
|
||||
}
|
||||
|
||||
data2Expected := "gauge my.prefix.192_168_0_1.mymeasurement 3.14 1289430000"
|
||||
data2, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if data2 != data2Expected {
|
||||
t.Errorf("expected %q, got %q", data2Expected, data2)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err = tcpServer.Accept()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.SetDeadline(time.Now().Add(1 * time.Second))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
reader = bufio.NewReader(conn)
|
||||
tp = textproto.NewReader(reader)
|
||||
|
||||
helloExpected = "hello version go/telegraf/1.1"
|
||||
hello, err = tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if hello != helloExpected {
|
||||
t.Errorf("expected %q, got %q", helloExpected, hello)
|
||||
return
|
||||
}
|
||||
|
||||
authExpected = "authenticate abc123token"
|
||||
auth, err = tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if auth != authExpected {
|
||||
t.Errorf("expected %q, got %q", authExpected, auth)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write([]byte("ok\nok\n"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
data3Expected := "increment my.prefix.192_168_0_1.my_histogram 3.14 1289430000"
|
||||
data3, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if data3 != data3Expected {
|
||||
t.Errorf("expected %q, got %q", data3Expected, data3)
|
||||
return
|
||||
}
|
||||
|
||||
data4Expected := "increment my.prefix.192_168_0_1_8888_123.bad_metric_name 1 1289430000"
|
||||
data4, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if data4 != data4Expected {
|
||||
t.Errorf("expected %q, got %q", data4Expected, data4)
|
||||
return
|
||||
}
|
||||
|
||||
data5Expected := "increment my.prefix.192_168_0_1.my_counter 3.14 1289430000"
|
||||
data5, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if data5 != data5Expected {
|
||||
t.Errorf("expected %q, got %q", data5Expected, data5)
|
||||
return
|
||||
}
|
||||
|
||||
data6Expected := ""
|
||||
data6, err := tp.ReadLine()
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Error(err)
|
||||
return
|
||||
} else if data6 != data6Expected {
|
||||
t.Errorf("expected %q, got %q", data6Expected, data6)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return tcpServer.Addr().(*net.TCPAddr).Port
|
||||
}
|
13
plugins/outputs/instrumental/sample.conf
Normal file
13
plugins/outputs/instrumental/sample.conf
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Configuration for sending metrics to an Instrumental project
|
||||
[[outputs.instrumental]]
|
||||
## Project API Token (required)
|
||||
api_token = "API Token" # required
|
||||
## Prefix the metrics with a given name
|
||||
prefix = ""
|
||||
## Stats output template (Graphite formatting)
|
||||
## see https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md#graphite
|
||||
template = "host.tags.measurement.field"
|
||||
## Timeout in seconds to connect
|
||||
timeout = "2s"
|
||||
## Debug true - Print communication to Instrumental
|
||||
debug = false
|
Loading…
Add table
Add a link
Reference in a new issue