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
2018-06-11 11:36:03 +02:00
// BalancerHandler includes functionality for load-balancing management.
type BalancerHandler interface {
ServeHTTP ( w http . ResponseWriter , req * http . Request )
Servers ( ) [ ] * url . URL
RemoveServer ( u * url . URL ) error
UpsertServer ( u * url . URL , options ... roundrobin . ServerOption ) error
}
// metricsRegistry is a local interface in the health check package, exposing only the required metrics
// necessary for the health check package. This makes it easier for the tests.
type metricsRegistry interface {
BackendServerUpGauge ( ) metrics . Gauge
2016-11-26 19:48:49 +01:00
}
2017-03-15 19:16:06 +01:00
// Options are the public health check options.
type Options struct {
2018-04-16 11:40:03 +02:00
Headers map [ string ] string
Hostname string
2018-05-14 12:08:03 +02:00
Scheme string
2017-11-21 05:06:03 -05:00
Path string
Port int
Transport http . RoundTripper
Interval time . Duration
2018-06-11 11:36:03 +02:00
LB BalancerHandler
2017-03-15 19:16:06 +01:00
}
2017-03-24 09:36:33 +01:00
func ( opt Options ) String ( ) string {
2018-04-16 11:40:03 +02:00
return fmt . Sprintf ( "[Hostname: %s Headers: %v Path: %s Port: %d Interval: %s]" , opt . Hostname , opt . Headers , opt . Path , opt . Port , opt . Interval )
2017-03-24 09:36:33 +01:00
}
2018-06-11 11:36:03 +02:00
// BackendConfig HealthCheck configuration for a backend
type BackendConfig 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
}
2018-06-11 11:36:03 +02:00
func ( b * BackendConfig ) newRequest ( serverURL * url . URL ) ( * http . Request , error ) {
u := & url . URL { }
* u = * serverURL
2016-11-26 19:48:49 +01:00
2018-06-11 11:36:03 +02:00
if len ( b . Scheme ) > 0 {
u . Scheme = b . Scheme
}
2016-11-26 19:48:49 +01:00
2018-06-11 11:36:03 +02:00
if b . Port != 0 {
u . Host = net . JoinHostPort ( u . Hostname ( ) , strconv . Itoa ( b . Port ) )
2017-03-09 16:27:31 +01:00
}
2016-11-26 19:48:49 +01:00
2018-06-11 11:36:03 +02:00
u . Path += b . Path
return http . NewRequest ( http . MethodGet , u . String ( ) , nil )
2018-01-26 11:58:03 +01:00
}
2018-06-11 11:36:03 +02:00
// this function adds additional http headers and hostname to http.request
func ( b * BackendConfig ) addHeadersAndHost ( req * http . Request ) * http . Request {
if b . Options . Hostname != "" {
req . Host = b . Options . Hostname
}
for k , v := range b . Options . Headers {
req . Header . Set ( k , v )
2017-03-09 16:27:31 +01:00
}
2018-06-11 11:36:03 +02:00
return req
}
// HealthCheck struct
type HealthCheck struct {
Backends map [ string ] * BackendConfig
metrics metricsRegistry
cancel context . CancelFunc
2016-11-26 19:48:49 +01:00
}
2018-04-16 11:40:03 +02:00
// SetBackendsConfiguration set backends configuration
2018-06-11 11:36:03 +02:00
func ( hc * HealthCheck ) SetBackendsConfiguration ( parentCtx context . Context , backends map [ string ] * BackendConfig ) {
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-06-11 11:36:03 +02:00
func ( hc * HealthCheck ) execute ( ctx context . Context , backend * BackendConfig ) {
2018-01-03 12:32:03 +01:00
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-09-07 09:40:03 +02:00
log . Debugf ( "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-06-11 11:36:03 +02:00
func ( hc * HealthCheck ) checkBackend ( backend * BackendConfig ) {
2018-01-03 12:32:03 +01:00
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
}
2018-06-11 11:36:03 +02:00
// GetHealthCheck returns the health check which is guaranteed to be a singleton.
func GetHealthCheck ( metrics metricsRegistry ) * HealthCheck {
once . Do ( func ( ) {
singleton = newHealthCheck ( metrics )
} )
return singleton
2017-05-10 14:28:57 -04:00
}
2018-06-11 11:36:03 +02:00
func newHealthCheck ( metrics metricsRegistry ) * HealthCheck {
return & HealthCheck {
Backends : make ( map [ string ] * BackendConfig ) ,
metrics : metrics ,
2018-04-16 11:40:03 +02:00
}
2018-06-11 11:36:03 +02:00
}
2018-05-22 09:22:03 +02:00
2018-06-11 11:36:03 +02:00
// NewBackendConfig Instantiate a new BackendConfig
func NewBackendConfig ( options Options , backendName string ) * BackendConfig {
return & BackendConfig {
Options : options ,
name : backendName ,
requestTimeout : 5 * time . Second ,
2018-04-16 11:40:03 +02: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.
2018-06-11 11:36:03 +02:00
func checkHealth ( serverURL * url . URL , backend * BackendConfig ) error {
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
}
2018-05-22 09:22:03 +02:00
2018-04-16 11:40:03 +02:00
req = backend . addHeadersAndHost ( req )
2017-05-10 14:28:57 -04:00
2018-05-22 09:22:03 +02:00
client := http . Client {
Timeout : backend . requestTimeout ,
Transport : backend . Options . Transport ,
2016-11-26 19:48:49 +01:00
}
2018-01-03 12:32:03 +01:00
2018-05-22 09:22:03 +02:00
resp , err := client . Do ( req )
if err != nil {
2018-01-03 12:32:03 +01:00
return fmt . Errorf ( "HTTP request failed: %s" , err )
}
2018-05-22 09:22:03 +02:00
defer resp . Body . Close ( )
2018-05-23 17:06:04 +02:00
if resp . StatusCode < http . StatusOK || resp . StatusCode >= http . StatusBadRequest {
return fmt . Errorf ( "received error status code: %v" , resp . StatusCode )
2018-05-22 09:22:03 +02:00
}
2018-01-03 12:32:03 +01:00
return nil
2016-11-26 19:48:49 +01:00
}