-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This enables fetching device statistics from UNMS, extracting the ping RTT values from it, and export them back to prometheus. Since fetching statistics requires talking to another, device-specific endpoint, the context timeout is increased from 5 to 30s. A future change should pass the request context from the promhttp endpoint down to the fetchDeviceData() method, to make the timout depend on Prometheus' scrape request life cycle.
- Loading branch information
Showing
5 changed files
with
255 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package exporter | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/ffddorf/unms-exporter/models" | ||
) | ||
|
||
const ( | ||
ms = time.Millisecond | ||
µs = time.Microsecond //nolint:asciicheck | ||
) | ||
|
||
type metricExpectation map[string]struct { | ||
actual interface{} | ||
satisfied bool | ||
} | ||
|
||
func comparePingMetrics(t *testing.T, expectations metricExpectation, actual *PingMetrics) { | ||
t.Helper() | ||
|
||
anyFailure := false | ||
for field, expectation := range expectations { | ||
if !expectation.satisfied { | ||
anyFailure = true | ||
t.Errorf("unexpected value for field %q: %v", field, expectation.actual) | ||
} | ||
} | ||
if anyFailure { | ||
t.FailNow() | ||
} | ||
} | ||
|
||
func TestDevice_PingMetrics_connected(t *testing.T) { | ||
t.Parallel() | ||
|
||
subject := Device{ | ||
Statistics: &models.DeviceStatistics{ | ||
Ping: models.ListOfCoordinates{{Y: 5}, {Y: 10}, {Y: 25}, {Y: 15}, {Y: 1}}, // x values are ignored | ||
}, | ||
} | ||
|
||
actual := subject.PingMetrics() | ||
if actual == nil { | ||
t.Error("expected PingMetrics() to return somthing, got nil") | ||
} | ||
|
||
comparePingMetrics(t, metricExpectation{ | ||
"packets sent": {actual.PacketsSent, actual.PacketsSent == 5}, | ||
"packets lost": {actual.PacketsLost, actual.PacketsLost == 0}, | ||
"rtt best": {actual.Best, actual.Best == 1*ms}, | ||
"rtt worst": {actual.Worst, actual.Worst == 25*ms}, | ||
"rtt median": {actual.Median, actual.Median == 10*ms}, | ||
"rtt meain": {actual.Mean, actual.Mean == 11200*µs}, // 11.2ms | ||
"rtt std dev": {actual.StdDev, 8350*µs < actual.StdDev && actual.StdDev < 8360*µs}, // ~8.352245ms | ||
}, actual) | ||
} | ||
|
||
func TestDevice_PingMetrics_missingPackets(t *testing.T) { | ||
t.Parallel() | ||
|
||
subject := Device{ | ||
Statistics: &models.DeviceStatistics{ | ||
Ping: models.ListOfCoordinates{nil, {Y: 100}, {Y: 250}, nil, {Y: 120}}, | ||
}, | ||
} | ||
|
||
actual := subject.PingMetrics() | ||
if actual == nil { | ||
t.Error("expected PingMetrics() to return somthing, got nil") | ||
} | ||
|
||
comparePingMetrics(t, metricExpectation{ | ||
"packets sent": {actual.PacketsSent, actual.PacketsSent == 5}, | ||
"packets lost": {actual.PacketsLost, actual.PacketsLost == 2}, | ||
"rtt best": {actual.Best, actual.Best == 100*ms}, | ||
"rtt worst": {actual.Worst, actual.Worst == 250*ms}, | ||
"rtt median": {actual.Median, actual.Median == 120*ms}, | ||
"rtt meain": {actual.Mean, 156666*µs < actual.Mean && actual.Mean < 156667*µs}, // 156.66666ms | ||
"rtt std dev": {actual.StdDev, 66499*µs < actual.StdDev && actual.StdDev < 66500*µs}, // ~66.499791ms | ||
}, actual) | ||
} | ||
|
||
func TestDevice_PingMetrics_disconnected(t *testing.T) { | ||
t.Parallel() | ||
|
||
if actual := (&Device{}).PingMetrics(); actual != nil { | ||
t.Errorf("expected PingMetrics() to return nil, got %+v", actual) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package exporter | ||
|
||
import ( | ||
"math" | ||
"sort" | ||
"time" | ||
) | ||
|
||
// PingMetrics is a dumb data point computed from a list of PingResults. | ||
type PingMetrics struct { | ||
PacketsSent int // number of packets sent | ||
PacketsLost int // number of packets lost | ||
Best time.Duration // best RTT | ||
Worst time.Duration // worst RTT | ||
Median time.Duration // median RTT | ||
Mean time.Duration // mean RTT | ||
StdDev time.Duration // RTT std deviation | ||
} | ||
|
||
// PingResult stores the information about a single ping, in particular | ||
// the round-trip time or whether the packet was lost. | ||
type PingResult struct { | ||
RTT time.Duration | ||
Lost bool | ||
} | ||
|
||
// PingHistory represents the ping history for a single node/device. | ||
type PingHistory []PingResult | ||
|
||
// NewHistory creates a new History object with a specific capacity. | ||
func NewHistory(capacity int) PingHistory { | ||
return make(PingHistory, 0, capacity) | ||
} | ||
|
||
// AddResult saves a ping result into the internal history. | ||
func (h *PingHistory) Add(rtt time.Duration, lost bool) { | ||
*h = append(*h, PingResult{RTT: rtt, Lost: lost}) | ||
} | ||
|
||
// Compute aggregates the result history into a single data point. | ||
func (h PingHistory) Compute() *PingMetrics { | ||
numFailure := 0 | ||
numTotal := len(h) | ||
|
||
if numTotal == 0 { | ||
return nil | ||
} | ||
|
||
data := make([]float64, 0, numTotal) | ||
var best, worst, mean, stddev, total, sumSquares float64 | ||
|
||
for _, curr := range h { | ||
if curr.Lost { | ||
numFailure++ | ||
continue | ||
} | ||
|
||
rtt := curr.RTT.Seconds() | ||
if rtt < best || len(data) == 0 { | ||
best = rtt | ||
} | ||
if rtt > worst || len(data) == 0 { | ||
worst = rtt | ||
} | ||
data = append(data, rtt) | ||
total += rtt | ||
} | ||
|
||
size := float64(numTotal - numFailure) | ||
mean = total / size | ||
for _, rtt := range data { | ||
sumSquares += math.Pow(rtt-mean, 2) | ||
} | ||
stddev = math.Sqrt(sumSquares / size) | ||
|
||
median := math.NaN() | ||
if l := len(data); l > 0 { | ||
sort.Float64Slice(data).Sort() | ||
if l%2 == 0 { | ||
median = (data[l/2-1] + data[l/2]) / 2 | ||
} else { | ||
median = data[l/2] | ||
} | ||
} | ||
|
||
return &PingMetrics{ | ||
PacketsSent: numTotal, | ||
PacketsLost: numFailure, | ||
Best: time.Duration(best * float64(time.Second)), | ||
Worst: time.Duration(worst * float64(time.Second)), | ||
Median: time.Duration(median * float64(time.Second)), | ||
Mean: time.Duration(mean * float64(time.Second)), | ||
StdDev: time.Duration(stddev * float64(time.Second)), | ||
} | ||
} |