fix(logs): properly parse Docker multiplexed stream format

Docker logs API returns multiplexed stream with 8-byte headers per frame,
not just newline-separated text. The old code split on \n which missed
frames that weren't newline-terminated.

Now properly parses the binary stream format:
- Reads 8-byte header (stream type + frame size)
- Extracts frame data based on size
- Splits frame content by newlines
- Correctly counts and displays requested number of lines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-02-04 22:26:18 -05:00
parent df9a4420e9
commit 66574efed2
+1 -1
View File
@@ -134,7 +134,7 @@
}, },
{ {
"parameters": { "parameters": {
"jsCode": "// Format logs output for Telegram\nconst rawOutput = $input.item.json.stdout || '';\nconst data = $('Build Logs Command').item.json;\n\n// HTML escape function\nfunction escapeHtml(text) {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n success: true,\n message: `No logs available for <b>${data.containerName}</b>`,\n containerName: data.containerName,\n lineCount: 0\n }\n };\n}\n\n// Strip Docker binary headers and process lines\nconst lines = rawOutput.split('\\n')\n .filter(line => line.length > 0)\n .map(line => {\n // Check if line starts with binary header (8-byte Docker stream header)\n if (line.length > 8 && line.charCodeAt(0) <= 2) {\n return line.substring(8);\n }\n return line;\n })\n .join('\\n');\n\n// Truncate for Telegram (4096 char limit, leave room for header)\nconst maxLen = 3800;\nconst truncated = lines.length > maxLen\n ? lines.substring(0, maxLen) + '\\n... (truncated)'\n : lines;\n\n// Escape HTML entities\nconst escaped = escapeHtml(truncated);\n\nconst lineCount = lines.split('\\n').length;\nconst header = `Logs for <b>${data.containerName}</b> (last ${lineCount} lines):\\n\\n`;\nconst formatted = header + '<pre>' + escaped + '</pre>';\n\nreturn {\n json: {\n success: true,\n message: formatted,\n containerName: data.containerName,\n lineCount: lineCount\n }\n};" "jsCode": "// Format logs output for Telegram\nconst rawOutput = $input.item.json.stdout || '';\nconst data = $('Build Logs Command').item.json;\n\n// HTML escape function\nfunction escapeHtml(text) {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n success: true,\n message: `No logs available for <b>${data.containerName}</b>`,\n containerName: data.containerName,\n lineCount: 0\n }\n };\n}\n\n// Parse Docker multiplexed stream format\n// Each frame: 8-byte header (byte 0 = stream type, bytes 4-7 = size) + data\nconst logLines = [];\nlet pos = 0;\nwhile (pos < rawOutput.length) {\n const streamType = rawOutput.charCodeAt(pos);\n // Check for valid Docker stream header (0=stdin, 1=stdout, 2=stderr)\n if (streamType <= 2 && pos + 8 <= rawOutput.length) {\n // Parse frame size from bytes 4-7 (big-endian)\n const size = (rawOutput.charCodeAt(pos + 4) << 24) |\n (rawOutput.charCodeAt(pos + 5) << 16) |\n (rawOutput.charCodeAt(pos + 6) << 8) |\n rawOutput.charCodeAt(pos + 7);\n // Extract frame data\n const frameData = rawOutput.substring(pos + 8, pos + 8 + size);\n // Split frame data by newlines and add each line\n frameData.split('\\n').forEach(line => {\n const trimmed = line.replace(/\\r/g, '').trim();\n if (trimmed) logLines.push(trimmed);\n });\n pos += 8 + size;\n } else {\n // Fallback: treat rest as plain text\n rawOutput.substring(pos).split('\\n').forEach(line => {\n const trimmed = line.replace(/\\r/g, '').trim();\n if (trimmed) logLines.push(trimmed);\n });\n break;\n }\n}\n\nconst lines = logLines.join('\\n');\n\n// Truncate for Telegram (4096 char limit, leave room for header)\nconst maxLen = 3800;\nconst truncated = lines.length > maxLen\n ? lines.substring(0, maxLen) + '\\n... (truncated)'\n : lines;\n\n// Escape HTML entities\nconst escaped = escapeHtml(truncated);\n\nconst lineCount = logLines.length;\nconst header = `Logs for <b>${data.containerName}</b> (last ${lineCount} lines):\\n\\n`;\nconst formatted = header + '<pre>' + escaped + '</pre>';\n\nreturn {\n json: {\n success: true,\n message: formatted,\n containerName: data.containerName,\n lineCount: lineCount\n }\n};"
}, },
"id": "e423b026-e619-4e3a-abd9-563e935cd74d", "id": "e423b026-e619-4e3a-abd9-563e935cd74d",
"name": "Format Logs", "name": "Format Logs",