Files
monlet/agent/internal/client/client.go
2026-05-26 16:41:27 +04:00

136 lines
3.1 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/monlet/agent/internal/scheduler"
)
const (
MaxBatchEvents = 200
)
type Client struct {
baseURL string
token string
agentID string
version string
http *http.Client
}
func New(baseURL, token, agentID, version string) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
token: token,
agentID: agentID,
version: version,
http: &http.Client{Timeout: 15 * time.Second},
}
}
type HeartbeatRequest struct {
AgentID string `json:"agent_id"`
ObservedAt time.Time `json:"observed_at"`
Hostname string `json:"hostname"`
Version string `json:"version"`
Mode string `json:"mode,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
type EventBatchRequest struct {
AgentID string `json:"agent_id"`
Events []scheduler.Event `json:"events"`
}
type EventBatchResponse struct {
Accepted int `json:"accepted"`
Deduplicated int `json:"deduplicated"`
}
func (c *Client) SendHeartbeat(ctx context.Context, hb HeartbeatRequest) error {
return c.post(ctx, "/api/v1/heartbeat", hb, nil)
}
func (c *Client) SendEvents(ctx context.Context, events []scheduler.Event) (*EventBatchResponse, error) {
if len(events) == 0 {
return &EventBatchResponse{}, nil
}
if len(events) > MaxBatchEvents {
return nil, fmt.Errorf("batch exceeds %d events", MaxBatchEvents)
}
req := EventBatchRequest{AgentID: c.agentID, Events: events}
var resp EventBatchResponse
if err := c.post(ctx, "/api/v1/events", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *Client) post(ctx context.Context, path string, body any, out any) error {
b, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return &HTTPError{Status: resp.StatusCode, Body: string(msg)}
}
if out != nil {
return json.NewDecoder(resp.Body).Decode(out)
}
return nil
}
type HTTPError struct {
Status int
Body string
}
func (e *HTTPError) Error() string { return fmt.Sprintf("http %d: %s", e.Status, e.Body) }
// Retryable reports whether the error should trigger a retry (network errors
// and 5xx are retryable; 4xx are dropped to avoid hot-loop on bad payload).
func Retryable(err error) bool {
if err == nil {
return false
}
var he *HTTPError
if asErr(err, &he) {
return he.Status >= 500
}
return true
}
func asErr(err error, target **HTTPError) bool {
for e := err; e != nil; {
if h, ok := e.(*HTTPError); ok {
*target = h
return true
}
u, ok := e.(interface{ Unwrap() error })
if !ok {
return false
}
e = u.Unwrap()
}
return false
}