-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathexporter.go
275 lines (230 loc) · 9.22 KB
/
exporter.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
package exporter
import (
"context"
"fmt"
"strconv"
"time"
"github.com/ffddorf/unms-exporter/client"
"github.com/ffddorf/unms-exporter/models"
openapi "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
prom "github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
)
var _ prom.Collector = (*Exporter)(nil)
const namespace = "unms"
type metricSpec struct {
help string
labels []string
}
func newSpec(help string, labels []string) metricSpec {
return metricSpec{help, labels}
}
var defaultLabels = []string{
"deviceId", "deviceName", "deviceMac", "role", "siteId", "siteName",
}
func (s metricSpec) intoDesc(name string) *prom.Desc {
labels := make([]string, 0, len(s.labels)+len(defaultLabels))
labels = append(labels, defaultLabels...)
labels = append(labels, s.labels...)
return prom.NewDesc(namespace+"_"+name, s.help, labels, prom.Labels{})
}
var interfaceLabels = []string{"ifName", "ifDescr", "ifPos", "ifType"}
var metricSpecs = map[string]metricSpec{
"device_cpu": newSpec("CPU usage in percent", nil),
"device_ram": newSpec("RAM usage in percent", nil),
"device_enabled": newSpec("Whether device is enabled", nil),
"device_maintenance": newSpec("Whether device is in maintenance", nil),
"device_uptime": newSpec("Duration the device is up in seconds", nil),
"device_last_seen": newSpec("Unix epoch when device was last seen", nil),
"device_last_backup": newSpec("Unix epoch when last backup was made", nil),
"interface_enabled": newSpec("Whether interface is enabled", interfaceLabels),
"interface_plugged": newSpec("Whether interface has a plugged link", interfaceLabels),
"interface_up": newSpec("Whether interface is up", interfaceLabels),
"interface_dropped": newSpec("Number of packets dropped on an interface", interfaceLabels),
"interface_errors": newSpec("Number of interface errors", interfaceLabels),
"interface_rx_bytes": newSpec("Bytes received on an interface", interfaceLabels),
"interface_tx_bytes": newSpec("Bytes sent on an interface", interfaceLabels),
"interface_rx_rate": newSpec("Receive rate on an interface", interfaceLabels),
"interface_tx_rate": newSpec("Transmit rate on an interface", interfaceLabels),
"interface_poe_power": newSpec("POE power output on an interface", interfaceLabels),
"wan_rx_bytes": newSpec("Bytes received on WAN interface", nil),
"wan_tx_bytes": newSpec("Bytes sent on WAN interface", nil),
"wan_rx_rate": newSpec("Receive rate on WAN interface", nil),
"wan_tx_rate": newSpec("Transmit rate on WAN interface", nil),
}
type Exporter struct {
api *client.UNMSAPI
metrics map[string]*prom.Desc
extras ExtraMetrics
// Internal metrics about the exporter
im internalMetrics
log logrus.FieldLogger
}
func New(log logrus.FieldLogger, host string, token string) *Exporter {
conf := client.DefaultTransportConfig()
conf.Schemes = []string{"https"}
conf.Host = host
api := client.NewHTTPClientWithConfig(strfmt.Default, conf)
client, ok := api.Transport.(*openapi.Runtime)
if !ok {
panic(fmt.Errorf("Invalid openapi transport: %T", api.Transport))
}
auth := openapi.APIKeyAuth("x-auth-token", "header", token)
client.DefaultAuthentication = auth
metrics := make(map[string]*prom.Desc)
for name, spec := range metricSpecs {
metrics[name] = spec.intoDesc(name)
}
im := newInternalMetrics()
return &Exporter{
api: api,
metrics: metrics,
im: im,
log: log,
}
}
func (e *Exporter) Describe(out chan<- *prom.Desc) {
e.DescribeContext(context.Background(), out)
}
func (e *Exporter) DescribeContext(ctx context.Context, out chan<- *prom.Desc) {
for _, desc := range e.metrics {
out <- desc
}
e.im.Describe(out)
}
func (e *Exporter) Collect(out chan<- prom.Metric) {
e.CollectContext(context.Background(), out)
}
func (e *Exporter) CollectContext(ctx context.Context, out chan<- prom.Metric) {
defer e.im.Collect(out)
if err := e.collectImpl(ctx, out); err != nil {
e.log.WithError(err).Warn("Metric collection failed")
e.im.errors.Inc()
} else {
e.im.success.Inc()
}
}
func derefOrFalse(in *bool) bool {
if in == nil {
return false
}
return *in
}
func boolToGauge(in bool) float64 {
if in {
return 1
}
return 0
}
func timeToGauge(ts strfmt.DateTime) float64 {
return float64(time.Time(ts).Unix())
}
func (e *Exporter) newMetric(name string, typ prom.ValueType, val float64, labels ...string) prom.Metric {
return prom.MustNewConstMetric(e.metrics[name], typ, val, labels...)
}
func (e *Exporter) collectImpl(ctx context.Context, out chan<- prom.Metric) error {
devices, err := e.fetchDeviceData(ctx)
if err != nil {
return err
}
for _, device := range devices {
siteID := "no-site-id"
siteName := "no-site"
if s := device.Identification.Site; s != nil {
if s.ID != nil {
siteID = *s.ID
}
if s.Name != "" {
siteName = s.Name
}
}
deviceLabels := []string{
*device.Identification.ID, // deviceId
device.Identification.Name, // deviceName
device.Identification.Mac, // mac
device.Identification.Role, // role
siteID,
siteName,
}
out <- e.newMetric("device_enabled", prom.GaugeValue, boolToGauge(derefOrFalse(device.Enabled)), deviceLabels...)
if device.Meta != nil {
out <- e.newMetric("device_maintenance", prom.GaugeValue, boolToGauge(derefOrFalse(device.Meta.Maintenance)), deviceLabels...)
}
if device.Overview != nil {
out <- e.newMetric("device_cpu", prom.GaugeValue, device.Overview.CPU, deviceLabels...)
out <- e.newMetric("device_ram", prom.GaugeValue, device.Overview.RAM, deviceLabels...)
out <- e.newMetric("device_uptime", prom.GaugeValue, device.Overview.Uptime, deviceLabels...)
out <- e.newMetric("device_last_seen", prom.CounterValue, timeToGauge(device.Overview.LastSeen), deviceLabels...)
}
if device.LatestBackup != nil && device.LatestBackup.Timestamp != nil {
out <- e.newMetric("device_last_backup", prom.GaugeValue, timeToGauge(*device.LatestBackup.Timestamp), deviceLabels...)
}
seenInterfaces := make(map[string]struct{})
var wanIF *models.DeviceInterfaceSchema
for _, intf := range device.Interfaces {
if intf.Identification == nil {
continue
}
// sometimes UNMS duplicates an interface in the list.
// skip it so we don't send duplicate metrics.
if _, ok := seenInterfaces[intf.Identification.Name]; ok {
continue
}
seenInterfaces[intf.Identification.Name] = struct{}{}
if intf.Identification.Name == device.Identification.WanInterfaceID {
wanIF = intf
}
intfLabels := make([]string, 0, len(deviceLabels)+len(interfaceLabels))
intfLabels = append(intfLabels, deviceLabels...)
intfLabels = append(intfLabels,
intf.Identification.Name, // ifName
derefOrEmpty(intf.Identification.Description), // ifDescr
strconv.FormatInt(intf.Identification.Position, 10), // ifPos
intf.Identification.Type, // ifType
)
out <- e.newMetric("interface_enabled", prom.GaugeValue, boolToGauge(intf.Enabled), intfLabels...)
if intf.Status != nil {
out <- e.newMetric("interface_plugged", prom.GaugeValue, boolToGauge(intf.Status.Plugged), intfLabels...)
out <- e.newMetric("interface_up", prom.GaugeValue, boolToGauge(intf.Status.Status == "active"), intfLabels...)
}
if intf.Statistics != nil {
out <- e.newMetric("interface_dropped", prom.CounterValue, intf.Statistics.Dropped, intfLabels...)
out <- e.newMetric("interface_errors", prom.CounterValue, intf.Statistics.Errors, intfLabels...)
out <- e.newMetric("interface_rx_bytes", prom.CounterValue, intf.Statistics.Rxbytes, intfLabels...)
out <- e.newMetric("interface_tx_bytes", prom.CounterValue, intf.Statistics.Txbytes, intfLabels...)
out <- e.newMetric("interface_rx_rate", prom.GaugeValue, intf.Statistics.Rxrate, intfLabels...)
out <- e.newMetric("interface_tx_rate", prom.GaugeValue, intf.Statistics.Txrate, intfLabels...)
out <- e.newMetric("interface_poe_power", prom.GaugeValue, intf.Statistics.PoePower, intfLabels...)
}
}
// WAN metrics
if wanIF != nil && wanIF.Statistics != nil {
out <- e.newMetric("wan_rx_bytes", prom.CounterValue, wanIF.Statistics.Rxbytes, deviceLabels...)
out <- e.newMetric("wan_tx_bytes", prom.CounterValue, wanIF.Statistics.Txbytes, deviceLabels...)
out <- e.newMetric("wan_rx_rate", prom.GaugeValue, wanIF.Statistics.Rxrate, deviceLabels...)
out <- e.newMetric("wan_tx_rate", prom.GaugeValue, wanIF.Statistics.Txrate, deviceLabels...)
}
// Ping metrics, if enabled
if e.extras.Ping {
ratio := 1.0
if ping := device.PingMetrics(); ping != nil {
if ping.PacketsSent > 0 {
ratio = float64(ping.PacketsLost) / float64(ping.PacketsSent)
}
out <- e.newMetric("ping_rtt_best_seconds", prom.GaugeValue, ping.Best.Seconds(), deviceLabels...)
out <- e.newMetric("ping_rtt_mean_seconds", prom.GaugeValue, ping.Mean.Seconds(), deviceLabels...)
out <- e.newMetric("ping_rtt_worst_seconds", prom.GaugeValue, ping.Worst.Seconds(), deviceLabels...)
out <- e.newMetric("ping_rtt_std_deviation_seconds", prom.GaugeValue, ping.StdDev.Seconds(), deviceLabels...)
}
out <- e.newMetric("ping_loss_ratio", prom.GaugeValue, ratio, deviceLabels...)
}
}
return nil
}
func derefOrEmpty(in *string) string {
if in == nil {
return ""
}
return *in
}