Skip to content

Commit 125d603

Browse files
committed
refactor: move VM metrics to dedicated lib/vm_metrics package
This addresses PR review feedback: - Create new lib/vm_metrics package for better separation of concerns - Export GenerateTAPName from lib/network (fixes TAP name mismatch bug) - Simplify API handler to use vm_metrics.Manager - Add comprehensive README.md for the feature - Remove utilization code from lib/resources package The TAP name bug was causing network stats to always be zero because the API handler used different truncation logic (10 chars, no lowercase) than the canonical implementation (8 chars, lowercase).
1 parent cafebe8 commit 125d603

23 files changed

+939
-689
lines changed

cmd/api/api/api.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ import (
1010
"github.com/kernel/hypeman/lib/network"
1111
"github.com/kernel/hypeman/lib/oapi"
1212
"github.com/kernel/hypeman/lib/resources"
13+
"github.com/kernel/hypeman/lib/vm_metrics"
1314
"github.com/kernel/hypeman/lib/volumes"
1415
)
1516

1617
// ApiService implements the oapi.StrictServerInterface
1718
type ApiService struct {
18-
Config *config.Config
19-
ImageManager images.Manager
20-
InstanceManager instances.Manager
21-
VolumeManager volumes.Manager
22-
NetworkManager network.Manager
23-
DeviceManager devices.Manager
24-
IngressManager ingress.Manager
25-
BuildManager builds.Manager
26-
ResourceManager *resources.Manager
19+
Config *config.Config
20+
ImageManager images.Manager
21+
InstanceManager instances.Manager
22+
VolumeManager volumes.Manager
23+
NetworkManager network.Manager
24+
DeviceManager devices.Manager
25+
IngressManager ingress.Manager
26+
BuildManager builds.Manager
27+
ResourceManager *resources.Manager
28+
VMMetricsManager *vm_metrics.Manager
2729
}
2830

2931
var _ oapi.StrictServerInterface = (*ApiService)(nil)
@@ -39,16 +41,18 @@ func New(
3941
ingressManager ingress.Manager,
4042
buildManager builds.Manager,
4143
resourceManager *resources.Manager,
44+
vmMetricsManager *vm_metrics.Manager,
4245
) *ApiService {
4346
return &ApiService{
44-
Config: config,
45-
ImageManager: imageManager,
46-
InstanceManager: instanceManager,
47-
VolumeManager: volumeManager,
48-
NetworkManager: networkManager,
49-
DeviceManager: deviceManager,
50-
IngressManager: ingressManager,
51-
BuildManager: buildManager,
52-
ResourceManager: resourceManager,
47+
Config: config,
48+
ImageManager: imageManager,
49+
InstanceManager: instanceManager,
50+
VolumeManager: volumeManager,
51+
NetworkManager: networkManager,
52+
DeviceManager: deviceManager,
53+
IngressManager: ingressManager,
54+
BuildManager: buildManager,
55+
ResourceManager: resourceManager,
56+
VMMetricsManager: vmMetricsManager,
5357
}
5458
}

cmd/api/api/instances.go

Lines changed: 29 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/kernel/hypeman/lib/network"
1818
"github.com/kernel/hypeman/lib/oapi"
1919
"github.com/kernel/hypeman/lib/resources"
20+
"github.com/kernel/hypeman/lib/vm_metrics"
2021
"github.com/samber/lo"
2122
)
2223

@@ -278,7 +279,6 @@ func (s *ApiService) GetInstance(ctx context.Context, request oapi.GetInstanceRe
278279
// The id parameter can be an instance ID, name, or ID prefix
279280
// Note: Resolution is handled by ResolveResource middleware
280281
func (s *ApiService) GetInstanceStats(ctx context.Context, request oapi.GetInstanceStatsRequestObject) (oapi.GetInstanceStatsResponseObject, error) {
281-
log := logger.FromContext(ctx)
282282
inst := mw.GetResolvedInstance[instances.Instance](ctx)
283283
if inst == nil {
284284
return oapi.GetInstanceStats500JSONResponse{
@@ -287,68 +287,38 @@ func (s *ApiService) GetInstanceStats(ctx context.Context, request oapi.GetInsta
287287
}, nil
288288
}
289289

290-
// Build stats response
291-
stats := oapi.InstanceStats{
292-
InstanceId: inst.Id,
293-
InstanceName: inst.Name,
294-
AllocatedVcpus: inst.Vcpus,
295-
AllocatedMemoryBytes: inst.Size + inst.HotplugSize,
296-
}
297-
298-
// Read /proc stats if we have a hypervisor PID
299-
if inst.HypervisorPID != nil {
300-
pid := *inst.HypervisorPID
301-
302-
// Read CPU from /proc/<pid>/stat
303-
cpuUsec, err := resources.ReadProcStat(pid)
304-
if err != nil {
305-
log.DebugContext(ctx, "failed to read proc stat", "pid", pid, "error", err)
306-
} else {
307-
stats.CpuSeconds = float64(cpuUsec) / 1_000_000.0
308-
}
290+
// Build instance info for metrics collection
291+
info := vm_metrics.BuildInstanceInfo(
292+
inst.Id,
293+
inst.Name,
294+
inst.HypervisorPID,
295+
inst.NetworkEnabled,
296+
inst.Vcpus,
297+
inst.Size+inst.HotplugSize,
298+
)
309299

310-
// Read memory from /proc/<pid>/statm
311-
rssBytes, vmsBytes, err := resources.ReadProcStatm(pid)
312-
if err != nil {
313-
log.DebugContext(ctx, "failed to read proc statm", "pid", pid, "error", err)
314-
} else {
315-
stats.MemoryRssBytes = int64(rssBytes)
316-
stats.MemoryVmsBytes = int64(vmsBytes)
317-
318-
// Compute utilization ratio
319-
if stats.AllocatedMemoryBytes > 0 {
320-
ratio := float64(rssBytes) / float64(stats.AllocatedMemoryBytes)
321-
stats.MemoryUtilizationRatio = &ratio
322-
}
323-
}
324-
}
300+
// Collect stats using vm_metrics manager
301+
vmStats := s.VMMetricsManager.GetInstanceStats(ctx, info)
325302

326-
// Read TAP stats if network is enabled
327-
if inst.NetworkEnabled {
328-
tapName := generateTAPName(inst.Id)
329-
rxBytes, txBytes, err := resources.ReadTAPStats(tapName)
330-
if err != nil {
331-
log.DebugContext(ctx, "failed to read TAP stats", "tap", tapName, "error", err)
332-
} else {
333-
stats.NetworkRxBytes = int64(rxBytes)
334-
stats.NetworkTxBytes = int64(txBytes)
335-
}
336-
}
337-
338-
return oapi.GetInstanceStats200JSONResponse(stats), nil
303+
// Map domain type to API type
304+
return oapi.GetInstanceStats200JSONResponse(vmStatsToOAPI(vmStats)), nil
339305
}
340306

341-
// generateTAPName generates TAP device name from instance ID
342-
func generateTAPName(instanceID string) string {
343-
// TAP name format: "hype-" + first 10 chars of instance ID
344-
// Max TAP name length is 15 chars (IFNAMSIZ - 1)
345-
prefix := "hype-"
346-
maxIDLen := 15 - len(prefix)
347-
idPart := instanceID
348-
if len(idPart) > maxIDLen {
349-
idPart = idPart[:maxIDLen]
350-
}
351-
return prefix + idPart
307+
// vmStatsToOAPI converts vm_metrics.VMStats to oapi.InstanceStats
308+
func vmStatsToOAPI(s *vm_metrics.VMStats) oapi.InstanceStats {
309+
stats := oapi.InstanceStats{
310+
InstanceId: s.InstanceID,
311+
InstanceName: s.InstanceName,
312+
CpuSeconds: s.CPUSeconds(),
313+
MemoryRssBytes: int64(s.MemoryRSSBytes),
314+
MemoryVmsBytes: int64(s.MemoryVMSBytes),
315+
NetworkRxBytes: int64(s.NetRxBytes),
316+
NetworkTxBytes: int64(s.NetTxBytes),
317+
AllocatedVcpus: s.AllocatedVcpus,
318+
AllocatedMemoryBytes: s.AllocatedMemoryBytes,
319+
MemoryUtilizationRatio: s.MemoryUtilizationRatio(),
320+
}
321+
return stats
352322
}
353323

354324
// DeleteInstance stops and deletes an instance

cmd/api/wire.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,27 @@ import (
1919
"github.com/kernel/hypeman/lib/registry"
2020
"github.com/kernel/hypeman/lib/resources"
2121
"github.com/kernel/hypeman/lib/system"
22+
"github.com/kernel/hypeman/lib/vm_metrics"
2223
"github.com/kernel/hypeman/lib/volumes"
2324
)
2425

2526
// application struct to hold initialized components
2627
type application struct {
27-
Ctx context.Context
28-
Logger *slog.Logger
29-
Config *config.Config
30-
ImageManager images.Manager
31-
SystemManager system.Manager
32-
NetworkManager network.Manager
33-
DeviceManager devices.Manager
34-
InstanceManager instances.Manager
35-
VolumeManager volumes.Manager
36-
IngressManager ingress.Manager
37-
BuildManager builds.Manager
38-
ResourceManager *resources.Manager
39-
Registry *registry.Registry
40-
ApiService *api.ApiService
28+
Ctx context.Context
29+
Logger *slog.Logger
30+
Config *config.Config
31+
ImageManager images.Manager
32+
SystemManager system.Manager
33+
NetworkManager network.Manager
34+
DeviceManager devices.Manager
35+
InstanceManager instances.Manager
36+
VolumeManager volumes.Manager
37+
IngressManager ingress.Manager
38+
BuildManager builds.Manager
39+
ResourceManager *resources.Manager
40+
VMMetricsManager *vm_metrics.Manager
41+
Registry *registry.Registry
42+
ApiService *api.ApiService
4143
}
4244

4345
// initializeApp is the injector function
@@ -56,6 +58,7 @@ func initializeApp() (*application, func(), error) {
5658
providers.ProvideIngressManager,
5759
providers.ProvideBuildManager,
5860
providers.ProvideResourceManager,
61+
providers.ProvideVMMetricsManager,
5962
providers.ProvideRegistry,
6063
api.New,
6164
wire.Struct(new(application), "*"),

cmd/api/wire_gen.go

Lines changed: 36 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/instances/manager.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package instances
33
import (
44
"context"
55
"fmt"
6-
"strings"
76
"sync"
87

98
"github.com/kernel/hypeman/lib/devices"
@@ -359,7 +358,7 @@ func (m *manager) ListRunningInstancesInfo(ctx context.Context) ([]resources.Ins
359358

360359
// Derive TAP device name if networking is enabled
361360
if inst.NetworkEnabled {
362-
info.TAPDevice = generateTAPName(inst.Id)
361+
info.TAPDevice = network.GenerateTAPName(inst.Id)
363362
}
364363

365364
infos = append(infos, info)
@@ -368,14 +367,3 @@ func (m *manager) ListRunningInstancesInfo(ctx context.Context) ([]resources.Ins
368367
return infos, nil
369368
}
370369

371-
// generateTAPName generates TAP device name from instance ID.
372-
// This matches the logic in network/allocate.go.
373-
func generateTAPName(instanceID string) string {
374-
// Use first 8 chars of instance ID
375-
// hype-{8chars} fits within 15-char Linux interface name limit
376-
shortID := instanceID
377-
if len(shortID) > 8 {
378-
shortID = shortID[:8]
379-
}
380-
return "hype-" + strings.ToLower(shortID)
381-
}

lib/instances/manager_test.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,38 +1002,3 @@ func (r *testInstanceResolver) ResolveInstance(ctx context.Context, nameOrID str
10021002
return nameOrID, nameOrID, nil
10031003
}
10041004

1005-
func TestGenerateTAPName(t *testing.T) {
1006-
tests := []struct {
1007-
name string
1008-
instanceID string
1009-
expected string
1010-
}{
1011-
{
1012-
name: "standard ID",
1013-
instanceID: "01HQVX7ABC123DEF456",
1014-
expected: "hype-01hqvx7a",
1015-
},
1016-
{
1017-
name: "short ID",
1018-
instanceID: "ABC123",
1019-
expected: "hype-abc123",
1020-
},
1021-
{
1022-
name: "exact 8 chars",
1023-
instanceID: "ABCDEFGH",
1024-
expected: "hype-abcdefgh",
1025-
},
1026-
{
1027-
name: "mixed case",
1028-
instanceID: "AbCdEfGhIjKl",
1029-
expected: "hype-abcdefgh",
1030-
},
1031-
}
1032-
1033-
for _, tt := range tests {
1034-
t.Run(tt.name, func(t *testing.T) {
1035-
result := generateTAPName(tt.instanceID)
1036-
assert.Equal(t, tt.expected, result)
1037-
})
1038-
}
1039-
}

0 commit comments

Comments
 (0)