feat: add proxy and statistics features

This commit is contained in:
2026-05-27 07:41:06 +02:00
parent 35643b2ea9
commit b3c7e67b78
23 changed files with 2139 additions and 147 deletions

View File

@ -124,7 +124,7 @@ func (h *Handler) ListContainers(w http.ResponseWriter, r *http.Request) {
IPAddress string `json:"ip_address"`
Container *agentv1.ContainerInfo `json:"container"`
}
var out []containerDTO
out := make([]containerDTO, 0)
for _, agent := range h.registry.List() {
for _, c := range agent.Containers {
out = append(out, containerDTO{
@ -193,7 +193,7 @@ func (h *Handler) ListImages(w http.ResponseWriter, r *http.Request) {
CreatedAt int64 `json:"created_at"`
IsOrphan bool `json:"is_orphan"`
}
var out []imageDTO
out := make([]imageDTO, 0)
for _, agent := range h.registry.List() {
for _, img := range agent.Images {
out = append(out, imageDTO{
@ -247,7 +247,7 @@ func (h *Handler) ListVolumes(w http.ResponseWriter, r *http.Request) {
Mountpoint string `json:"mountpoint"`
IsOrphan bool `json:"is_orphan"`
}
var out []volumeDTO
out := make([]volumeDTO, 0)
for _, agent := range h.registry.List() {
for _, vol := range agent.Volumes {
out = append(out, volumeDTO{
@ -301,7 +301,7 @@ func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
Scope string `json:"scope"`
IsOrphan bool `json:"is_orphan"`
}
var out []networkDTO
out := make([]networkDTO, 0)
for _, agent := range h.registry.List() {
for _, net := range agent.Networks {
out = append(out, networkDTO{
@ -753,6 +753,129 @@ func (h *Handler) UpdateNow(w http.ResponseWriter, r *http.Request) {
jsonOK(w, map[string]string{"command_id": cmdID})
}
// ── Stats ─────────────────────────────────────────────────────────────────────
type processDTO struct {
Pid uint32 `json:"pid"`
Name string `json:"name"`
Cmd string `json:"cmd"`
CpuPct float64 `json:"cpu_pct"`
MemRss uint64 `json:"mem_rss"`
}
type netIfaceDTO struct {
Name string `json:"name"`
BytesRecv uint64 `json:"bytes_recv"`
BytesSent uint64 `json:"bytes_sent"`
BytesRecvRate uint64 `json:"bytes_recv_rate"`
BytesSentRate uint64 `json:"bytes_sent_rate"`
}
type diskDTO struct {
Path string `json:"path"`
Total uint64 `json:"total"`
Used uint64 `json:"used"`
Free uint64 `json:"free"`
}
type statsSnapshotDTO struct {
CpuPct float64 `json:"cpu_pct"`
CpuPerCore []float64 `json:"cpu_per_core"`
MemTotal uint64 `json:"mem_total"`
MemUsed uint64 `json:"mem_used"`
MemAvailable uint64 `json:"mem_available"`
NetInterfaces []netIfaceDTO `json:"net_interfaces"`
Processes []processDTO `json:"processes"`
Disks []diskDTO `json:"disks"`
Timestamp int64 `json:"timestamp"`
}
func protoStatsToDTO(s *agentv1.StatsSnapshot) *statsSnapshotDTO {
if s == nil {
return nil
}
dto := &statsSnapshotDTO{
CpuPct: s.CpuPct,
CpuPerCore: s.CpuPerCore,
MemTotal: s.MemTotal,
MemUsed: s.MemUsed,
MemAvailable: s.MemAvailable,
Timestamp: s.Timestamp,
}
if dto.CpuPerCore == nil {
dto.CpuPerCore = []float64{}
}
for _, iface := range s.NetInterfaces {
dto.NetInterfaces = append(dto.NetInterfaces, netIfaceDTO{
Name: iface.Name,
BytesRecv: iface.BytesRecv,
BytesSent: iface.BytesSent,
BytesRecvRate: iface.BytesRecvRate,
BytesSentRate: iface.BytesSentRate,
})
}
if dto.NetInterfaces == nil {
dto.NetInterfaces = []netIfaceDTO{}
}
for _, p := range s.Processes {
dto.Processes = append(dto.Processes, processDTO{
Pid: p.Pid,
Name: p.Name,
Cmd: p.Cmd,
CpuPct: p.CpuPct,
MemRss: p.MemRss,
})
}
if dto.Processes == nil {
dto.Processes = []processDTO{}
}
for _, d := range s.Disks {
dto.Disks = append(dto.Disks, diskDTO{
Path: d.Path,
Total: d.Total,
Used: d.Used,
Free: d.Free,
})
}
if dto.Disks == nil {
dto.Disks = []diskDTO{}
}
return dto
}
// ListStats handles GET /api/v1/stats — returns system stats for all connected agents.
func (h *Handler) ListStats(w http.ResponseWriter, r *http.Request) {
type statsDTO struct {
AgentID string `json:"agent_id"`
Hostname string `json:"hostname"`
Alias string `json:"alias"`
IPAddress string `json:"ip_address"`
Stats *statsSnapshotDTO `json:"stats"`
}
out := make([]statsDTO, 0)
for _, agent := range h.registry.List() {
out = append(out, statsDTO{
AgentID: agent.ID,
Hostname: agent.Hostname,
Alias: agent.Alias,
IPAddress: agent.IPAddress,
Stats: protoStatsToDTO(agent.Stats),
})
}
jsonOK(w, out)
}
// GetAgentStats handles GET /api/v1/agents/{agentID}/stats — returns stats for a single agent.
func (h *Handler) GetAgentStats(w http.ResponseWriter, r *http.Request) {
agentID := chi.URLParam(r, "agentID")
state, ok := h.registry.Get(agentID)
if !ok {
http.Error(w, "agent not connected", http.StatusNotFound)
return
}
jsonOK(w, protoStatsToDTO(state.Stats))
}
func jsonOK(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)