diff --git a/assets/components/FuzzySearchModal.spec.ts b/assets/components/FuzzySearchModal.spec.ts index 80b90e90..d56846fd 100644 --- a/assets/components/FuzzySearchModal.spec.ts +++ b/assets/components/FuzzySearchModal.spec.ts @@ -42,6 +42,8 @@ function createFuzzySearchModal() { "host", {}, "running", + 0, + 0, [], ), new Container( @@ -55,6 +57,8 @@ function createFuzzySearchModal() { "host", {}, "running", + 0, + 0, [], ), new Container( @@ -68,6 +72,8 @@ function createFuzzySearchModal() { "host", {}, "running", + 0, + 0, [], ), ], diff --git a/assets/components/HostList.vue b/assets/components/HostList.vue index e7737897..df644990 100644 --- a/assets/components/HostList.vue +++ b/assets/components/HostList.vue @@ -51,7 +51,7 @@ :style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalMem / host.memTotal) * 100)};`" role="progressbar" > - {{ formatBytes(weightedStats[host.id].weighted.totalMem, 1) }} + {{ formatBytes(weightedStats[host.id].weighted.totalMem, { decimals: 1, short: true }) }} diff --git a/assets/components/LogViewer/EventSource.spec.ts b/assets/components/LogViewer/EventSource.spec.ts index 47d4b31b..15d7d074 100644 --- a/assets/components/LogViewer/EventSource.spec.ts +++ b/assets/components/LogViewer/EventSource.spec.ts @@ -105,6 +105,8 @@ describe("", () => { "localhost", {}, "created", + 0, + 0, [], ), }, diff --git a/assets/components/LogViewer/MultiContainerStat.vue b/assets/components/LogViewer/MultiContainerStat.vue index 30f25ff5..9e463113 100644 --- a/assets/components/LogViewer/MultiContainerStat.vue +++ b/assets/components/LogViewer/MultiContainerStat.vue @@ -1,7 +1,17 @@ @@ -15,6 +25,7 @@ const { containers } = defineProps<{ const totalStat = ref({ cpu: 0, memory: 0, memoryUsage: 0 }); const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 }); +const { hosts } = useHosts(); watch( () => containers, @@ -42,6 +53,19 @@ watch( { 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(() => { totalStat.value = containers.reduce( (acc, { stat }) => { diff --git a/assets/components/LogViewer/StatMonitor.vue b/assets/components/LogViewer/StatMonitor.vue index 76b0ebbe..9bc5c39c 100644 --- a/assets/components/LogViewer/StatMonitor.vue +++ b/assets/components/LogViewer/StatMonitor.vue @@ -1,25 +1,31 @@ diff --git a/assets/components/LogViewer/StatSparkline.vue b/assets/components/LogViewer/StatSparkline.vue index e7ed0c1e..7cf8d15a 100644 --- a/assets/components/LogViewer/StatSparkline.vue +++ b/assets/components/LogViewer/StatSparkline.vue @@ -11,7 +11,7 @@ import { scaleLinear } from "d3-scale"; import { area, curveStep } from "d3-shape"; const d3 = { extent, scaleLinear, area, curveStep }; -const { data, width = 150, height = 30 } = defineProps<{ data: Point[]; width?: number; height?: number }>(); +const { data, width = 175, height = 30 } = defineProps<{ data: Point[]; width?: number; height?: number }>(); const x = d3.scaleLinear().range([0, width]); const y = d3.scaleLinear().range([height, 0]); diff --git a/assets/models/Container.ts b/assets/models/Container.ts index ff701563..73d35c8b 100644 --- a/assets/models/Container.ts +++ b/assets/models/Container.ts @@ -38,6 +38,8 @@ export class Container { public readonly host: string, public readonly labels = {} as Record, public state: ContainerState, + public readonly cpuLimit: number, + public readonly memoryLimit: number, stats: Stat[], public readonly group?: string, public health?: ContainerHealth, diff --git a/assets/stores/container.ts b/assets/stores/container.ts index 10ddee35..8c7e97d0 100644 --- a/assets/stores/container.ts +++ b/assets/stores/container.ts @@ -154,6 +154,8 @@ export const useContainerStore = defineStore("container", () => { c.host, c.labels, c.state, + c.cpuLimit, + c.memoryLimit, c.stats, c.group, c.health, diff --git a/assets/types/Container.d.ts b/assets/types/Container.d.ts index 4639e69a..9d6ddb5e 100644 --- a/assets/types/Container.d.ts +++ b/assets/types/Container.d.ts @@ -16,6 +16,8 @@ export type ContainerJson = { readonly status: string; readonly state: ContainerState; readonly host: string; + readonly cpuLimit: number; + readonly memoryLimit: number; readonly labels: Record; readonly stats: ContainerStat[]; readonly health?: ContainerHealth; diff --git a/assets/utils/index.ts b/assets/utils/index.ts index b9c328ca..70e1753b 100644 --- a/assets/utils/index.ts +++ b/assets/utils/index.ts @@ -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"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 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, path: string[]) { diff --git a/internal/container/types.go b/internal/container/types.go index 8f94e31a..afefe288 100644 --- a/internal/container/types.go +++ b/internal/container/types.go @@ -24,6 +24,8 @@ type Container struct { Tty bool `json:"-"` Labels map[string]string `json:"labels,omitempty"` Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"` + MemoryLimit int64 `json:"memoryLimit"` + CPULimit float64 `json:"cpuLimit"` Group string `json:"group,omitempty"` FullyLoaded bool `json:"-,omitempty"` } @@ -54,7 +56,7 @@ func ParseContainerFilter(commaValues string) (ContainerLabels, error) { return filter, nil } - for _, val := range strings.Split(commaValues, ",") { + for val := range strings.SplitSeq(commaValues, ",") { pos := strings.Index(val, "=") if pos == -1 { return nil, fmt.Errorf("invalid filter: %s", filter) diff --git a/internal/docker/client.go b/internal/docker/client.go index 2dac4b4a..f27b76bc 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -388,6 +388,11 @@ func newContainerFromJSON(c docker.InspectResponse, host string) container.Conta 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{ ID: c.ID[:12], 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 Group: group, Tty: c.Config.Tty, + MemoryLimit: c.HostConfig.Memory, + CPULimit: CPULimit, FullyLoaded: true, } diff --git a/internal/docker/client_test.go b/internal/docker/client_test.go index 36c8b221..70c14d08 100644 --- a/internal/docker/client_test.go +++ b/internal/docker/client_test.go @@ -184,7 +184,11 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) { proxy := new(mockedProxy) 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) 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{}} 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("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)