Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resource usage stats to the compose page #700

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading