feat: sistema de agente ligero para monitoreo de servidores
Backend: - Servidor model: AgentToken, AgentLastSeen, MetricasJson nuevos campos - AgentHeartbeat: endpoint público POST /agent/heartbeat (auth por token) - GenerateAgentToken: endpoint protegido POST /app/servidor/:id/agent-token - Ruta pública /agent/install.sh sirve el script de instalación Agente Go (agent/): - agent/main.go: binario independiente con gopsutil - Recolecta RAM, CPU, Disco, Uptime, OS, Load average - Lee agent.yml o flags --api-url / --token - Envía POST a /agent/heartbeat cada N segundos - Instala como servicio systemd via install.sh - agent/go.mod: módulo independiente (usite-agent) - agent/agent.yml.sample: configuración de ejemplo - agent/install.sh: instalador en 1 curl para Linux (systemd) Frontend servidor_dashboard.html: - Cards: badge ● En línea / ● Fuera / ○ Sin agente - Barras de progreso live: RAM%, CPU%, Disco% - Valores: GB usado/total, núcleos, uptime, OS real - Tiempo desde último reporte - Botón 'Agente' en cada card → modal de configuración - Modal agente: estado, token con copy, comando curl 1 línea, instalación manual/avanzada con collapse, métricas actuales Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
f52e266527
commit
d9d3f8a5eb
@@ -0,0 +1,14 @@
|
||||
# Configuración del agente de monitoreo u-site
|
||||
# Copia este archivo como agent.yml y edítalo.
|
||||
|
||||
# URL del panel de administración (sin barra al final)
|
||||
api_url: https://admin.u-site.app
|
||||
|
||||
# Token generado desde el panel en Servidor → Configurar Agente
|
||||
token: PEGAR_TOKEN_AQUI
|
||||
|
||||
# Intervalo de reporte en segundos (mínimo recomendado: 30)
|
||||
interval: 30
|
||||
|
||||
# Activar logs detallados (true para debug)
|
||||
debug: false
|
||||
@@ -0,0 +1,19 @@
|
||||
module usite-agent
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/shirou/gopsutil/v3 v3.24.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — Instalador del agente usite-agent
|
||||
# Uso:
|
||||
# curl -sL https://admin.u-site.app/agent/install.sh | bash -s -- --token=TU_TOKEN --url=https://admin.u-site.app
|
||||
# o descargando primero:
|
||||
# bash install.sh --token=TU_TOKEN --url=https://admin.u-site.app [--interval=30] [--dir=/opt/usite-agent]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colores ───────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
|
||||
# ── Parámetros ────────────────────────────────────────────────────────────────
|
||||
TOKEN=""
|
||||
API_URL=""
|
||||
INTERVAL=30
|
||||
INSTALL_DIR="/opt/usite-agent"
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--token=*) TOKEN="${arg#*=}" ;;
|
||||
--url=*) API_URL="${arg#*=}" ;;
|
||||
--interval=*) INTERVAL="${arg#*=}" ;;
|
||||
--dir=*) INSTALL_DIR="${arg#*=}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$TOKEN" ]] && error "Falta --token=TU_TOKEN"
|
||||
[[ -z "$API_URL" ]] && error "Falta --url=https://admin.u-site.app"
|
||||
|
||||
# ── Detectar arquitectura ─────────────────────────────────────────────────────
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
case $ARCH in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
armv7l) ARCH="arm" ;;
|
||||
*) error "Arquitectura no soportada: $ARCH" ;;
|
||||
esac
|
||||
|
||||
BINARY_NAME="usite-agent-${OS}-${ARCH}"
|
||||
DOWNLOAD_URL="${API_URL}/agent/download/${BINARY_NAME}"
|
||||
|
||||
info "Sistema: ${OS}/${ARCH}"
|
||||
info "Directorio de instalación: ${INSTALL_DIR}"
|
||||
info "Intervalo de reporte: ${INTERVAL}s"
|
||||
|
||||
# ── Crear directorio ──────────────────────────────────────────────────────────
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# ── Descargar binario ─────────────────────────────────────────────────────────
|
||||
info "Descargando agente desde ${DOWNLOAD_URL}..."
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL "$DOWNLOAD_URL" -o "${INSTALL_DIR}/usite-agent" || error "No se pudo descargar el binario"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -q "$DOWNLOAD_URL" -O "${INSTALL_DIR}/usite-agent" || error "No se pudo descargar el binario"
|
||||
else
|
||||
error "Se requiere curl o wget para descargar el agente"
|
||||
fi
|
||||
chmod +x "${INSTALL_DIR}/usite-agent"
|
||||
ok "Binario descargado y listo"
|
||||
|
||||
# ── Crear configuración ───────────────────────────────────────────────────────
|
||||
cat > "${INSTALL_DIR}/agent.yml" <<EOF
|
||||
api_url: ${API_URL}
|
||||
token: ${TOKEN}
|
||||
interval: ${INTERVAL}
|
||||
debug: false
|
||||
EOF
|
||||
ok "Configuración creada en ${INSTALL_DIR}/agent.yml"
|
||||
|
||||
# ── Crear servicio systemd ────────────────────────────────────────────────────
|
||||
if command -v systemctl &>/dev/null; then
|
||||
cat > /etc/systemd/system/usite-agent.service <<EOF
|
||||
[Unit]
|
||||
Description=u-site Server Monitoring Agent
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${INSTALL_DIR}/usite-agent --config=${INSTALL_DIR}/agent.yml
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=usite-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable usite-agent
|
||||
systemctl start usite-agent
|
||||
ok "Servicio systemd 'usite-agent' instalado y activo"
|
||||
info "Comandos útiles:"
|
||||
info " Ver estado: systemctl status usite-agent"
|
||||
info " Ver logs: journalctl -u usite-agent -f"
|
||||
info " Detener: systemctl stop usite-agent"
|
||||
info " Desinstalar: systemctl disable usite-agent && rm /etc/systemd/system/usite-agent.service"
|
||||
else
|
||||
warn "systemd no disponible. Ejecuta manualmente:"
|
||||
warn " ${INSTALL_DIR}/usite-agent --config=${INSTALL_DIR}/agent.yml &"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "✅ usite-agent instalado correctamente"
|
||||
info "El agente empezará a reportar métricas a ${API_URL} cada ${INTERVAL} segundos."
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/load"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type Config struct {
|
||||
APIURL string `yaml:"api_url"` // https://admin.u-site.app
|
||||
Token string `yaml:"token"` // token generado desde el panel
|
||||
Interval int `yaml:"interval"` // segundos entre reportes (default: 30)
|
||||
Debug bool `yaml:"debug"`
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
f, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no se pudo leer %s: %w", path, err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(f, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("config YAML inválido: %w", err)
|
||||
}
|
||||
if cfg.Interval <= 0 {
|
||||
cfg.Interval = 30
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// ── Métricas ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type Metricas struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Uptime uint64 `json:"uptime_seg"`
|
||||
UptimeStr string `json:"uptime"`
|
||||
RAM RAMInfo `json:"ram"`
|
||||
CPU CPUInfo `json:"cpu"`
|
||||
Disco DiscoInfo `json:"disco"`
|
||||
LoadAvg LoadInfo `json:"load_avg"`
|
||||
ReportadoEn string `json:"reportado_en"`
|
||||
}
|
||||
|
||||
type RAMInfo struct {
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsadoGB float64 `json:"usado_gb"`
|
||||
LibreGB float64 `json:"libre_gb"`
|
||||
Porcentaje float64 `json:"porcentaje"`
|
||||
}
|
||||
|
||||
type CPUInfo struct {
|
||||
Nucleos int `json:"nucleos"`
|
||||
Porcentaje float64 `json:"porcentaje"`
|
||||
}
|
||||
|
||||
type DiscoInfo struct {
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsadoGB float64 `json:"usado_gb"`
|
||||
LibreGB float64 `json:"libre_gb"`
|
||||
Porcentaje float64 `json:"porcentaje"`
|
||||
Ruta string `json:"ruta"`
|
||||
}
|
||||
|
||||
type LoadInfo struct {
|
||||
Load1 float64 `json:"load1"`
|
||||
Load5 float64 `json:"load5"`
|
||||
Load15 float64 `json:"load15"`
|
||||
}
|
||||
|
||||
func round2(v float64) float64 {
|
||||
return float64(int(v*100)) / 100
|
||||
}
|
||||
|
||||
func uptimeStr(secs uint64) string {
|
||||
d := secs / 86400
|
||||
h := (secs % 86400) / 3600
|
||||
m := (secs % 3600) / 60
|
||||
if d > 0 {
|
||||
return fmt.Sprintf("%dd %dh %dm", d, h, m)
|
||||
}
|
||||
return fmt.Sprintf("%dh %dm", h, m)
|
||||
}
|
||||
|
||||
func collectMetrics() (*Metricas, error) {
|
||||
m := &Metricas{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
ReportadoEn: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Hostname
|
||||
if hn, err := os.Hostname(); err == nil {
|
||||
m.Hostname = hn
|
||||
}
|
||||
|
||||
// Uptime
|
||||
if info, err := host.Info(); err == nil {
|
||||
m.Uptime = info.Uptime
|
||||
m.UptimeStr = uptimeStr(info.Uptime)
|
||||
if info.OS != "" {
|
||||
m.OS = info.Platform + " " + info.PlatformVersion
|
||||
}
|
||||
}
|
||||
|
||||
// RAM
|
||||
if v, err := mem.VirtualMemory(); err == nil {
|
||||
gb := 1024.0 * 1024 * 1024
|
||||
m.RAM = RAMInfo{
|
||||
TotalGB: round2(float64(v.Total) / gb),
|
||||
UsadoGB: round2(float64(v.Used) / gb),
|
||||
LibreGB: round2(float64(v.Free) / gb),
|
||||
Porcentaje: round2(v.UsedPercent),
|
||||
}
|
||||
}
|
||||
|
||||
// CPU
|
||||
m.CPU.Nucleos = runtime.NumCPU()
|
||||
if pcts, err := cpu.Percent(1*time.Second, false); err == nil && len(pcts) > 0 {
|
||||
m.CPU.Porcentaje = round2(pcts[0])
|
||||
}
|
||||
|
||||
// Disco (raíz /)
|
||||
root := "/"
|
||||
if runtime.GOOS == "windows" {
|
||||
root = "C:\\"
|
||||
}
|
||||
if d, err := disk.Usage(root); err == nil {
|
||||
gb := 1024.0 * 1024 * 1024
|
||||
m.Disco = DiscoInfo{
|
||||
TotalGB: round2(float64(d.Total) / gb),
|
||||
UsadoGB: round2(float64(d.Used) / gb),
|
||||
LibreGB: round2(float64(d.Free) / gb),
|
||||
Porcentaje: round2(d.UsedPercent),
|
||||
Ruta: root,
|
||||
}
|
||||
}
|
||||
|
||||
// Load average (Linux/macOS)
|
||||
if la, err := load.Avg(); err == nil {
|
||||
m.LoadAvg = LoadInfo{Load1: round2(la.Load1), Load5: round2(la.Load5), Load15: round2(la.Load15)}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ── Reporte ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type HeartbeatRequest struct {
|
||||
Token string `json:"token"`
|
||||
Metricas string `json:"metricas"`
|
||||
}
|
||||
|
||||
func sendHeartbeat(cfg *Config, metricas *Metricas) error {
|
||||
metJSON, err := json.Marshal(metricas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := HeartbeatRequest{
|
||||
Token: cfg.Token,
|
||||
Metricas: string(metJSON),
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
url := cfg.APIURL + "/agent/heartbeat"
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "usite-agent/1.0")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("API respondió %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "agent.yml", "ruta al archivo de configuración")
|
||||
apiURL := flag.String("api-url", "", "URL del API (sobreescribe config)")
|
||||
token := flag.String("token", "", "token del servidor (sobreescribe config)")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
// Si no hay config pero se pasaron flags, usarlos directamente
|
||||
if *apiURL == "" || *token == "" {
|
||||
log.Fatalf("Error cargando config: %v\nUso: usite-agent --api-url=https://... --token=...", err)
|
||||
}
|
||||
cfg = &Config{APIURL: *apiURL, Token: *token, Interval: 30}
|
||||
}
|
||||
|
||||
// Flags sobreescriben config
|
||||
if *apiURL != "" {
|
||||
cfg.APIURL = *apiURL
|
||||
}
|
||||
if *token != "" {
|
||||
cfg.Token = *token
|
||||
}
|
||||
|
||||
if cfg.APIURL == "" || cfg.Token == "" {
|
||||
log.Fatal("api_url y token son requeridos")
|
||||
}
|
||||
|
||||
log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds", cfg.APIURL, cfg.Interval)
|
||||
|
||||
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Enviar inmediatamente al iniciar
|
||||
reportar := func() {
|
||||
m, err := collectMetrics()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Error recolectando métricas: %v", err)
|
||||
return
|
||||
}
|
||||
if cfg.Debug {
|
||||
b, _ := json.MarshalIndent(m, "", " ")
|
||||
log.Printf("📊 Métricas:\n%s", string(b))
|
||||
}
|
||||
if err := sendHeartbeat(cfg, m); err != nil {
|
||||
log.Printf("⚠️ Error enviando heartbeat: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ Heartbeat enviado | RAM: %.1f%% | CPU: %.1f%% | Disco: %.1f%%",
|
||||
m.RAM.Porcentaje, m.CPU.Porcentaje, m.Disco.Porcentaje)
|
||||
}
|
||||
}
|
||||
|
||||
reportar()
|
||||
for range ticker.C {
|
||||
reportar()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user