package watchdog import ( "context" "fmt" "io" "net" "net/http" "net/url" "time" ) // DefaultTimeout is used to limit checks duration const DefaultTimeout = time.Second * 10 // GetHTTP creates a CheckFunc that operates as follows. // The check makes a request with GET method to provided addr using http.DefaultClient. // If request fails within specified timeout the returned status is StatusDown. // The function then tries to read response body. If it fails, // the returned status is StatusDown. // // If request succeeds but reponse code is not 200, the returned // status is StatusDown and response body is contained in the // returned error. // // GetHTTP return an error if addr can not be parsed with url.Parse. // If zero timeout is provided then the DefaultTimeout (10 second) is used. func GetHTTP(addr string, timeout time.Duration) (CheckFunc, error) { u, err := url.Parse(addr) if err != nil { return nil, fmt.Errorf("coulf not parse URL: %w", err) } if timeout == 0 { timeout = DefaultTimeout } return func(ctx context.Context) (Status, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return StatusUnknown, fmt.Errorf("failed to create http request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return StatusDown, fmt.Errorf("do request failed: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return StatusDown, fmt.Errorf("err reading response body: %w", err) } if resp.StatusCode != http.StatusOK { return StatusDown, fmt.Errorf("got HTTP response code %d, body: %s", resp.StatusCode, string(body)) } return StatusOK, nil }, nil } // HeadHTTP creates a CheckFunc that operates as follows. // The check make a request with HEAD method to provided addr using http.DefaultClient. // If request fails within specified timeout the returned status is StatusDown. // // If request succeeds but reponse code is not 200, the returned // status is StatusDown. // // HeadHTTP return an error if addr can not be parsed with url.Parse. // If zero timeout is provided then the DefaultTimeout (10 second) is used. func HeadHTTP(addr string, timeout time.Duration) (CheckFunc, error) { u, err := url.Parse(addr) if err != nil { return nil, fmt.Errorf("coulf not parse URL: %w", err) } if timeout == 0 { timeout = DefaultTimeout } return func(ctx context.Context) (Status, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil) if err != nil { return StatusUnknown, fmt.Errorf("failed to create http request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return StatusDown, fmt.Errorf("do request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return StatusDown, fmt.Errorf("got HTTP response code %d", resp.StatusCode) } return StatusOK, nil }, nil } // DialTCP creates a CheckFunc that may be used to check tcp connectivity // to a host. // // The check tries to net.DialTimeout to the provided addr. If it fails, // the returned status is StatusDown. // // No validation of addr is made. // // If zero timeout is provided then the DefaultTimeout (10 second) is used. func DialTCP(addr string, timeout time.Duration) (CheckFunc, error) { if timeout == 0 { timeout = DefaultTimeout } return func(ctx context.Context) (Status, error) { deadline := time.Now().Add(timeout) if t, ok := ctx.Deadline(); ok && t.Before(deadline) { deadline = t } conn, err := net.DialTimeout("tcp", addr, time.Until(deadline)) if err != nil { return StatusDown, fmt.Errorf("error dialing: %w", err) } defer conn.Close() return StatusOK, nil }, nil }