github.com/avenga/couper@v1.12.2/handler/transport/probe.go (about) 1 package transport 2 3 import ( 4 "context" 5 goerror "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/sirupsen/logrus" 14 15 "github.com/avenga/couper/config" 16 "github.com/avenga/couper/config/request" 17 "github.com/avenga/couper/errors" 18 "github.com/avenga/couper/eval" 19 "github.com/avenga/couper/handler/middleware" 20 "github.com/avenga/couper/logging" 21 ) 22 23 const ( 24 StateInvalid state = iota 25 StateOk 26 StateFailing 27 StateDown 28 ) 29 30 var healthStateLabels = []string{ 31 "invalid", 32 "healthy", 33 "failing", 34 "unhealthy", 35 } 36 37 var _ context.Context = &eval.Context{} 38 39 type state int 40 41 func (s state) String() string { 42 return healthStateLabels[s] 43 } 44 45 type HealthInfo struct { 46 Error string 47 Healthy bool 48 Origin string 49 State string 50 } 51 52 type Probe struct { 53 //configurable settings 54 backendName string 55 log *logrus.Entry 56 opts *config.HealthCheck 57 58 //variables reflecting status of probe 59 client *http.Client 60 counter uint 61 failure uint 62 state state 63 status int 64 65 listener ProbeStateChange 66 67 uidFunc middleware.UIDFunc 68 } 69 70 type ProbeStateChange interface { 71 OnProbeChange(info *HealthInfo) 72 } 73 74 func NewProbe(log *logrus.Entry, tc *Config, opts *config.HealthCheck, listener ProbeStateChange) { 75 // do not start go-routine on config check (-watch) 76 if _, exist := opts.Context.Value(request.ConfigDryRun).(bool); exist { 77 return 78 } 79 80 client := &http.Client{ 81 CheckRedirect: func(req *http.Request, via []*http.Request) error { 82 return http.ErrUseLastResponse 83 }, 84 Transport: logging.NewUpstreamLog(log, 85 NewTransport(tc. 86 WithTarget(opts.Request.URL.Scheme, opts.Request.URL.Host, opts.Request.URL.Host, ""), 87 log), 88 tc.NoProxyFromEnv), 89 } 90 91 p := &Probe{ 92 backendName: tc.BackendName, 93 log: log.WithField("url", opts.Request.URL.String()), 94 opts: opts, 95 96 client: client, 97 state: StateInvalid, 98 99 listener: listener, 100 101 uidFunc: middleware.NewUIDFunc(opts.RequestUIDFormat), 102 } 103 104 go p.probe(opts.Context) 105 } 106 107 func (p *Probe) probe(c context.Context) { 108 for { 109 select { 110 case <-c.Done(): 111 p.log.Warn("shutdown health probe") 112 return 113 default: 114 } 115 ctx, cancel := context.WithTimeout(context.Background(), p.opts.Timeout) 116 ctx = context.WithValue(ctx, request.RoundTripName, "health-check") 117 uid := p.uidFunc() 118 ctx = context.WithValue(ctx, request.UID, uid) 119 120 res, err := p.client.Do(p.opts.Request.Clone(ctx)) 121 cancel() 122 123 p.counter++ 124 prevState := p.state 125 p.status = 0 126 if res != nil { 127 p.status = res.StatusCode 128 } 129 130 var errorMessage string 131 if err != nil || !p.opts.ExpectedStatus[res.StatusCode] || !contains(res.Body, p.opts.ExpectedText) { 132 if p.failure++; p.failure < p.opts.FailureThreshold { 133 p.state = StateFailing 134 } else { 135 p.state = StateDown 136 } 137 if err == nil { 138 if !p.opts.ExpectedStatus[res.StatusCode] { 139 errorMessage = "unexpected status code: " + strconv.Itoa(p.status) 140 } else { 141 errorMessage = "unexpected text" 142 } 143 } else { 144 unwrapped := goerror.Unwrap(err) 145 if unwrapped != nil { 146 err = unwrapped 147 } 148 149 if gerr, ok := err.(errors.GoError); ok { 150 // Upstream log wraps a possible transport deadline into a backend error 151 if gerr.Unwrap() == context.DeadlineExceeded { 152 errorMessage = fmt.Sprintf("backend error: connecting to %s '%s' failed: i/o timeout", 153 p.backendName, p.opts.Request.URL.Hostname()) 154 } else { 155 errorMessage = gerr.LogError() 156 } 157 } else { 158 errorMessage = err.Error() 159 } 160 } 161 } else { 162 p.failure = 0 163 p.state = StateOk 164 errorMessage = "" 165 } 166 167 if prevState != p.state { 168 newState := p.state.String() 169 info := &HealthInfo{ 170 Error: errorMessage, 171 Healthy: p.state != StateDown, 172 Origin: p.opts.Request.URL.Host, 173 State: newState, 174 } 175 176 if p.listener != nil { 177 p.listener.OnProbeChange(info) 178 } 179 180 message := fmt.Sprintf("new health state: %s", newState) 181 182 log := p.log.WithField("uid", uid) 183 switch p.state { 184 case StateOk: 185 log.Info(message) 186 case StateFailing: 187 log.Warn(message) 188 case StateDown: 189 log.WithError(errors.BackendUnhealthy.Message(errorMessage + ": " + message)).Error() 190 } 191 } 192 193 time.Sleep(p.opts.Interval) 194 } 195 } 196 197 func (p Probe) String() string { 198 return fmt.Sprintf("check #%d for backend %q: state: %s (%d/%d), HTTP status: %d", p.counter, p.backendName, p.state, p.failure, p.opts.FailureThreshold, p.status) 199 } 200 201 func contains(reader io.ReadCloser, text string) bool { 202 defer reader.Close() // free resp body related connection 203 204 if text == "" { 205 return true 206 } 207 208 bytes, _ := io.ReadAll(reader) 209 return strings.Contains(string(bytes), text) 210 }