239 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			239 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package core
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/TwinProduction/gatus/client"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// HostHeader is the name of the header used to specify the host
 | |
| 	HostHeader = "Host"
 | |
| 
 | |
| 	// ContentTypeHeader is the name of the header used to specify the content type
 | |
| 	ContentTypeHeader = "Content-Type"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	// ErrServiceWithNoCondition is the error with which Gatus will panic if a service is configured with no conditions
 | |
| 	ErrServiceWithNoCondition = errors.New("you must specify at least one condition per service")
 | |
| 
 | |
| 	// ErrServiceWithNoURL is the error with which Gatus will panic if a service is configured with no url
 | |
| 	ErrServiceWithNoURL = errors.New("you must specify an url for each service")
 | |
| 
 | |
| 	// ErrServiceWithNoName is the error with which Gatus will panic if a service is configured with no name
 | |
| 	ErrServiceWithNoName = errors.New("you must specify a name for each service")
 | |
| )
 | |
| 
 | |
| // Service is the configuration of a monitored endpoint
 | |
| type Service struct {
 | |
| 	// Name of the service. Can be anything.
 | |
| 	Name string `yaml:"name"`
 | |
| 
 | |
| 	// Group the service is a part of. Used for grouping multiple services together on the front end.
 | |
| 	Group string `yaml:"group,omitempty"`
 | |
| 
 | |
| 	// URL to send the request to
 | |
| 	URL string `yaml:"url"`
 | |
| 
 | |
| 	// DNS is the configuration of DNS monitoring
 | |
| 	DNS *DNS `yaml:"dns,omitempty"`
 | |
| 
 | |
| 	// Method of the request made to the url of the service
 | |
| 	Method string `yaml:"method,omitempty"`
 | |
| 
 | |
| 	// Body of the request
 | |
| 	Body string `yaml:"body,omitempty"`
 | |
| 
 | |
| 	// GraphQL is whether to wrap the body in a query param ({"query":"$body"})
 | |
| 	GraphQL bool `yaml:"graphql,omitempty"`
 | |
| 
 | |
| 	// Headers of the request
 | |
| 	Headers map[string]string `yaml:"headers,omitempty"`
 | |
| 
 | |
| 	// Interval is the duration to wait between every status check
 | |
| 	Interval time.Duration `yaml:"interval,omitempty"`
 | |
| 
 | |
| 	// Conditions used to determine the health of the service
 | |
| 	Conditions []*Condition `yaml:"conditions"`
 | |
| 
 | |
| 	// Alerts is the alerting configuration for the service in case of failure
 | |
| 	Alerts []*Alert `yaml:"alerts"`
 | |
| 
 | |
| 	// Insecure is whether to skip verifying the server's certificate chain and host name
 | |
| 	Insecure bool `yaml:"insecure,omitempty"`
 | |
| 
 | |
| 	// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
 | |
| 	NumberOfFailuresInARow int
 | |
| 
 | |
| 	// NumberOfFailuresInARow is the number of successful evaluations in a row
 | |
| 	NumberOfSuccessesInARow int
 | |
| }
 | |
| 
 | |
| // ValidateAndSetDefaults validates the service's configuration and sets the default value of fields that have one
 | |
| func (service *Service) ValidateAndSetDefaults() {
 | |
| 	// Set default values
 | |
| 	if service.Interval == 0 {
 | |
| 		service.Interval = 1 * time.Minute
 | |
| 	}
 | |
| 	if len(service.Method) == 0 {
 | |
| 		service.Method = http.MethodGet
 | |
| 	}
 | |
| 	if len(service.Headers) == 0 {
 | |
| 		service.Headers = make(map[string]string)
 | |
| 	}
 | |
| 	// Automatically add "Content-Type: application/json" header if there's no Content-Type set
 | |
| 	// and service.GraphQL is set to true
 | |
| 	if _, contentTypeHeaderExists := service.Headers[ContentTypeHeader]; !contentTypeHeaderExists && service.GraphQL {
 | |
| 		service.Headers[ContentTypeHeader] = "application/json"
 | |
| 	}
 | |
| 	for _, alert := range service.Alerts {
 | |
| 		if alert.FailureThreshold <= 0 {
 | |
| 			alert.FailureThreshold = 3
 | |
| 		}
 | |
| 		if alert.SuccessThreshold <= 0 {
 | |
| 			alert.SuccessThreshold = 2
 | |
| 		}
 | |
| 	}
 | |
| 	if len(service.Name) == 0 {
 | |
| 		panic(ErrServiceWithNoName)
 | |
| 	}
 | |
| 	if len(service.URL) == 0 {
 | |
| 		panic(ErrServiceWithNoURL)
 | |
| 	}
 | |
| 	if len(service.Conditions) == 0 {
 | |
| 		panic(ErrServiceWithNoCondition)
 | |
| 	}
 | |
| 	if service.DNS != nil {
 | |
| 		service.DNS.validateAndSetDefault()
 | |
| 		return
 | |
| 	}
 | |
| 	// Make sure that the request can be created
 | |
| 	_, err := http.NewRequest(service.Method, service.URL, bytes.NewBuffer([]byte(service.Body)))
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // EvaluateHealth sends a request to the service's URL and evaluates the conditions of the service.
 | |
| func (service *Service) EvaluateHealth() *Result {
 | |
| 	result := &Result{Success: true, Errors: []string{}}
 | |
| 	service.getIP(result)
 | |
| 	if len(result.Errors) == 0 {
 | |
| 		service.call(result)
 | |
| 	} else {
 | |
| 		result.Success = false
 | |
| 	}
 | |
| 	for _, condition := range service.Conditions {
 | |
| 		success := condition.evaluate(result)
 | |
| 		if !success {
 | |
| 			result.Success = false
 | |
| 		}
 | |
| 	}
 | |
| 	result.Timestamp = time.Now()
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // GetAlertsTriggered returns a slice of alerts that have been triggered
 | |
| func (service *Service) GetAlertsTriggered() []Alert {
 | |
| 	var alerts []Alert
 | |
| 	if service.NumberOfFailuresInARow == 0 {
 | |
| 		return alerts
 | |
| 	}
 | |
| 	for _, alert := range service.Alerts {
 | |
| 		if alert.Enabled && alert.FailureThreshold == service.NumberOfFailuresInARow {
 | |
| 			alerts = append(alerts, *alert)
 | |
| 			continue
 | |
| 		}
 | |
| 	}
 | |
| 	return alerts
 | |
| }
 | |
| 
 | |
| func (service *Service) getIP(result *Result) {
 | |
| 	if service.DNS != nil {
 | |
| 		result.Hostname = strings.TrimSuffix(service.URL, ":53")
 | |
| 	} else {
 | |
| 		urlObject, err := url.Parse(service.URL)
 | |
| 		if err != nil {
 | |
| 			result.Errors = append(result.Errors, err.Error())
 | |
| 			return
 | |
| 		}
 | |
| 		result.Hostname = urlObject.Hostname()
 | |
| 	}
 | |
| 	ips, err := net.LookupIP(result.Hostname)
 | |
| 	if err != nil {
 | |
| 		result.Errors = append(result.Errors, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 	result.IP = ips[0].String()
 | |
| }
 | |
| 
 | |
| func (service *Service) call(result *Result) {
 | |
| 	var request *http.Request
 | |
| 	var response *http.Response
 | |
| 	var err error
 | |
| 	isServiceDNS := service.DNS != nil
 | |
| 	isServiceTCP := strings.HasPrefix(service.URL, "tcp://")
 | |
| 	isServiceICMP := strings.HasPrefix(service.URL, "icmp://")
 | |
| 	isServiceHTTP := !isServiceDNS && !isServiceTCP && !isServiceICMP
 | |
| 	if isServiceHTTP {
 | |
| 		request = service.buildHTTPRequest()
 | |
| 	}
 | |
| 	startTime := time.Now()
 | |
| 	if isServiceDNS {
 | |
| 		service.DNS.query(service.URL, result)
 | |
| 		result.Duration = time.Since(startTime)
 | |
| 	} else if isServiceTCP {
 | |
| 		result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(service.URL, "tcp://"))
 | |
| 		result.Duration = time.Since(startTime)
 | |
| 	} else if isServiceICMP {
 | |
| 		result.Connected, result.Duration = client.Ping(strings.TrimPrefix(service.URL, "icmp://"))
 | |
| 	} else {
 | |
| 		response, err = client.GetHTTPClient(service.Insecure).Do(request)
 | |
| 		result.Duration = time.Since(startTime)
 | |
| 		if err != nil {
 | |
| 			result.Errors = append(result.Errors, err.Error())
 | |
| 			return
 | |
| 		}
 | |
| 		if response.TLS != nil && len(response.TLS.PeerCertificates) > 0 {
 | |
| 			certificate := response.TLS.PeerCertificates[0]
 | |
| 			result.CertificateExpiration = certificate.NotAfter.Sub(time.Now())
 | |
| 		}
 | |
| 		result.HTTPStatus = response.StatusCode
 | |
| 		result.Connected = response.StatusCode > 0
 | |
| 		result.Body, err = ioutil.ReadAll(response.Body)
 | |
| 		if err != nil {
 | |
| 			result.Errors = append(result.Errors, err.Error())
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (service *Service) buildHTTPRequest() *http.Request {
 | |
| 	var bodyBuffer *bytes.Buffer
 | |
| 	if service.GraphQL {
 | |
| 		graphQlBody := map[string]string{
 | |
| 			"query": service.Body,
 | |
| 		}
 | |
| 		body, _ := json.Marshal(graphQlBody)
 | |
| 		bodyBuffer = bytes.NewBuffer(body)
 | |
| 	} else {
 | |
| 		bodyBuffer = bytes.NewBuffer([]byte(service.Body))
 | |
| 	}
 | |
| 	request, _ := http.NewRequest(service.Method, service.URL, bodyBuffer)
 | |
| 	for k, v := range service.Headers {
 | |
| 		request.Header.Set(k, v)
 | |
| 		if k == HostHeader {
 | |
| 			request.Host = v
 | |
| 		}
 | |
| 	}
 | |
| 	return request
 | |
| }
 |