mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
Overhaul: real metrics, Git integration UI, open-source hygiene, root cleanup
Backend: - GET /services/:id/metrics with real Docker container stats - DELETE /git/providers/:id and GET repo branches endpoint Frontend: - Service detail shows real telemetry or honest empty states - Live status distinguishes idle/polling/reconnecting - Git providers section in Settings; repo+branch pickers in Add Service - Real logo, compact Projects header, canvas fitView fix - index.css deduplicated (1725 -> 1384 lines) - Fixed smoke tests, lint errors, ToastProvider/context unification Repo hygiene: - Added LICENSE (MIT), CONTRIBUTING.md, SECURITY.md; rewrote README - Untracked .env.prod and data/letsencrypt secrets; fixed .gitignore - Removed legacy root src/internal/cmd and stale configs/docs/artifacts - Moved guides and design references under docs/ Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
355a97bab4
commit
9f3b196032
499 files changed
+4471
-94235
No files matched your search
@@ -0,0 +1,207 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"containr/internal/database"
|
||||
"containr/internal/docker"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ServiceMetricsResponse is the aggregated live telemetry for a service,
|
||||
// sourced from Docker container stats for containers named
|
||||
// containr-<serviceID>-<deploymentID>-<index>.
|
||||
type ServiceMetricsResponse struct {
|
||||
ServiceID string `json:"service_id"`
|
||||
Status string `json:"status"` // ok, no_containers, docker_unavailable
|
||||
Instances []ServiceInstanceMetrics `json:"instances"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsageBytes uint64 `json:"memory_usage_bytes"`
|
||||
MemoryLimitBytes uint64 `json:"memory_limit_bytes"`
|
||||
NetworkRxBytes uint64 `json:"network_rx_bytes"`
|
||||
NetworkTxBytes uint64 `json:"network_tx_bytes"`
|
||||
CollectedAt time.Time `json:"collected_at"`
|
||||
}
|
||||
|
||||
// ServiceInstanceMetrics holds live stats for one running container.
|
||||
type ServiceInstanceMetrics struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsageBytes uint64 `json:"memory_usage_bytes"`
|
||||
MemoryLimitBytes uint64 `json:"memory_limit_bytes"`
|
||||
NetworkRxBytes uint64 `json:"network_rx_bytes"`
|
||||
NetworkTxBytes uint64 `json:"network_tx_bytes"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
// handleGetServiceMetrics returns real resource usage for the service's
|
||||
// Docker containers. Empty result when the service has never deployed.
|
||||
func handleGetServiceMetrics(c *gin.Context) {
|
||||
dbValue, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
db := dbValue.(*database.DB)
|
||||
|
||||
serviceID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerID string
|
||||
var status string
|
||||
err = db.QueryRow(
|
||||
`SELECT p.owner_id, COALESCE(s.status, '')
|
||||
FROM services s JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(&ownerID, &status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
if ownerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
response := ServiceMetricsResponse{
|
||||
ServiceID: serviceID.String(),
|
||||
Status: "ok",
|
||||
Instances: []ServiceInstanceMetrics{},
|
||||
CollectedAt: time.Now(),
|
||||
}
|
||||
|
||||
dockerValue, exists := c.Get("docker_client")
|
||||
if !exists || dockerValue == nil {
|
||||
response.Status = "docker_unavailable"
|
||||
c.JSON(http.StatusOK, gin.H{"metrics": response})
|
||||
return
|
||||
}
|
||||
dockerClient := dockerValue.(*docker.Client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
containers, err := dockerClient.ListContainers(ctx, false)
|
||||
if err != nil {
|
||||
response.Status = "docker_unavailable"
|
||||
c.JSON(http.StatusOK, gin.H{"metrics": response})
|
||||
return
|
||||
}
|
||||
|
||||
namePrefix := fmt.Sprintf("containr-%s-", serviceID.String())
|
||||
matched := 0
|
||||
for _, ctr := range containers {
|
||||
name := ""
|
||||
for _, n := range ctr.Names {
|
||||
name = strings.TrimPrefix(n, "/")
|
||||
if strings.HasPrefix(name, namePrefix) {
|
||||
break
|
||||
}
|
||||
name = ""
|
||||
}
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
matched++
|
||||
|
||||
instance := ServiceInstanceMetrics{
|
||||
ContainerID: ctr.ID,
|
||||
Name: name,
|
||||
State: ctr.State,
|
||||
}
|
||||
|
||||
if info, err := dockerClient.GetContainer(ctx, ctr.ID); err == nil {
|
||||
if info.State != nil && info.State.StartedAt != "" {
|
||||
if parsed, perr := time.Parse(time.RFC3339Nano, info.State.StartedAt); perr == nil {
|
||||
instance.StartedAt = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctr.State == "running" {
|
||||
if stats, err := readContainerStats(ctx, dockerClient, ctr.ID); err == nil {
|
||||
instance.CPUPercent = stats.cpuPercent
|
||||
instance.MemoryUsageBytes = stats.memoryUsage
|
||||
instance.MemoryLimitBytes = stats.memoryLimit
|
||||
instance.NetworkRxBytes = stats.networkRx
|
||||
instance.NetworkTxBytes = stats.networkTx
|
||||
}
|
||||
}
|
||||
|
||||
response.Instances = append(response.Instances, instance)
|
||||
response.CPUPercent += instance.CPUPercent
|
||||
response.MemoryUsageBytes += instance.MemoryUsageBytes
|
||||
response.MemoryLimitBytes += instance.MemoryLimitBytes
|
||||
response.NetworkRxBytes += instance.NetworkRxBytes
|
||||
response.NetworkTxBytes += instance.NetworkTxBytes
|
||||
}
|
||||
|
||||
if matched == 0 {
|
||||
response.Status = "no_containers"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"metrics": response})
|
||||
}
|
||||
|
||||
type containerStatsSnapshot struct {
|
||||
cpuPercent float64
|
||||
memoryUsage uint64
|
||||
memoryLimit uint64
|
||||
networkRx uint64
|
||||
networkTx uint64
|
||||
}
|
||||
|
||||
func readContainerStats(ctx context.Context, client *docker.Client, containerID string) (*containerStatsSnapshot, error) {
|
||||
reader, err := client.GetContainerStats(ctx, containerID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Body.Close()
|
||||
|
||||
var stats containertypes.StatsResponse
|
||||
if err := json.NewDecoder(reader.Body).Decode(&stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshot := &containerStatsSnapshot{
|
||||
memoryUsage: stats.MemoryStats.Usage,
|
||||
memoryLimit: stats.MemoryStats.Limit,
|
||||
}
|
||||
|
||||
cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage)
|
||||
systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage)
|
||||
onlineCPUs := float64(stats.CPUStats.OnlineCPUs)
|
||||
if onlineCPUs == 0 {
|
||||
onlineCPUs = float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
|
||||
}
|
||||
if cpuDelta > 0 && systemDelta > 0 && onlineCPUs > 0 {
|
||||
snapshot.cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100
|
||||
}
|
||||
|
||||
for _, net := range stats.Networks {
|
||||
snapshot.networkRx += net.RxBytes
|
||||
snapshot.networkTx += net.TxBytes
|
||||
}
|
||||
|
||||
return snapshot, nil
|
||||
}
|
||||
Reference in new issue
Block a user