Skip to content

Commit b186216

Browse files
authored
Merge pull request #22 from featheredtoast/monitor-multiple
1.0 fixes, adds ability to monitor multiple servers, and future save file
2 parents fdf7a67 + 3737d15 commit b186216

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2404
-980
lines changed

Companion/exporter/collector_runner.go

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,83 @@ package exporter
22

33
import (
44
"context"
5+
"log"
6+
"regexp"
57
"time"
8+
9+
"github.com/prometheus/client_golang/prometheus"
610
)
711

12+
func SanitizeSessionName(sessionName string) string {
13+
re := regexp.MustCompile(`[^\w\s]`)
14+
return re.ReplaceAllString(sessionName, "")
15+
}
16+
817
type CollectorRunner struct {
9-
collectors []Collector
10-
ctx context.Context
11-
cancel context.CancelFunc
18+
collectors []Collector
19+
ctx context.Context
20+
cancel context.CancelFunc
21+
frmBaseUrl string
22+
sessionName string
1223
}
1324

1425
type Collector interface {
15-
Collect()
26+
Collect(string, string)
27+
DropCache()
28+
}
29+
30+
type SessionInfo struct {
31+
SessionName string `json:"SessionName"`
1632
}
1733

18-
func NewCollectorRunner(ctx context.Context, collectors ...Collector) *CollectorRunner {
34+
func NewCollectorRunner(ctx context.Context, frmBaseUrl string, collectors ...Collector) *CollectorRunner {
1935
ctx, cancel := context.WithCancel(ctx)
2036
return &CollectorRunner{
21-
ctx: ctx,
22-
cancel: cancel,
23-
collectors: collectors,
37+
ctx: ctx,
38+
cancel: cancel,
39+
collectors: collectors,
40+
frmBaseUrl: frmBaseUrl,
41+
sessionName: "default",
2442
}
2543
}
2644

27-
func (c *CollectorRunner) Start() {
28-
c.Collect()
29-
for {
30-
select {
31-
case <-c.ctx.Done():
32-
return
33-
case <-Clock.After(5 * time.Second):
34-
c.Collect()
45+
func (c *CollectorRunner) updateSessionName() {
46+
details := SessionInfo{}
47+
err := retrieveData(c.frmBaseUrl+"/getSessionInfo", &details)
48+
if err != nil {
49+
log.Printf("error reading session name from FRM: %s\n", err)
50+
return
51+
}
52+
newSessionName := SanitizeSessionName(details.SessionName)
53+
if newSessionName != "" && newSessionName != c.sessionName {
54+
log.Printf("%s has a new session name: %s\n", c.frmBaseUrl, newSessionName)
55+
for _, metric := range RegisteredMetrics {
56+
metric.DeletePartialMatch(prometheus.Labels{"url": c.frmBaseUrl, "session_name": c.sessionName})
57+
}
58+
for _, collector := range c.collectors {
59+
collector.DropCache()
3560
}
61+
c.sessionName = newSessionName
3662
}
3763
}
3864

65+
func (c *CollectorRunner) Start() error {
66+
c.updateSessionName()
67+
c.Collect(c.frmBaseUrl, c.sessionName)
68+
t := Clock.TickerFunc(c.ctx, 5*time.Second, func() error {
69+
c.updateSessionName()
70+
c.Collect(c.frmBaseUrl, c.sessionName)
71+
return nil
72+
})
73+
return t.Wait()
74+
}
75+
3976
func (c *CollectorRunner) Stop() {
4077
c.cancel()
4178
}
4279

43-
func (c *CollectorRunner) Collect() {
80+
func (c *CollectorRunner) Collect(server string, sessionName string) {
4481
for _, collector := range c.collectors {
45-
collector.Collect()
82+
collector.Collect(server, sessionName)
4683
}
4784
}

Companion/exporter/collector_runner_test.go

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ package exporter_test
22

33
import (
44
"context"
5+
"time"
56

67
"github.com/AP-Hunt/FicsitRemoteMonitoringCompanion/Companion/exporter"
7-
"github.com/benbjohnson/clock"
88
. "github.com/onsi/ginkgo/v2"
9+
"github.com/coder/quartz"
910
. "github.com/onsi/gomega"
10-
"time"
1111
)
1212

1313
type TestCollector struct {
@@ -19,42 +19,49 @@ func NewTestCollector() *TestCollector {
1919
counter: 0,
2020
}
2121
}
22-
func (t *TestCollector) Collect() {
22+
func (t *TestCollector) Collect(url string, sessionName string) {
2323
t.counter = t.counter + 1
2424
}
25+
func (t *TestCollector) DropCache() {}
2526

2627
var _ = Describe("CollectorRunner", func() {
28+
var url string
29+
30+
BeforeEach(func() {
31+
FRMServer.Reset()
32+
url = FRMServer.server.URL
33+
FRMServer.ReturnsSessionInfoData(exporter.SessionInfo{
34+
SessionName: "test",
35+
})
36+
})
37+
2738
Describe("Basic Functionality", func() {
2839
It("runs on init and on each timeout", func() {
2940
ctx, cancel := context.WithCancel(context.Background())
30-
testTime := clock.NewMock()
41+
timeout, _ := context.WithTimeout(ctx, 10*time.Second)
42+
defer cancel()
43+
testTime := quartz.NewMock(GinkgoTB())
3144
exporter.Clock = testTime
45+
trap := testTime.Trap().TickerFunc()
46+
defer trap.Close()
3247

3348
c1 := NewTestCollector()
3449
c2 := NewTestCollector()
35-
runner := exporter.NewCollectorRunner(ctx, c1, c2)
50+
runner := exporter.NewCollectorRunner(ctx, url, c1, c2)
3651
go runner.Start()
37-
testTime.Add(5 * time.Second)
38-
testTime.Add(5 * time.Second)
39-
testTime.Add(5 * time.Second)
40-
cancel()
52+
call := trap.MustWait(timeout)
53+
call.Release()
54+
55+
for i := 0; i < 2; i++ {
56+
_, w := testTime.AdvanceNext()
57+
w.MustWait(ctx)
58+
}
4159
Expect(c1.counter).To(Equal(3))
4260
Expect(c2.counter).To(Equal(3))
4361
})
4462

45-
It("does not run after being canceled", func() {
46-
ctx, cancel := context.WithCancel(context.Background())
47-
testTime := clock.NewMock()
48-
exporter.Clock = testTime
49-
50-
c1 := NewTestCollector()
51-
runner := exporter.NewCollectorRunner(ctx, c1)
52-
go runner.Start()
53-
testTime.Add(5 * time.Second)
54-
cancel()
55-
testTime.Add(5 * time.Second)
56-
testTime.Add(5 * time.Second)
57-
Expect(c1.counter).To(Equal(1))
63+
It("sanitizes session name", func() {
64+
Expect(exporter.SanitizeSessionName(`it's giving -- 123456!@#$%^&*() yo hollar "'"`)).To(Equal(`its giving 123456 yo hollar ` ))
5865
})
5966
})
6067
})

Companion/exporter/common.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package exporter
22

33
import (
44
"encoding/json"
5-
"github.com/benbjohnson/clock"
5+
"fmt"
6+
"github.com/coder/quartz"
67
"log"
78
"net/http"
89
"regexp"
@@ -12,7 +13,7 @@ import (
1213

1314
var timeRegex = regexp.MustCompile(`\d\d:\d\d:\d\d`)
1415

15-
var Clock = clock.New()
16+
var Clock = quartz.NewReal()
1617

1718
func parseTimeSeconds(timeStr string) *float64 {
1819
match := timeRegex.FindStringSubmatch(timeStr)
@@ -42,6 +43,10 @@ func retrieveData(frmAddress string, details any) error {
4243
return err
4344
}
4445

46+
if resp.StatusCode != 200 {
47+
return fmt.Errorf("non-200 returned when retireving data: %d", resp.StatusCode)
48+
}
49+
4550
defer resp.Body.Close()
4651

4752
decoder := json.NewDecoder(resp.Body)

Companion/exporter/drone_station_collector.go

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,81 +3,111 @@ package exporter
33
import (
44
"log"
55
"strconv"
6+
7+
"github.com/prometheus/client_golang/prometheus"
68
)
79

810
type DroneStationCollector struct {
9-
FRMAddress string
11+
endpoint string
12+
metricsDropper *MetricsDropper
13+
}
14+
15+
type DroneFuelInventory struct {
16+
Name string `json:"Name"`
17+
Amount float64 `json:"Amount"`
18+
}
19+
20+
type DroneActiveFuel struct {
21+
Name string `json:"FuelName"`
22+
Rate float64 `json:"EstimatedFuelCostRate"`
1023
}
1124

1225
type DroneStationDetails struct {
13-
Id string `json:"ID"`
14-
HomeStation string `json:"Name"`
15-
PairedStation string `json:"PairedStation"`
16-
DroneStatus string `json:"DroneStatus"`
17-
AvgIncRate float64 `json:"AvgIncRate"`
18-
AvgIncStack float64 `json:"AvgIncStack"`
19-
AvgOutRate float64 `json:"AvgOutRate"`
20-
AvgOutStack float64 `json"AvgOutStack"`
21-
AvgRndTrip string `json:"AvgRndTrip"`
22-
AvgTotalIncRate float64 `json:"AvgTotalIncRate"`
23-
AvgTotalIncStack float64 `json:"AvgTotalIncStack"`
24-
AvgTotalOutRate float64 `json:"AvgTotalOutRate"`
25-
AvgTotalOutStack float64 `json:"AvgTotalOutStack"`
26-
AvgTripIncAmt float64 `json:"AvgTripIncAmt"`
27-
EstRndTrip string `json:"EstRndTrip"`
28-
EstTotalTransRate float64 `json:"EstTotalTransRate"`
29-
EstTransRate float64 `json:"EstTransRate"`
30-
EstLatestTotalIncStack float64 `json:"EstLatestTotalIncStack"`
31-
EstLatestTotalOutStack float64 `json:"EstLatestTotalOutStack"`
32-
LatestIncStack float64 `json:"LatestIncStack"`
33-
LatestOutStack float64 `json:"LatestOutStack"`
34-
LatestRndTrip string `json:"LatestRndTrip"`
35-
LatestTripIncAmt float64 `json:"LatestTripIncAmt"`
36-
LatestTripOutAmt float64 `json:"LatestTripOutAmt"`
37-
MedianRndTrip string `json:"MedianRndTrip"`
38-
MedianTripIncAmt float64 `json:"MedianTripIncAmt"`
39-
MedianTripOutAmt float64 `json:"MedianTripOutAmt"`
40-
EstBatteryRate float64 `json:"EstBatteryRate"`
41-
PowerInfo PowerInfo `json:"PowerInfo"`
26+
Id string `json:"ID"`
27+
HomeStation string `json:"Name"`
28+
PairedStation string `json:"PairedStation"`
29+
DroneStatus string `json:"DroneStatus"`
30+
AvgIncRate float64 `json:"AvgIncRate"`
31+
AvgIncStack float64 `json:"AvgIncStack"`
32+
AvgOutRate float64 `json:"AvgOutRate"`
33+
AvgOutStack float64 `json"AvgOutStack"`
34+
AvgRndTrip string `json:"AvgRndTrip"`
35+
AvgTotalIncRate float64 `json:"AvgTotalIncRate"`
36+
AvgTotalIncStack float64 `json:"AvgTotalIncStack"`
37+
AvgTotalOutRate float64 `json:"AvgTotalOutRate"`
38+
AvgTotalOutStack float64 `json:"AvgTotalOutStack"`
39+
AvgTripIncAmt float64 `json:"AvgTripIncAmt"`
40+
EstTotalTransRate float64 `json:"EstTotalTransRate"`
41+
EstTransRate float64 `json:"EstTransRate"`
42+
EstLatestTotalIncStack float64 `json:"EstLatestTotalIncStack"`
43+
EstLatestTotalOutStack float64 `json:"EstLatestTotalOutStack"`
44+
LatestIncStack float64 `json:"LatestIncStack"`
45+
LatestOutStack float64 `json:"LatestOutStack"`
46+
LatestRndTrip float64 `json:"LatestRndTrip"`
47+
LatestTripIncAmt float64 `json:"LatestTripIncAmt"`
48+
LatestTripOutAmt float64 `json:"LatestTripOutAmt"`
49+
MedianRndTrip string `json:"MedianRndTrip"`
50+
MedianTripIncAmt float64 `json:"MedianTripIncAmt"`
51+
MedianTripOutAmt float64 `json:"MedianTripOutAmt"`
52+
PowerInfo PowerInfo `json:"PowerInfo"`
53+
Fuel []DroneFuelInventory `json:"FuelInventory"`
54+
ActiveFuel DroneActiveFuel `json:"ActiveFuel"`
4255
}
4356

44-
func NewDroneStationCollector(frmAddress string) *DroneStationCollector {
57+
func NewDroneStationCollector(endpoint string) *DroneStationCollector {
4558
return &DroneStationCollector{
46-
FRMAddress: frmAddress,
59+
endpoint: endpoint,
60+
metricsDropper: NewMetricsDropper(
61+
DronePortFuelRate,
62+
DronePortFuelAmount,
63+
DronePortRndTrip,
64+
),
4765
}
4866
}
4967

50-
func (c *DroneStationCollector) Collect() {
68+
func (c *DroneStationCollector) Collect(frmAddress string, sessionName string) {
5169
details := []DroneStationDetails{}
52-
err := retrieveData(c.FRMAddress, &details)
70+
err := retrieveData(frmAddress+c.endpoint, &details)
5371
if err != nil {
72+
c.metricsDropper.DropStaleMetricLabels()
5473
log.Printf("error reading drone station statistics from FRM: %s\n", err)
5574
return
5675
}
5776

5877
powerInfo := map[float64]float64{}
78+
maxPowerInfo := map[float64]float64{}
5979
for _, d := range details {
80+
c.metricsDropper.CacheFreshMetricLabel(prometheus.Labels{"url": frmAddress, "session_name": sessionName, "id": d.Id})
6081
id := d.Id
6182
home := d.HomeStation
6283
paired := d.PairedStation
6384

64-
DronePortBatteryRate.WithLabelValues(id, home, paired).Set(d.EstBatteryRate)
65-
66-
roundTrip := parseTimeSeconds(d.LatestRndTrip)
67-
if roundTrip != nil {
68-
DronePortRndTrip.WithLabelValues(id, home, paired).Set(*roundTrip)
85+
if len(d.Fuel) > 0 {
86+
DronePortFuelAmount.WithLabelValues(id, home, d.Fuel[0].Name, frmAddress, sessionName).Set(d.Fuel[0].Amount)
87+
DronePortFuelRate.WithLabelValues(id, home, d.Fuel[0].Name, frmAddress, sessionName).Set(d.ActiveFuel.Rate)
88+
DronePortRndTrip.WithLabelValues(id, home, paired, frmAddress, sessionName).Set(d.LatestRndTrip)
6989
}
7090

71-
val, ok := powerInfo[d.PowerInfo.CircuitId]
91+
val, ok := powerInfo[d.PowerInfo.CircuitGroupId]
7292
if ok {
73-
powerInfo[d.PowerInfo.CircuitId] = val + d.PowerInfo.PowerConsumed
93+
powerInfo[d.PowerInfo.CircuitGroupId] = val + d.PowerInfo.PowerConsumed
7494
} else {
75-
powerInfo[d.PowerInfo.CircuitId] = d.PowerInfo.PowerConsumed
95+
powerInfo[d.PowerInfo.CircuitGroupId] = d.PowerInfo.PowerConsumed
96+
}
97+
val, ok = maxPowerInfo[d.PowerInfo.CircuitGroupId]
98+
if ok {
99+
maxPowerInfo[d.PowerInfo.CircuitGroupId] = val + d.PowerInfo.MaxPowerConsumed
100+
} else {
101+
maxPowerInfo[d.PowerInfo.CircuitGroupId] = d.PowerInfo.MaxPowerConsumed
76102
}
77103
}
78104

79105
for circuitId, powerConsumed := range powerInfo {
80106
cid := strconv.FormatFloat(circuitId, 'f', -1, 64)
81-
DronePortPower.WithLabelValues(cid).Set(powerConsumed)
107+
DronePortPower.WithLabelValues(cid, frmAddress, sessionName).Set(powerConsumed)
82108
}
109+
110+
c.metricsDropper.DropStaleMetricLabels()
83111
}
112+
113+
func (c *DroneStationCollector) DropCache() {}

0 commit comments

Comments
 (0)