mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 06:28:42 +01:00
feat: adds limit to docker containers (#3766)
This commit is contained in:
@@ -42,6 +42,8 @@ function createFuzzySearchModal() {
|
|||||||
"host",
|
"host",
|
||||||
{},
|
{},
|
||||||
"running",
|
"running",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
new Container(
|
new Container(
|
||||||
@@ -55,6 +57,8 @@ function createFuzzySearchModal() {
|
|||||||
"host",
|
"host",
|
||||||
{},
|
{},
|
||||||
"running",
|
"running",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
new Container(
|
new Container(
|
||||||
@@ -68,6 +72,8 @@ function createFuzzySearchModal() {
|
|||||||
"host",
|
"host",
|
||||||
{},
|
{},
|
||||||
"running",
|
"running",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalMem / host.memTotal) * 100)};`"
|
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalMem / host.memTotal) * 100)};`"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
>
|
>
|
||||||
{{ formatBytes(weightedStats[host.id].weighted.totalMem, 1) }}
|
{{ formatBytes(weightedStats[host.id].weighted.totalMem, { decimals: 1, short: true }) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ describe("<ContainerEventSource />", () => {
|
|||||||
"localhost",
|
"localhost",
|
||||||
{},
|
{},
|
||||||
"created",
|
"created",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<StatMonitor :data="memoryData" label="mem" :stat-value="formatBytes(totalStat.memoryUsage)" />
|
<StatMonitor
|
||||||
<StatMonitor :data="cpuData" label="load" :stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'" />
|
:data="memoryData"
|
||||||
|
label="mem"
|
||||||
|
:stat-value="formatBytes(totalStat.memoryUsage)"
|
||||||
|
:limit="formatBytes(limits.memory, { short: true, decimals: 1 })"
|
||||||
|
/>
|
||||||
|
<StatMonitor
|
||||||
|
:data="cpuData"
|
||||||
|
label="load"
|
||||||
|
:stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'"
|
||||||
|
:limit="limits.cpu.toFixed(0) + ' CPUs'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -15,6 +25,7 @@ const { containers } = defineProps<{
|
|||||||
|
|
||||||
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
|
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||||
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
||||||
|
const { hosts } = useHosts();
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => containers,
|
() => containers,
|
||||||
@@ -42,6 +53,19 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const limits = computed(() => {
|
||||||
|
const limit = containers.reduce(
|
||||||
|
(acc, c) => {
|
||||||
|
return {
|
||||||
|
cpu: acc.cpu + c.cpuLimit > 0 ? c.cpuLimit : hosts.value[c.host].nCPU,
|
||||||
|
memory: acc.memory + c.memoryLimit > 0 ? c.memoryLimit : hosts.value[c.host].memTotal,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ cpu: 0, memory: 0 },
|
||||||
|
);
|
||||||
|
return limit;
|
||||||
|
});
|
||||||
|
|
||||||
useIntervalFn(() => {
|
useIntervalFn(() => {
|
||||||
totalStat.value = containers.reduce(
|
totalStat.value = containers.reduce(
|
||||||
(acc, { stat }) => {
|
(acc, { stat }) => {
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="hover:text-secondary relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
<div class="hover:text-secondary relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
||||||
<div class="border-primary hidden overflow-hidden rounded-xs border px-px pt-1 pb-px md:flex">
|
<div class="border-primary overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden">
|
||||||
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
|
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-base-200 inline-flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
|
<div class="bg-base-200 inline-flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
|
||||||
<div class="font-light uppercase">{{ label }}</div>
|
<div class="font-light uppercase">{{ label }}</div>
|
||||||
<div class="font-bold select-none">
|
<div class="font-bold select-none">
|
||||||
{{ mouseOver ? (selectedPoint?.value ?? selectedPoint?.y ?? statValue) : statValue }}
|
{{ mouseOver ? (selectedPoint?.value ?? selectedPoint?.y ?? statValue) : statValue }}
|
||||||
|
<span v-if="limit !== -1 && !mouseOver" class="max-md:hidden"> / {{ limit }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
|
const {
|
||||||
|
data,
|
||||||
let selectedPoint: Point<unknown> | undefined = $ref();
|
label,
|
||||||
|
statValue,
|
||||||
function onSelectedPoint(point: Point<unknown>) {
|
limit = -1,
|
||||||
selectedPoint = point;
|
} = defineProps<{
|
||||||
}
|
data: Point<unknown>[];
|
||||||
|
label: string;
|
||||||
let mouseOver = $ref(false);
|
statValue: string | number;
|
||||||
|
limit?: string | number;
|
||||||
|
}>();
|
||||||
|
const selectedPoint = ref<Point<unknown> | undefined>();
|
||||||
|
const onSelectedPoint = (point: Point<unknown>) => (selectedPoint.value = point);
|
||||||
|
const mouseOver = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { scaleLinear } from "d3-scale";
|
|||||||
import { area, curveStep } from "d3-shape";
|
import { area, curveStep } from "d3-shape";
|
||||||
|
|
||||||
const d3 = { extent, scaleLinear, area, curveStep };
|
const d3 = { extent, scaleLinear, area, curveStep };
|
||||||
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
const { data, width = 175, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
||||||
const x = d3.scaleLinear().range([0, width]);
|
const x = d3.scaleLinear().range([0, width]);
|
||||||
const y = d3.scaleLinear().range([height, 0]);
|
const y = d3.scaleLinear().range([height, 0]);
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export class Container {
|
|||||||
public readonly host: string,
|
public readonly host: string,
|
||||||
public readonly labels = {} as Record<string, string>,
|
public readonly labels = {} as Record<string, string>,
|
||||||
public state: ContainerState,
|
public state: ContainerState,
|
||||||
|
public readonly cpuLimit: number,
|
||||||
|
public readonly memoryLimit: number,
|
||||||
stats: Stat[],
|
stats: Stat[],
|
||||||
public readonly group?: string,
|
public readonly group?: string,
|
||||||
public health?: ContainerHealth,
|
public health?: ContainerHealth,
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ export const useContainerStore = defineStore("container", () => {
|
|||||||
c.host,
|
c.host,
|
||||||
c.labels,
|
c.labels,
|
||||||
c.state,
|
c.state,
|
||||||
|
c.cpuLimit,
|
||||||
|
c.memoryLimit,
|
||||||
c.stats,
|
c.stats,
|
||||||
c.group,
|
c.group,
|
||||||
c.health,
|
c.health,
|
||||||
|
|||||||
2
assets/types/Container.d.ts
vendored
2
assets/types/Container.d.ts
vendored
@@ -16,6 +16,8 @@ export type ContainerJson = {
|
|||||||
readonly status: string;
|
readonly status: string;
|
||||||
readonly state: ContainerState;
|
readonly state: ContainerState;
|
||||||
readonly host: string;
|
readonly host: string;
|
||||||
|
readonly cpuLimit: number;
|
||||||
|
readonly memoryLimit: number;
|
||||||
readonly labels: Record<string, string>;
|
readonly labels: Record<string, string>;
|
||||||
readonly stats: ContainerStat[];
|
readonly stats: ContainerStat[];
|
||||||
readonly health?: ContainerHealth;
|
readonly health?: ContainerHealth;
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
export function formatBytes(bytes: number, decimals = 2) {
|
export function formatBytes(
|
||||||
|
bytes: number,
|
||||||
|
{ decimals = 2, short = false }: { decimals?: number; short?: boolean } = { decimals: 2, short: false },
|
||||||
|
) {
|
||||||
if (bytes === 0) return "0 Bytes";
|
if (bytes === 0) return "0 Bytes";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
|
||||||
|
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
||||||
|
if (short) {
|
||||||
|
return value + sizes[i].charAt(0);
|
||||||
|
} else {
|
||||||
|
return value + " " + sizes[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDeep(obj: Record<string, any>, path: string[]) {
|
export function getDeep(obj: Record<string, any>, path: string[]) {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ type Container struct {
|
|||||||
Tty bool `json:"-"`
|
Tty bool `json:"-"`
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||||
|
MemoryLimit int64 `json:"memoryLimit"`
|
||||||
|
CPULimit float64 `json:"cpuLimit"`
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
FullyLoaded bool `json:"-,omitempty"`
|
FullyLoaded bool `json:"-,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -54,7 +56,7 @@ func ParseContainerFilter(commaValues string) (ContainerLabels, error) {
|
|||||||
return filter, nil
|
return filter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, val := range strings.Split(commaValues, ",") {
|
for val := range strings.SplitSeq(commaValues, ",") {
|
||||||
pos := strings.Index(val, "=")
|
pos := strings.Index(val, "=")
|
||||||
if pos == -1 {
|
if pos == -1 {
|
||||||
return nil, fmt.Errorf("invalid filter: %s", filter)
|
return nil, fmt.Errorf("invalid filter: %s", filter)
|
||||||
|
|||||||
@@ -388,6 +388,11 @@ func newContainerFromJSON(c docker.InspectResponse, host string) container.Conta
|
|||||||
group = c.Config.Labels["dev.dozzle.group"]
|
group = c.Config.Labels["dev.dozzle.group"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CPULimit := float64(0)
|
||||||
|
if c.HostConfig.CPUPeriod > 0 {
|
||||||
|
CPULimit = float64(c.HostConfig.CPUQuota) / float64(c.HostConfig.CPUPeriod)
|
||||||
|
}
|
||||||
|
|
||||||
container := container.Container{
|
container := container.Container{
|
||||||
ID: c.ID[:12],
|
ID: c.ID[:12],
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -399,6 +404,8 @@ func newContainerFromJSON(c docker.InspectResponse, host string) container.Conta
|
|||||||
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
||||||
Group: group,
|
Group: group,
|
||||||
Tty: c.Config.Tty,
|
Tty: c.Config.Tty,
|
||||||
|
MemoryLimit: c.HostConfig.Memory,
|
||||||
|
CPULimit: CPULimit,
|
||||||
FullyLoaded: true,
|
FullyLoaded: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,7 +184,11 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
|||||||
proxy := new(mockedProxy)
|
proxy := new(mockedProxy)
|
||||||
|
|
||||||
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||||
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &docker.Config{Tty: false}}
|
|
||||||
|
json := docker.InspectResponse{
|
||||||
|
ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state, HostConfig: &docker.HostConfig{}},
|
||||||
|
Config: &docker.Config{Tty: false},
|
||||||
|
}
|
||||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||||
|
|
||||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||||
@@ -213,7 +217,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
|||||||
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
|
||||||
|
|
||||||
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
state := &docker.State{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||||
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &docker.Config{Tty: false}}
|
json := docker.InspectResponse{ContainerJSONBase: &docker.ContainerJSONBase{ID: "abcdefghijkl", State: state, HostConfig: &docker.HostConfig{}}, Config: &docker.Config{Tty: false}}
|
||||||
|
|
||||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||||
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user