1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,55 @@
# Redis Time Series Output Plugin
This plugin writes metrics to a [Redis time-series][redists] server.
⭐ Telegraf v1.0.0
🏷️ datastore
💻 all
[redists]: https://redis.io/timeseries
## 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 `username` and
`password` 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
# Publishes metrics to a redis timeseries server
[[outputs.redistimeseries]]
## The address of the RedisTimeSeries server.
address = "127.0.0.1:6379"
## Redis ACL credentials
# username = ""
# password = ""
# database = 0
## Timeout for operations such as ping or sending metrics
# timeout = "10s"
## Enable attempt to convert string fields to numeric values
## If "false" or in case the string value cannot be converted the string
## field will be dropped.
# convert_string_fields = true
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
# insecure_skip_verify = false
```

View file

@ -0,0 +1,123 @@
//go:generate ../../../tools/readme_config_includer/generator
package redistimeseries
import (
"context"
_ "embed"
"errors"
"fmt"
"strconv"
"time"
"github.com/redis/go-redis/v9"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/outputs"
)
//go:embed sample.conf
var sampleConfig string
type RedisTimeSeries struct {
Address string `toml:"address"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Database int `toml:"database"`
ConvertStringFields bool `toml:"convert_string_fields"`
Timeout config.Duration `toml:"timeout"`
Log telegraf.Logger `toml:"-"`
tls.ClientConfig
client *redis.Client
}
func (r *RedisTimeSeries) Connect() error {
if r.Address == "" {
return errors.New("redis address must be specified")
}
username, err := r.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
defer username.Destroy()
password, err := r.Password.Get()
if err != nil {
return fmt.Errorf("getting password failed: %w", err)
}
defer password.Destroy()
r.client = redis.NewClient(&redis.Options{
Addr: r.Address,
Username: username.String(),
Password: password.String(),
DB: r.Database,
})
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(r.Timeout))
defer cancel()
return r.client.Ping(ctx).Err()
}
func (r *RedisTimeSeries) Close() error {
return r.client.Close()
}
func (*RedisTimeSeries) Description() string {
return "Plugin for sending metrics to RedisTimeSeries"
}
func (*RedisTimeSeries) SampleConfig() string {
return sampleConfig
}
func (r *RedisTimeSeries) Write(metrics []telegraf.Metric) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(r.Timeout))
defer cancel()
for _, m := range metrics {
for name, fv := range m.Fields() {
key := m.Name() + "_" + name
var value float64
switch v := fv.(type) {
case float64:
value = v
case string:
if !r.ConvertStringFields {
r.Log.Debugf("Dropping string field %q of metric %q", name, m.Name())
continue
}
var err error
value, err = strconv.ParseFloat(v, 64)
if err != nil {
r.Log.Debugf("Converting string field %q of metric %q failed: %v", name, m.Name(), err)
continue
}
default:
var err error
value, err = internal.ToFloat64(v)
if err != nil {
r.Log.Errorf("Converting field %q (%T) of metric %q failed: %v", name, v, m.Name(), err)
continue
}
}
resp := r.client.TSAddWithArgs(ctx, key, m.Time().UnixMilli(), value, &redis.TSOptions{Labels: m.Tags()})
if err := resp.Err(); err != nil {
return fmt.Errorf("adding sample %q failed: %w", key, err)
}
}
}
return nil
}
func init() {
outputs.Add("redistimeseries", func() telegraf.Output {
return &RedisTimeSeries{
ConvertStringFields: true,
Timeout: config.Duration(10 * time.Second),
}
})
}

View file

@ -0,0 +1,170 @@
package redistimeseries
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/docker/go-connections/nat"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/outputs"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil"
)
func TestConnectAndWriteIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
servicePort := "6379"
container := testutil.Container{
Image: "redislabs/redistimeseries",
ExposedPorts: []string{servicePort},
WaitingFor: wait.ForListeningPort(nat.Port(servicePort)),
}
require.NoError(t, container.Start(), "failed to start container")
defer container.Terminate()
redis := &RedisTimeSeries{
Address: fmt.Sprintf("%s:%s", container.Address, container.Ports[servicePort]),
ConvertStringFields: true,
Timeout: config.Duration(10 * time.Second),
}
// Verify that we can connect to the RedisTimeSeries server
require.NoError(t, redis.Connect())
// Verify that we can successfully write data to the RedisTimeSeries server
require.NoError(t, redis.Write(testutil.MockMetrics()))
}
func TestCases(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
const servicePort = "6379"
// Get all testcase directories
folders, err := os.ReadDir("testcases")
require.NoError(t, err)
// Register the plugin
outputs.Add("redistimeseries", func() telegraf.Output {
return &RedisTimeSeries{
ConvertStringFields: true,
Timeout: config.Duration(10 * time.Second),
}
})
for _, f := range folders {
// Only handle folders
if !f.IsDir() {
continue
}
t.Run(f.Name(), func(t *testing.T) {
testcasePath := filepath.Join("testcases", f.Name())
configFilename := filepath.Join(testcasePath, "telegraf.conf")
inputFilename := filepath.Join(testcasePath, "input.influx")
expectedFilename := filepath.Join(testcasePath, "expected.out")
expectedErrorFilename := filepath.Join(testcasePath, "expected.err")
// Get parser to parse input and expected output
parser := &influx.Parser{}
require.NoError(t, parser.Init())
// Load the input data
input, err := testutil.ParseMetricsFromFile(inputFilename, parser)
require.NoError(t, err)
// Read the expected output if any
var expected []string
if _, err := os.Stat(expectedFilename); err == nil {
expected, err = testutil.ParseLinesFromFile(expectedFilename)
require.NoError(t, err)
}
// Read the expected output if any
var expectedError string
if _, err := os.Stat(expectedErrorFilename); err == nil {
expectedErrors, err := testutil.ParseLinesFromFile(expectedErrorFilename)
require.NoError(t, err)
require.Len(t, expectedErrors, 1)
expectedError = expectedErrors[0]
}
// Configure the plugin
cfg := config.NewConfig()
require.NoError(t, cfg.LoadConfig(configFilename))
require.Len(t, cfg.Outputs, 1)
// Setup a test-container
container := testutil.Container{
Image: "redis/redis-stack-server:latest",
ExposedPorts: []string{servicePort},
Env: map[string]string{},
WaitingFor: wait.ForListeningPort(nat.Port(servicePort)),
}
require.NoError(t, container.Start(), "failed to start container")
defer container.Terminate()
address := container.Address + ":" + container.Ports[servicePort]
// Setup the plugin
plugin := cfg.Outputs[0].Output.(*RedisTimeSeries)
plugin.Address = address
plugin.Log = testutil.Logger{}
// Connect and write the metric(s)
require.NoError(t, plugin.Connect())
defer plugin.Close()
err = plugin.Write(input)
if expectedError != "" {
require.ErrorContains(t, err, expectedError)
return
}
require.NoError(t, err)
// // Check the metric nevertheless as we might get some metrics despite errors.
actual := getAllRecords(t.Context(), address)
require.ElementsMatch(t, expected, actual)
})
}
}
func getAllRecords(testContext context.Context, address string) []string {
client := redis.NewClient(&redis.Options{Addr: address})
ctx, cancel := context.WithTimeout(testContext, 10*time.Second)
defer cancel()
var records []string
keys := client.Keys(ctx, "*")
for _, key := range keys.Val() {
info := client.TSInfo(ctx, key)
var labels string
if l, found := info.Val()["labels"]; found {
lmap := l.(map[interface{}]interface{})
collection := make([]string, 0, len(lmap))
for k, v := range lmap {
collection = append(collection, fmt.Sprintf("%v=%v", k, v))
}
if len(collection) > 0 {
labels = " " + strings.Join(collection, " ")
}
}
result := client.TSRange(ctx, key, 0, int(time.Now().UnixMilli()))
for _, point := range result.Val() {
records = append(records, fmt.Sprintf("%s: %f %d%s", result.Args()[1], point.Value, point.Timestamp, labels))
}
}
return records
}

View file

@ -0,0 +1,23 @@
# Publishes metrics to a redis timeseries server
[[outputs.redistimeseries]]
## The address of the RedisTimeSeries server.
address = "127.0.0.1:6379"
## Redis ACL credentials
# username = ""
# password = ""
# database = 0
## Timeout for operations such as ping or sending metrics
# timeout = "10s"
## Enable attempt to convert string fields to numeric values
## If "false" or in case the string value cannot be converted the string
## field will be dropped.
# convert_string_fields = true
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
# insecure_skip_verify = false

View file

@ -0,0 +1,6 @@
weather_temperature: 23.100000 1696489223000 location=somewhere
weather_humidity: 52.300000 1696489223000 location=somewhere
weather_windspeed: 3.200000 1696489223000 location=somewhere
weather_temperature: 23.200000 1696489223100 location=somewhere
weather_humidity: 52.100000 1696489223100 location=somewhere
weather_windspeed: 13.200000 1696489223100 location=somewhere

View file

@ -0,0 +1,2 @@
weather,location=somewhere temperature=23.1,humidity=52.3,windspeed=3.2 1696489223000000000
weather,location=somewhere temperature=23.2,humidity=52.1,windspeed=13.2 1696489223100000000

View file

@ -0,0 +1,2 @@
[[outputs.redistimeseries]]
address = "127.0.0.1:6379"

View file

@ -0,0 +1,6 @@
weather_temperature: 23.100000 1696489223000 location=somewhere
weather_humidity: 52.300000 1696489223000 location=somewhere
weather_windspeed: 3.200000 1696489223000 location=somewhere
weather_temperature: 23.200000 1696489223100 location=somewhere
weather_humidity: 52.100000 1696489223100 location=somewhere
weather_windspeed: 13.200000 1696489223100 location=somewhere

View file

@ -0,0 +1,2 @@
weather,location=somewhere temperature=23.1,humidity=52.3,windspeed=3.2 1696489223000000000
weather,location=somewhereelse temperature=23.2,humidity=52.1,windspeed=13.2 1696489223100000000

View file

@ -0,0 +1,2 @@
[[outputs.redistimeseries]]
address = "127.0.0.1:6379"

View file

@ -0,0 +1,3 @@
example_operational: 1.000000 1696489223000
example_hours: 10.000000 1696489223000
example_value: 42.000000 1696489223000

View file

@ -0,0 +1 @@
example value=42i,status="OK",hours="10",operational=true 1696489223000000000

View file

@ -0,0 +1,2 @@
[[outputs.redistimeseries]]
address = "127.0.0.1:6379"

View file

@ -0,0 +1,2 @@
example_operational: 1.000000 1696489223000
example_value: 42.000000 1696489223000

View file

@ -0,0 +1 @@
example value=42i,status="OK",hours="10",operational=true 1696489223000000000

View file

@ -0,0 +1,3 @@
[[outputs.redistimeseries]]
address = "127.0.0.1:6379"
convert_string_fields = false