feat: bootstrap supply intelligence baseline
This commit is contained in:
125
internal/probe/executor.go
Normal file
125
internal/probe/executor.go
Normal file
@@ -0,0 +1,125 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user