mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
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:
1
apps/monitoring/.gitignore
vendored
Normal file
1
apps/monitoring/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
monitoring.db
|
||||
0
apps/monitoring/LICENSE.md
Normal file
0
apps/monitoring/LICENSE.md
Normal file
154
apps/monitoring/README.md
Normal file
154
apps/monitoring/README.md
Normal 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;
|
||||
}
|
||||
```
|
||||
57
apps/monitoring/config/metrics.go
Normal file
57
apps/monitoring/config/metrics.go
Normal 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
|
||||
}
|
||||
61
apps/monitoring/containers/config.go
Normal file
61
apps/monitoring/containers/config.go
Normal 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
|
||||
}
|
||||
270
apps/monitoring/containers/monitor.go
Normal file
270
apps/monitoring/containers/monitor.go
Normal 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
|
||||
}
|
||||
48
apps/monitoring/containers/types.go
Normal file
48
apps/monitoring/containers/types.go
Normal 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"`
|
||||
}
|
||||
52
apps/monitoring/database/cleanup.go
Normal file
52
apps/monitoring/database/cleanup.go
Normal 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
|
||||
}
|
||||
160
apps/monitoring/database/containers.go
Normal file
160
apps/monitoring/database/containers.go
Normal 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"`
|
||||
}
|
||||
47
apps/monitoring/database/db.go
Normal file
47
apps/monitoring/database/db.go
Normal 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
|
||||
}
|
||||
115
apps/monitoring/database/server.go
Normal file
115
apps/monitoring/database/server.go
Normal 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
34
apps/monitoring/go.mod
Normal 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
61
apps/monitoring/go.sum
Normal 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
164
apps/monitoring/main.go
Normal 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)))
|
||||
}
|
||||
39
apps/monitoring/middleware/auth.go
Normal file
39
apps/monitoring/middleware/auth.go
Normal 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()
|
||||
}
|
||||
}
|
||||
261
apps/monitoring/monitoring/monitor.go
Normal file
261
apps/monitoring/monitoring/monitor.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user