From 749d00daee909ba9c20cb0eb80edd537bc6c1d9c Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 6 Jan 2026 18:55:42 +0100 Subject: [PATCH] feat(docker): decode and fetch container logs --- packages/request-handler/src/docker.ts | 69 +++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 0d2c02809..f6f109275 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import type { ContainerInfo, ContainerStats } from "dockerode"; +import type { Container, ContainerInfo, ContainerStats } from "dockerode"; import { db, like, or } from "@homarr/db"; import { icons } from "@homarr/db/schema"; @@ -21,6 +21,73 @@ const dockerInstances = DockerSingleton.getInstances(); const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? ""; +const findContainerByIdAsync = async (id: string) => { + const containers = await Promise.all( + dockerInstances.map(async ({ instance }) => { + const container = instance.getContainer(id); + + return await new Promise((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); +}; + +const decodeDockerLogs = (logs: Buffer | string) => { + if (typeof logs === "string") { + return logs; + } + + // Docker multiplexed stream: 1 byte stream type, 3 bytes padding, 4 bytes big-endian length, then payload + // Repeat until buffer consumed. + let cursor = 0; + const parts: string[] = []; + + while (cursor < logs.length) { + // Defensive: need at least 8 bytes header + if (cursor + 8 > logs.length) { + break; + } + + const length = logs.readUInt32BE(cursor + 4); + const start = cursor + 8; + const end = start + length; + if (end > logs.length) { + break; + } + + const payload = logs.subarray(start, end); + parts.push(payload.toString("utf-8")); + cursor = end; + } + + return parts.join(""); +}; + async function getContainersWithStatsAsync() { const containers = await Promise.all( dockerInstances.map(async ({ instance, host }) => {