package reporter import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "net/http" "time" "mfd-monitor-agent/internal/status" ) // defaultRetryDelay 是单次上报失败后重试前等待的默认时长。 const defaultRetryDelay = 2 * time.Second // Provider 提供用于外发上报的缓存 HostStatus。 type Provider interface { // Get 返回最新快照及快照是否已就绪。 Get() (status.HostStatus, bool) } // Client 将 HostStatus 快照 POST 到配置的中心端地址。 type Client struct { endpoint string client *http.Client } // NewClient 使用给定端点与请求超时创建上报客户端。 func NewClient(endpoint string, timeout time.Duration) *Client { return &Client{ endpoint: endpoint, client: &http.Client{Timeout: timeout}, } } // Report 将一份 HostStatus 以 JSON POST 上报;非 2xx 响应作为错误返回。 // 请求超时由 Client 内部的 http.Client.Timeout 控制。 func (c *Client) Report(snapshot status.HostStatus) error { body, err := json.Marshal(snapshot) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, c.endpoint, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := c.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("report returned HTTP %d", resp.StatusCode) } return nil } // Run 在 ctx 取消前按间隔 POST 最新缓存快照。 func Run(ctx context.Context, provider Provider, client *Client, interval time.Duration, logger *slog.Logger) { runWithRetryDelay(ctx, provider, client, interval, defaultRetryDelay, logger) } // runWithRetryDelay 运行上报循环;单次失败后按 retryDelay 重试一次。 func runWithRetryDelay(ctx context.Context, provider Provider, client *Client, interval time.Duration, retryDelay time.Duration, logger *slog.Logger) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: // 上报只读取缓存,避免与 HTTP 查询之外再触发一轮昂贵采集。 snapshot, ok := provider.Get() if !ok { // 首次快照未就绪时跳过本轮,上报循环继续等待下一周期。 continue } if err := reportWithRetry(ctx, client, snapshot, retryDelay); err != nil { // 上报失败只写日志,不影响本地 HTTP 服务与后续采集。 logger.Warn("status report failed", "error", err) } } } } // reportWithRetry 上报快照;失败时在可取消的等待后重试一次。 func reportWithRetry(ctx context.Context, client *Client, snapshot status.HostStatus, retryDelay time.Duration) error { err := client.Report(snapshot) if err == nil { return nil } // 重试等待受 ctx 控制,agent 退出时不会被固定退避时间拖住。 select { case <-ctx.Done(): return ctx.Err() case <-time.After(retryDelay): } if retryErr := client.Report(snapshot); retryErr != nil { return retryErr } return nil }