126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
|
|
package probe
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
// HTTPClient defines the interface for making HTTP requests during probing
|
||
|
|
type HTTPClient interface {
|
||
|
|
Do(req *http.Request) (*http.Response, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
// DefaultHTTPClient wraps the standard http.Client
|
||
|
|
type DefaultHTTPClient struct {
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewDefaultHTTPClient creates a client with sensible probe timeouts
|
||
|
|
func NewDefaultHTTPClient() *DefaultHTTPClient {
|
||
|
|
return &DefaultHTTPClient{
|
||
|
|
client: &http.Client{
|
||
|
|
Timeout: 30 * time.Second,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *DefaultHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||
|
|
return c.client.Do(req)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ProbeTarget represents an account to be probed
|
||
|
|
type ProbeTarget struct {
|
||
|
|
AccountID int64
|
||
|
|
Platform string
|
||
|
|
Endpoint string
|
||
|
|
AuthHeader string // Bearer token or API key
|
||
|
|
}
|
||
|
|
|
||
|
|
// ProbeOutcome is the result of executing a probe against a target
|
||
|
|
type ProbeOutcome struct {
|
||
|
|
AccountID int64
|
||
|
|
Platform string
|
||
|
|
StatusCode int
|
||
|
|
TransportError error
|
||
|
|
LatencyMs int
|
||
|
|
ResponseBody string // truncated, for debugging
|
||
|
|
RequestID string
|
||
|
|
ExecutedAt time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
// ProbeExecutor sends HTTP requests to supplier endpoints and classifies results
|
||
|
|
type ProbeExecutor struct {
|
||
|
|
httpClient HTTPClient
|
||
|
|
now func() time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewProbeExecutor creates a probe executor with the given HTTP client.
|
||
|
|
// If client is nil, uses http.DefaultClient.
|
||
|
|
func NewProbeExecutor(client HTTPClient) *ProbeExecutor {
|
||
|
|
if client == nil {
|
||
|
|
client = http.DefaultClient
|
||
|
|
}
|
||
|
|
return &ProbeExecutor{
|
||
|
|
httpClient: client,
|
||
|
|
now: func() time.Time { return time.Now().UTC() },
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ExecuteProbe runs a single probe against the target account
|
||
|
|
// It makes an HTTP GET request to the platform's health endpoint
|
||
|
|
func (e *ProbeExecutor) ExecuteProbe(ctx context.Context, target ProbeTarget) (ProbeOutcome, error) {
|
||
|
|
requestID := uuid.New().String()
|
||
|
|
executedAt := e.now()
|
||
|
|
|
||
|
|
if target.Endpoint == "" {
|
||
|
|
return ProbeOutcome{}, ErrInvalidProbeTarget
|
||
|
|
}
|
||
|
|
|
||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.Endpoint, nil)
|
||
|
|
if err != nil {
|
||
|
|
return ProbeOutcome{}, fmt.Errorf("%w: %v", ErrInvalidProbeTarget, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
req.Header.Set("User-Agent", "supply-intelligence-probe/1.0")
|
||
|
|
req.Header.Set("Accept", "application/json")
|
||
|
|
if target.AuthHeader != "" {
|
||
|
|
req.Header.Set("Authorization", target.AuthHeader)
|
||
|
|
}
|
||
|
|
|
||
|
|
start := time.Now()
|
||
|
|
resp, err := e.httpClient.Do(req)
|
||
|
|
latencyMs := int(time.Since(start).Milliseconds())
|
||
|
|
|
||
|
|
outcome := ProbeOutcome{
|
||
|
|
AccountID: target.AccountID,
|
||
|
|
Platform: target.Platform,
|
||
|
|
LatencyMs: latencyMs,
|
||
|
|
RequestID: requestID,
|
||
|
|
ExecutedAt: executedAt,
|
||
|
|
}
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
outcome.TransportError = err
|
||
|
|
return outcome, nil // return outcome with transport error set
|
||
|
|
}
|
||
|
|
|
||
|
|
if resp != nil {
|
||
|
|
defer resp.Body.Close()
|
||
|
|
outcome.StatusCode = resp.StatusCode
|
||
|
|
|
||
|
|
// Read truncated body for debugging (max 1KB)
|
||
|
|
bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||
|
|
outcome.ResponseBody = string(bodyBytes)
|
||
|
|
}
|
||
|
|
|
||
|
|
return outcome, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
var ErrInvalidProbeTarget = errors.New("invalid probe target")
|