Feat/monitoring (#1267) Cloud Version

* feat: add start monitoring remote servers

* reafctor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor:

* refactor: add metrics

* feat: add disk monitoring

* refactor: translate to english

* refacotor: add stats

* refactor: remove color

* feat: add log server metrics

* refactor: remove unused deps

* refactor: add origin

* refactor: add logs

* refactor: update

* feat: add series monitoring

* refactor: add system monitoring

* feat: add benchmark to optimize data

* refactor: update fn

* refactor: remove comments

* refactor: update

* refactor: exclude items

* feat: add refresh rate

* feat: add monitoring remote servers

* refactor: update

* refactor: remove unsued volumes

* refactor: update monitoring

* refactor: add more presets

* feat: add container metrics

* feat: add docker monitoring

* refactor: update conversion

* refactor: remove unused code

* refactor: update

* refactor: add docker compose logs

* refactor: add docker cli

* refactor: add install curl

* refactor: add get update

* refactor: add monitoring remote servers

* refactor: add containers config

* feat: add container specification

* refactor: update path

* refactor: add server filter

* refactor: simplify logic

* fix: verify if file exist before get stats

* refactor: update

* refactor: remove unused deps

* test: add test for containers

* refactor: update

* refactor add memory collector

* refactor: update

* refactor: update

* refactor: update

* refactor: remove

* refactor: add memory

* refactor: add server memory usage

* refactor: change memory

* refactor: update

* refactor: update

* refactor: add container metrics

* refactor: comment code

* refactor: mount proc bind

* refactor: change interval with node cron

* refactor: remove opening file

* refactor: use streams

* refactor: remove unused ws

* refactor: disable live when is all

* refactor: add sqlite

* refactor: update

* feat: add golang benchmark

* refactor: update go

* refactor: update dockerfile

* refactor: update db

* refactor: add env

* refactor: separate logic

* refactor: split logic

* refactor: update logs

* refactor: update dockerfile

* refactor: hide .env

* refactor: update

* chore: hide ,.ebnv

* refactor: add end angle

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update monitoring

* refactor: add mount db

* refactor: add metrics and url callback

* refactor: add middleware

* refactor: add threshold property

* feat: add memory and cpu threshold notification

* feat: send notifications to the server

* feat: add metrics for dokploy server

* refactor: add dokploy server to monitoring

* refactor: update methods

* refactor: add admin to useeffect

* refactor: stop monitoring containers if elements are 0

* refactor: cancel request if appName is empty

* refactor: reuse methods

* chore; add feat monitoring

* refactor: set base url

* refactor: adjust monitoring

* refactor: delete migrations

* feat: add columns

* fix: add missing flag

* refactor: add free metrics

* refactor: add paid monitoring

* refactor: update methods

* feat: improve ui

* feat: add container stats

* refactor: add all container metrics

* refactor: add color primary

* refactor: change default rate limiting refresher

* refactor: update retention days

* refactor: use json instead of individual properties

* refactor: lint

* refactor: pass json env

* refactor: update

* refactor: delete

* refactor: update

* refactor: fix types

* refactor: add retention days

* chore: add license

* refactor: create db

* refactor: update path

* refactor: update setup

* refactor: update

* refactor: create files

* refactor: update

* refactor: delete

* refactor: update

* refactor: update token metrics

* fix: typechecks

* refactor: setup web server

* refactor: update error handling and add monitoring

* refactor: add local storage save

* refactor: add spacing

* refactor: update

* refactor: upgrade drizzle

* refactor: delete

* refactor: uppgrade drizzle kit

* refactor: update search with jsonB

* chore: upgrade drizzle

* chore: update packages

* refactor: add missing type

* refactor: add serverType

* refactor: update url

* refactor: update

* refactor: update

* refactor: hide monitoring on self hosted

* refactor: update server

* refactor: update

* refactor: update

* refactor: pin node version
This commit is contained in:
Mauricio Siu
2025-02-02 14:08:06 -06:00
committed by GitHub
parent 8c69d2a085
commit 74a0f5e992
150 changed files with 36173 additions and 11538 deletions

1
apps/monitoring/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
monitoring.db

View File

154
apps/monitoring/README.md Normal file
View File

@@ -0,0 +1,154 @@
# Dokploy Monitoring (Go Version)
Application that powers Dokploy's monitoring service.
You can use it for monitoring any external service.
## Requirements
- Go 1.21
## Configuration
Create a `.env` file in the root of the project with the following content:
```shell
METRICS_CONFIG='{
"server": {
"refreshRate": 25,
"port": 3001,
"type": "Remote | Dokploy",
"token": "metrics",
"urlCallback": "http://localhost:3000/api/trpc/notification.receiveNotification",
"retentionDays": 7,
"cronJob": "0 0 * * *",
"thresholds": {
"cpu": 0,
"memory": 0
}
},
"containers": {
"refreshRate": 25,
"services": {
"include": ["testing-elasticsearch-14649e"],
"exclude": []
}
}
}'
```
## Installation
```bash
go mod download
```
## Execution
```bash
go run main.go
```
## Endpoints
- `GET /health` - Check service health status (no authentication required)
- `GET /metrics?limit=<number|all>` - Get server metrics (default limit: 50)
- `GET /metrics/containers?limit=<number|all>&appName=<name>` - Get container metrics for a specific application (default limit: 50)
## Features
### Server
- CPU Usage (%)
- Memory Usage (%)
- Disk
- Network
- CPU Model
- Operating System
- Kernel
- Architecture
- Threads
Example response:
| Field | Value |
| ------------------ | --------------------------- |
| timestamp | 2025-01-19T21:44:54.232164Z |
| cpu | 24.57% |
| cpu_model | Apple M1 Pro |
| cpu_cores | 8 |
| cpu_physical_cores | 1 |
| cpu_speed | 3228.0 MHz |
| os | darwin |
| distro | darwin |
| kernel | 23.4.0 |
| arch | arm64 |
| mem_used | 81.91% |
| mem_used_gb | 13.11 GB |
| mem_total | 16.0 GB |
| uptime | 752232s |
| disk_used | 89.34% |
| total_disk | 460.43 GB |
| network_in | 54.78 MB |
| network_out | 31.72 MB |
### Containers
Compatible with all Docker container types (standalone containers, Docker Compose, and Docker Swarm stacks). Note: When monitoring Docker Compose or Swarm stacks, use the `--p` flag to properly identify all services within the stack.
Example response:
| Field | Value |
| -------------- | ------------------------------------- |
| id | 1 |
| timestamp | 2025-01-19T22:16:30.796129Z |
| container_id | 7428f5a49039 |
| container_name | testing-elasticsearch-14649e-kibana-1 |
Metrics JSON:
```json
{
"timestamp": "2025-01-19T22:16:30.796129Z",
"CPU": 83.76,
"Memory": {
"percentage": 0.03,
"used": 2.262,
"total": 7.654,
"usedUnit": "MB",
"totalUnit": "GB"
},
"Network": {
"input": 306,
"output": 0,
"inputUnit": "B",
"outputUnit": "B"
},
"BlockIO": {
"read": 28.7,
"write": 0,
"readUnit": "kB",
"writeUnit": "B"
},
"Container": "7428f5a49039",
"ID": "7428f5a49039",
"Name": "testing-elasticsearch-14649e-kibana-1"
}
```
## Notifications
Dokploy uses a callback URL to send notifications when metrics exceed configured thresholds. Notifications are sent via POST request in the following format:
Note: Setting a threshold to 0 disables notifications for that metric.
```typescript
interface Notification {
Type: "Memory" | "CPU";
Value: number;
Threshold: number;
Message: string;
Timestamp: string;
Token: string;
}
```

View File

@@ -0,0 +1,57 @@
package config
import (
"encoding/json"
"log"
"os"
"sync"
)
type Config struct {
Server struct {
ServerType string `json:"type"`
RefreshRate int `json:"refreshRate"`
Port int `json:"port"`
Token string `json:"token"`
UrlCallback string `json:"urlCallback"`
CronJob string `json:"cronJob"`
RetentionDays int `json:"retentionDays"`
Thresholds struct {
CPU int `json:"cpu"`
Memory int `json:"memory"`
} `json:"thresholds"`
} `json:"server"`
Containers struct {
RefreshRate int `json:"refreshRate"`
Services struct {
Include []string `json:"include"`
Exclude []string `json:"exclude"`
} `json:"services"`
} `json:"containers"`
}
var (
config *Config
configOnce sync.Once
)
func GetMetricsConfig() *Config {
configOnce.Do(func() {
configJSON := os.Getenv("METRICS_CONFIG")
if configJSON == "" {
log.Fatal("METRICS_CONFIG environment variable is required")
}
config = &Config{}
if err := json.Unmarshal([]byte(configJSON), config); err != nil {
log.Fatalf("Error parsing METRICS_CONFIG: %v", err)
}
// Validate required fields
if config.Server.Token == "" || config.Server.UrlCallback == "" {
log.Fatal("token and urlCallback are required in the configuration")
}
})
return config
}

View File

@@ -0,0 +1,61 @@
package containers
import (
"strings"
"github.com/mauriciogm/dokploy/apps/monitoring/config"
)
var monitorConfig *MonitoringConfig
func LoadConfig() error {
cfg := config.GetMetricsConfig()
monitorConfig = &MonitoringConfig{
IncludeServices: make([]string, len(cfg.Containers.Services.Include)),
ExcludeServices: make([]string, len(cfg.Containers.Services.Exclude)),
}
// Convert Include services
for i, svc := range cfg.Containers.Services.Include {
monitorConfig.IncludeServices[i] = svc
}
// Convert Exclude services
for i, appName := range cfg.Containers.Services.Exclude {
monitorConfig.ExcludeServices[i] = appName
}
return nil
}
func ShouldMonitorContainer(containerName string) bool {
if monitorConfig == nil {
return false
}
for _, excluded := range monitorConfig.ExcludeServices {
if strings.Contains(containerName, excluded) {
return false
}
}
if len(monitorConfig.IncludeServices) > 0 {
for _, included := range monitorConfig.IncludeServices {
if strings.Contains(containerName, included) {
return true
}
}
return false
}
return true
}
func GetServiceName(containerName string) string {
name := strings.TrimPrefix(containerName, "/")
parts := strings.Split(name, "-")
if len(parts) > 1 {
return strings.Join(parts[:len(parts)-1], "-")
}
return name
}

View File

@@ -0,0 +1,270 @@
package containers
import (
"encoding/json"
"fmt"
"log"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"github.com/mauriciogm/dokploy/apps/monitoring/config"
"github.com/mauriciogm/dokploy/apps/monitoring/database"
)
type ContainerMonitor struct {
db *database.DB
isRunning bool
mu sync.Mutex
stopChan chan struct{}
}
func NewContainerMonitor(db *database.DB) (*ContainerMonitor, error) {
if err := db.InitContainerMetricsTable(); err != nil {
return nil, fmt.Errorf("failed to initialize container metrics table: %v", err)
}
return &ContainerMonitor{
db: db,
stopChan: make(chan struct{}),
}, nil
}
func (cm *ContainerMonitor) Start() error {
if err := LoadConfig(); err != nil {
return fmt.Errorf("error loading config: %v", err)
}
// Check if there are services to monitor
if len(monitorConfig.IncludeServices) == 0 {
log.Printf("No services to monitor. Skipping container metrics collection")
return nil
}
metricsConfig := config.GetMetricsConfig()
refreshRate := metricsConfig.Containers.RefreshRate
if refreshRate == 0 {
refreshRate = 60 // default refresh rate
}
duration := time.Duration(refreshRate) * time.Second
// log.Printf("Container metrics collection will run every %d seconds for services: %v", refreshRate, monitorConfig.IncludeServices)
ticker := time.NewTicker(duration)
go func() {
for {
select {
case <-ticker.C:
// Check again in case the configuration has changed
if len(monitorConfig.IncludeServices) == 0 {
log.Printf("No services to monitor. Stopping metrics collection")
ticker.Stop()
return
}
cm.collectMetrics()
case <-cm.stopChan:
ticker.Stop()
return
}
}
}()
return nil
}
func (cm *ContainerMonitor) Stop() {
close(cm.stopChan)
}
func (cm *ContainerMonitor) collectMetrics() {
cm.mu.Lock()
if cm.isRunning {
cm.mu.Unlock()
log.Println("Previous collection still running, skipping...")
return
}
cm.isRunning = true
cm.mu.Unlock()
defer func() {
cm.mu.Lock()
cm.isRunning = false
cm.mu.Unlock()
}()
cmd := exec.Command("docker", "stats", "--no-stream", "--format",
`{"BlockIO":"{{.BlockIO}}","CPUPerc":"{{.CPUPerc}}","ID":"{{.ID}}","MemPerc":"{{.MemPerc}}","MemUsage":"{{.MemUsage}}","Name":"{{.Name}}","NetIO":"{{.NetIO}}"}`)
output, err := cmd.CombinedOutput()
// log.Printf("Output: %s", string(output))
if err != nil {
log.Printf("Error getting docker stats: %v", err)
return
}
lines := string(output)
if lines == "" {
return
}
seenServices := make(map[string]bool)
for _, line := range strings.Split(lines, "\n") {
if line == "" {
continue
}
var container Container
if err := json.Unmarshal([]byte(line), &container); err != nil {
log.Printf("Error parsing container data: %v", err)
continue
}
if !ShouldMonitorContainer(container.Name) {
continue
}
serviceName := GetServiceName(container.Name)
if seenServices[serviceName] {
continue
}
seenServices[serviceName] = true
// log.Printf("Container: %+v", container)
// Process metrics
metric := processContainerMetrics(container)
// log.Printf("Saving metrics for %s: %+v", serviceName, metric)
if err := cm.db.SaveContainerMetric(metric); err != nil {
log.Printf("Error saving metrics for %s: %v", serviceName, err)
}
}
}
func processContainerMetrics(container Container) *database.ContainerMetric {
// Process CPU
cpu, _ := strconv.ParseFloat(strings.TrimSuffix(container.CPUPerc, "%"), 64)
// Process Memory
memPerc, _ := strconv.ParseFloat(strings.TrimSuffix(container.MemPerc, "%"), 64)
memParts := strings.Split(container.MemUsage, " / ")
var usedValue, totalValue float64
var usedUnit, totalUnit string
if len(memParts) == 2 {
// Process used memory
usedParts := strings.Fields(memParts[0])
if len(usedParts) > 0 {
usedValue, _ = strconv.ParseFloat(strings.TrimRight(usedParts[0], "MiBGiB"), 64)
usedUnit = strings.TrimLeft(usedParts[0], "0123456789.")
// Convert MiB to MB and GiB to GB
if usedUnit == "MiB" {
usedUnit = "MB"
} else if usedUnit == "GiB" {
usedUnit = "GB"
}
}
// Process total memory
totalParts := strings.Fields(memParts[1])
if len(totalParts) > 0 {
totalValue, _ = strconv.ParseFloat(strings.TrimRight(totalParts[0], "MiBGiB"), 64)
totalUnit = strings.TrimLeft(totalParts[0], "0123456789.")
// Convert MiB to MB and GiB to GB
if totalUnit == "MiB" {
totalUnit = "MB"
} else if totalUnit == "GiB" {
totalUnit = "GB"
}
}
}
// Process Network I/O
netParts := strings.Split(container.NetIO, " / ")
var netInValue, netOutValue float64
var netInUnit, netOutUnit string
if len(netParts) == 2 {
// Process input
inParts := strings.Fields(netParts[0])
if len(inParts) > 0 {
netInValue, _ = strconv.ParseFloat(strings.TrimRight(inParts[0], "kMGTB"), 64)
netInUnit = strings.TrimLeft(inParts[0], "0123456789.")
}
// Process output
outParts := strings.Fields(netParts[1])
if len(outParts) > 0 {
netOutValue, _ = strconv.ParseFloat(strings.TrimRight(outParts[0], "kMGTB"), 64)
netOutUnit = strings.TrimLeft(outParts[0], "0123456789.")
}
}
// Process Block I/O
blockParts := strings.Split(container.BlockIO, " / ")
var blockReadValue, blockWriteValue float64
var blockReadUnit, blockWriteUnit string
if len(blockParts) == 2 {
// Process read
readParts := strings.Fields(blockParts[0])
if len(readParts) > 0 {
blockReadValue, _ = strconv.ParseFloat(strings.TrimRight(readParts[0], "kMGTB"), 64)
blockReadUnit = strings.TrimLeft(readParts[0], "0123456789.")
}
// Process write
writeParts := strings.Fields(blockParts[1])
if len(writeParts) > 0 {
blockWriteValue, _ = strconv.ParseFloat(strings.TrimRight(writeParts[0], "kMGTB"), 64)
blockWriteUnit = strings.TrimLeft(writeParts[0], "0123456789.")
}
}
return &database.ContainerMetric{
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
CPU: cpu,
Memory: database.MemoryMetric{
Percentage: memPerc,
Used: usedValue,
Total: totalValue,
UsedUnit: usedUnit,
TotalUnit: totalUnit,
},
Network: database.NetworkMetric{
Input: netInValue,
Output: netOutValue,
InputUnit: netInUnit,
OutputUnit: netOutUnit,
},
BlockIO: database.BlockIOMetric{
Read: blockReadValue,
Write: blockWriteValue,
ReadUnit: blockReadUnit,
WriteUnit: blockWriteUnit,
},
Container: container.ID,
ID: container.ID,
Name: container.Name,
}
}
func parseValue(value string) (float64, string) {
parts := strings.Fields(value)
if len(parts) < 1 {
return 0, "B"
}
v, _ := strconv.ParseFloat(parts[0], 64)
unit := strings.TrimLeft(value, "0123456789.")
return v, unit
}

View File

@@ -0,0 +1,48 @@
package containers
type Container struct {
BlockIO string `json:"BlockIO"`
CPUPerc string `json:"CPUPerc"`
ID string `json:"ID"`
MemPerc string `json:"MemPerc"`
MemUsage string `json:"MemUsage"`
Name string `json:"Name"`
NetIO string `json:"NetIO"`
}
type ContainerMetric struct {
Timestamp string `json:"timestamp"`
CPU float64 `json:"CPU"`
Memory MemoryMetric `json:"Memory"`
Network NetworkMetric `json:"Network"`
BlockIO BlockIOMetric `json:"BlockIO"`
Container string `json:"Container"`
ID string `json:"ID"`
Name string `json:"Name"`
}
type MemoryMetric struct {
Percentage float64 `json:"percentage"`
Used float64 `json:"used"`
Total float64 `json:"total"`
Unit string `json:"unit"`
}
type NetworkMetric struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
InputUnit string `json:"inputUnit"`
OutputUnit string `json:"outputUnit"`
}
type BlockIOMetric struct {
Read float64 `json:"read"`
Write float64 `json:"write"`
ReadUnit string `json:"readUnit"`
WriteUnit string `json:"writeUnit"`
}
type MonitoringConfig struct {
IncludeServices []string `json:"includeServices"`
ExcludeServices []string `json:"excludeServices"`
}

View File

@@ -0,0 +1,52 @@
package database
import (
"database/sql"
"log"
"time"
"github.com/robfig/cron/v3"
)
// CleanupMetrics deletes metrics older than the retention period
func CleanupMetrics(db *sql.DB, retentionDays int) error {
cutoffDate := time.Now().AddDate(0, 0, -retentionDays)
cutoffDateStr := cutoffDate.UTC().Format(time.RFC3339Nano)
containerQuery := `DELETE FROM container_metrics WHERE timestamp < ?`
_, err := db.Exec(containerQuery, cutoffDateStr)
if err != nil {
return err
}
serverQuery := `DELETE FROM server_metrics WHERE timestamp < ?`
_, err = db.Exec(serverQuery, cutoffDateStr)
if err != nil {
return err
}
log.Printf("Metrics deleted (older than %d days)", retentionDays)
log.Printf("Cutoff date for both tables: %s", cutoffDateStr)
return nil
}
// StartMetricsCleanup starts a cron job to periodically clean up metrics
func StartMetricsCleanup(db *sql.DB, retentionDays int, cronExpression string) (*cron.Cron, error) {
c := cron.New()
_, err := c.AddFunc(cronExpression, func() {
if err := CleanupMetrics(db, retentionDays); err != nil {
log.Printf("Error during metrics cleanup: %v", err)
}
})
if err != nil {
return nil, err
}
c.Start()
log.Printf("Started metrics cleanup job (retention: %d days, cron: %s)",
retentionDays, cronExpression)
return c, nil
}

View File

@@ -0,0 +1,160 @@
package database
import (
"encoding/json"
"fmt"
"strings"
)
func (db *DB) InitContainerMetricsTable() error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS container_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
container_id TEXT NOT NULL,
container_name TEXT NOT NULL,
metrics_json TEXT NOT NULL
)
`)
if err != nil {
return fmt.Errorf("error creating container_metrics table: %v", err)
}
// Crear índices para mejorar el rendimiento
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_metrics_timestamp ON container_metrics(timestamp)`)
if err != nil {
return fmt.Errorf("error creating timestamp index: %v", err)
}
_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_metrics_name ON container_metrics(container_name)`)
if err != nil {
return fmt.Errorf("error creating name index: %v", err)
}
return nil
}
func (db *DB) SaveContainerMetric(metric *ContainerMetric) error {
metricsJSON, err := json.Marshal(metric)
if err != nil {
return fmt.Errorf("error marshaling metrics: %v", err)
}
_, err = db.Exec(`
INSERT INTO container_metrics (timestamp, container_id, container_name, metrics_json)
VALUES (?, ?, ?, ?)
`, metric.Timestamp, metric.ID, metric.Name, string(metricsJSON))
return err
}
func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]ContainerMetric, error) {
name := strings.TrimPrefix(containerName, "/")
parts := strings.Split(name, "-")
if len(parts) > 1 {
containerName = strings.Join(parts[:len(parts)-1], "-")
}
query := `
WITH recent_metrics AS (
SELECT metrics_json
FROM container_metrics
WHERE container_name LIKE ? || '%'
ORDER BY timestamp DESC
LIMIT ?
)
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
`
rows, err := db.Query(query, containerName, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []ContainerMetric
for rows.Next() {
var metricsJSON string
err := rows.Scan(&metricsJSON)
if err != nil {
return nil, err
}
var metric ContainerMetric
if err := json.Unmarshal([]byte(metricsJSON), &metric); err != nil {
return nil, err
}
metrics = append(metrics, metric)
}
return metrics, nil
}
func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, error) {
name := strings.TrimPrefix(containerName, "/")
parts := strings.Split(name, "-")
if len(parts) > 1 {
containerName = strings.Join(parts[:len(parts)-1], "-")
}
query := `
WITH recent_metrics AS (
SELECT metrics_json
FROM container_metrics
WHERE container_name LIKE ? || '%'
ORDER BY timestamp DESC
)
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
`
rows, err := db.Query(query, containerName)
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []ContainerMetric
for rows.Next() {
var metricsJSON string
err := rows.Scan(&metricsJSON)
if err != nil {
return nil, err
}
var metric ContainerMetric
if err := json.Unmarshal([]byte(metricsJSON), &metric); err != nil {
return nil, err
}
metrics = append(metrics, metric)
}
return metrics, nil
}
type ContainerMetric struct {
Timestamp string `json:"timestamp"`
CPU float64 `json:"CPU"`
Memory MemoryMetric `json:"Memory"`
Network NetworkMetric `json:"Network"`
BlockIO BlockIOMetric `json:"BlockIO"`
Container string `json:"Container"`
ID string `json:"ID"`
Name string `json:"Name"`
}
type MemoryMetric struct {
Percentage float64 `json:"percentage"`
Used float64 `json:"used"`
Total float64 `json:"total"`
UsedUnit string `json:"usedUnit"`
TotalUnit string `json:"totalUnit"`
}
type NetworkMetric struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
InputUnit string `json:"inputUnit"`
OutputUnit string `json:"outputUnit"`
}
type BlockIOMetric struct {
Read float64 `json:"read"`
Write float64 `json:"write"`
ReadUnit string `json:"readUnit"`
WriteUnit string `json:"writeUnit"`
}

View File

@@ -0,0 +1,47 @@
package database
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
type DB struct {
*sql.DB
}
func InitDB() (*DB, error) {
db, err := sql.Open("sqlite3", "./monitoring.db")
if err != nil {
return nil, err
}
// Create metrics table if it doesn't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS server_metrics (
timestamp TEXT PRIMARY KEY,
cpu REAL,
cpu_model TEXT,
cpu_cores INTEGER,
cpu_physical_cores INTEGER,
cpu_speed REAL,
os TEXT,
distro TEXT,
kernel TEXT,
arch TEXT,
mem_used REAL,
mem_used_gb REAL,
mem_total REAL,
uptime INTEGER,
disk_used REAL,
total_disk REAL,
network_in REAL,
network_out REAL
)
`)
if err != nil {
return nil, err
}
return &DB{db}, nil
}

View File

@@ -0,0 +1,115 @@
package database
import (
"time"
_ "github.com/mattn/go-sqlite3"
)
type ServerMetric struct {
Timestamp string `json:"timestamp"`
CPU float64 `json:"cpu"`
CPUModel string `json:"cpuModel"`
CPUCores int32 `json:"cpuCores"`
CPUPhysicalCores int32 `json:"cpuPhysicalCores"`
CPUSpeed float64 `json:"cpuSpeed"`
OS string `json:"os"`
Distro string `json:"distro"`
Kernel string `json:"kernel"`
Arch string `json:"arch"`
MemUsed float64 `json:"memUsed"`
MemUsedGB float64 `json:"memUsedGB"`
MemTotal float64 `json:"memTotal"`
Uptime uint64 `json:"uptime"`
DiskUsed float64 `json:"diskUsed"`
TotalDisk float64 `json:"totalDisk"`
NetworkIn float64 `json:"networkIn"`
NetworkOut float64 `json:"networkOut"`
}
func (db *DB) SaveMetric(metric ServerMetric) error {
if metric.Timestamp == "" {
metric.Timestamp = time.Now().UTC().Format(time.RFC3339Nano)
}
_, err := db.Exec(`
INSERT INTO server_metrics (timestamp, cpu, cpu_model, cpu_cores, cpu_physical_cores, cpu_speed, os, distro, kernel, arch, mem_used, mem_used_gb, mem_total, uptime, disk_used, total_disk, network_in, network_out)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, metric.Timestamp, metric.CPU, metric.CPUModel, metric.CPUCores, metric.CPUPhysicalCores, metric.CPUSpeed, metric.OS, metric.Distro, metric.Kernel, metric.Arch, metric.MemUsed, metric.MemUsedGB, metric.MemTotal, metric.Uptime, metric.DiskUsed, metric.TotalDisk, metric.NetworkIn, metric.NetworkOut)
return err
}
func (db *DB) GetMetricsInRange(start, end time.Time) ([]ServerMetric, error) {
rows, err := db.Query(`
SELECT timestamp, cpu, cpu_model, cpu_cores, cpu_physical_cores, cpu_speed, os, distro, kernel, arch, mem_used, mem_used_gb, mem_total, uptime, disk_used, total_disk, network_in, network_out
FROM server_metrics
WHERE timestamp BETWEEN ? AND ?
ORDER BY timestamp ASC
`, start.UTC().Format(time.RFC3339Nano), end.UTC().Format(time.RFC3339Nano))
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []ServerMetric
for rows.Next() {
var m ServerMetric
err := rows.Scan(&m.Timestamp, &m.CPU, &m.CPUModel, &m.CPUCores, &m.CPUPhysicalCores, &m.CPUSpeed, &m.OS, &m.Distro, &m.Kernel, &m.Arch, &m.MemUsed, &m.MemUsedGB, &m.MemTotal, &m.Uptime, &m.DiskUsed, &m.TotalDisk, &m.NetworkIn, &m.NetworkOut)
if err != nil {
return nil, err
}
metrics = append(metrics, m)
}
return metrics, nil
}
func (db *DB) GetLastNMetrics(n int) ([]ServerMetric, error) {
rows, err := db.Query(`
WITH recent_metrics AS (
SELECT timestamp, cpu, cpu_model, cpu_cores, cpu_physical_cores, cpu_speed, os, distro, kernel, arch, mem_used, mem_used_gb, mem_total, uptime, disk_used, total_disk, network_in, network_out
FROM server_metrics
ORDER BY timestamp DESC
LIMIT ?
)
SELECT * FROM recent_metrics
ORDER BY timestamp ASC
`, n)
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []ServerMetric
for rows.Next() {
var m ServerMetric
err := rows.Scan(&m.Timestamp, &m.CPU, &m.CPUModel, &m.CPUCores, &m.CPUPhysicalCores, &m.CPUSpeed, &m.OS, &m.Distro, &m.Kernel, &m.Arch, &m.MemUsed, &m.MemUsedGB, &m.MemTotal, &m.Uptime, &m.DiskUsed, &m.TotalDisk, &m.NetworkIn, &m.NetworkOut)
if err != nil {
return nil, err
}
metrics = append(metrics, m)
}
return metrics, nil
}
func (db *DB) GetAllMetrics() ([]ServerMetric, error) {
rows, err := db.Query(`
SELECT timestamp, cpu, cpu_model, cpu_cores, cpu_physical_cores, cpu_speed, os, distro, kernel, arch, mem_used, mem_used_gb, mem_total, uptime, disk_used, total_disk, network_in, network_out
FROM server_metrics
ORDER BY timestamp ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []ServerMetric
for rows.Next() {
var m ServerMetric
err := rows.Scan(&m.Timestamp, &m.CPU, &m.CPUModel, &m.CPUCores, &m.CPUPhysicalCores, &m.CPUSpeed, &m.OS, &m.Distro, &m.Kernel, &m.Arch, &m.MemUsed, &m.MemUsedGB, &m.MemTotal, &m.Uptime, &m.DiskUsed, &m.TotalDisk, &m.NetworkIn, &m.NetworkOut)
if err != nil {
return nil, err
}
metrics = append(metrics, m)
}
return metrics, nil
}

34
apps/monitoring/go.mod Normal file
View File

@@ -0,0 +1,34 @@
module github.com/mauriciogm/dokploy/apps/monitoring
go 1.20
require (
github.com/gofiber/fiber/v2 v2.52.6
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.24
github.com/shirou/gopsutil/v3 v3.24.5
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sys v0.28.0 // indirect
)
replace github.com/mauriciogm/dokploy/apps/monitoring => ./

61
apps/monitoring/go.sum Normal file
View File

@@ -0,0 +1,61 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
github.com/zcalusic/sysinfo v1.1.3/go.mod h1:NX+qYnWGtJVPV0yWldff9uppNKU4h40hJIRPf/pGLv4=
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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

164
apps/monitoring/main.go Normal file
View File

@@ -0,0 +1,164 @@
package main
import (
"log"
"os"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/joho/godotenv"
"github.com/mauriciogm/dokploy/apps/monitoring/config"
"github.com/mauriciogm/dokploy/apps/monitoring/containers"
"github.com/mauriciogm/dokploy/apps/monitoring/database"
"github.com/mauriciogm/dokploy/apps/monitoring/middleware"
"github.com/mauriciogm/dokploy/apps/monitoring/monitoring"
)
func main() {
godotenv.Load()
// Get configuration
cfg := config.GetMetricsConfig()
token := cfg.Server.Token
METRICS_URL_CALLBACK := cfg.Server.UrlCallback
log.Printf("Environment variables:")
log.Printf("METRICS_CONFIG: %s", os.Getenv("METRICS_CONFIG"))
if token == "" || METRICS_URL_CALLBACK == "" {
log.Fatal("token and urlCallback are required in the configuration")
}
db, err := database.InitDB()
if err != nil {
log.Fatal(err)
}
// Iniciar el sistema de limpieza de métricas
cleanupCron, err := database.StartMetricsCleanup(db.DB, cfg.Server.RetentionDays, cfg.Server.CronJob)
if err != nil {
log.Fatalf("Error starting metrics cleanup system: %v", err)
}
defer cleanupCron.Stop()
app := fiber.New()
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
}))
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
})
})
app.Use(func(c *fiber.Ctx) error {
if c.Path() == "/health" {
return c.Next()
}
return middleware.AuthMiddleware()(c)
})
app.Get("/metrics", func(c *fiber.Ctx) error {
limit := c.Query("limit", "50")
var metrics []monitoring.SystemMetrics
if limit == "all" {
dbMetrics, err := db.GetAllMetrics()
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to fetch metrics",
})
}
for _, m := range dbMetrics {
metrics = append(metrics, monitoring.ConvertToSystemMetrics(m))
}
} else {
n, err := strconv.Atoi(limit)
if err != nil {
n = 50
}
dbMetrics, err := db.GetLastNMetrics(n)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to fetch metrics",
})
}
for _, m := range dbMetrics {
metrics = append(metrics, monitoring.ConvertToSystemMetrics(m))
}
}
return c.JSON(metrics)
})
containerMonitor, err := containers.NewContainerMonitor(db)
if err != nil {
log.Fatalf("Failed to create container monitor: %v", err)
}
if err := containerMonitor.Start(); err != nil {
log.Fatalf("Failed to start container monitor: %v", err)
}
defer containerMonitor.Stop()
app.Get("/metrics/containers", func(c *fiber.Ctx) error {
limit := c.Query("limit", "50")
appName := c.Query("appName", "")
if appName == "" {
return c.JSON([]database.ContainerMetric{})
}
var metrics []database.ContainerMetric
var err error
if limit == "all" {
metrics, err = db.GetAllMetricsContainer(appName)
} else {
limitNum, parseErr := strconv.Atoi(limit)
if parseErr != nil {
limitNum = 50
}
metrics, err = db.GetLastNContainerMetrics(appName, limitNum)
}
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Error getting container metrics: " + err.Error(),
})
}
return c.JSON(metrics)
})
go func() {
refreshRate := cfg.Server.RefreshRate
duration := time.Duration(refreshRate) * time.Second
log.Printf("Refreshing server metrics every %v", duration)
ticker := time.NewTicker(duration)
defer ticker.Stop()
for range ticker.C {
metrics := monitoring.GetServerMetrics()
if err := db.SaveMetric(metrics); err != nil {
log.Printf("Error saving metrics: %v", err)
}
if err := monitoring.CheckThresholds(metrics); err != nil {
log.Printf("Error checking thresholds: %v", err)
}
}
}()
port := cfg.Server.Port
if port == 0 {
port = 3001
}
log.Printf("Server starting on port %d", port)
log.Fatal(app.Listen(":" + strconv.Itoa(port)))
}

View File

@@ -0,0 +1,39 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/mauriciogm/dokploy/apps/monitoring/config"
)
func AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
expectedToken := config.GetMetricsConfig().Server.Token
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(401).JSON(fiber.Map{
"error": "Authorization header is required",
})
}
// Check if the header starts with "Bearer "
if !strings.HasPrefix(authHeader, "Bearer ") {
return c.Status(401).JSON(fiber.Map{
"error": "Invalid authorization format. Use 'Bearer TOKEN'",
})
}
// Extract the token
token := strings.TrimPrefix(authHeader, "Bearer ")
if token != expectedToken {
return c.Status(401).JSON(fiber.Map{
"error": "Invalid token",
})
}
return c.Next()
}
}

View File

@@ -0,0 +1,261 @@
package monitoring
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
"github.com/mauriciogm/dokploy/apps/monitoring/config"
"github.com/mauriciogm/dokploy/apps/monitoring/database"
)
type SystemMetrics struct {
CPU string `json:"cpu"`
CPUModel string `json:"cpuModel"`
CPUCores int32 `json:"cpuCores"`
CPUPhysicalCores int32 `json:"cpuPhysicalCores"`
CPUSpeed float64 `json:"cpuSpeed"`
OS string `json:"os"`
Distro string `json:"distro"`
Kernel string `json:"kernel"`
Arch string `json:"arch"`
MemUsed string `json:"memUsed"`
MemUsedGB string `json:"memUsedGB"`
MemTotal string `json:"memTotal"`
Uptime uint64 `json:"uptime"`
DiskUsed string `json:"diskUsed"`
TotalDisk string `json:"totalDisk"`
NetworkIn string `json:"networkIn"`
NetworkOut string `json:"networkOut"`
Timestamp string `json:"timestamp"`
}
type AlertPayload struct {
ServerType string `json:"ServerType"`
Type string `json:"Type"`
Value float64 `json:"Value"`
Threshold float64 `json:"Threshold"`
Message string `json:"Message"`
Timestamp string `json:"Timestamp"`
Token string `json:"Token"`
}
func getRealOS() string {
if content, err := os.ReadFile("/etc/os-release"); err == nil {
lines := strings.Split(string(content), "\n")
var id, name, version string
for _, line := range lines {
if strings.HasPrefix(line, "PRETTY_NAME=") {
return strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
} else if strings.HasPrefix(line, "NAME=") {
name = strings.Trim(strings.TrimPrefix(line, "NAME="), "\"")
} else if strings.HasPrefix(line, "VERSION=") {
version = strings.Trim(strings.TrimPrefix(line, "VERSION="), "\"")
} else if strings.HasPrefix(line, "ID=") {
id = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
}
}
if name != "" && version != "" {
return fmt.Sprintf("%s %s", name, version)
}
if name != "" {
return name
}
if id != "" {
return id
}
}
if content, err := os.ReadFile("/etc/system-release"); err == nil {
text := strings.ToLower(string(content))
switch {
case strings.Contains(text, "red hat"):
return "rhel"
case strings.Contains(text, "centos"):
return "centos"
case strings.Contains(text, "fedora"):
return "fedora"
}
}
cmd := exec.Command("uname", "-a")
if output, err := cmd.Output(); err == nil {
osInfo := strings.ToLower(string(output))
switch {
case strings.Contains(osInfo, "debian"):
return "debian"
case strings.Contains(osInfo, "ubuntu"):
return "ubuntu"
case strings.Contains(osInfo, "centos"):
return "centos"
case strings.Contains(osInfo, "fedora"):
return "fedora"
case strings.Contains(osInfo, "red hat"):
return "rhel"
case strings.Contains(osInfo, "arch"):
return "arch"
case strings.Contains(osInfo, "darwin"):
return "darwin"
}
}
return runtime.GOOS
}
func GetServerMetrics() database.ServerMetric {
v, _ := mem.VirtualMemory()
c, _ := cpu.Percent(0, false)
cpuInfo, _ := cpu.Info()
diskInfo, _ := disk.Usage("/")
netInfo, _ := net.IOCounters(false)
hostInfo, _ := host.Info()
distro := getRealOS()
cpuModel := ""
if len(cpuInfo) > 0 {
cpuModel = fmt.Sprintf("%s %s", cpuInfo[0].VendorID, cpuInfo[0].ModelName)
}
memTotalGB := float64(v.Total) / 1024 / 1024 / 1024
memUsedGB := float64(v.Used) / 1024 / 1024 / 1024
memUsedPercent := (memUsedGB / memTotalGB) * 100
var networkIn, networkOut float64
if len(netInfo) > 0 {
networkIn = float64(netInfo[0].BytesRecv) / 1024 / 1024
networkOut = float64(netInfo[0].BytesSent) / 1024 / 1024
}
return database.ServerMetric{
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
CPU: c[0],
CPUModel: cpuModel,
CPUCores: int32(runtime.NumCPU()),
CPUPhysicalCores: int32(len(cpuInfo)),
CPUSpeed: float64(cpuInfo[0].Mhz),
OS: getRealOS(),
Distro: distro,
Kernel: hostInfo.KernelVersion,
Arch: hostInfo.KernelArch,
MemUsed: memUsedPercent,
MemUsedGB: memUsedGB,
MemTotal: memTotalGB,
Uptime: hostInfo.Uptime,
DiskUsed: float64(diskInfo.UsedPercent),
TotalDisk: float64(diskInfo.Total) / 1024 / 1024 / 1024,
NetworkIn: networkIn,
NetworkOut: networkOut,
}
}
func ConvertToSystemMetrics(metric database.ServerMetric) SystemMetrics {
return SystemMetrics{
CPU: fmt.Sprintf("%.2f", metric.CPU),
CPUModel: metric.CPUModel,
CPUCores: metric.CPUCores,
CPUPhysicalCores: metric.CPUPhysicalCores,
CPUSpeed: metric.CPUSpeed,
OS: metric.OS,
Distro: metric.Distro,
Kernel: metric.Kernel,
Arch: metric.Arch,
MemUsed: fmt.Sprintf("%.2f", metric.MemUsed),
MemUsedGB: fmt.Sprintf("%.2f", metric.MemUsedGB),
MemTotal: fmt.Sprintf("%.2f", metric.MemTotal),
Uptime: metric.Uptime,
DiskUsed: fmt.Sprintf("%.2f", metric.DiskUsed),
TotalDisk: fmt.Sprintf("%.2f", metric.TotalDisk),
NetworkIn: fmt.Sprintf("%.2f", metric.NetworkIn),
NetworkOut: fmt.Sprintf("%.2f", metric.NetworkOut),
Timestamp: metric.Timestamp,
}
}
func CheckThresholds(metrics database.ServerMetric) error {
cfg := config.GetMetricsConfig()
cpuThreshold := float64(cfg.Server.Thresholds.CPU)
memThreshold := float64(cfg.Server.Thresholds.Memory)
callbackURL := cfg.Server.UrlCallback
metricsToken := cfg.Server.Token
// log.Printf("CPU threshold: %.2f%%", cpuThreshold)
// log.Printf("Current CPU usage: %.2f%%", metrics.CPU)
// log.Printf("Memory threshold: %.2f%%", memThreshold)
// log.Printf("Callback URL: %s", callbackURL)
// log.Printf("Metrics token: %s", metricsToken)
if cpuThreshold == 0 && memThreshold == 0 {
return nil
}
if cpuThreshold > 0 && metrics.CPU > cpuThreshold {
alert := AlertPayload{
ServerType: cfg.Server.ServerType,
Type: "CPU",
Value: metrics.CPU,
Threshold: cpuThreshold,
Message: fmt.Sprintf("CPU usage (%.2f%%) exceeded threshold (%.2f%%)", metrics.CPU, cpuThreshold),
Timestamp: metrics.Timestamp,
Token: metricsToken,
}
if err := sendAlert(callbackURL, alert); err != nil {
return fmt.Errorf("failed to send CPU alert: %v", err)
}
}
if memThreshold > 0 && metrics.MemUsed > memThreshold {
alert := AlertPayload{
ServerType: cfg.Server.ServerType,
Type: "Memory",
Value: metrics.MemUsed,
Threshold: memThreshold,
Message: fmt.Sprintf("Memory usage (%.2f%%) exceeded threshold (%.2f%%)", metrics.MemUsed, memThreshold),
Timestamp: metrics.Timestamp,
Token: metricsToken,
}
if err := sendAlert(callbackURL, alert); err != nil {
return fmt.Errorf("failed to send memory alert: %v", err)
}
}
return nil
}
func sendAlert(callbackURL string, payload AlertPayload) error {
if callbackURL == "" {
return fmt.Errorf("callback URL is not set")
}
wrappedPayload := map[string]interface{}{
"json": payload,
}
jsonData, err := json.Marshal(wrappedPayload)
if err != nil {
return fmt.Errorf("failed to marshal alert payload: %v", err)
}
resp, err := http.Post(callbackURL, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to send POST request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("received non-OK response status: %s, body: %s", resp.Status, string(bodyBytes))
}
return nil
}