feat(docker): add container logs modal and stats columns (#4807)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
cc24f070db
commit
dbf021e04a
16 files changed
+666
-65
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import type { Readable } from "node:stream";
|
||||
import dayjs from "dayjs";
|
||||
import type { ContainerInfo, ContainerStats } from "dockerode";
|
||||
import type { Container, ContainerInfo, ContainerStats } from "dockerode";
|
||||
import type Dockerode from "dockerode";
|
||||
|
||||
import { bestMatch } from "@homarr/common";
|
||||
@@ -11,6 +12,7 @@ import { extractContainerImageName } from "@homarr/definitions";
|
||||
import type { ContainerState, Port } from "@homarr/docker";
|
||||
import { dockerLabels, DockerSingleton } from "@homarr/docker";
|
||||
|
||||
import { createDockerLogStreamProcessor, decodeDockerLogs } from "./docker-log-decode";
|
||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||
|
||||
const logger = createLogger({ module: "dockerRequestHandler" });
|
||||
@@ -183,6 +185,84 @@ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
|
||||
|
||||
const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image);
|
||||
|
||||
const findContainerByIdAsync = async (id: string) => {
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const containers = await Promise.all(
|
||||
dockerInstances.map(async ({ instance }) => {
|
||||
const container = instance.getContainer(id);
|
||||
|
||||
return await new Promise<Container | null>((resolve) => {
|
||||
container.inspect((err, data) => {
|
||||
if (err || !data) {
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(container);
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return containers.find((container) => container) ?? null;
|
||||
};
|
||||
|
||||
export const getContainerLogsAsync = async (id: string, tail = 200) => {
|
||||
const container = await findContainerByIdAsync(id);
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawLogs = await container.logs({
|
||||
tail,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
follow: false,
|
||||
});
|
||||
|
||||
return decodeDockerLogs(rawLogs);
|
||||
};
|
||||
|
||||
export const streamContainerLogsAsync = async (
|
||||
id: string,
|
||||
tail: number,
|
||||
onData: (data: string) => void,
|
||||
onError: (err: Error) => void,
|
||||
) => {
|
||||
const container = await findContainerByIdAsync(id);
|
||||
if (!container) {
|
||||
onError(new Error("Container not found"));
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const stream = (await container.logs({
|
||||
tail,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
follow: true,
|
||||
})) as Readable;
|
||||
|
||||
const MAX_MESSAGE_SIZE = 1024 * 1024;
|
||||
const processChunk = createDockerLogStreamProcessor(onData, onError, MAX_MESSAGE_SIZE);
|
||||
|
||||
const handleChunk = (chunk: Buffer) => {
|
||||
const shouldContinue = processChunk(chunk);
|
||||
if (!shouldContinue) {
|
||||
stream.removeListener("data", handleChunk);
|
||||
stream.removeListener("error", onError);
|
||||
stream.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
stream.on("data", handleChunk);
|
||||
stream.on("error", onError);
|
||||
|
||||
return () => {
|
||||
stream.removeListener("data", handleChunk);
|
||||
stream.removeListener("error", onError);
|
||||
stream.destroy();
|
||||
};
|
||||
};
|
||||
|
||||
async function getContainersWithStatsAsync() {
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const results = await Promise.allSettled(
|
||||
@@ -275,8 +355,6 @@ export function calculateMemoryUsage(stats: ContainerStats): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
|
||||
// See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
|
||||
return (
|
||||
stats.memory_stats.usage -
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
||||
Reference in new issue
Block a user