fix: add go.sum and fixes
This commit is contained in:
@@ -7,118 +7,326 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"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"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
psnet "github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CPU CPUMetric `json:"cpu"`
|
||||
Memory MemMetric `json:"memory"`
|
||||
Disk []DiskMetric `json:"disk"`
|
||||
Network NetMetric `json:"network"`
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🐍 OPHION Agent - Observability Collector
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
type Config struct {
|
||||
ServerURL string
|
||||
APIKey string
|
||||
Hostname string
|
||||
CollectInterval time.Duration
|
||||
DockerEnabled bool
|
||||
}
|
||||
|
||||
type CPUMetric struct {
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
Cores int `json:"cores"`
|
||||
type Metric struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Service string `json:"service"`
|
||||
Host string `json:"host"`
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
MetricType string `json:"metric_type"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type MemMetric struct {
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
UsedPercent float64 `json:"used_percent"`
|
||||
}
|
||||
|
||||
type DiskMetric struct {
|
||||
Path string `json:"path"`
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
UsedPercent float64 `json:"used_percent"`
|
||||
}
|
||||
|
||||
type NetMetric struct {
|
||||
BytesSent uint64 `json:"bytes_sent"`
|
||||
BytesRecv uint64 `json:"bytes_recv"`
|
||||
type ContainerStats struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsage uint64 `json:"memory_usage"`
|
||||
MemoryLimit uint64 `json:"memory_limit"`
|
||||
MemoryPercent float64 `json:"memory_percent"`
|
||||
NetRx uint64 `json:"net_rx"`
|
||||
NetTx uint64 `json:"net_tx"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
serverURL := os.Getenv("OPHION_SERVER")
|
||||
if serverURL == "" {
|
||||
serverURL = "http://localhost:8080"
|
||||
}
|
||||
config := loadConfig()
|
||||
|
||||
apiKey := os.Getenv("OPHION_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("OPHION_API_KEY is required")
|
||||
log.Printf("🐍 OPHION Agent starting")
|
||||
log.Printf(" Server: %s", config.ServerURL)
|
||||
log.Printf(" Host: %s", config.Hostname)
|
||||
log.Printf(" Interval: %s", config.CollectInterval)
|
||||
log.Printf(" Docker: %v", config.DockerEnabled)
|
||||
|
||||
// Handle shutdown
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
ticker := time.NewTicker(config.CollectInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Collect immediately
|
||||
collect(config)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
log.Println("🛑 Shutting down agent...")
|
||||
return
|
||||
case <-ticker.C:
|
||||
collect(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() *Config {
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
interval := 30 * time.Second
|
||||
log.Printf("🐍 OPHION Agent starting - reporting to %s every %s", serverURL, interval)
|
||||
if v := os.Getenv("OPHION_INTERVAL"); v != "" {
|
||||
if d, err := time.ParseDuration(v); err == nil {
|
||||
interval = d
|
||||
}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
for range ticker.C {
|
||||
metrics := collectMetrics()
|
||||
sendMetrics(serverURL, apiKey, metrics)
|
||||
dockerEnabled := true
|
||||
if v := os.Getenv("OPHION_DOCKER"); v == "false" || v == "0" {
|
||||
dockerEnabled = false
|
||||
}
|
||||
|
||||
return &Config{
|
||||
ServerURL: getEnv("OPHION_SERVER", "http://localhost:8080"),
|
||||
APIKey: getEnv("OPHION_API_KEY", ""),
|
||||
Hostname: getEnv("OPHION_HOSTNAME", hostname),
|
||||
CollectInterval: interval,
|
||||
DockerEnabled: dockerEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func collectMetrics() Metrics {
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
cpuPercent, _ := cpu.Percent(time.Second, false)
|
||||
cpuUsage := 0.0
|
||||
if len(cpuPercent) > 0 {
|
||||
cpuUsage = cpuPercent[0]
|
||||
func getEnv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func collect(config *Config) {
|
||||
var metrics []Metric
|
||||
now := time.Now()
|
||||
host := config.Hostname
|
||||
|
||||
// System metrics
|
||||
metrics = append(metrics, collectSystemMetrics(now, host)...)
|
||||
|
||||
// Docker metrics
|
||||
if config.DockerEnabled {
|
||||
metrics = append(metrics, collectDockerMetrics(now, host)...)
|
||||
}
|
||||
|
||||
memInfo, _ := mem.VirtualMemory()
|
||||
|
||||
diskInfo, _ := disk.Usage("/")
|
||||
disks := []DiskMetric{{
|
||||
Path: "/",
|
||||
Total: diskInfo.Total,
|
||||
Used: diskInfo.Used,
|
||||
UsedPercent: diskInfo.UsedPercent,
|
||||
}}
|
||||
|
||||
netIO, _ := net.IOCounters(false)
|
||||
netMetric := NetMetric{}
|
||||
if len(netIO) > 0 {
|
||||
netMetric.BytesSent = netIO[0].BytesSent
|
||||
netMetric.BytesRecv = netIO[0].BytesRecv
|
||||
}
|
||||
|
||||
return Metrics{
|
||||
Hostname: hostname,
|
||||
Timestamp: time.Now(),
|
||||
CPU: CPUMetric{
|
||||
UsagePercent: cpuUsage,
|
||||
Cores: runtime.NumCPU(),
|
||||
},
|
||||
Memory: MemMetric{
|
||||
Total: memInfo.Total,
|
||||
Used: memInfo.Used,
|
||||
UsedPercent: memInfo.UsedPercent,
|
||||
},
|
||||
Disk: disks,
|
||||
Network: netMetric,
|
||||
// Send to server
|
||||
if len(metrics) > 0 {
|
||||
sendMetrics(config, metrics)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMetrics(serverURL, apiKey string, metrics Metrics) {
|
||||
data, _ := json.Marshal(metrics)
|
||||
|
||||
req, _ := http.NewRequest("POST", serverURL+"/api/v1/metrics", bytes.NewBuffer(data))
|
||||
func collectSystemMetrics(now time.Time, hostname string) []Metric {
|
||||
var metrics []Metric
|
||||
svc := "system"
|
||||
|
||||
// CPU
|
||||
cpuPercent, err := cpu.Percent(time.Second, false)
|
||||
if err == nil && len(cpuPercent) > 0 {
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "cpu.usage_percent", Value: cpuPercent[0], MetricType: "gauge",
|
||||
})
|
||||
}
|
||||
|
||||
// Load average
|
||||
loadAvg, err := load.Avg()
|
||||
if err == nil {
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "cpu.load_avg_1", Value: loadAvg.Load1, MetricType: "gauge",
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "cpu.load_avg_5", Value: loadAvg.Load5, MetricType: "gauge",
|
||||
})
|
||||
}
|
||||
|
||||
// Memory
|
||||
memInfo, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "memory.used_percent", Value: memInfo.UsedPercent, MetricType: "gauge",
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "memory.used_bytes", Value: float64(memInfo.Used), MetricType: "gauge",
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "memory.available_bytes", Value: float64(memInfo.Available), MetricType: "gauge",
|
||||
})
|
||||
}
|
||||
|
||||
// Disk
|
||||
partitions, _ := disk.Partitions(false)
|
||||
for _, p := range partitions {
|
||||
if strings.HasPrefix(p.Mountpoint, "/snap") ||
|
||||
strings.HasPrefix(p.Mountpoint, "/sys") ||
|
||||
strings.HasPrefix(p.Mountpoint, "/proc") ||
|
||||
strings.HasPrefix(p.Mountpoint, "/dev") ||
|
||||
strings.HasPrefix(p.Mountpoint, "/run") {
|
||||
continue
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(p.Mountpoint)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := map[string]string{"path": p.Mountpoint, "device": p.Device}
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "disk.used_percent", Value: usage.UsedPercent, MetricType: "gauge", Tags: tags,
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "disk.used_bytes", Value: float64(usage.Used), MetricType: "gauge", Tags: tags,
|
||||
})
|
||||
}
|
||||
|
||||
// Network
|
||||
netIO, err := psnet.IOCounters(false)
|
||||
if err == nil && len(netIO) > 0 {
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "network.bytes_sent", Value: float64(netIO[0].BytesSent), MetricType: "counter",
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "network.bytes_recv", Value: float64(netIO[0].BytesRecv), MetricType: "counter",
|
||||
})
|
||||
}
|
||||
|
||||
// Uptime
|
||||
hostInfo, err := host.Info()
|
||||
if err == nil {
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "host.uptime_seconds", Value: float64(hostInfo.Uptime), MetricType: "gauge",
|
||||
})
|
||||
}
|
||||
|
||||
// Cores
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "host.cpu_cores", Value: float64(runtime.NumCPU()), MetricType: "gauge",
|
||||
})
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
func collectDockerMetrics(now time.Time, hostname string) []Metric {
|
||||
var metrics []Metric
|
||||
svc := "docker"
|
||||
|
||||
// Check if docker is available
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
return metrics
|
||||
}
|
||||
|
||||
// Get container stats using docker CLI (simpler, no SDK needed)
|
||||
out, err := exec.Command("docker", "stats", "--no-stream", "--format",
|
||||
"{{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}").Output()
|
||||
if err != nil {
|
||||
return metrics
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
runningCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
runningCount++
|
||||
|
||||
parts := strings.Split(line, "\t")
|
||||
if len(parts) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
id := parts[0]
|
||||
name := parts[1]
|
||||
cpuStr := strings.TrimSuffix(parts[2], "%")
|
||||
memStr := strings.TrimSuffix(parts[4], "%")
|
||||
|
||||
var cpuPercent, memPercent float64
|
||||
fmt.Sscanf(cpuStr, "%f", &cpuPercent)
|
||||
fmt.Sscanf(memStr, "%f", &memPercent)
|
||||
|
||||
tags := map[string]string{"container": name, "container_id": id}
|
||||
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "container.cpu_percent", Value: cpuPercent, MetricType: "gauge", Tags: tags,
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "container.memory_percent", Value: memPercent, MetricType: "gauge", Tags: tags,
|
||||
})
|
||||
}
|
||||
|
||||
// Total containers
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "containers.running", Value: float64(runningCount), MetricType: "gauge",
|
||||
})
|
||||
|
||||
// Get all containers count
|
||||
out, err = exec.Command("docker", "ps", "-a", "-q").Output()
|
||||
if err == nil {
|
||||
total := len(strings.Split(strings.TrimSpace(string(out)), "\n"))
|
||||
if strings.TrimSpace(string(out)) == "" {
|
||||
total = 0
|
||||
}
|
||||
metrics = append(metrics, Metric{
|
||||
Timestamp: now, Service: svc, Host: hostname,
|
||||
Name: "containers.total", Value: float64(total), MetricType: "gauge",
|
||||
})
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
func sendMetrics(config *Config, metrics []Metric) {
|
||||
data, err := json.Marshal(map[string]any{"metrics": metrics})
|
||||
if err != nil {
|
||||
log.Printf("Error marshaling metrics: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", config.ServerURL+"/api/v1/metrics", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
log.Printf("Error creating request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
if config.APIKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIKey)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
@@ -128,8 +336,10 @@ func sendMetrics(serverURL, apiKey string, metrics Metrics) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
log.Printf("✓ Metrics sent: CPU=%.1f%% MEM=%.1f%%",
|
||||
metrics.CPU.UsagePercent, metrics.Memory.UsedPercent)
|
||||
if resp.StatusCode >= 400 {
|
||||
log.Printf("Server returned error: %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📤 Sent %d metrics", len(metrics))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user