feat(10-05): extract logs flow to Container Logs sub-workflow

Created n8n-container-logs.json:
- Execute Workflow Trigger entry point
- Parse and validate input (containerId/Name, lineCount, chatId, messageId)
- Query Docker to find container by name if needed
- Execute docker logs command
- Format output for Telegram (HTML escape, truncate, add header)
- Return success/message/containerName/lineCount

Updated main workflow:
- Add Prepare Text Logs Input (text command path)
- Add Execute Text Logs sub-workflow node
- Add Prepare Inline Logs Input (inline keyboard path)
- Add Execute Inline Logs sub-workflow node
- Add Format Inline Logs Result (adds keyboard)
- Remove 14 obsolete inline logs nodes
- Node count: 208 -> 199 (-9)

Sub-workflow has placeholder ID - will be updated after n8n import in Task 4.
This commit is contained in:
Lucas Berger
2026-02-04 13:56:51 -05:00
parent 89e459f96c
commit 6471dcecd6
2 changed files with 388 additions and 458 deletions
+255
View File
@@ -0,0 +1,255 @@
{
"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\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 exact match\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n throw new Error(`Container \"${containerName}\" not found`);\n}\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}&timestamps=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, '&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};"
},
"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": []
}