-
Notifications
You must be signed in to change notification settings - Fork 0
/
device.go
227 lines (187 loc) · 6.41 KB
/
device.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
// Copyright 2017 Inca Roads LLC. All rights reserved.
// Use of this source code is governed by licenses granted by the
// copyright holder including that found in the LICENSE file.
// Device monitoring
package main
import (
"fmt"
"sort"
"time"
)
// Describes every device that has sent us a message
type seenDevice struct {
deviceUID string
deviceID uint32
label string
seen time.Time
everRecentlySeen bool
notifiedAsUnseen bool
minutesAgo int64
}
var seenDevices []seenDevice
// Class used to sort seen devices
type byDeviceKey []seenDevice
func (a byDeviceKey) Len() int { return len(a) }
func (a byDeviceKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byDeviceKey) Less(i, j int) bool {
// Primary:
// By capture time, most recent last (so that the most recent is nearest your attention, at the bottom in Slack)
if a[i].seen.Before(a[j].seen) {
return true
} else if a[i].seen.After(a[j].seen) {
return false
}
// Secondary
// In an attempt to keep things reasonably deterministic, use deviceUID
if a[i].deviceUID < a[j].deviceUID {
return true
} else if a[i].deviceUID > a[j].deviceUID {
return false
}
return false
}
// Keep track of all devices that have logged data via ttserve
func trackDevice(DeviceUID string, DeviceID uint32, whenSeen time.Time) {
var dev seenDevice
dev.deviceUID = DeviceUID
dev.deviceID = DeviceID
// Attempt to update the existing entry if we can find it
found := false
for i := 0; i < len(seenDevices); i++ {
if dev.deviceUID == seenDevices[i].deviceUID {
// Only pay attention to things that have truly recently come or gone
minutesAgo := int64(time.Since(whenSeen) / time.Minute)
if minutesAgo < deviceWarningAfterMinutes(dev.deviceUID) {
seenDevices[i].everRecentlySeen = true
// Notify when the device comes back
if seenDevices[i].notifiedAsUnseen {
message := AgoMinutes(uint32(time.Since(seenDevices[i].seen) / time.Minute))
sendToSafecastOps(fmt.Sprintf("** NOTE ** Device %s has returned after %s", seenDevices[i].deviceUID, message), SlackMsgUnsolicitedOps)
}
// Mark as having been seen on the latest date of any file having that time
seenDevices[i].notifiedAsUnseen = false
}
// Always track the most recent seen date
if seenDevices[i].seen.Before(whenSeen) {
seenDevices[i].seen = whenSeen
}
found = true
break
}
}
// Add a new array entry if necessary
if !found {
dev.seen = whenSeen
dev.minutesAgo = int64(time.Since(dev.seen) / time.Minute)
dev.everRecentlySeen = dev.minutesAgo < deviceWarningAfterMinutes(dev.deviceUID)
dev.notifiedAsUnseen = false
dev.label = SafecastDeviceUIDType(dev.deviceUID)
seenDevices = append(seenDevices, dev)
}
}
// Update message ages and notify
func sendExpiredSafecastDevicesToSlack() {
// Compute an expiration time
// Sweep through all devices that we've seen
for i := 0; i < len(seenDevices); i++ {
// Update when we've last seen the device
expiration := time.Now().Add(-(time.Duration(deviceWarningAfterMinutes(seenDevices[i].deviceUID)) * time.Minute))
seenDevices[i].minutesAgo = int64(time.Since(seenDevices[i].seen) / time.Minute)
// Notify Slack once and only once when a device has expired
if !seenDevices[i].notifiedAsUnseen && seenDevices[i].everRecentlySeen {
if seenDevices[i].seen.Before(expiration) {
seenDevices[i].notifiedAsUnseen = true
sendToSafecastOps(fmt.Sprintf("** Warning ** Device %s hasn't been seen for %s",
seenDevices[i].deviceUID,
AgoMinutes(uint32(seenDevices[i].minutesAgo))), SlackMsgUnsolicitedOps)
}
}
}
}
// Refresh the labels on cached devices
func refreshDeviceSummaryLabels() {
// First, age out the expired devices and recompute when last seen
sendExpiredSafecastDevicesToSlack()
// Next sort the device list
sortedDevices := seenDevices
sort.Sort(byDeviceKey(sortedDevices))
// Sweep over all these devices in sorted order, refreshing label
for i := 0; i < len(sortedDevices); i++ {
sortedDevices[i].label, _, _, _, _ = GetDeviceStatusSummary(sortedDevices[i].deviceUID)
}
}
// Get the number of minutes after which to expire a device
func deviceWarningAfterMinutes(deviceUID string) int64 {
// On 2017-08-14 Ray changed to only warn very rarely, because it was getting
// far, far too noisy in the ops channel with lots of devices.
return 24 * 60
}
// Get a summary of devices that are older than this many minutes ago
func sendSafecastDeviceSummaryToSlack(user string, header string, fOffline bool) {
// Force a re-read of the sheet, just to ensure that it reflects the lastest changes
sheetInvalidateCache()
// First, age out the expired devices and recompute when last seen
sendExpiredSafecastDevicesToSlack()
// Next sort the device list
sortedDevices := seenDevices
sort.Sort(byDeviceKey(sortedDevices))
// Finally, sweep over all these devices in sorted order,
// generating a single large text string to be sent as a Slack message
s := header
numAdded := 0
numPending := 0
for i := 0; i < len(sortedDevices); i++ {
// Skip if the online state doesn't match
isOffline := sortedDevices[i].minutesAgo > (12 * 60)
if isOffline != fOffline {
continue
}
// Add it to the summary
if s != "" {
s += "\n"
}
id := sortedDevices[i].deviceUID
label, gps, _, _, summary := GetDeviceStatusSummary(id)
// Refresh cached label
sortedDevices[i].label = label
s += fmt.Sprintf("<http://%s%s%s|%s> ", TTServerHTTPAddress, TTServerTopicDeviceStatus, id, id)
s += fmt.Sprintf("<http://%s%s%s|chk> ", TTServerHTTPAddress, TTServerTopicDeviceCheck, id)
s += fmt.Sprintf("<http://%s%s%s%s.json|log> ", TTServerHTTPAddress, TTServerTopicDeviceLog, time.Now().UTC().Format("2006-01"+DeviceLogSep()), DeviceUIDFilename(id))
if gps != "" {
s += gps + " "
} else {
s += "gps "
}
if sortedDevices[i].minutesAgo != 0 {
s += fmt.Sprintf("%s ago", AgoMinutes(uint32(sortedDevices[i].minutesAgo)))
}
if label != "" {
s += fmt.Sprintf(" %s", label)
}
if summary != "" {
s += " " + summary
}
// Display
numAdded++
numPending++
if numPending > 9 {
sendToSafecastOps(s, SlackMsgReply)
s = ""
numPending = 0
time.Sleep(500 * time.Millisecond)
}
}
// None
if numAdded == 0 {
if fOffline {
s = "All devices are currently online."
} else {
s = "All devices are currently offline."
}
numPending++
}
// Send it to Slack
if numPending > 0 {
sendToSafecastOps(s, SlackMsgReply)
}
}