forked from statping/statping
-
Notifications
You must be signed in to change notification settings - Fork 0
/
services.go
463 lines (423 loc) · 12.5 KB
/
services.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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"encoding/json"
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"sort"
"strconv"
"time"
)
type Service struct {
*types.Service
}
// Select will return the *types.Service struct for Service
func (s *Service) Select() *types.Service {
return s.Service
}
// ReturnService will convert *types.Service to *core.Service
func ReturnService(s *types.Service) *Service {
return &Service{s}
}
func Services() []types.ServiceInterface {
return CoreApp.Services
}
// SelectService returns a *core.Service from in memory
func SelectService(id int64) *Service {
for _, s := range Services() {
if s.Select().Id == id {
return s.(*Service)
}
}
return nil
}
// SelectServicer returns a types.ServiceInterface from in memory
func SelectServicer(id int64) types.ServiceInterface {
for _, s := range Services() {
if s.Select().Id == id {
return s
}
}
return nil
}
// CheckinProcess runs the checkin routine for each checkin attached to service
func (s *Service) CheckinProcess() {
checkins := s.Checkins()
for _, c := range checkins {
c.Start()
go c.Routine()
}
}
// Checkins will return a slice of Checkins for a Service
func (s *Service) Checkins() []*Checkin {
var checkin []*Checkin
checkinDB().Where("service = ?", s.Id).Find(&checkin)
return checkin
}
// LimitedCheckins will return a slice of Checkins for a Service
func (s *Service) LimitedCheckins() []*Checkin {
var checkin []*Checkin
checkinDB().Where("service = ?", s.Id).Limit(10).Find(&checkin)
return checkin
}
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup.
func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
var services []*Service
db := servicesDB().Find(&services).Order("order_id desc")
if db.Error != nil {
utils.Log(3, fmt.Sprintf("service error: %v", db.Error))
return nil, db.Error
}
CoreApp.Services = nil
for _, service := range services {
if start {
service.Start()
service.CheckinProcess()
}
fails := service.LimitedFailures(limitedFailures)
for _, f := range fails {
service.Failures = append(service.Failures, f)
}
CoreApp.Services = append(CoreApp.Services, service)
}
sort.Sort(ServiceOrder(CoreApp.Services))
return services, db.Error
}
// reorderServices will sort the services based on 'order_id'
func reorderServices() {
sort.Sort(ServiceOrder(CoreApp.Services))
}
// ToJSON will convert a service to a JSON string
func (s *Service) ToJSON() string {
data, _ := json.Marshal(s)
return string(data)
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() float64 {
total, _ := s.TotalHits()
if total == 0 {
return float64(0)
}
sum, _ := s.Sum()
avg := sum / float64(total) * 100
amount := fmt.Sprintf("%0.0f", avg*10)
val, _ := strconv.ParseFloat(amount, 10)
return val
}
// Online24 returns the service's uptime percent within last 24 hours
func (s *Service) Online24() float32 {
ago := time.Now().Add(-24 * time.Hour)
return s.OnlineSince(ago)
}
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *Service) OnlineSince(ago time.Time) float32 {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
s.Online24Hours = 100.00
return s.Online24Hours
}
total, _ := s.TotalHitsSince(ago)
if total == 0 {
s.Online24Hours = 0
return s.Online24Hours
}
avg := float64(failed) / float64(total) * 100
avg = 100 - avg
if avg < 0 {
avg = 0
}
amount, _ := strconv.ParseFloat(fmt.Sprintf("%0.2f", avg), 10)
s.Online24Hours = float32(amount)
return s.Online24Hours
}
// DateScan struct is for creating the charts.js graph JSON array
type DateScan struct {
CreatedAt string `json:"x,omitempty"`
Value int64 `json:"y"`
}
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []DateScan `json:"data"`
}
// lastFailure returns the last failure a service had
func (s *Service) lastFailure() *failure {
limited := s.LimitedFailures(1)
if len(limited) == 0 {
return nil
}
last := limited[len(limited)-1]
return last
}
// SmallText returns a short description about a services status
// service.SmallText()
// // Online since Monday 3:04:05PM, Jan _2 2006
func (s *Service) SmallText() string {
last := s.LimitedFailures(1)
hits, _ := s.LimitedHits()
zone := CoreApp.Timezone
if s.Online {
if len(last) == 0 {
return fmt.Sprintf("Online since %v", utils.Timezoner(s.CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
} else {
return fmt.Sprintf("Online, last failure was %v", utils.Timezoner(hits[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
}
}
if len(last) > 0 {
lastFailure := s.lastFailure()
got, _ := timeago.TimeAgoWithTime(time.Now().Add(s.Downtime()), time.Now())
return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError())
} else {
return fmt.Sprintf("%v is currently offline", s.Name)
}
}
// DowntimeText will return the amount of downtime for a service based on the duration
// service.DowntimeText()
// // Service has been offline for 15 minutes
func (s *Service) DowntimeText() string {
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
}
// Dbtimestamp will return a SQL query for grouping by date
func Dbtimestamp(group string, column string) string {
seconds := 3600
switch group {
case "minute":
seconds = 60
case "hour":
seconds = 3600
case "day":
seconds = 86400
case "week":
seconds = 604800
case "month":
seconds = 2592000
case "year":
seconds = 31557600
default:
seconds = 60
}
switch CoreApp.DbConnection {
case "mysql":
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(%v) AS value", column)
case "postgres":
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(%v) AS value", group, column)
default:
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(%v) as value", seconds, seconds, column)
}
}
// Downtime returns the amount of time of a offline service
func (s *Service) Downtime() time.Duration {
hits, _ := s.Hits()
fail := s.lastFailure()
if fail == nil {
return time.Duration(0)
}
if len(hits) == 0 {
return time.Now().UTC().Sub(fail.CreatedAt.UTC())
}
since := fail.CreatedAt.UTC().Sub(fail.CreatedAt.UTC())
return since
}
// GraphDataRaw will return all the hits between 2 times for a Service
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
var d []DateScan
var amount int64
model := service.(*Service).HitsBetween(start, end, group, column)
model.Count(&amount)
if amount == 0 {
return &DateScanObj{[]DateScan{}}
}
model = model.Order("timeframe asc", false).Group("timeframe")
rows, err := model.Rows()
if err != nil {
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
}
for rows.Next() {
var gd DateScan
var createdAt string
var value float64
var createdTime time.Time
var err error
rows.Scan(&createdAt, &value)
if CoreApp.DbConnection == "postgres" {
createdTime, err = time.Parse(types.TIME_NANO, createdAt)
if err != nil {
utils.Log(4, fmt.Errorf("issue parsing time from database: %v to %v", createdAt, types.TIME_NANO))
}
} else {
createdTime, err = time.Parse(types.TIME, createdAt)
}
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME)
gd.Value = int64(value * 1000)
d = append(d, gd)
}
return &DateScanObj{d}
}
// ToString will convert the DateScanObj into a JSON string for the charts to render
func (d *DateScanObj) ToString() string {
data, err := json.Marshal(d.Array)
if err != nil {
utils.Log(2, err)
return "{}"
}
return string(data)
}
// AvgUptime24 returns a service's average online status for last 24 hours
func (s *Service) AvgUptime24() string {
ago := time.Now().Add(-24 * time.Hour)
return s.AvgUptime(ago)
}
// AvgUptime returns average online status for last 24 hours
func (s *Service) AvgUptime(ago time.Time) string {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
return "100"
}
total, _ := s.TotalHitsSince(ago)
if total == 0 {
return "0.00"
}
percent := float64(failed) / float64(total) * 100
percent = 100 - percent
if percent < 0 {
percent = 0
}
amount := fmt.Sprintf("%0.2f", percent)
if amount == "100.00" {
amount = "100"
}
return amount
}
// TotalUptime returns the total uptime percent of a service
func (s *Service) TotalUptime() string {
hits, _ := s.TotalHits()
failures, _ := s.TotalFailures()
percent := float64(failures) / float64(hits) * 100
percent = 100 - percent
if percent < 0 {
percent = 0
}
amount := fmt.Sprintf("%0.2f", percent)
if amount == "100.00" {
amount = "100"
}
return amount
}
// index returns a services index int for updating the []*core.Services slice
func (s *Service) index() int {
for k, service := range CoreApp.Services {
if s.Id == service.(*Service).Id {
return k
}
}
return 0
}
// updateService will update a service in the []*core.Services slice
func updateService(service *Service) {
index := service.index()
CoreApp.Services[index] = service
}
// Delete will remove a service from the database, it will also end the service checking go routine
func (s *Service) Delete() error {
i := s.index()
err := servicesDB().Delete(s)
if err.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to delete service %v. %v", s.Name, err.Error))
return err.Error
}
s.Close()
slice := CoreApp.Services
CoreApp.Services = append(slice[:i], slice[i+1:]...)
reorderServices()
notifier.OnDeletedService(s.Service)
return err.Error
}
// UpdateSingle will update a single column for a service
func (s *Service) UpdateSingle(attr ...interface{}) error {
return servicesDB().Model(s).Update(attr).Error
}
// Update will update a service in the database, the service's checking routine can be restarted by passing true
func (s *Service) Update(restart bool) error {
err := servicesDB().Update(&s)
if err.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
return err.Error
}
// clear the notification queue for a service
if !s.AllowNotifications.Bool {
for _, n := range CoreApp.Notifications {
notif := n.(notifier.Notifier).Select()
notif.ResetUniqueQueue(s.Id)
}
}
if restart {
s.Close()
s.Start()
s.SleepDuration = time.Duration(s.Interval) * time.Second
go s.CheckQueue(true)
}
reorderServices()
updateService(s)
notifier.OnUpdatedService(s.Service)
return err.Error
}
// Create will create a service and insert it into the database
func (s *Service) Create(check bool) (int64, error) {
s.CreatedAt = time.Now()
db := servicesDB().Create(s)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error))
return 0, db.Error
}
s.Start()
go s.CheckQueue(check)
CoreApp.Services = append(CoreApp.Services, s)
reorderServices()
notifier.OnNewService(s.Service)
return s.Id, nil
}
// Messages returns all Messages for a Service
func (s *Service) Messages() []*Message {
messages := SelectServiceMessages(s.Id)
return messages
}
// ActiveMessages returns all Messages for a Service
func (s *Service) ActiveMessages() []*Message {
var messages []*Message
msgs := SelectServiceMessages(s.Id)
for _, m := range msgs {
if m.StartOn.UTC().After(time.Now().UTC()) {
messages = append(messages, m)
}
}
return messages
}
// ServicesCount returns the amount of services inside the []*core.Services slice
func (c *Core) ServicesCount() int {
return len(c.Services)
}
// CountOnline
func (c *Core) CountOnline() int {
amount := 0
for _, s := range CoreApp.Services {
if s.Select().Online {
amount++
}
}
return amount
}