Files
2026-05-07 10:16:46 +08:00

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