{ "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, '>');\n}\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n success: true,\n message: `No logs available for ${data.containerName}`,\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 ${data.containerName} (last ${lineCount} lines):\\n\\n`;\nconst formatted = header + '
' + escaped + '';\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" }