feat: adds network usage for each container (#4340)

This commit is contained in:
Amir Raminfar
2026-01-12 14:11:19 -08:00
committed by GitHub
parent 21367bcc36
commit d44ab349b9
13 changed files with 92 additions and 32 deletions
+2
View File
@@ -122,6 +122,8 @@ declare module 'vue' {
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
PhArrowDown: typeof import('~icons/ph/arrow-down')['default']
PhArrowUp: typeof import('~icons/ph/arrow-up')['default']
Popup: typeof import('./components/Popup.vue')['default']
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
@@ -1,5 +1,11 @@
<template>
<div class="flex gap-1 md:gap-4">
<div class="grid min-w-15 grid-cols-[auto_1fr] items-center gap-0.5 text-xs leading-none max-md:hidden">
<PhArrowUp class="text-primary" />
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
<PhArrowDown class="text-secondary" />
<span class="tabular-nums">{{ formatBytes(networkRate.rx, { short: true, decimals: 1 }) }}/s</span>
</div>
<StatMonitor
:data="cpuData"
:icon="PhCpu"
@@ -35,9 +41,10 @@ const { containers } = defineProps<{
containers: Container[];
}>();
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 });
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
const { hosts } = useHosts();
const networkRate = ref({ rx: 0, tx: 0 });
const roundCPU = (num: number) => (Number.isInteger(num) ? num.toFixed(0) : num.toFixed(1));
@@ -65,12 +72,15 @@ watch(
cpu: acc.cpu + item.cpu / cores,
memory: acc.memory + item.memory,
memoryUsage: acc.memoryUsage + item.memoryUsage,
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0 },
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
);
initial.push(stat);
}
totalStat.value = initial[0];
reset({ initial: initial.reverse() });
},
{ immediate: true },
@@ -92,6 +102,7 @@ const limits = computed(() => {
});
useIntervalFn(() => {
const previousStat = totalStat.value;
totalStat.value = containers.reduce(
(acc, container) => {
const cores = toContainerCores(container);
@@ -99,10 +110,17 @@ useIntervalFn(() => {
cpu: acc.cpu + container.stat.cpu / cores,
memory: acc.memory + container.stat.memory,
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0 },
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
);
networkRate.value = {
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
tx: Math.max(0, totalStat.value.networkTxTotal - previousStat.networkTxTotal),
};
}, 1000);
const cpuData = computed(() =>
+1 -1
View File
@@ -10,7 +10,7 @@
</div>
<div class="bg-base-200 flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
<component :is="icon" class="text-sm" />
<div class="font-bold select-none">
<div class="font-bold tabular-nums select-none">
{{ displayValue }}
<span v-if="limit !== -1 && !mouseOver" class="max-md:hidden"> / {{ limit }} </span>
</div>
+2 -2
View File
@@ -4,8 +4,8 @@
<component :is="icon" class="text-lg" />
<span>{{ label }}</span>
</div>
<div class="mb-1.5 text-lg font-semibold">{{ formattedValue }}</div>
<div class="text-base-content/60 mb-1 text-xs max-md:hidden">
<div class="mb-1.5 text-lg font-semibold tabular-nums">{{ formattedValue }}</div>
<div class="text-base-content/60 mb-1 text-xs tabular-nums max-md:hidden">
avg {{ formatValue(average) }} pk {{ formatValue(peak) }}
</div>
<BarChart class="h-8" :chartData="percentData" :barClass="barClass" />
+3 -1
View File
@@ -51,7 +51,9 @@ export class Container {
public readonly group?: string,
public health?: ContainerHealth,
) {
this._stat = ref(stats.at(-1) || ({ cpu: 0, memory: 0, memoryUsage: 0 } as Stat));
this._stat = ref(
stats.at(-1) || ({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 } as Stat),
);
const { history } = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
this._statsHistory = history;
const { movingAverage } = useExponentialMovingAverage(this._stat, 0.2);
+2
View File
@@ -3,6 +3,8 @@ export interface ContainerStat {
readonly cpu: number;
readonly memory: number;
readonly memoryUsage: number;
readonly networkRxTotal: number;
readonly networkTxTotal: number;
}
export type ContainerJson = {
+27 -9
View File
@@ -261,13 +261,15 @@ func (x *Container) GetFullyLoaded() bool {
}
type ContainerStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
NetworkRxTotal uint64 `protobuf:"varint,5,opt,name=networkRxTotal,proto3" json:"networkRxTotal,omitempty"`
NetworkTxTotal uint64 `protobuf:"varint,6,opt,name=networkTxTotal,proto3" json:"networkTxTotal,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ContainerStat) Reset() {
@@ -328,6 +330,20 @@ func (x *ContainerStat) GetMemoryPercent() float64 {
return 0
}
func (x *ContainerStat) GetNetworkRxTotal() uint64 {
if x != nil {
return x.NetworkRxTotal
}
return 0
}
func (x *ContainerStat) GetNetworkTxTotal() uint64 {
if x != nil {
return x.NetworkTxTotal
}
return 0
}
type LogFragment struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
@@ -832,14 +848,16 @@ const file_types_proto_rawDesc = "" +
"\vfullyLoaded\x18\x13 \x01(\bR\vfullyLoaded\x1a9\n" +
"\vLabelsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x87\x01\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd7\x01\n" +
"\rContainerStat\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1e\n" +
"\n" +
"cpuPercent\x18\x02 \x01(\x01R\n" +
"cpuPercent\x12 \n" +
"\vmemoryUsage\x18\x03 \x01(\x01R\vmemoryUsage\x12$\n" +
"\rmemoryPercent\x18\x04 \x01(\x01R\rmemoryPercent\"'\n" +
"\rmemoryPercent\x18\x04 \x01(\x01R\rmemoryPercent\x12&\n" +
"\x0enetworkRxTotal\x18\x05 \x01(\x04R\x0enetworkRxTotal\x12&\n" +
"\x0enetworkTxTotal\x18\x06 \x01(\x04R\x0enetworkTxTotal\"'\n" +
"\vLogFragment\x12\x18\n" +
"\amessage\x18\x01 \x01(\tR\amessage\"\x88\x02\n" +
"\bLogEvent\x12\x0e\n" +
+6 -4
View File
@@ -171,10 +171,12 @@ func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_Stre
case stat := <-stats:
out.Send(&pb.StreamStatsResponse{
Stat: &pb.ContainerStat{
Id: stat.ID,
CpuPercent: stat.CPUPercent,
MemoryPercent: stat.MemoryPercent,
MemoryUsage: stat.MemoryUsage,
Id: stat.ID,
CpuPercent: stat.CPUPercent,
MemoryPercent: stat.MemoryPercent,
MemoryUsage: stat.MemoryUsage,
NetworkRxTotal: stat.NetworkRxTotal,
NetworkTxTotal: stat.NetworkTxTotal,
},
})
case <-out.Context().Done():
+6 -4
View File
@@ -98,10 +98,12 @@ func FromProto(c *pb.Container) Container {
// ContainerStat represent stats instant for a container
type ContainerStat struct {
ID string `json:"id"`
CPUPercent float64 `json:"cpu"`
MemoryPercent float64 `json:"memory"`
MemoryUsage float64 `json:"memoryUsage"`
ID string `json:"id"`
CPUPercent float64 `json:"cpu"`
MemoryPercent float64 `json:"memory"`
MemoryUsage float64 `json:"memoryUsage"`
NetworkRxTotal uint64 `json:"networkRxTotal"`
NetworkTxTotal uint64 `json:"networkTxTotal"`
}
// ContainerEvent represents events that are triggered
+13 -4
View File
@@ -198,6 +198,7 @@ func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan
mem, memLimit float64
previousCPU uint64
previousSystem uint64
networkRx, networkTx uint64
)
daemonOSType := response.OSType
@@ -213,15 +214,23 @@ func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan
mem = float64(v.MemoryStats.PrivateWorkingSet)
}
// Calculate total network bytes across all interfaces
for _, netStats := range v.Networks {
networkRx += netStats.RxBytes
networkTx += netStats.TxBytes
}
if cpuPercent > 0 || mem > 0 {
select {
case <-ctx.Done():
return nil
case stats <- container.ContainerStat{
ID: id,
CPUPercent: cpuPercent,
MemoryPercent: memPercent,
MemoryUsage: mem,
ID: id,
CPUPercent: cpuPercent,
MemoryPercent: memPercent,
MemoryUsage: mem,
NetworkRxTotal: networkRx,
NetworkTxTotal: networkTx,
}:
}
}
+2 -1
View File
@@ -293,7 +293,8 @@ func (k *K8sClient) ContainerEvents(ctx context.Context, ch chan<- container.Con
}
func (k *K8sClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
panic("not implemented")
// Stats collection is implemented in stats_collector.go using K8s metrics API
panic("not implemented - use K8sStatsCollector instead")
}
func (k *K8sClient) Ping(ctx context.Context) error {
+5 -3
View File
@@ -105,9 +105,11 @@ func (sc *K8sStatsCollector) Start(parentCtx context.Context) bool {
for _, pod := range metricList.Items {
for _, c := range pod.Containers {
stat := container.ContainerStat{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
NetworkRxTotal: 0, // K8s metrics API doesn't expose network stats by default
NetworkTxTotal: 0, // Would require custom metrics or cAdvisor integration
}
log.Trace().Interface("stat", stat).Msg("k8s stats")
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
+2
View File
@@ -34,6 +34,8 @@ message ContainerStat {
double cpuPercent = 2;
double memoryUsage = 3;
double memoryPercent = 4;
uint64 networkRxTotal = 5;
uint64 networkTxTotal = 6;
}
message LogFragment {