github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/monitor/notification.go (about) 1 package monitor 2 3 import ( 4 "fmt" 5 "strconv" 6 "time" 7 8 "github.com/evergreen-ci/evergreen" 9 "github.com/evergreen-ci/evergreen/model/host" 10 "github.com/evergreen-ci/evergreen/model/user" 11 "github.com/mongodb/grip" 12 "github.com/pkg/errors" 13 ) 14 15 // the warning thresholds for spawned hosts, in decreasing order of recency 16 var ( 17 spawnWarningThresholds = []time.Duration{ 18 time.Duration(2) * time.Hour, 19 time.Duration(12) * time.Hour, 20 } 21 22 // the threshold for what is considered "slow provisioning" 23 slowProvisioningThreshold = 20 * time.Minute 24 ) 25 26 const ( 27 // the key for a host's notifications about slow provisioning 28 slowProvisioningWarning = "late-provision-warning" 29 ) 30 31 // a function that outputs any necessary notifications 32 type notificationBuilder func(*evergreen.Settings) ([]notification, error) 33 34 // contains info about a notification that should be sent 35 type notification struct { 36 recipient string 37 subject string 38 message string 39 threshold string 40 host host.Host 41 42 // to fire after the notification is sent - usually intended to set db 43 // fields indicating that the notification has been sent 44 callback func(host.Host, string) error 45 } 46 47 // spawnHostExpirationWarnings is a notificationBuilder to build any necessary 48 // warnings about hosts that will be expiring soon (but haven't expired yet) 49 func spawnHostExpirationWarnings(settings *evergreen.Settings) ([]notification, 50 error) { 51 52 grip.Info("Building spawned host expiration warnings...") 53 54 // sanity check, since the thresholds are supplied in code 55 if len(spawnWarningThresholds) == 0 { 56 grip.Warningln("there are no currently set warning thresholds for spawned hosts;", 57 "users will not receive emails warning them of imminent host expiration") 58 return nil, nil 59 } 60 61 // assumed to be the first warning threshold (the least recent one) 62 firstWarningThreshold := 63 spawnWarningThresholds[len(spawnWarningThresholds)-1] 64 65 // find all spawned hosts that have passed at least one warning threshold 66 now := time.Now() 67 thresholdTime := now.Add(firstWarningThreshold) 68 hosts, err := host.Find(host.ByExpiringBetween(now, thresholdTime)) 69 if err != nil { 70 return nil, errors.Wrap(err, "error finding spawned hosts that will be expiring soon") 71 } 72 73 // the eventual list of warning notifications to be sent 74 warnings := []notification{} 75 76 for _, h := range hosts { 77 78 // figure out the most recent expiration notification threshold the host 79 // has crossed 80 threshold := lastWarningThresholdCrossed(&h) 81 82 // for keying into the host's notifications map 83 thresholdKey := strconv.Itoa(int(threshold.Minutes())) 84 85 // if a notification has already been sent for the threshold for this 86 // host, skip it 87 if h.Notifications[thresholdKey] { 88 continue 89 } 90 91 grip.Infof("Warning needed for threshold '%s' for host %s", thresholdKey, h.Id) 92 93 // fetch information about the user we are notifying 94 userToNotify, err := user.FindOne(user.ById(h.StartedBy)) 95 if err != nil { 96 return nil, errors.Wrapf(err, "error finding user to notify by Id %v", h.StartedBy) 97 } 98 99 // if we didn't find a user (in the case of testing) set the timezone to "" 100 // to avoid triggering a nil pointer exception 101 timezone := "" 102 if userToNotify != nil { 103 timezone = userToNotify.Settings.Timezone 104 } 105 106 var expirationTimeFormatted string 107 // use our fetched information to load proper time zone to notify the user with 108 // (if time zone is empty, defaults to UTC) 109 loc, err := time.LoadLocation(timezone) 110 if err != nil { 111 grip.Errorf("loading timezone for email format with user_id %s: %+v", 112 userToNotify.Id, err) 113 expirationTimeFormatted = h.ExpirationTime.Format(time.RFC1123) 114 } else { 115 expirationTimeFormatted = h.ExpirationTime.In(loc).Format(time.RFC1123) 116 } 117 // we need to send a notification for the threshold for this host 118 hostNotification := notification{ 119 recipient: h.StartedBy, 120 subject: fmt.Sprintf("%v host termination reminder", h.Distro.Id), 121 message: fmt.Sprintf("Your %v host with id %v will be terminated"+ 122 " at %v. Visit %v to extend its lifetime.", 123 h.Distro.Id, h.Id, 124 expirationTimeFormatted, 125 settings.Ui.Url+"/ui/spawn"), 126 threshold: thresholdKey, 127 host: h, 128 callback: func(h host.Host, thresholdKey string) error { 129 return h.SetExpirationNotification(thresholdKey) 130 }, 131 } 132 133 // add it to the list 134 warnings = append(warnings, hostNotification) 135 } 136 137 grip.Infof("Built %d warnings about imminently expiring hosts", len(warnings)) 138 139 return warnings, nil 140 } 141 142 // determine the most recently crossed expiration notification threshold for 143 // the host. any host passed into this function is assumed to have crossed 144 // at least the least recent threshold 145 func lastWarningThresholdCrossed(host *host.Host) time.Duration { 146 147 // how long til the host expires 148 tilExpiration := host.ExpirationTime.Sub(time.Now()) 149 150 // iterate through the thresholds - since they are kept in sorted order, 151 // the first one crossed will be the most recent one crossed 152 for _, threshold := range spawnWarningThresholds { 153 if tilExpiration <= threshold { 154 return threshold 155 } 156 } 157 158 // should never be reached 159 return time.Duration(0) 160 } 161 162 // slowProvisioningWarnings is a notificationBuilder to build any necessary 163 // warnings about hosts that are taking a long time to provision 164 func slowProvisioningWarnings(settings *evergreen.Settings) ([]notification, 165 error) { 166 167 grip.Info("Building warnings for hosts taking a long time to provision...") 168 169 if settings.Notify.SMTP == nil { 170 return []notification{}, errors.New("no notification emails configured") 171 } 172 173 // fetch all hosts that are taking too long to provision 174 threshold := time.Now().Add(-slowProvisioningThreshold) 175 hosts, err := host.Find(host.ByUnprovisionedSince(threshold)) 176 if err != nil { 177 return nil, errors.Wrap(err, "error finding unprovisioned hosts") 178 } 179 180 // the list of warning notifications that will be returned 181 warnings := []notification{} 182 183 for _, h := range hosts { 184 185 // if a warning has been sent for the host, skip it 186 if h.Notifications[slowProvisioningWarning] { 187 continue 188 } 189 190 grip.Infoln("Slow-provisioning warning needs to be sent for host", h.Id) 191 192 // build the notification 193 hostNotification := notification{ 194 recipient: settings.Notify.SMTP.AdminEmail[0], 195 subject: fmt.Sprintf("Host %v taking a long time to provision", 196 h.Id), 197 message: fmt.Sprintf("See %v/ui/host/%v", 198 settings.Ui.Url, h.Id), 199 threshold: slowProvisioningWarning, 200 host: h, 201 callback: func(h host.Host, s string) error { 202 return h.SetExpirationNotification(s) 203 }, 204 } 205 206 // add it to the final list 207 warnings = append(warnings, hostNotification) 208 209 } 210 211 grip.Infof("Built %d warnings about hosts taking a long time to provision", len(warnings)) 212 213 return warnings, nil 214 }