forked from influxdata/telegraf
-
Notifications
You must be signed in to change notification settings - Fork 0
/
unbound.go
221 lines (182 loc) · 5.85 KB
/
unbound.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package unbound
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)
type runner func(cmdName string, Timeout internal.Duration, UseSudo bool, Server string, ThreadAsTag bool, ConfigFile string) (*bytes.Buffer, error)
// Unbound is used to store configuration values
type Unbound struct {
Binary string
Timeout internal.Duration
UseSudo bool
Server string
ThreadAsTag bool
ConfigFile string
filter filter.Filter
run runner
}
var defaultBinary = "/usr/sbin/unbound-control"
var defaultTimeout = internal.Duration{Duration: time.Second}
var sampleConfig = `
## Address of server to connect to, read from unbound conf default, optionally ':port'
## Will lookup IP if given a hostname
server = "127.0.0.1:8953"
## If running as a restricted user you can prepend sudo for additional access:
# use_sudo = false
## The default location of the unbound-control binary can be overridden with:
# binary = "/usr/sbin/unbound-control"
## The default location of the unbound config file can be overridden with:
# config_file = "/etc/unbound/unbound.conf"
## The default timeout of 1s can be overriden with:
# timeout = "1s"
## When set to true, thread metrics are tagged with the thread id.
##
## The default is false for backwards compatibility, and will be changed to
## true in a future version. It is recommended to set to true on new
## deployments.
thread_as_tag = false
`
// Description displays what this plugin is about
func (s *Unbound) Description() string {
return "A plugin to collect stats from the Unbound DNS resolver"
}
// SampleConfig displays configuration instructions
func (s *Unbound) SampleConfig() string {
return sampleConfig
}
// Shell out to unbound_stat and return the output
func unboundRunner(cmdName string, Timeout internal.Duration, UseSudo bool, Server string, ThreadAsTag bool, ConfigFile string) (*bytes.Buffer, error) {
cmdArgs := []string{"stats_noreset"}
if Server != "" {
host, port, err := net.SplitHostPort(Server)
if err != nil { // No port was specified
host = 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(), Timeout.Duration)
defer lookUpCancel()
serverIps, err := resolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, fmt.Errorf("error looking up ip for server: %s: %s", Server, err)
}
if len(serverIps) == 0 {
return nil, fmt.Errorf("error no ip for server: %s: %s", Server, err)
}
server := serverIps[0].IP.String()
if port != "" {
server = server + "@" + port
}
cmdArgs = append([]string{"-s", server}, cmdArgs...)
}
if ConfigFile != "" {
cmdArgs = append([]string{"-c", ConfigFile}, cmdArgs...)
}
cmd := exec.Command(cmdName, cmdArgs...)
if UseSudo {
cmdArgs = append([]string{cmdName}, cmdArgs...)
cmd = exec.Command("sudo", cmdArgs...)
}
var out bytes.Buffer
cmd.Stdout = &out
err := internal.RunTimeout(cmd, Timeout.Duration)
if err != nil {
return &out, fmt.Errorf("error running unbound-control: %s (%s %v)", err, cmdName, cmdArgs)
}
return &out, nil
}
// Gather collects stats from unbound-control and adds them to the Accumulator
//
// All the dots in stat name will replaced by underscores. Histogram statistics will not be collected.
func (s *Unbound) Gather(acc telegraf.Accumulator) error {
// Always exclude histrogram statistics
statExcluded := []string{"histogram.*"}
filterExcluded, err := filter.Compile(statExcluded)
if err != nil {
return err
}
out, err := s.run(s.Binary, s.Timeout, s.UseSudo, s.Server, s.ThreadAsTag, s.ConfigFile)
if err != nil {
return fmt.Errorf("error gathering metrics: %s", 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.Replace(stat, ".", "_", -1)
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
}
func init() {
inputs.Add("unbound", func() telegraf.Input {
return &Unbound{
run: unboundRunner,
Binary: defaultBinary,
Timeout: defaultTimeout,
UseSudo: false,
Server: "",
ThreadAsTag: false,
ConfigFile: "",
}
})
}