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  }