Skip to content

Commit

Permalink
Add resource usage stats to the compose page
Browse files Browse the repository at this point in the history
* Also fix bug where container status wasn't showing when `docker compose ps` returned an array
  • Loading branch information
justwiebe committed Jan 3, 2025
1 parent a65a9f5 commit ba868b6
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 10 deletions.
15 changes: 15 additions & 0 deletions backend/agent-socket-handlers/docker-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ export class DockerSocketHandler extends AgentSocketHandler {
}
});

// Docker stats
agentSocket.on("dockerStats", async (callback) => {
try {
checkLogin(socket);

const dockerStats = Object.fromEntries(await server.getDockerStats());
callbackResult({
ok: true,
dockerStats,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// getExternalNetworkList
agentSocket.on("getDockerNetworkList", async (callback) => {
try {
Expand Down
29 changes: 29 additions & 0 deletions backend/dockge-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,35 @@ export class DockgeServer {
return list;
}

async getDockerStats() : Promise<Map<string, object>> {
let stats = new Map<string, object>();

try {
let res = await childProcessAsync.spawn("docker", [ "stats", "--format", "json", "--no-stream" ], {
encoding: "utf-8",
});

if (!res.stdout) {
return stats;
}

let lines = res.stdout?.toString().split("\n");

for (let line of lines) {
try {
let obj = JSON.parse(line);
stats.set(obj.Name, obj);
} catch (e) {
}
}

return stats;
} catch (e) {
log.error("getDockerStats", e);
return stats;
}
}

get stackDirFullPath() {
return path.resolve(this.stacksDir);
}
Expand Down
19 changes: 14 additions & 5 deletions backend/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ export class Stack {
}

async getServiceStatusList() {
let statusList = new Map<string, number>();
let statusList = new Map<string, Array<object>>();

try {
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
Expand All @@ -511,13 +511,23 @@ export class Stack {

let lines = res.stdout?.toString().split("\n");

const addLine = (obj: { Service: string, State: string, Name: string, Health: string }) => {
if (!statusList.has(obj.Service)) {
statusList.set(obj.Service, []);
}
statusList.get(obj.Service)?.push({
status: obj.Health || obj.State,
name: obj.Name
});
};

for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
if (obj instanceof Array) {
obj.forEach(addLine);
} else {
statusList.set(obj.Service, obj.Health);
addLine(obj);
}
} catch (e) {
}
Expand All @@ -528,6 +538,5 @@ export class Stack {
log.error("getServiceStatusList", e);
return statusList;
}

}
}
1 change: 1 addition & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare module 'vue' {
BModal: typeof import('bootstrap-vue-next')['BModal']
Confirm: typeof import('./src/components/Confirm.vue')['default']
Container: typeof import('./src/components/Container.vue')['default']
DockerStat: typeof import('./src/components/DockerStat.vue')['default']
General: typeof import('./src/components/settings/General.vue')['default']
HiddenInput: typeof import('./src/components/HiddenInput.vue')['default']
Login: typeof import('./src/components/Login.vue')['default']
Expand Down
60 changes: 57 additions & 3 deletions frontend/src/components/Container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,32 @@
{{ $t("deleteContainer") }}
</button>
</div>
<div v-else-if="statsInstances.length > 0" class="mt-2">
<div class="d-flex align-items-center gap-3">
<template v-if="!expandedStats">
<div class="stats">
{{ $t('CPU') }}: {{ statsInstances[0].CPUPerc }}
</div>
<div class="stats">
{{ $t('memoryAbbreviated') }}: {{ statsInstances[0].MemUsage }}
</div>
</template>
<div class="d-flex flex-grow-1 justify-content-end">
<button class="btn btn-sm btn-normal" @click="expandedStats = !expandedStats">
<font-awesome-icon :icon="expandedStats ? 'chevron-up' : 'chevron-down'" />
</button>
</div>
</div>
<transition name="slide-fade" appear>
<div v-if="expandedStats" class="d-flex flex-column gap-3 mt-2">
<DockerStat
v-for="stat in statsInstances"
:key="stat.Name"
:stat="stat"
/>
</div>
</transition>
</div>

<transition name="slide-fade" appear>
<div v-if="isEditMode && showConfig" class="config mt-3">
Expand Down Expand Up @@ -138,10 +164,12 @@
import { defineComponent } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { parseDockerPort } from "../../../common/util-common";
import DockerStat from "./DockerStat.vue";
export default defineComponent({
components: {
FontAwesomeIcon,
DockerStat
},
props: {
name: {
Expand All @@ -156,16 +184,21 @@ export default defineComponent({
type: Boolean,
default: false,
},
status: {
type: String,
default: "N/A",
serviceStatus: {
type: Object,
default: null,
},
dockerStats: {
type: Object,
default: null
}
},
emits: [
],
data() {
return {
showConfig: false,
expandedStats: false,
};
},
computed: {
Expand Down Expand Up @@ -266,6 +299,22 @@ export default defineComponent({
return "";
}
},
statsInstances() {
if (!this.serviceStatus) {
return [];
}
return this.serviceStatus
.map(s => this.dockerStats[s.name])
.filter(s => !!s)
.sort((a, b) => a.Name.localeCompare(b.Name));
},
status() {
if (!this.serviceStatus) {
return "N/A";
}
return this.serviceStatus[0].status;
}
},
mounted() {
if (this.first) {
Expand Down Expand Up @@ -308,5 +357,10 @@ export default defineComponent({
align-items: center;
justify-content: end;
}
.stats {
font-size: 0.8rem;
color: #6c757d;
}
}
</style>
94 changes: 94 additions & 0 deletions frontend/src/components/DockerStat.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<template>
<div class="stats-container">
<div class="stats-title">
{{ stat.Name }}
</div>
<div class="d-flex justify-content-between stats gap-2 mt-1">
<div class="stat">
<div class="stat-label">
{{ $t('CPU') }}
</div>
<div>
{{ stat.CPUPerc }}
</div>
</div>
<div class="stat">
<div class="stat-label">
{{ $t('memory') }}
</div>
<div>
{{ stat.MemUsage }} ({{ stat.MemPerc }})
</div>
</div>
<div class="stat">
<div class="stat-label">
{{ $t('networkIO') }}
</div>
<div>
{{ stat.NetIO }}
</div>
</div>
<div class="stat">
<div class="stat-label">
{{ $t('blockIO') }}
</div>
<div>
{{ stat.BlockIO }}
</div>
</div>
</div>
</div>
</template>

<script>
export default {
props: {
stat: {
type: Object,
required: true
}
},
};
</script>

<style lang="scss" scoped>
.stats-container {
container-type: inline-size;
.stats {
container-type: inline-size;
.stat {
display: flex;
flex-direction: column;
gap: 4px;
}
@container (width < 420px) {
flex-direction: column;
.stat {
flex-direction: row;
}
.stat-label::after {
content: ':'
}
}
}
}
.stats {
font-size: 0.8rem;
color: #6c757d;
}
.stat-label {
font-weight: bold;
}
.stats-title {
font-size: 0.9rem;
color: var(--bs-heading-color);
}
</style>
2 changes: 2 additions & 0 deletions frontend/src/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
faAward,
faLink,
faChevronDown,
faChevronUp,
faSignOutAlt,
faPen,
faExternalLinkSquareAlt,
Expand Down Expand Up @@ -88,6 +89,7 @@ library.add(
faAward,
faLink,
faChevronDown,
faChevronUp,
faSignOutAlt,
faPen,
faExternalLinkSquareAlt,
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,10 @@
"New Container Name...": "New Container Name...",
"Network name...": "Network name...",
"Select a network...": "Select a network...",
"NoNetworksAvailable": "No networks available. You need to add internal networks or enable external networks in the right side first."
"NoNetworksAvailable": "No networks available. You need to add internal networks or enable external networks in the right side first.",
"CPU": "CPU",
"memory": "Memory",
"memoryAbbreviated": "Mem",
"networkIO": "Network I/O",
"blockIO": "Block I/O"
}
Loading

0 comments on commit ba868b6

Please sign in to comment.