2016-11-26 19:48:49 +01:00
package healthcheck
import (
2016-11-30 22:49:57 +01:00
"context"
2017-03-24 09:36:33 +01:00
"fmt"
2017-05-10 14:28:57 -04:00
"net"
2016-11-26 19:48:49 +01:00
"net/http"
"net/url"
2017-05-10 14:28:57 -04:00
"strconv"
2016-11-26 19:48:49 +01:00
"sync"
"time"
2017-01-31 22:55:02 +01:00
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
2018-01-26 11:58:03 +01:00
"github.com/go-kit/kit/metrics"
2017-01-31 22:55:02 +01:00
"github.com/vulcand/oxy/roundrobin"
2016-11-26 19:48:49 +01:00
)
var singleton * HealthCheck
var once sync . Once
2017-04-20 12:51:06 -04:00
// GetHealthCheck returns the health check which is guaranteed to be a singleton.
2018-01-26 11:58:03 +01:00
func GetHealthCheck ( metrics metricsRegistry ) * HealthCheck {
2016-11-26 19:48:49 +01:00
once . Do ( func ( ) {
2018-01-26 11:58:03 +01:00
singleton = newHealthCheck ( metrics )
2016-11-26 19:48:49 +01:00
} )
return singleton
}
2017-03-15 19:16:06 +01:00
// Options are the public health check options.
type Options struct {
2017-11-21 05:06:03 -05:00
Path string
Port int
Transport http . RoundTripper
Interval time . Duration
LB LoadBalancer
2017-03-15 19:16:06 +01:00
}
2017-03-24 09:36:33 +01:00
func ( opt Options ) String ( ) string {
2017-09-18 15:50:03 +02:00
return fmt . Sprintf ( "[Path: %s Port: %d Interval: %s]" , opt . Path , opt . Port , opt . Interval )
2017-03-24 09:36:33 +01:00
}
2016-11-26 19:48:49 +01:00
// BackendHealthCheck HealthCheck configuration for a backend
type BackendHealthCheck struct {
2017-03-15 19:16:06 +01:00
Options
2018-01-03 12:32:03 +01:00
name string
2017-03-15 19:16:06 +01:00
disabledURLs [ ] * url . URL
2017-03-09 16:27:31 +01:00
requestTimeout time . Duration
2016-11-26 19:48:49 +01:00
}
//HealthCheck struct
type HealthCheck struct {
Backends map [ string ] * BackendHealthCheck
2018-01-26 11:58:03 +01:00
metrics metricsRegistry
2016-11-30 22:49:57 +01:00
cancel context . CancelFunc
2016-11-26 19:48:49 +01:00
}
2017-03-15 19:16:06 +01:00
// LoadBalancer includes functionality for load-balancing management.
type LoadBalancer interface {
2016-11-26 19:48:49 +01:00
RemoveServer ( u * url . URL ) error
UpsertServer ( u * url . URL , options ... roundrobin . ServerOption ) error
Servers ( ) [ ] * url . URL
}
2018-01-26 11:58:03 +01:00
func newHealthCheck ( metrics metricsRegistry ) * HealthCheck {
2017-03-09 16:27:31 +01:00
return & HealthCheck {
Backends : make ( map [ string ] * BackendHealthCheck ) ,
2018-01-26 11:58:03 +01:00
metrics : metrics ,
2017-03-09 16:27:31 +01:00
}
2016-11-26 19:48:49 +01:00
}
2018-01-26 11:58:03 +01:00
// metricsRegistry is a local interface in the healthcheck package, exposing only the required metrics
// necessary for the healthcheck package. This makes it easier for the tests.
type metricsRegistry interface {
BackendServerUpGauge ( ) metrics . Gauge
}
2016-11-26 19:48:49 +01:00
// NewBackendHealthCheck Instantiate a new BackendHealthCheck
2018-01-03 12:32:03 +01:00
func NewBackendHealthCheck ( options Options , backendName string ) * BackendHealthCheck {
2017-03-09 16:27:31 +01:00
return & BackendHealthCheck {
2017-03-15 19:16:06 +01:00
Options : options ,
2018-01-03 12:32:03 +01:00
name : backendName ,
2017-03-09 16:27:31 +01:00
requestTimeout : 5 * time . Second ,
}
2016-11-26 19:48:49 +01:00
}
2016-11-29 19:30:51 +01:00
//SetBackendsConfiguration set backends configuration
2017-02-06 09:31:20 +01:00
func ( hc * HealthCheck ) SetBackendsConfiguration ( parentCtx context . Context , backends map [ string ] * BackendHealthCheck ) {
2016-11-26 19:48:49 +01:00
hc . Backends = backends
2016-11-30 22:49:57 +01:00
if hc . cancel != nil {
hc . cancel ( )
}
2017-01-31 22:55:02 +01:00
ctx , cancel := context . WithCancel ( parentCtx )
2016-11-30 22:49:57 +01:00
hc . cancel = cancel
2016-11-26 19:48:49 +01:00
2018-01-15 17:27:37 +01:00
for _ , backend := range backends {
2017-03-09 16:27:31 +01:00
currentBackend := backend
2017-01-31 22:55:02 +01:00
safe . Go ( func ( ) {
2018-01-03 12:32:03 +01:00
hc . execute ( ctx , currentBackend )
2017-01-31 22:55:02 +01:00
} )
2016-11-30 22:49:57 +01:00
}
2016-11-26 19:48:49 +01:00
}
2018-01-03 12:32:03 +01:00
func ( hc * HealthCheck ) execute ( ctx context . Context , backend * BackendHealthCheck ) {
log . Debugf ( "Initial health check for backend: %q" , backend . name )
hc . checkBackend ( backend )
2017-03-09 16:27:31 +01:00
ticker := time . NewTicker ( backend . Interval )
defer ticker . Stop ( )
2017-04-11 17:10:46 +02:00
for {
select {
case <- ctx . Done ( ) :
2018-01-03 12:32:03 +01:00
log . Debug ( "Stopping current health check goroutines of backend: %s" , backend . name )
2017-04-11 17:10:46 +02:00
return
case <- ticker . C :
2018-01-03 12:32:03 +01:00
log . Debugf ( "Refreshing health check for backend: %s" , backend . name )
hc . checkBackend ( backend )
2017-03-09 16:27:31 +01:00
}
}
}
2018-01-03 12:32:03 +01:00
func ( hc * HealthCheck ) checkBackend ( backend * BackendHealthCheck ) {
enabledURLs := backend . LB . Servers ( )
2017-04-11 17:10:46 +02:00
var newDisabledURLs [ ] * url . URL
2018-01-03 12:32:03 +01:00
for _ , url := range backend . disabledURLs {
2018-01-26 11:58:03 +01:00
serverUpMetricValue := float64 ( 0 )
2018-01-03 12:32:03 +01:00
if err := checkHealth ( url , backend ) ; err == nil {
log . Warnf ( "Health check up: Returning to server list. Backend: %q URL: %q" , backend . name , url . String ( ) )
backend . LB . UpsertServer ( url , roundrobin . Weight ( 1 ) )
2018-01-26 11:58:03 +01:00
serverUpMetricValue = 1
2017-04-11 17:10:46 +02:00
} else {
2018-01-03 12:32:03 +01:00
log . Warnf ( "Health check still failing. Backend: %q URL: %q Reason: %s" , backend . name , url . String ( ) , err )
2017-04-11 17:10:46 +02:00
newDisabledURLs = append ( newDisabledURLs , url )
}
2018-01-26 11:58:03 +01:00
labelValues := [ ] string { "backend" , backend . name , "url" , url . String ( ) }
hc . metrics . BackendServerUpGauge ( ) . With ( labelValues ... ) . Set ( serverUpMetricValue )
2017-04-11 17:10:46 +02:00
}
2018-01-03 12:32:03 +01:00
backend . disabledURLs = newDisabledURLs
2017-03-09 16:27:31 +01:00
2017-04-11 17:10:46 +02:00
for _ , url := range enabledURLs {
2018-01-26 11:58:03 +01:00
serverUpMetricValue := float64 ( 1 )
2018-01-03 12:32:03 +01:00
if err := checkHealth ( url , backend ) ; err != nil {
log . Warnf ( "Health check failed: Remove from server list. Backend: %q URL: %q Reason: %s" , backend . name , url . String ( ) , err )
backend . LB . RemoveServer ( url )
backend . disabledURLs = append ( backend . disabledURLs , url )
2018-01-26 11:58:03 +01:00
serverUpMetricValue = 0
2017-04-11 17:10:46 +02:00
}
2018-01-26 11:58:03 +01:00
labelValues := [ ] string { "backend" , backend . name , "url" , url . String ( ) }
hc . metrics . BackendServerUpGauge ( ) . With ( labelValues ... ) . Set ( serverUpMetricValue )
2017-04-11 17:10:46 +02:00
}
2017-03-09 16:27:31 +01:00
}
2017-05-10 14:28:57 -04:00
func ( backend * BackendHealthCheck ) newRequest ( serverURL * url . URL ) ( * http . Request , error ) {
2017-09-18 15:50:03 +02:00
if backend . Port == 0 {
2017-11-20 09:40:03 +01:00
return http . NewRequest ( http . MethodGet , serverURL . String ( ) + backend . Path , nil )
2017-05-10 14:28:57 -04:00
}
// copy the url and add the port to the host
u := & url . URL { }
* u = * serverURL
2017-09-18 15:50:03 +02:00
u . Host = net . JoinHostPort ( u . Hostname ( ) , strconv . Itoa ( backend . Port ) )
2017-05-10 14:28:57 -04:00
u . Path = u . Path + backend . Path
2017-11-20 09:40:03 +01:00
return http . NewRequest ( http . MethodGet , u . String ( ) , nil )
2017-05-10 14:28:57 -04:00
}
2018-01-03 12:32:03 +01:00
// checkHealth returns a nil error in case it was successful and otherwise
// a non-nil error with a meaningful description why the health check failed.
func checkHealth ( serverURL * url . URL , backend * BackendHealthCheck ) error {
2016-11-30 22:48:09 +01:00
client := http . Client {
2017-11-21 05:06:03 -05:00
Timeout : backend . requestTimeout ,
Transport : backend . Options . Transport ,
2016-11-30 22:48:09 +01:00
}
2017-05-10 14:28:57 -04:00
req , err := backend . newRequest ( serverURL )
if err != nil {
2018-01-03 12:32:03 +01:00
return fmt . Errorf ( "failed to create HTTP request: %s" , err )
2017-05-10 14:28:57 -04:00
}
resp , err := client . Do ( req )
2017-03-09 16:27:31 +01:00
if err == nil {
defer resp . Body . Close ( )
2016-11-26 19:48:49 +01:00
}
2018-01-03 12:32:03 +01:00
switch {
case err != nil :
return fmt . Errorf ( "HTTP request failed: %s" , err )
case resp . StatusCode != http . StatusOK :
return fmt . Errorf ( "received non-200 status code: %v" , resp . StatusCode )
}
return nil
2016-11-26 19:48:49 +01:00
}