1
0
Fork 0
telegraf/plugins/inputs/unbound/unbound.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

189 lines
4.7 KiB
Go

//go:generate ../../../tools/readme_config_includer/generator
package unbound
import (
"bufio"
"bytes"
"context"
_ "embed"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
var (
defaultBinary = "/usr/sbin/unbound-control"
defaultTimeout = config.Duration(time.Second)
)
type Unbound struct {
Binary string `toml:"binary"`
Timeout config.Duration `toml:"timeout"`
UseSudo bool `toml:"use_sudo"`
Server string `toml:"server"`
ThreadAsTag bool `toml:"thread_as_tag"`
ConfigFile string `toml:"config_file"`
run runner
}
type runner func(unbound Unbound) (*bytes.Buffer, error)
func (*Unbound) SampleConfig() string {
return sampleConfig
}
func (s *Unbound) Gather(acc telegraf.Accumulator) error {
// Always exclude histogram statistics
statExcluded := []string{"histogram.*"}
filterExcluded, err := filter.Compile(statExcluded)
if err != nil {
return err
}
out, err := s.run(*s)
if err != nil {
return fmt.Errorf("error gathering metrics: %w", err)
}
// Process values
fields := make(map[string]interface{})
fieldsThreads := make(map[string]map[string]interface{})
scanner := bufio.NewScanner(out)
for scanner.Scan() {
cols := strings.Split(scanner.Text(), "=")
// Check split correctness
if len(cols) != 2 {
continue
}
stat := cols[0]
value := cols[1]
// Filter value
if filterExcluded.Match(stat) {
continue
}
fieldValue, err := strconv.ParseFloat(value, 64)
if err != nil {
acc.AddError(fmt.Errorf("expected a numerical value for %s = %v", stat, value))
continue
}
// is this a thread related value?
if s.ThreadAsTag && strings.HasPrefix(stat, "thread") {
// split the stat
statTokens := strings.Split(stat, ".")
// make sure we split something
if len(statTokens) > 1 {
// set the thread identifier
threadID := strings.TrimPrefix(statTokens[0], "thread")
// make sure we have a proper thread ID
if _, err = strconv.Atoi(threadID); err == nil {
// create new slice without the thread identifier (skip first token)
threadTokens := statTokens[1:]
// re-define stat
field := strings.Join(threadTokens[:], "_")
if fieldsThreads[threadID] == nil {
fieldsThreads[threadID] = make(map[string]interface{})
}
fieldsThreads[threadID][field] = fieldValue
}
}
} else {
field := strings.ReplaceAll(stat, ".", "_")
fields[field] = fieldValue
}
}
acc.AddFields("unbound", fields, nil)
if s.ThreadAsTag && len(fieldsThreads) > 0 {
for thisThreadID, thisThreadFields := range fieldsThreads {
thisThreadTag := map[string]string{"thread": thisThreadID}
acc.AddFields("unbound_threads", thisThreadFields, thisThreadTag)
}
}
return nil
}
// Shell out to unbound_stat and return the output
func unboundRunner(unbound Unbound) (*bytes.Buffer, error) {
cmdArgs := []string{"stats_noreset"}
if unbound.Server != "" {
host, port, err := net.SplitHostPort(unbound.Server)
if err != nil { // No port was specified
host = unbound.Server
port = ""
}
// Unbound control requires an IP address, and we want to be nice to the user
resolver := net.Resolver{}
ctx, lookUpCancel := context.WithTimeout(context.Background(), time.Duration(unbound.Timeout))
defer lookUpCancel()
serverIps, err := resolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, fmt.Errorf("error looking up ip for server %q: %w", unbound.Server, err)
}
if len(serverIps) == 0 {
return nil, fmt.Errorf("error no ip for server %q: %w", unbound.Server, err)
}
server := serverIps[0].IP.String()
if port != "" {
server = server + "@" + port
}
cmdArgs = append([]string{"-s", server}, cmdArgs...)
}
if unbound.ConfigFile != "" {
cmdArgs = append([]string{"-c", unbound.ConfigFile}, cmdArgs...)
}
cmd := exec.Command(unbound.Binary, cmdArgs...)
if unbound.UseSudo {
cmdArgs = append([]string{unbound.Binary}, cmdArgs...)
cmd = exec.Command("sudo", cmdArgs...)
}
var out bytes.Buffer
cmd.Stdout = &out
err := internal.RunTimeout(cmd, time.Duration(unbound.Timeout))
if err != nil {
return &out, fmt.Errorf("error running unbound-control %q %q: %w", unbound.Binary, cmdArgs, err)
}
return &out, nil
}
func init() {
inputs.Add("unbound", func() telegraf.Input {
return &Unbound{
run: unboundRunner,
Binary: defaultBinary,
Timeout: defaultTimeout,
UseSudo: false,
Server: "",
ThreadAsTag: false,
ConfigFile: "",
}
})
}