1
0
Fork 0
telegraf/plugins/inputs/ntpq/ntpq.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

314 lines
7 KiB
Go

//go:generate ../../../tools/readme_config_includer/generator
package ntpq
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"math/bits"
"os/exec"
"regexp"
"slices"
"strconv"
"strings"
"github.com/kballard/go-shellquote"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
// Due to problems with a parsing, we have to use regexp expression in order
// to remove string that starts from '(' and ends with space
// see: https://github.com/influxdata/telegraf/issues/2386
var reBrackets = regexp.MustCompile(`\s+\([\S]*`)
type elementType int64
const (
none elementType = iota
tag
fieldFloat
fieldDuration
fieldIntDecimal
fieldIntOctal
fieldIntRatio8
fieldIntBits
)
type NTPQ struct {
DNSLookup bool `toml:"dns_lookup" deprecated:"1.24.0;1.35.0;add '-n' to 'options' instead to skip DNS lookup"`
Options string `toml:"options"`
Servers []string `toml:"servers"`
ReachFormat string `toml:"reach_format"`
runQ func(string) ([]byte, error)
}
type column struct {
name string
etype elementType
}
// Mapping of ntpq header names to tag keys
var tagHeaders = map[string]string{
"remote": "remote",
"refid": "refid",
"st": "stratum",
"t": "type",
}
// Mapping of fields
var fieldElements = map[string]elementType{
"delay": fieldFloat,
"jitter": fieldFloat,
"offset": fieldFloat,
"reach": fieldIntDecimal,
"poll": fieldDuration,
"when": fieldDuration,
}
func (*NTPQ) SampleConfig() string {
return sampleConfig
}
func (n *NTPQ) Init() error {
if len(n.Servers) == 0 {
n.Servers = []string{""}
}
if n.runQ == nil {
options, err := shellquote.Split(n.Options)
if err != nil {
return fmt.Errorf("splitting options failed: %w", err)
}
if !n.DNSLookup {
if !slices.Contains(options, "-n") {
options = append(options, "-n")
}
}
if !slices.Contains(options, "-p") {
options = append(options, "-p")
}
n.runQ = func(server string) ([]byte, error) {
bin, err := exec.LookPath("ntpq")
if err != nil {
return nil, err
}
// Needs to be last argument
args := make([]string, 0, len(options)+1)
args = append(args, options...)
if server != "" {
args = append(args, server)
}
cmd := exec.Command(bin, args...)
return cmd.Output()
}
}
switch n.ReachFormat {
case "", "octal":
n.ReachFormat = "octal"
// Interpret the field as decimal integer returning
// the raw (octal) representation
fieldElements["reach"] = fieldIntDecimal
case "decimal":
// Interpret the field as octal integer returning
// decimal number representation
fieldElements["reach"] = fieldIntOctal
case "count":
// Interpret the field as bits set returning
// the number of bits set
fieldElements["reach"] = fieldIntBits
case "ratio":
// Interpret the field as ratio between the number of
// bits set and the maximum available bits set (8).
fieldElements["reach"] = fieldIntRatio8
default:
return fmt.Errorf("unknown 'reach_format' %q", n.ReachFormat)
}
return nil
}
func (n *NTPQ) Gather(acc telegraf.Accumulator) error {
for _, server := range n.Servers {
n.gatherServer(acc, server)
}
return nil
}
func (n *NTPQ) gatherServer(acc telegraf.Accumulator, server string) {
var msgPrefix string
if server != "" {
msgPrefix = fmt.Sprintf("[%s] ", server)
}
out, err := n.runQ(server)
if err != nil {
acc.AddError(fmt.Errorf("%s%w", msgPrefix, err))
return
}
scanner := bufio.NewScanner(bytes.NewReader(out))
// Look for the header
var columns []column
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
_, elements := processLine(line)
if len(elements) < 2 {
continue
}
for _, el := range elements {
// Check if the element is a tag
if name, isTag := tagHeaders[el]; isTag {
columns = append(columns, column{
name: name,
etype: tag,
})
continue
}
// Add a field
if etype, isField := fieldElements[el]; isField {
columns = append(columns, column{
name: el,
etype: etype,
})
continue
}
// Skip the column if not found
columns = append(columns, column{etype: none})
}
break
}
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
prefix, elements := processLine(line)
if len(elements) != len(columns) {
continue
}
tags := make(map[string]string)
fields := make(map[string]interface{})
if prefix != "" {
tags["state_prefix"] = prefix
}
if server != "" {
tags["source"] = server
}
for i, raw := range elements {
col := columns[i]
switch col.etype {
case none:
continue
case tag:
tags[col.name] = raw
case fieldFloat:
value, err := strconv.ParseFloat(raw, 64)
if err != nil {
msg := fmt.Sprintf("%sparsing %q (%v) as float failed", msgPrefix, col.name, raw)
acc.AddError(fmt.Errorf("%s: %w", msg, err))
continue
}
fields[col.name] = value
case fieldDuration:
// Ignore fields only containing a minus
if raw == "-" {
continue
}
factor := int64(1)
suffix := raw[len(raw)-1:]
switch suffix {
case "d":
factor = 24 * 3600
case "h":
factor = 3600
case "m":
factor = 60
default:
suffix = ""
}
value, err := strconv.ParseInt(strings.TrimSuffix(raw, suffix), 10, 64)
if err != nil {
msg := fmt.Sprintf("%sparsing %q (%v) as duration failed", msgPrefix, col.name, raw)
acc.AddError(fmt.Errorf("%s: %w", msg, err))
continue
}
fields[col.name] = value * factor
case fieldIntDecimal:
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
acc.AddError(fmt.Errorf("parsing %q (%v) as int failed: %w", col.name, raw, err))
continue
}
fields[col.name] = value
case fieldIntOctal:
value, err := strconv.ParseInt(raw, 8, 64)
if err != nil {
acc.AddError(fmt.Errorf("parsing %q (%v) as int failed: %w", col.name, raw, err))
continue
}
fields[col.name] = value
case fieldIntBits:
value, err := strconv.ParseUint(raw, 8, 64)
if err != nil {
acc.AddError(fmt.Errorf("parsing %q (%v) as int failed: %w", col.name, raw, err))
continue
}
fields[col.name] = bits.OnesCount64(value)
case fieldIntRatio8:
value, err := strconv.ParseUint(raw, 8, 64)
if err != nil {
acc.AddError(fmt.Errorf("parsing %q (%v) as int failed: %w", col.name, raw, err))
continue
}
fields[col.name] = float64(bits.OnesCount64(value)) / float64(8)
}
}
acc.AddFields("ntpq", fields, tags)
}
}
func processLine(line string) (string, []string) {
// if there is an ntpq state prefix, remove it and make it it's own tag
// see https://github.com/influxdata/telegraf/issues/1161
var prefix string
if strings.ContainsAny(string(line[0]), "*#o+x.-") {
prefix = string(line[0])
line = strings.TrimLeft(line, "*#o+x.-")
}
line = reBrackets.ReplaceAllString(line, "")
return prefix, strings.Fields(line)
}
func init() {
inputs.Add("ntpq", func() telegraf.Input {
return &NTPQ{
DNSLookup: true,
}
})
}