feat(aggregators.final): Allow to specify output strategy (#14062)
This commit is contained in:
parent
1f35dd9b48
commit
a8af81c8c9
|
|
@ -29,12 +29,35 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
period = "30s"
|
period = "30s"
|
||||||
## If true, the original metric will be dropped by the
|
## If true, the original metric will be dropped by the
|
||||||
## aggregator and will not get sent to the output plugins.
|
## aggregator and will not get sent to the output plugins.
|
||||||
drop_original = false
|
# drop_original = false
|
||||||
|
|
||||||
## The time that a series is not updated until considering it final.
|
## The time that a series is not updated until considering it final.
|
||||||
series_timeout = "5m"
|
# series_timeout = "5m"
|
||||||
|
|
||||||
|
## Output strategy, supported values:
|
||||||
|
## timeout -- output a metric if no new input arrived for `series_timeout`;
|
||||||
|
## useful for filling gaps in input data
|
||||||
|
## periodic -- ouput the last received metric every `period`; useful to
|
||||||
|
## downsample the input data
|
||||||
|
# output_strategy = "timeout"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Output strategy
|
||||||
|
|
||||||
|
By default (`output_strategy = "timeout"`) the plugin will only emit a metric
|
||||||
|
for the period if the last received one is older than the series_timeout. This
|
||||||
|
will not guarantee a regular output of a `final` metric e.g. if the
|
||||||
|
series-timeout is a multiple of the gathering interval for an input. In this
|
||||||
|
case metric sporadically arrive in the timeout phase of the period and emitting
|
||||||
|
the `final` metric is surpressed.
|
||||||
|
This can be helpful to fill in gaps in the data if no input arrived in time.
|
||||||
|
|
||||||
|
Contrary to this, `output_strategy = "periodic"` will always output a `final`
|
||||||
|
metric at the end of the period irrespectively of when the last metric arrived,
|
||||||
|
the `series_timout` is ignored.
|
||||||
|
This is helpful if you for example want to downsample input data arriving at a
|
||||||
|
high rate and require a periodic output of the `final` metric.
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
Measurement and tags are unchanged, fields are emitted with the suffix
|
Measurement and tags are unchanged, fields are emitted with the suffix
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package final
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
|
|
@ -14,6 +15,7 @@ import (
|
||||||
var sampleConfig string
|
var sampleConfig string
|
||||||
|
|
||||||
type Final struct {
|
type Final struct {
|
||||||
|
OutputStrategy string `toml:"output_strategy"`
|
||||||
SeriesTimeout config.Duration `toml:"series_timeout"`
|
SeriesTimeout config.Duration `toml:"series_timeout"`
|
||||||
|
|
||||||
// The last metric for all series which are active
|
// The last metric for all series which are active
|
||||||
|
|
@ -23,7 +25,6 @@ type Final struct {
|
||||||
func NewFinal() *Final {
|
func NewFinal() *Final {
|
||||||
return &Final{
|
return &Final{
|
||||||
SeriesTimeout: config.Duration(5 * time.Minute),
|
SeriesTimeout: config.Duration(5 * time.Minute),
|
||||||
metricCache: make(map[uint64]telegraf.Metric),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,6 +32,23 @@ func (*Final) SampleConfig() string {
|
||||||
return sampleConfig
|
return sampleConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Final) Init() error {
|
||||||
|
// Check options and set defaults
|
||||||
|
switch m.OutputStrategy {
|
||||||
|
case "":
|
||||||
|
m.OutputStrategy = "timeout"
|
||||||
|
case "timeout", "periodic":
|
||||||
|
// Do nothing, those are valid
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid 'output_strategy': %q", m.OutputStrategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the cache
|
||||||
|
m.metricCache = make(map[uint64]telegraf.Metric)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Final) Add(in telegraf.Metric) {
|
func (m *Final) Add(in telegraf.Metric) {
|
||||||
id := in.HashID()
|
id := in.HashID()
|
||||||
m.metricCache[id] = in
|
m.metricCache[id] = in
|
||||||
|
|
@ -41,7 +59,11 @@ func (m *Final) Push(acc telegraf.Accumulator) {
|
||||||
acc.SetPrecision(time.Nanosecond)
|
acc.SetPrecision(time.Nanosecond)
|
||||||
|
|
||||||
for id, metric := range m.metricCache {
|
for id, metric := range m.metricCache {
|
||||||
if time.Since(metric.Time()) > time.Duration(m.SeriesTimeout) {
|
if m.OutputStrategy == "timeout" && time.Since(metric.Time()) <= time.Duration(m.SeriesTimeout) {
|
||||||
|
// We output on timeout but the last metric of the series was
|
||||||
|
// younger than that. So skip the output for this period.
|
||||||
|
continue
|
||||||
|
}
|
||||||
fields := map[string]interface{}{}
|
fields := map[string]interface{}{}
|
||||||
for _, field := range metric.FieldList() {
|
for _, field := range metric.FieldList() {
|
||||||
fields[field.Key+"_final"] = field.Value
|
fields[field.Key+"_final"] = field.Value
|
||||||
|
|
@ -50,7 +72,6 @@ func (m *Final) Push(acc telegraf.Accumulator) {
|
||||||
delete(m.metricCache, id)
|
delete(m.metricCache, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Final) Reset() {
|
func (m *Final) Reset() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import (
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/metric"
|
"github.com/influxdata/telegraf/metric"
|
||||||
"github.com/influxdata/telegraf/testutil"
|
"github.com/influxdata/telegraf/testutil"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSimple(t *testing.T) {
|
func TestSimple(t *testing.T) {
|
||||||
acc := testutil.Accumulator{}
|
acc := testutil.Accumulator{}
|
||||||
final := NewFinal()
|
final := NewFinal()
|
||||||
|
require.NoError(t, final.Init())
|
||||||
|
|
||||||
tags := map[string]string{"foo": "bar"}
|
tags := map[string]string{"foo": "bar"}
|
||||||
m1 := metric.New("m1",
|
m1 := metric.New("m1",
|
||||||
|
|
@ -48,6 +50,7 @@ func TestSimple(t *testing.T) {
|
||||||
func TestTwoTags(t *testing.T) {
|
func TestTwoTags(t *testing.T) {
|
||||||
acc := testutil.Accumulator{}
|
acc := testutil.Accumulator{}
|
||||||
final := NewFinal()
|
final := NewFinal()
|
||||||
|
require.NoError(t, final.Init())
|
||||||
|
|
||||||
tags1 := map[string]string{"foo": "bar"}
|
tags1 := map[string]string{"foo": "bar"}
|
||||||
tags2 := map[string]string{"foo": "baz"}
|
tags2 := map[string]string{"foo": "baz"}
|
||||||
|
|
@ -94,6 +97,7 @@ func TestLongDifference(t *testing.T) {
|
||||||
acc := testutil.Accumulator{}
|
acc := testutil.Accumulator{}
|
||||||
final := NewFinal()
|
final := NewFinal()
|
||||||
final.SeriesTimeout = config.Duration(30 * time.Second)
|
final.SeriesTimeout = config.Duration(30 * time.Second)
|
||||||
|
require.NoError(t, final.Init())
|
||||||
tags := map[string]string{"foo": "bar"}
|
tags := map[string]string{"foo": "bar"}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
@ -142,3 +146,129 @@ func TestLongDifference(t *testing.T) {
|
||||||
}
|
}
|
||||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.SortMetrics())
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.SortMetrics())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOutputStrategyInvalid(t *testing.T) {
|
||||||
|
final := &Final{
|
||||||
|
OutputStrategy: "no way",
|
||||||
|
SeriesTimeout: config.Duration(30 * time.Second),
|
||||||
|
}
|
||||||
|
require.ErrorContains(t, final.Init(), `invalid 'output_strategy'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputStrategyTimeout(t *testing.T) {
|
||||||
|
final := &Final{
|
||||||
|
OutputStrategy: "timeout",
|
||||||
|
SeriesTimeout: config.Duration(30 * time.Second),
|
||||||
|
}
|
||||||
|
require.NoError(t, final.Init())
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
tags := map[string]string{"foo": "bar"}
|
||||||
|
m1 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(1)},
|
||||||
|
now.Add(time.Second*-290))
|
||||||
|
m2 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(2)},
|
||||||
|
now.Add(time.Second*-275))
|
||||||
|
m3 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(3)},
|
||||||
|
now.Add(time.Second*-100))
|
||||||
|
m4 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(4)},
|
||||||
|
now.Add(time.Second*-20))
|
||||||
|
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
final.Add(m1)
|
||||||
|
final.Add(m2)
|
||||||
|
final.Push(&acc)
|
||||||
|
final.Add(m3)
|
||||||
|
final.Push(&acc)
|
||||||
|
final.Add(m4)
|
||||||
|
final.Push(&acc)
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{
|
||||||
|
"a_final": 2,
|
||||||
|
},
|
||||||
|
now.Add(time.Second*-275),
|
||||||
|
),
|
||||||
|
testutil.MustMetric(
|
||||||
|
"m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{
|
||||||
|
"a_final": 3,
|
||||||
|
},
|
||||||
|
now.Add(time.Second*-100),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.SortMetrics())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputStrategyPeriodic(t *testing.T) {
|
||||||
|
final := &Final{
|
||||||
|
OutputStrategy: "periodic",
|
||||||
|
SeriesTimeout: config.Duration(30 * time.Second),
|
||||||
|
}
|
||||||
|
require.NoError(t, final.Init())
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
tags := map[string]string{"foo": "bar"}
|
||||||
|
m1 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(1)},
|
||||||
|
now.Add(time.Second*-290))
|
||||||
|
m2 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(2)},
|
||||||
|
now.Add(time.Second*-275))
|
||||||
|
m3 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(3)},
|
||||||
|
now.Add(time.Second*-100))
|
||||||
|
m4 := metric.New("m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a": int64(4)},
|
||||||
|
now.Add(time.Second*-20))
|
||||||
|
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
final.Add(m1)
|
||||||
|
final.Add(m2)
|
||||||
|
final.Push(&acc)
|
||||||
|
final.Add(m3)
|
||||||
|
final.Push(&acc)
|
||||||
|
final.Add(m4)
|
||||||
|
final.Push(&acc)
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
metric.New(
|
||||||
|
"m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{
|
||||||
|
"a_final": 2,
|
||||||
|
},
|
||||||
|
now.Add(time.Second*-275),
|
||||||
|
),
|
||||||
|
metric.New(
|
||||||
|
"m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{
|
||||||
|
"a_final": 3,
|
||||||
|
},
|
||||||
|
now.Add(time.Second*-100),
|
||||||
|
),
|
||||||
|
metric.New(
|
||||||
|
"m",
|
||||||
|
tags,
|
||||||
|
map[string]interface{}{"a_final": int64(4)},
|
||||||
|
now.Add(time.Second*-20),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.SortMetrics())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,14 @@
|
||||||
period = "30s"
|
period = "30s"
|
||||||
## If true, the original metric will be dropped by the
|
## If true, the original metric will be dropped by the
|
||||||
## aggregator and will not get sent to the output plugins.
|
## aggregator and will not get sent to the output plugins.
|
||||||
drop_original = false
|
# drop_original = false
|
||||||
|
|
||||||
## The time that a series is not updated until considering it final.
|
## The time that a series is not updated until considering it final.
|
||||||
series_timeout = "5m"
|
# series_timeout = "5m"
|
||||||
|
|
||||||
|
## Output strategy, supported values:
|
||||||
|
## timeout -- output a metric if no new input arrived for `series_timeout`;
|
||||||
|
## useful for filling gaps in input data
|
||||||
|
## periodic -- ouput the last received metric every `period`; useful to
|
||||||
|
## downsample the input data
|
||||||
|
# output_strategy = "timeout"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue