hunterlong/statup

View on GitHub
types/services/methods.go

Summary

Maintainability
A
3 hrs
Test Coverage
package services

import (
    "crypto/sha1"
    "crypto/tls"
    "crypto/x509"
    "encoding/hex"
    "fmt"
    "github.com/statping/statping/types"
    "github.com/statping/statping/types/errors"
    "github.com/statping/statping/types/failures"
    "github.com/statping/statping/types/hits"
    "github.com/statping/statping/utils"
    "io/ioutil"
    "sort"
    "strconv"
    "time"
)

const limitedFailures = 25

func (s *Service) LoadTLSCert() (*tls.Config, error) {
    if s.TLSCert.String == "" || s.TLSCertKey.String == "" {
        return nil, nil
    }

    // load TLS cert and key from file path or PEM format
    var cert tls.Certificate
    var err error
    tlsCertExtension := utils.FileExtension(s.TLSCert.String)
    tlsCertKeyExtension := utils.FileExtension(s.TLSCertKey.String)
    if tlsCertExtension == "" && tlsCertKeyExtension == "" {
        cert, err = tls.X509KeyPair([]byte(s.TLSCert.String), []byte(s.TLSCertKey.String))
    } else {
        cert, err = tls.LoadX509KeyPair(s.TLSCert.String, s.TLSCertKey.String)
    }
    if err != nil {
        return nil, errors.Wrap(err, "issue loading X509KeyPair")
    }

    config := &tls.Config{
        Certificates:       []tls.Certificate{cert},
        InsecureSkipVerify: s.TLSCertRoot.String == "",
    }

    if s.TLSCertRoot.String == "" {
        return config, nil
    }

    // create Root CA pool or use Root CA provided
    rootCA := s.TLSCertRoot.String
    caCert, err := ioutil.ReadFile(rootCA)
    if err != nil {
        return nil, errors.Wrap(err, "issue reading root CA file: "+rootCA)
    }
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    config.RootCAs = caCertPool

    return config, nil
}

func (s Service) Duration() time.Duration {
    return time.Duration(s.Interval) * time.Second
}

// Start will create a channel for the service checking go routine
func (s Service) UptimeData(hits []*hits.Hit, fails []*failures.Failure) (*UptimeSeries, error) {
    if len(hits) == 0 {
        return nil, errors.New("service does not have any successful hits")
    }
    // if theres no failures, then its been online 100%,
    // return a series from created time, to current.
    if len(fails) == 0 {
        fistHit := hits[0]
        duration := utils.Now().Sub(fistHit.CreatedAt).Milliseconds()
        set := []series{
            {
                Start:    fistHit.CreatedAt,
                End:      utils.Now(),
                Duration: duration,
                Online:   true,
            },
        }
        out := &UptimeSeries{
            Start:    fistHit.CreatedAt,
            End:      utils.Now(),
            Uptime:   duration,
            Downtime: 0,
            Series:   set,
        }
        return out, nil
    }

    tMap := make(map[time.Time]bool)

    for _, v := range hits {
        tMap[v.CreatedAt] = true
    }
    for _, v := range fails {
        tMap[v.CreatedAt] = false
    }

    var servs []ser
    for t, v := range tMap {
        s := ser{
            Time:   t,
            Online: v,
        }
        servs = append(servs, s)
    }
    if len(servs) == 0 {
        return nil, errors.New("error generating uptime data structure")
    }
    sort.Sort(ByTime(servs))

    var allTimes []series
    online := servs[0].Online
    thisTime := servs[0].Time
    for i := 0; i < len(servs); i++ {
        v := servs[i]
        if v.Online != online {
            s := series{
                Start:    thisTime,
                End:      v.Time,
                Duration: v.Time.Sub(thisTime).Milliseconds(),
                Online:   online,
            }
            allTimes = append(allTimes, s)
            thisTime = v.Time
            online = v.Online
        }
    }
    if len(allTimes) == 0 {
        return nil, errors.New("error generating uptime series structure")
    }

    first := servs[0].Time
    last := servs[len(servs)-1].Time
    if !s.Online {
        s := series{
            Start:    allTimes[len(allTimes)-1].End,
            End:      utils.Now(),
            Duration: utils.Now().Sub(last).Milliseconds(),
            Online:   s.Online,
        }
        allTimes = append(allTimes, s)
    } else {
        l := allTimes[len(allTimes)-1]
        s := series{
            Start:    l.Start,
            End:      utils.Now(),
            Duration: utils.Now().Sub(l.Start).Milliseconds(),
            Online:   true,
        }
        allTimes = append(allTimes, s)
    }

    response := &UptimeSeries{
        Start:    first,
        End:      last,
        Uptime:   addDurations(allTimes, true),
        Downtime: addDurations(allTimes, false),
        Series:   allTimes,
    }

    return response, nil
}

func addDurations(s []series, on bool) int64 {
    var dur int64
    for _, v := range s {
        if v.Online == on {
            dur += v.Duration
        }
    }
    return dur
}

// Start will create a channel for the service checking go routine
func (s *Service) Start() {
    if s.IsRunning() {
        return
    }
    s.Running = make(chan bool)
}

// Close will stop the go routine that is checking if service is online or not
func (s *Service) Close() {
    if s.IsRunning() {
        close(s.Running)
    }
}

func humanMicro(val int64) string {
    if val < 10000 {
        return fmt.Sprintf("%d μs", val)
    }
    return fmt.Sprintf("%0.0f ms", float64(val)*0.001)
}

// IsRunning returns true if the service go routine is running
func (s *Service) IsRunning() bool {
    if s.Running == nil {
        return false
    }
    select {
    case <-s.Running:
        return false
    default:
        return true
    }
}

func (s Service) Hash() string {
    format := fmt.Sprintf("name:%sdomain:%sport:%dtype:%smethod:%s", s.Name, s.Domain, s.Port, s.Type, s.Method)
    h := sha1.New()
    h.Write([]byte(format))
    return hex.EncodeToString(h.Sum(nil))
}

// SelectAllServices returns a slice of *core.Service to be store on []*core.Services
// should only be called once on startup.
func SelectAllServices(start bool) (map[int64]*Service, error) {
    if len(allServices) > 0 {
        return allServices, nil
    }
    for _, s := range all() {
        s.Failures = s.AllFailures().LastAmount(limitedFailures)
        s.prevOnline = true
        // collect initial service stats
        s.UpdateStats()
        allServices[s.Id] = s
        if start {
            CheckinProcess(s)
        }
    }
    return allServices, nil
}

func (s *Service) UpdateStats() *Service {
    s.Online24Hours = s.OnlineDaysPercent(1)
    s.Online7Days = s.OnlineDaysPercent(7)
    s.AvgResponse = s.AvgTime()
    s.FailuresLast24Hours = s.FailuresSince(utils.Now().Add(-time.Hour * 24)).Count()

    allFails := s.AllFailures()
    if s.LastOffline.IsZero() {
        lastFail := allFails.Last()
        if lastFail != nil {
            s.LastOffline = lastFail.CreatedAt
        }
    }

    s.Stats = &Stats{
        Failures: allFails.Count(),
        Hits:     s.AllHits().Count(),
        FirstHit: s.FirstHit().CreatedAt,
    }
    return s
}

// AvgTime will return the average amount of time for a service to response back successfully
func (s Service) AvgTime() int64 {
    return s.AllHits().Avg()
}

// OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s Service) OnlineDaysPercent(days int) float32 {
    ago := utils.Now().Add(-time.Duration(days) * types.Day)
    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 {
    failsList := s.FailuresSince(ago).Count()
    hitsList := s.HitsSince(ago).Count()

    if failsList == 0 {
        s.Online24Hours = 100.00
        return s.Online24Hours
    }

    if hitsList == 0 {
        s.Online24Hours = 0
        return s.Online24Hours
    }

    avg := (float64(failsList) / float64(hitsList)) * 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
}

// Uptime returns the duration of how long the service was online
func (s Service) Uptime() utils.Duration {
    return utils.Duration{Duration: utils.Now().Sub(s.LastOffline)}
}

// Downtime returns the duration of how long the service has been offline
func (s Service) Downtime() utils.Duration {
    return utils.Duration{Duration: utils.Now().Sub(s.LastOnline)}
}