prober.go RawHistoryFolder
1// Package httpprober provides HTTP probing functionality for health checks.
2package httpprober
3
4import (
5 "context"
6 "crypto/tls"
7 "fmt"
8 "net/http"
9 "time"
10
11 out "github.com/bnema/gordon/internal/boundaries/out"
12)
13
14// DefaultTimeout is the default timeout for HTTP probes.
15const DefaultTimeout = 5 * time.Second
16
17// Prober implements the HTTPProber interface for health checking.
18type Prober struct {
19 client *http.Client
20 timeout time.Duration
21}
22
23// Option configures the Prober.
24type Option func(*Prober)
25
26// WithTimeout sets the probe timeout.
27func WithTimeout(timeout time.Duration) Option {
28 return func(p *Prober) {
29 p.timeout = timeout
30 }
31}
32
33// WithHTTPClient sets a custom HTTP client.
34func WithHTTPClient(client *http.Client) Option {
35 return func(p *Prober) {
36 p.client = client
37 }
38}
39
40// New creates a new HTTP prober.
41func New(opts ...Option) *Prober {
42 p := &Prober{
43 timeout: DefaultTimeout,
44 }
45
46 for _, opt := range opts {
47 opt(p)
48 }
49
50 if p.client == nil {
51 p.client = &http.Client{
52 Timeout: p.timeout,
53 Transport: &http.Transport{
54 // #nosec G402 - InsecureSkipVerify is intentional for health probing
55 // internal services that may use self-signed certificates.
56 // This prober only checks reachability, not data integrity.
57 TLSClientConfig: &tls.Config{
58 InsecureSkipVerify: true,
59 },
60 DisableKeepAlives: true,
61 },
62 // Don't follow redirects - we want to see the actual response
63 CheckRedirect: func(req *http.Request, via []*http.Request) error {
64 return http.ErrUseLastResponse
65 },
66 }
67 }
68
69 return p
70}
71
72// Probe sends an HTTP GET request and returns the status code and response time.
73func (p *Prober) Probe(ctx context.Context, url string) (int, int64, error) {
74 start := time.Now()
75
76 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
77 if err != nil {
78 return 0, 0, fmt.Errorf("failed to create request: %w", err)
79 }
80
81 // Set a recognisable user agent so access-log filters can exclude probe traffic.
82 req.Header.Set("User-Agent", out.HealthCheckUserAgentPrefix+"1.0")
83
84 resp, err := p.client.Do(req)
85 if err != nil {
86 elapsed := time.Since(start).Milliseconds()
87 return 0, elapsed, fmt.Errorf("request failed: %w", err)
88 }
89 defer resp.Body.Close()
90
91 elapsed := time.Since(start).Milliseconds()
92 return resp.StatusCode, elapsed, nil
93}