dev-pod-api-build/internal/k8s/metrics.go
2026-04-16 04:16:36 +00:00

135 lines
3.4 KiB
Go

package k8s
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// UsageRecorder records resource usage samples into the store.
type UsageRecorder interface {
RecordResourceSample(ctx context.Context, userID, podName string, cpuMillicores, memBytes float64) error
}
// ResourceSampler periodically samples pod resource usage and records it.
type ResourceSampler struct {
client *Client
recorder UsageRecorder
logger *slog.Logger
interval time.Duration
}
// NewResourceSampler creates a new resource sampler with a 60-second interval.
func NewResourceSampler(client *Client, recorder UsageRecorder, logger *slog.Logger) *ResourceSampler {
return &ResourceSampler{
client: client,
recorder: recorder,
logger: logger,
interval: 60 * time.Second,
}
}
// Start runs the sampling loop until the context is cancelled.
func (s *ResourceSampler) Start(ctx context.Context) {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
s.logger.Info("resource sampler started", "interval", s.interval)
for {
select {
case <-ctx.Done():
s.logger.Info("resource sampler stopped")
return
case <-ticker.C:
s.sample(ctx)
}
}
}
// podMetricsList mirrors the k8s metrics API response.
type podMetricsList struct {
Items []podMetrics `json:"items"`
}
type podMetrics struct {
Metadata podMetricsMetadata `json:"metadata"`
Containers []containerMetrics `json:"containers"`
}
type podMetricsMetadata struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Labels map[string]string `json:"labels"`
}
type containerMetrics struct {
Name string `json:"name"`
Usage map[string]string `json:"usage"`
}
func (s *ResourceSampler) sample(ctx context.Context) {
// List dev-pod namespaces
nsList, err := s.client.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{
LabelSelector: "managed-by=dev-pod-api",
})
if err != nil {
s.logger.Error("failed to list namespaces for sampling", "error", err)
return
}
for _, ns := range nsList.Items {
user := ns.Labels["user"]
if user == "" {
continue
}
// Query metrics API for this namespace
data, err := s.client.Clientset.Discovery().RESTClient().Get().
AbsPath(fmt.Sprintf("/apis/metrics.k8s.io/v1beta1/namespaces/%s/pods", ns.Name)).
DoRaw(ctx)
if err != nil {
s.logger.Warn("failed to get pod metrics", "namespace", ns.Name, "error", err)
continue
}
var metrics podMetricsList
if err := json.Unmarshal(data, &metrics); err != nil {
s.logger.Warn("failed to parse pod metrics", "namespace", ns.Name, "error", err)
continue
}
for _, pm := range metrics.Items {
if !strings.HasPrefix(pm.Metadata.Name, "dev-pod-") {
continue
}
podName := strings.TrimPrefix(pm.Metadata.Name, "dev-pod-")
var totalCPU, totalMem float64
for _, cm := range pm.Containers {
if cpuStr, ok := cm.Usage["cpu"]; ok {
q, err := resource.ParseQuantity(cpuStr)
if err == nil {
totalCPU += float64(q.MilliValue())
}
}
if memStr, ok := cm.Usage["memory"]; ok {
q, err := resource.ParseQuantity(memStr)
if err == nil {
totalMem += float64(q.Value())
}
}
}
if err := s.recorder.RecordResourceSample(ctx, user, podName, totalCPU, totalMem); err != nil {
s.logger.Warn("failed to record sample", "user", user, "pod", podName, "error", err)
}
}
}
}