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}
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
}