Show Node-RED logs from within Node-RED (Linux only)

I know that some folk have difficulty accessing Node-RED logs and sharing them. So it occurred to me that it might be useful to be able to surface the log in Node-RED itself.

Then you could forward it anywhere you like. To a UIBUILDER page for example :wink:

So here is a test flow that does the first part.

[{"id":"4ae8ae735f8ce4f8","type":"group","z":"4a3fcf357a20bf56","name":"TEST: Acces to journalctl","style":{"label":true},"nodes":["7ffadef3ea96cc9d","a6b7bf014cec9e5c","fa7b810c980cd879","4bc60421eda247ff"],"x":94,"y":1499,"w":552,"h":122},{"id":"7ffadef3ea96cc9d","type":"inject","z":"4a3fcf357a20bf56","g":"4ae8ae735f8ce4f8","name":"journalctl","props":[{"p":"topic","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"journalctl","payload":"nrmain","payloadType":"str","x":220,"y":1540,"wires":[["a6b7bf014cec9e5c"]]},{"id":"a6b7bf014cec9e5c","type":"function","z":"4a3fcf357a20bf56","g":"4ae8ae735f8ce4f8","name":"set up/cancel journalctl","func":"// const Journalctl = require('journalctl')\n\n/* \n    identifier: Just output logs of the given syslog identifier (cf. man journalctl, option '-t')\n    unit: Just output logs originated from the given unit file (cf. man journalctl, option '-u')\n    filter: An array of matches to filter by (cf. man journalctl, matches)\n    all: Show all fields in full, even if they include unprintable characters or are very long. (cf. man journalctl, option '-a')\n    lines: Show the most recent journal events and limit the number of events shown (cf. man journalctl, option '-n')\n    since: Start showing entries on or newer than the specified date (cf. man journalctl, option '-S')\n*/\nconst opts = {\n    unit: msg.payload,\n    lines: 50, // to allow for flows not starting immediately\n}\n\n// We have to track this to be able to cancel it\nlet journalctl = global.get('journalctl')\n\n// If already exists, done run again\nif (journalctl) {\n    // 'stop' will end the listener, unload the lib and delete the global\n    if (msg.payload === 'stop') {\n        node.warn('stopping journalctl')\n        journalctl.stop( () => {\n            global.set('journalctl', undefined)\n        })\n        return\n    }\n\n    node.warn('journalctl is already running.\\nSend a payload of \"stop\" to end.')\n    return\n}\n\n// If it doesn't exist, create and save the reference\njournalctl = new Journalctl(opts)\nglobal.set('journalctl', journalctl)\n\n\n// Set up a persistent listener for new journal entries\n// NOTE: THIS WILL CARRY ON WORKING\n// Even after the function node completes\n// You only ever need to run it once\njournalctl.on('event', (event) => {\n    node.send({\n        topic: 'journalctl-event',\n        payload: event,\n    })\n})\nnode.warn('journalctl listener started')","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"Journalctl","module":"journalctl"}],"x":420,"y":1540,"wires":[["fa7b810c980cd879"]]},{"id":"fa7b810c980cd879","type":"debug","z":"4a3fcf357a20bf56","g":"4ae8ae735f8ce4f8","name":"journalctl","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload.MESSAGE","targetType":"msg","statusVal":"","statusType":"counter","x":585,"y":1540,"wires":[],"l":false},{"id":"4bc60421eda247ff","type":"inject","z":"4a3fcf357a20bf56","g":"4ae8ae735f8ce4f8","name":"journalctl-stop","props":[{"p":"topic","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"0","topic":"journalctl-stop","payload":"stop","payloadType":"str","x":210,"y":1580,"wires":[["a6b7bf014cec9e5c"]]}]


You will need to change the systemd service name in the inject called journalctl to whatever you are using - mine is set to something custom.

Note that you will probably also need to use visudo -e to let your node-red user to run journalctl without a password. Please let me know cause my server is already set up that way.

There is a slight issue in that flows don't start until a couple of seconds after Node-RED has started, so this flow doesn't show the startup log even though I've told it to show 50 lines. Stopping and restarting does the trick. I'll try to find a way around that.

So not perfect but may be of use.

Let me know if you want to see a UIBUILDER page that shows the log.

2 Likes

Hi Julian,

Thanks for this.
I did make 2 changes.

  • Ensure things are disposed of, if the Node is deleted. I feel this is often overlooked (by myself included)
/* On Stop */
const journalctl = global.get('journalctl')
if (journalctl) {
    node.warn('stopping journalctl')
    journalctl.stop(() => {
        global.set('journalctl', undefined)
    })
}
  • When sending "stop", and it already is stopped, it creates another instance - So I only start if payload IS NOT stop
if(msg.payload !== 'stop'){
    journalctl = new Journalctl(opts)
    global.set('journalctl', journalctl)

    // Set up a persistent listener for new journal entries
    // NOTE: THIS WILL CARRY ON WORKING
    // Even after the function node completes
    // You only ever need to run it once
    journalctl.on('event', (event) => {
        node.send({
            topic: 'journalctl-event',
            payload: event,
        })
    })
    node.warn('journalctl listener started')
}

Changes

[{"id":"4ae8ae735f8ce4f8","type":"group","z":"7eb0ef81c62ddec8","name":"TEST: Acces to journalctl","style":{"label":true},"nodes":["7ffadef3ea96cc9d","a6b7bf014cec9e5c","fa7b810c980cd879","4bc60421eda247ff"],"x":254,"y":244,"w":552,"h":122},{"id":"7ffadef3ea96cc9d","type":"inject","z":"7eb0ef81c62ddec8","g":"4ae8ae735f8ce4f8","name":"journalctl","props":[{"p":"topic","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"journalctl","payload":"nrmain","payloadType":"str","x":380,"y":285,"wires":[["a6b7bf014cec9e5c"]]},{"id":"a6b7bf014cec9e5c","type":"function","z":"7eb0ef81c62ddec8","g":"4ae8ae735f8ce4f8","name":"set up/cancel journalctl","func":"// const Journalctl = require('journalctl')\n\n/* \n    identifier: Just output logs of the given syslog identifier (cf. man journalctl, option '-t')\n    unit: Just output logs originated from the given unit file (cf. man journalctl, option '-u')\n    filter: An array of matches to filter by (cf. man journalctl, matches)\n    all: Show all fields in full, even if they include unprintable characters or are very long. (cf. man journalctl, option '-a')\n    lines: Show the most recent journal events and limit the number of events shown (cf. man journalctl, option '-n')\n    since: Start showing entries on or newer than the specified date (cf. man journalctl, option '-S')\n*/\nconst opts = {\n    unit: msg.payload,\n    lines: 50, // to allow for flows not starting immediately\n}\n\n// We have to track this to be able to cancel it\nlet journalctl = global.get('journalctl')\n\n// If already exists, done run again\nif (journalctl) {\n    // 'stop' will end the listener, unload the lib and delete the global\n    if (msg.payload === 'stop') {\n        node.warn('stopping journalctl')\n        journalctl.stop( () => {\n            global.set('journalctl', undefined)\n        })\n        return\n    }\n\n    node.warn('journalctl is already running.\\nSend a payload of \"stop\" to end.')\n    return\n}\n\n// If it doesn't exist, create and save the reference, Only if not stopping\nif(msg.payload !== 'stop'){\n    journalctl = new Journalctl(opts)\n    global.set('journalctl', journalctl)\n\n    // Set up a persistent listener for new journal entries\n    // NOTE: THIS WILL CARRY ON WORKING\n    // Even after the function node completes\n    // You only ever need to run it once\n    journalctl.on('event', (event) => {\n        node.send({\n            topic: 'journalctl-event',\n            payload: event,\n        })\n    })\n    node.warn('journalctl listener started')\n}\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"const journalctl = global.get('journalctl')\nif (journalctl) {\n    node.warn('stopping journalctl')\n    journalctl.stop(() => {\n        global.set('journalctl', undefined)\n    })\n}","libs":[{"var":"Journalctl","module":"journalctl"}],"x":580,"y":285,"wires":[["fa7b810c980cd879"]]},{"id":"fa7b810c980cd879","type":"debug","z":"7eb0ef81c62ddec8","g":"4ae8ae735f8ce4f8","name":"journalctl","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload.MESSAGE","targetType":"msg","statusVal":"","statusType":"counter","x":745,"y":285,"wires":[],"l":false},{"id":"4bc60421eda247ff","type":"inject","z":"7eb0ef81c62ddec8","g":"4ae8ae735f8ce4f8","name":"journalctl-stop","props":[{"p":"topic","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":"0","topic":"journalctl-stop","payload":"stop","payloadType":"str","x":370,"y":325,"wires":[["a6b7bf014cec9e5c"]]}]
1 Like

Nice, thanks for that. I will add to my reference flow.

It is weird because the main logic took me far too long to work out - I miss not having GitHub Copilot in the function node editor! Strange how quickly you come to lean on it. :person_shrugging:

1 Like

I cant tell you how many times I create an instance of something, and wonder why it is still running.... realising I didn't kill it in the on stop tab :grimacing:

1 Like