Skip to content
Open
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
64 changes: 60 additions & 4 deletions apps/dokploy/components/dashboard/docker/logs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@ const LOG_STYLES: Record<LogType, LogStyle> = {
},
} as const;

// Collapse Docker progress lines that update the same image (same hash prefix)
function collapseProgressLines(lines: string[]): string[] {
const result: string[] = [];

for (const line of lines) {
// Detect Docker progress lines: start with 12-char hash followed by space and [
// Pattern: <hash> [progress bar] or just <hash>
const progressMatch = line.match(/^([a-f0-9]{12})(\s+\[.*\].*)?$/i);

if (progressMatch) {
const hash = progressMatch[1];
// Search backwards for the last line with the same hash
let found = false;
for (let i = result.length - 1; i >= 0; i--) {
const existingMatch = result[i]?.match(
/^([a-f0-9]{12})(\s+\[.*\].*)?$/i,
);
if (existingMatch && existingMatch[1] === hash) {
// Replace the existing line with the new progress update
result[i] = line;
found = true;
break;
}
}
if (!found) {
// First occurrence of this hash, add the line
result.push(line);
}
} else {
// Not a progress line, add normally
result.push(line);
}
}

return result;
}

export function parseLogs(logString: string): LogLine[] {
// Regex to match the log line format
// Example of return :
Expand All @@ -51,13 +88,32 @@ export function parseLogs(logString: string): LogLine[] {
const logRegex =
/^(?:(?<lineNumber>\d+)\s+)?(?<timestamp>(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC))?\s*(?<message>[\s\S]*)$/;

return logString
.split("\n")
// Normalize \r\n to \n, then handle \r (progress updates) by splitting and processing
const normalized = logString.replace(/\r\n/g, "\n");
const lines = normalized
.split(/\r/)
.flatMap((segment) => segment.split("\n"));

const trimmedLines = lines
.map((line) => line.trim())
.filter((line) => line !== "")
.filter((line) => line !== "");
const collapsedLines = collapseProgressLines(trimmedLines);

return collapsedLines
.map((line) => {
const match = line.match(logRegex);
if (!match) return null;

if (!match) {
// If line doesn't match the standard format but has content, treat it as a message without timestamp
// This handles progress lines and other output without standard format
return line.trim()
? {
rawTimestamp: null,
timestamp: null,
message: line.trim(),
}
: null;
}

const { timestamp, message } = match.groups ?? {};

Expand Down