66574efed2
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>
256 lines
10 KiB
JSON
256 lines
10 KiB
JSON
{
|
|
"name": "Container Logs",
|
|
"nodes": [
|
|
{
|
|
"parameters": {},
|
|
"id": "711c56f9-46d5-41dd-aa5c-e7e4230793f3",
|
|
"name": "Execute Workflow Trigger",
|
|
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
240,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse and validate input\nconst input = $json;\n\n// Get container identifier (ID or name)\nconst containerId = input.containerId || '';\nconst containerName = input.containerName || '';\nconst lineCount = input.lineCount || 50;\nconst chatId = input.chatId;\nconst messageId = input.messageId || 0;\nconst responseMode = input.responseMode || 'text';\n\nif (!containerId && !containerName) {\n throw new Error('Either containerId or containerName required');\n}\n\nif (!chatId) {\n throw new Error('chatId required');\n}\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n lineCount: Math.min(Math.max(parseInt(lineCount), 1), 1000),\n chatId: chatId,\n messageId: messageId,\n responseMode: responseMode\n }\n};"
|
|
},
|
|
"id": "dac65a64-173a-4536-b3c5-0699704380fa",
|
|
"name": "Parse Input",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
460,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get container ID if needed\nconst data = $json;\n\n// If we already have container ID, pass through\nif (data.containerId) {\n return {\n json: {\n ...data,\n useDirectId: true\n }\n };\n}\n\n// Otherwise, need to query Docker to find by name\nreturn {\n json: {\n ...data,\n useDirectId: false,\n dockerCommand: 'curl -s --max-time 5 \"http://docker-socket-proxy:2375/v1.47/containers/json?all=1\"'\n }\n};"
|
|
},
|
|
"id": "d6db7666-5ecd-4200-8231-ffa6307e4c39",
|
|
"name": "Check Container ID",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "has-id",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "check-direct",
|
|
"leftValue": "={{ $json.useDirectId }}",
|
|
"rightValue": "true",
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "true"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "direct"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "818a4c33-e150-423e-a333-adf9843aac05",
|
|
"name": "Route ID Check",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
900,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.dockerCommand }}"
|
|
},
|
|
"id": "83cbcda1-68c5-46ee-bbb6-2eb5f1ae9077",
|
|
"name": "Query Docker",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Find container by name (supports fuzzy matching)\nconst dockerOutput = $input.item.json.stdout;\nconst data = $('Check Container ID').item.json;\nconst containerName = data.containerName.toLowerCase();\n\n// Parse Docker response\nlet containers;\ntry {\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n throw new Error('Failed to parse Docker response');\n}\n\n// Normalize name function\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find containers that match (fuzzy - includes search term)\nconst matches = containers.filter(c => normalizeName(c.Names[0]).includes(containerName));\n\nif (matches.length === 0) {\n throw new Error(`Container \"${containerName}\" not found`);\n}\n\nif (matches.length > 1) {\n const matchNames = matches.map(c => normalizeName(c.Names[0])).join(', ');\n throw new Error(`Multiple containers match \"${containerName}\": ${matchNames}`);\n}\n\nconst container = matches[0];\n\nreturn {\n json: {\n ...data,\n containerId: container.Id,\n containerName: normalizeName(container.Names[0])\n }\n};"
|
|
},
|
|
"id": "52dd705b-dd3b-4fdc-8484-276845857ad0",
|
|
"name": "Find Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Docker logs command\nconst data = $json;\nconst containerId = data.containerId;\nconst lineCount = data.lineCount;\n\nconst cmd = `curl -s --max-time 10 \"http://docker-socket-proxy:2375/v1.47/containers/${containerId}/logs?stdout=1&stderr=1&tail=${lineCount}×tamps=1\"`;\n\nreturn {\n json: {\n ...data,\n logsCommand: cmd\n }\n};"
|
|
},
|
|
"id": "86619651-82f0-459b-a969-d210d2dd6361",
|
|
"name": "Build Logs Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.logsCommand }}"
|
|
},
|
|
"id": "b602549b-e526-4207-a41d-74a936060a25",
|
|
"name": "Execute Logs",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1780,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"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, '&').replace(/</g, '<').replace(/>/g, '>');\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",
|
|
"name": "Format Logs",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
300
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Execute Workflow Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Input",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Input": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Container ID",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Container ID": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route ID Check",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route ID Check": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Query Docker",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Query Docker": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Find Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Find Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Logs Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": true,
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"versionId": "c2a9969f-2928-41f9-be03-9692ae242751",
|
|
"meta": {
|
|
"instanceId": "unraid-docker-manager"
|
|
},
|
|
"tags": [],
|
|
"id": "oE7aO2GhbksXDEIw"
|
|
} |