diff --git a/plugins/inputs/internal/README.md b/plugins/inputs/internal/README.md index c837eb832..2acb122a0 100644 --- a/plugins/inputs/internal/README.md +++ b/plugins/inputs/internal/README.md @@ -21,6 +21,10 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. [[inputs.internal]] ## If true, collect telegraf memory stats. # collect_memstats = true + + ## If true, collect metrics from Go's runtime.metrics. For a full list see: + ## https://pkg.go.dev/runtime/metrics + # collect_gostats = false ``` ## Metrics diff --git a/plugins/inputs/internal/internal.go b/plugins/inputs/internal/internal.go index 526076c57..160a79d2a 100644 --- a/plugins/inputs/internal/internal.go +++ b/plugins/inputs/internal/internal.go @@ -3,7 +3,9 @@ package internal import ( _ "embed" + "fmt" "runtime" + "runtime/metrics" "strings" "github.com/influxdata/telegraf" @@ -16,13 +18,8 @@ import ( var sampleConfig string type Self struct { - CollectMemstats bool -} - -func NewSelf() telegraf.Input { - return &Self{ - CollectMemstats: true, - } + CollectMemstats bool `toml:"collect_memstats"` + CollectGostats bool `toml:"collect_gostats"` } func (*Self) SampleConfig() string { @@ -30,42 +27,116 @@ func (*Self) SampleConfig() string { } func (s *Self) Gather(acc telegraf.Accumulator) error { - if s.CollectMemstats { - m := &runtime.MemStats{} - runtime.ReadMemStats(m) - fields := map[string]interface{}{ - "alloc_bytes": m.Alloc, // bytes allocated and not yet freed - "total_alloc_bytes": m.TotalAlloc, // bytes allocated (even if freed) - "sys_bytes": m.Sys, // bytes obtained from system (sum of XxxSys below) - "pointer_lookups": m.Lookups, // number of pointer lookups - "mallocs": m.Mallocs, // number of mallocs - "frees": m.Frees, // number of frees - // Main allocation heap statistics. - "heap_alloc_bytes": m.HeapAlloc, // bytes allocated and not yet freed (same as Alloc above) - "heap_sys_bytes": m.HeapSys, // bytes obtained from system - "heap_idle_bytes": m.HeapIdle, // bytes in idle spans - "heap_in_use_bytes": m.HeapInuse, // bytes in non-idle span - "heap_released_bytes": m.HeapReleased, // bytes released to the OS - "heap_objects": m.HeapObjects, // total number of allocated objects - "num_gc": m.NumGC, - } - acc.AddFields("internal_memstats", fields, map[string]string{}) - } - - telegrafVersion := inter.Version - goVersion := strings.TrimPrefix(runtime.Version(), "go") - for _, m := range selfstat.Metrics() { if m.Name() == "internal_agent" { - m.AddTag("go_version", goVersion) + m.AddTag("go_version", strings.TrimPrefix(runtime.Version(), "go")) } - m.AddTag("version", telegrafVersion) + m.AddTag("version", inter.Version) acc.AddFields(m.Name(), m.Fields(), m.Tags(), m.Time()) } + if s.CollectMemstats { + collectMemStat(acc) + } + + if s.CollectGostats { + collectGoStat(acc) + } + return nil } -func init() { - inputs.Add("internal", NewSelf) +func collectMemStat(acc telegraf.Accumulator) { + m := &runtime.MemStats{} + runtime.ReadMemStats(m) + fields := map[string]any{ + "alloc_bytes": m.Alloc, // bytes allocated and not yet freed + "total_alloc_bytes": m.TotalAlloc, // bytes allocated (even if freed) + "sys_bytes": m.Sys, // bytes obtained from system (sum of XxxSys below) + "pointer_lookups": m.Lookups, // number of pointer lookups + "mallocs": m.Mallocs, // number of mallocs + "frees": m.Frees, // number of frees + + // Main allocation heap statistics. + "heap_alloc_bytes": m.HeapAlloc, // bytes allocated and not yet freed (same as Alloc above) + "heap_sys_bytes": m.HeapSys, // bytes obtained from system + "heap_idle_bytes": m.HeapIdle, // bytes in idle spans + "heap_in_use_bytes": m.HeapInuse, // bytes in non-idle span + "heap_released_bytes": m.HeapReleased, // bytes released to the OS + "heap_objects": m.HeapObjects, // total number of allocated objects + "num_gc": m.NumGC, + } + acc.AddFields("internal_memstats", fields, map[string]string{}) +} + +func collectGoStat(acc telegraf.Accumulator) { + descs := metrics.All() + samples := make([]metrics.Sample, len(descs)) + for i := range samples { + samples[i].Name = descs[i].Name + } + metrics.Read(samples) + + fields := map[string]any{} + for _, sample := range samples { + name := sanitizeName(sample.Name) + + switch sample.Value.Kind() { + case metrics.KindUint64: + fields[name] = sample.Value.Uint64() + case metrics.KindFloat64: + fields[name] = sample.Value.Float64() + case metrics.KindFloat64Histogram: + // The histogram may be quite large, so let's just pull out + // a crude estimate for the median for the sake of this example. + fields[name] = medianBucket(sample.Value.Float64Histogram()) + default: + // This may happen as new metrics get added. + // + // The safest thing to do here is to simply log it somewhere + // as something to look into, but ignore it for now. + // In the worst case, you might temporarily miss out on a new metric. + fmt.Printf("%s: unexpected metric Kind: %v\n", name, sample.Value.Kind()) + } + } + + tags := map[string]string{ + "go_version": strings.TrimPrefix(runtime.Version(), "go"), + } + acc.AddFields("internal_gostats", fields, tags) +} + +// Converts /cpu/classes/gc/mark/assist:cpu-seconds to cpu_classes_gc_mark_assist_cpu_seconds +func sanitizeName(name string) string { + name = strings.TrimPrefix(name, "/") + name = strings.ReplaceAll(name, "/", "_") + name = strings.ReplaceAll(name, ":", "_") + name = strings.ReplaceAll(name, "-", "_") + return name +} + +func medianBucket(h *metrics.Float64Histogram) float64 { + total := uint64(0) + for _, count := range h.Counts { + total += count + } + thresh := total / 2 + total = 0 + for i, count := range h.Counts { + total += count + if total >= thresh { + return h.Buckets[i] + } + } + + // default value in case something above did not work + return 0.0 +} + +func init() { + inputs.Add("internal", func() telegraf.Input { + return &Self{ + CollectMemstats: true, + } + }) } diff --git a/plugins/inputs/internal/internal_test.go b/plugins/inputs/internal/internal_test.go index a4dc2eda8..ba06a675b 100644 --- a/plugins/inputs/internal/internal_test.go +++ b/plugins/inputs/internal/internal_test.go @@ -1,6 +1,7 @@ package internal import ( + "fmt" "testing" "github.com/influxdata/telegraf/selfstat" @@ -10,7 +11,9 @@ import ( ) func TestSelfPlugin(t *testing.T) { - s := NewSelf() + s := Self{ + CollectMemstats: true, + } acc := &testutil.Accumulator{} require.NoError(t, s.Gather(acc)) @@ -64,3 +67,48 @@ func TestSelfPlugin(t *testing.T) { }, ) } + +func TestNoMemStat(t *testing.T) { + s := Self{ + CollectMemstats: false, + CollectGostats: false, + } + acc := &testutil.Accumulator{} + + require.NoError(t, s.Gather(acc)) + require.False(t, acc.HasMeasurement("internal_memstats")) + require.False(t, acc.HasMeasurement("internal_gostats")) +} + +func TestGostats(t *testing.T) { + s := Self{ + CollectMemstats: false, + CollectGostats: true, + } + acc := &testutil.Accumulator{} + + require.NoError(t, s.Gather(acc)) + require.False(t, acc.HasMeasurement("internal_memstats")) + require.True(t, acc.HasMeasurement("internal_gostats")) + + var metric *testutil.Metric + for _, m := range acc.Metrics { + if m.Measurement == "internal_gostats" { + metric = m + break + } + } + + require.NotNil(t, metric) + require.Equal(t, metric.Measurement, "internal_gostats") + require.Equal(t, len(metric.Tags), 1) + require.Contains(t, metric.Tags, "go_version") + + for name, value := range metric.Fields { + switch value.(type) { + case int64, uint64, float64: + default: + require.True(t, false, fmt.Sprintf("field %s is of non-numeric type %T\n", name, value)) + } + } +} diff --git a/plugins/inputs/internal/sample.conf b/plugins/inputs/internal/sample.conf index 4292b8e5e..eafc744df 100644 --- a/plugins/inputs/internal/sample.conf +++ b/plugins/inputs/internal/sample.conf @@ -2,3 +2,7 @@ [[inputs.internal]] ## If true, collect telegraf memory stats. # collect_memstats = true + + ## If true, collect metrics from Go's runtime.metrics. For a full list see: + ## https://pkg.go.dev/runtime/metrics + # collect_gostats = false