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")