-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathdatabase.go
297 lines (255 loc) · 9.12 KB
/
database.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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
"github.com/nitishm/go-rejson/v4"
)
var (
rdb *redis.Client // set in main()
rjh *rejson.Handler // set in main()
redisAddr = "database:6379" // set in .env
redisPass = "" // set in .env
heartbeatStats *HeartbeatStats = nil
heartbeatDevices *[]HeartbeatDevice = nil
)
// SetupDatabase creates the ReJSON handler and Redis client
func SetupDatabase() (*redis.Client, *rejson.Handler) {
rh := rejson.NewReJSONHandler()
client := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: redisPass,
})
// If connection doesn't work, panic
if _, err := client.Ping(context.Background()).Result(); err != nil {
log.Fatalf("- Failed to ping Redis server: %v\n", err)
}
// We have a working connection
log.Printf("- Connected to Redis at %s", redisAddr)
rh.SetGoRedisClient(client)
return client, rh
}
// SetupDatabaseSaving will run SaveLocalInDatabase every minute with a ticket
func SetupDatabaseSaving() {
ticker := time.NewTicker(1 * time.Minute)
go func() {
for {
select {
case <-ticker.C:
SaveLocalInDatabase()
}
}
}()
}
// SetupLocalValues will look for existing stats and devices in the database, and read them to local values
func SetupLocalValues() {
if res, err := rjh.JSONGet("stats", "."); err != nil {
log.Printf("- Error loading stats (using default): %v\n", err)
heartbeatStats = defaultHeartbeatStats
} else {
var stats HeartbeatStats
if err = json.Unmarshal(res.([]byte), &stats); err != nil {
panic(err)
}
log.Printf("- Loaded stats: %v\n", stats)
heartbeatStats = &stats
}
if res, err := rjh.JSONGet("devices", "."); err != nil {
log.Printf("- Error loading devices (using default): %v\n", err)
heartbeatDevices = defaultHeartbeatDevices
} else {
var devices []HeartbeatDevice
if err = json.Unmarshal(res.([]byte), &devices); err != nil {
panic(err)
}
log.Printf("- Loaded devices: %v\n", devices)
heartbeatDevices = &devices
}
}
// SaveLocalInDatabase will save local copies of small database stats and devices to the database every 5 minutes
func SaveLocalInDatabase() {
// This is also called when viewing the stats page, but we want to run it here to avoid missing time
// if nobody looks at the stats page
UpdateUptime()
log.Printf("- Autosaving database, uptime is %v\n", heartbeatStats.TotalUptime)
if _, err := rjh.JSONSet("stats", ".", heartbeatStats); err != nil {
log.Fatalf("Error saving stats: %v", err)
}
if _, err := rjh.JSONSet("devices", ".", heartbeatDevices); err != nil {
log.Fatalf("Error saving devices: %v", err)
}
}
// FormattedInfo will return the formatted info, displayed on the main page
func FormattedInfo() HeartbeatInfo {
currentTime := time.Now()
lastBeat := GetLastBeat()
UpdateLastBeatFmtV(lastBeat, currentTime)
return HeartbeatInfo{
LastSeen: LastSeen(),
TimeDifference: heartbeatStats.LastBeatFormatted,
MissingBeat: LongestAbsence(),
TotalBeats: heartbeatStats.TotalBeatsFormatted,
}
}
// LastSeen will return the formatted date of the last timestamp received from a beat
func LastSeen() string {
lastBeat := GetLastBeat()
if lastBeat == nil {
return "Never"
}
return time.Unix(lastBeat.Timestamp, 0).Format(timeFormat)
}
// LongestAbsence will return HeartbeatStats.LongestMissingBeat unless the current missing beat is longer
func LongestAbsence() string {
lastBeat := GetMostRecentBeat()
// This will happen when GetLastBeat returned a nil, and heartbeatDevices is empty
if lastBeat == nil {
heartbeatStats.LastBeatFormatted = "Never"
return "Never"
}
diff := time.Now().Unix() - lastBeat.Timestamp
// If current absence is bigger than record absence, return current absence
if diff > heartbeatStats.LongestMissingBeat {
heartbeatStats.LongestMissingBeat = diff
return FormattedTime(diff)
} else {
return FormattedTime(heartbeatStats.LongestMissingBeat)
}
}
func UpdateNumDevices() {
heartbeatStats.TotalDevicesFormatted = FormattedNum(int64(len(*heartbeatDevices)))
}
// UpdateUptime will update the uptime statistics
func UpdateUptime() {
now := time.Now().UnixMilli()
diff := now - uptimeTimer
uptimeTimer = now
heartbeatStats.TotalUptime += diff
heartbeatStats.TotalUptimeFormatted = FormattedTime(heartbeatStats.TotalUptime / 1000)
}
// UpdateLastBeatFmt will update the formatted last beat statistic
func UpdateLastBeatFmt() {
currentTime := time.Now()
lastBeat := GetLastBeat()
if lastBeat != nil {
heartbeatStats.LastBeatFormatted = TimeDifference(lastBeat.Timestamp, currentTime)
}
}
// UpdateLastBeatFmtV will update the formatted last beat statistic
func UpdateLastBeatFmtV(lastBeat *HeartbeatBeat, currentTime time.Time) {
if lastBeat != nil {
heartbeatStats.LastBeatFormatted = TimeDifference(lastBeat.Timestamp, currentTime)
}
}
// UpdateTotalBeats will update the formatted total beats statistic
func UpdateTotalBeats() {
heartbeatStats.TotalBeats += 1
heartbeatStats.TotalBeatsFormatted = FormattedNum(heartbeatStats.TotalBeats)
}
// UpdateDevice will update the LastBeat of a device
func UpdateDevice(beat HeartbeatBeat) {
var device HeartbeatDevice
n := -1
for index, tmpDevice := range *heartbeatDevices {
if tmpDevice.DeviceName == beat.DeviceName {
device = tmpDevice
n = index
break // name should only ever be matching once
}
}
if n == -1 { // couldn't find an existing device with the name
device = HeartbeatDevice{beat.DeviceName, beat, 0, 0}
}
diff := time.Now().Unix() - device.LastBeat.Timestamp
if diff > device.LongestMissingBeat {
device.LongestMissingBeat = diff
}
// We want to update the longest absence here (heartbeatStats.LongestMissingBeat) in case
// device.LongestMissingBeat > heartbeatStats.LongestMissingBeat *and* other devices haven't pinged recently
LongestAbsence()
device.LastBeat = beat
device.TotalBeats += 1
UpdateTotalBeats()
if n == -1 { // if device doesn't exist, append it, else replace it
*heartbeatDevices = append(*heartbeatDevices, device)
PostMessage("New device added", fmt.Sprintf("A new device called `%s` was added on <t:%v:d> at <t:%v:T>", beat.DeviceName, beat.Timestamp, beat.Timestamp), EmbedColorGreen, WebhookLevelSimple)
} else {
(*heartbeatDevices)[n] = device
}
}
// UpdateLastBeat will save the last beat and insert a new HeartbeatBeat into beats
func UpdateLastBeat(deviceName string, timestamp int64) error {
oldLastBeat := GetMostRecentBeat()
lastBeat := HeartbeatBeat{deviceName, timestamp}
UpdateDevice(lastBeat)
if _, err := rjh.JSONSet("last_beat", ".", lastBeat); err != nil {
return err
}
lastBeatArr := []HeartbeatBeat{lastBeat}
err := appendOrCreateArr("beats", ".", lastBeat, lastBeatArr)
if err == nil {
PostMessage("Successful beat", fmt.Sprintf("From `%s` on <t:%v:d> at <t:%v:T>", deviceName, timestamp, timestamp), EmbedColorBlue, WebhookLevelAll)
if oldLastBeat != nil && time.Duration(timestamp-oldLastBeat.Timestamp)*time.Second > 1*time.Hour {
PostMessage(
"Absence longer than 1 hour",
fmt.Sprintf(
"From <t:%v> to <t:%v>\nUTC Data: `%s,%s`",
oldLastBeat.Timestamp, timestamp,
FormattedUTCData(oldLastBeat.Timestamp), FormattedUTCData(timestamp),
),
EmbedColorOrange, WebhookLevelLongAbsence,
)
}
}
return err
}
// GetMostRecentBeat will return the most recent beat regardless of device, not just last-inserted beat
func GetMostRecentBeat() *HeartbeatBeat {
lastBeat := GetLastBeat()
for _, device := range *heartbeatDevices {
// This technically shouldn't be possible, as UpdateDevice is called inside UpdateLastBeat.
// Nevertheless, we would rather avoid a random panic from accessing a nil reference.
if lastBeat == nil {
lastBeat = &device.LastBeat
}
// If this device has a more recent beat than the most recent beat's device, use it instead.
// The reasoning behind this is, if we suddenly get a new beat from a device that hasn't sent a beat in a while
// we don't want it to set the longest absence to the old device's oldest absence, as this new device
// has sent beats more recently, and you have not actually been absent as long as the original lastBeat here.
if device.LastBeat.Timestamp > lastBeat.Timestamp {
lastBeat = &device.LastBeat
}
}
return lastBeat
}
// GetLastBeat will get the last beat, and return nilLastBeat if there was an error retrieving it (likely no beat)
func GetLastBeat() *HeartbeatBeat {
res, err := rjh.JSONGet("last_beat", ".")
if err != nil {
log.Printf("- Failed to get last beat: %v", err)
return nil
}
var lastBeat HeartbeatBeat
if err = json.Unmarshal(res.([]byte), &lastBeat); err != nil {
panic(err)
}
return &lastBeat
}
// appendOrCreateArr will append an obj to key in path, or create objArr in key in path if it doesn't exist
func appendOrCreateArr(key string, path string, obj interface{}, objArr interface{}) error {
// Check if key exists
if _, err := rjh.JSONGet(key, path); err != nil {
// Key doesn't exist, insert a new array with objArr
if _, err := rjh.JSONSet(key, path, objArr); err != nil {
return err
}
} else {
// Key does exist, append obj to existing key
_, err := rjh.JSONArrAppend(key, path, obj)
return err
}
return nil
}