mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
e4aca0db13
Expose a dedicated container metrics endpoint and surface current CPU, memory, and root filesystem usage in the bot container view. This gives operators a quick health snapshot while degrading cleanly on unsupported backends.
143 lines
3.0 KiB
Go
143 lines
3.0 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/v2/core/mount"
|
|
"github.com/containerd/errdefs"
|
|
|
|
ctr "github.com/memohai/memoh/internal/containerd"
|
|
)
|
|
|
|
const unsupportedReasonBackend = "backend_not_supported"
|
|
|
|
func (m *Manager) GetContainerMetrics(ctx context.Context, botID string) (*ContainerMetricsResult, error) {
|
|
result := &ContainerMetricsResult{
|
|
Supported: true,
|
|
Status: ContainerMetricsStatus{
|
|
Exists: false,
|
|
},
|
|
}
|
|
|
|
containerID, err := m.ContainerID(ctx, botID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrContainerNotFound) {
|
|
return result, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
info, err := m.service.GetContainer(ctx, containerID)
|
|
if err != nil {
|
|
if errdefs.IsNotFound(err) {
|
|
return result, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
result.Status.Exists = true
|
|
|
|
taskInfo, err := m.service.GetTaskInfo(ctx, containerID)
|
|
if err == nil {
|
|
result.Status.TaskRunning = taskInfo.Status == ctr.TaskStatusRunning
|
|
} else if !errdefs.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
|
|
runtimeMetrics, err := m.service.GetContainerMetrics(ctx, containerID)
|
|
switch {
|
|
case err == nil:
|
|
result.CPU = runtimeMetrics.CPU
|
|
result.Memory = runtimeMetrics.Memory
|
|
result.SampledAt = runtimeMetrics.SampledAt
|
|
case errors.Is(err, ctr.ErrNotSupported):
|
|
result.Supported = false
|
|
result.UnsupportedReason = unsupportedReasonBackend
|
|
case errdefs.IsNotFound(err):
|
|
// Task is not running, so CPU and memory metrics are unavailable.
|
|
default:
|
|
return nil, err
|
|
}
|
|
|
|
if result.Supported {
|
|
storage, err := m.collectStorageMetrics(ctx, info)
|
|
if err != nil {
|
|
if errors.Is(err, ctr.ErrNotSupported) {
|
|
return result, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
result.Storage = storage
|
|
if result.SampledAt.IsZero() {
|
|
result.SampledAt = time.Now()
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (m *Manager) collectStorageMetrics(ctx context.Context, info ctr.ContainerInfo) (*ContainerStorageMetrics, error) {
|
|
mounts, err := m.snapshotMounts(ctx, info)
|
|
if err != nil {
|
|
if errors.Is(err, errMountNotSupported) {
|
|
return nil, ctr.ErrNotSupported
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var usedBytes uint64
|
|
if err := mount.WithReadonlyTempMount(ctx, mounts, func(root string) error {
|
|
if _, statErr := os.Stat(root); statErr != nil {
|
|
if os.IsNotExist(statErr) {
|
|
return nil
|
|
}
|
|
return statErr
|
|
}
|
|
|
|
size, sizeErr := dirSize(root)
|
|
if sizeErr != nil {
|
|
return sizeErr
|
|
}
|
|
usedBytes = size
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ContainerStorageMetrics{
|
|
Path: "/",
|
|
UsedBytes: usedBytes,
|
|
}, nil
|
|
}
|
|
|
|
func dirSize(root string) (uint64, error) {
|
|
var size uint64
|
|
err := filepath.WalkDir(root, func(_ string, entry fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileSize := info.Size()
|
|
if fileSize > 0 {
|
|
size += uint64(fileSize) //nolint:gosec // file sizes are checked to be positive before conversion
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return size, nil
|
|
}
|