├── .gitignore ├── __mocks__ ├── json-stream.js └── child_process.js ├── json-stream.js ├── package.json ├── LICENSE ├── journalctl.js ├── README.md └── __tests__ ├── json-stream.js └── journalctl.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /__mocks__/json-stream.js: -------------------------------------------------------------------------------- 1 | module.exports = jest.fn(); 2 | module.exports.prototype.decode = jest.fn(); 3 | -------------------------------------------------------------------------------- /__mocks__/child_process.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | module.exports.spawn = jest.fn(() => { 4 | module.exports.__spawn = new EventEmitter(); 5 | module.exports.__spawn.stdout = new EventEmitter(); 6 | module.exports.__spawn.kill = jest.fn(); 7 | return module.exports.__spawn; 8 | }); 9 | -------------------------------------------------------------------------------- /json-stream.js: -------------------------------------------------------------------------------- 1 | function JSONStream (cb) { 2 | this.cb = cb; 3 | this.obj = 0; 4 | this.str = false; 5 | this.esc = false; 6 | } 7 | 8 | JSONStream.prototype.decode = function (str) { 9 | for (let i = 0; i < str.length; i++) this.decodeChar(str[i]); 10 | }; 11 | 12 | JSONStream.prototype.decodeChar = function (c) { 13 | // Start catching new object 14 | if (!this.str && c === '{' && this.obj++ === 0) { 15 | this.data = ''; 16 | } 17 | 18 | // Add character 19 | this.data += c; 20 | 21 | // Hide brackets in strings 22 | if (c === '"' && !this.esc) this.str = !this.str; 23 | 24 | // Track escape chars 25 | if (!this.esc && c === '\\') { 26 | this.esc = true; 27 | } else if (this.esc) { 28 | this.esc = false; 29 | } 30 | 31 | // Stop at closing bracket 32 | if (!this.str && c === '}' && --this.obj === 0) { 33 | this.cb(JSON.parse(this.data)); 34 | } 35 | }; 36 | 37 | module.exports = JSONStream; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "journalctl", 3 | "version": "1.1.0", 4 | "description": "Module for consuming the Systemd Journal", 5 | "main": "journalctl.js", 6 | "keywords": [ 7 | "logging", 8 | "systemd", 9 | "journald", 10 | "journalctl" 11 | ], 12 | "scripts": { 13 | "test": "happiness && jest", 14 | "watch": "jest --watchAll" 15 | }, 16 | "author": "Juergen Fitschen ", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/jue89/node-journalctl.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/jue89/node-journalctl/issues" 23 | }, 24 | "homepage": "https://github.com/jue89/node-journalctl#readme", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "happiness": "^10.0.2", 28 | "jest": "^24.1.0" 29 | }, 30 | "happiness": { 31 | "globals": [ 32 | "jest", 33 | "describe", 34 | "test", 35 | "expect", 36 | "beforeEach" 37 | ] 38 | }, 39 | "jest": { 40 | "automock": false, 41 | "clearMocks": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Juergen Fitschen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /journalctl.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const EventEmitter = require('events'); 3 | const util = require('util'); 4 | const JSONStream = require('./json-stream.js'); 5 | 6 | function Journalctl (opts) { 7 | EventEmitter.call(this); 8 | 9 | // Decode opts 10 | const args = ['-f', '-o', 'json']; 11 | if (opts === undefined) opts = {}; 12 | if (opts.all) args.push('-a'); 13 | if (opts.lines) args.push('-n', opts.lines); 14 | if (opts.since) args.push('-S', opts.since); 15 | if (opts.identifier) args.push('-t', opts.identifier); 16 | if (opts.unit) args.push('-u', opts.unit); 17 | if (opts.filter) { 18 | if (!(opts.filter instanceof Array)) opts.filter = [opts.filter]; 19 | opts.filter.forEach((f) => args.push(f)); 20 | } 21 | 22 | // Start journalctl 23 | this.journalctl = childProcess.spawn('journalctl', args); 24 | 25 | // Setup decoder 26 | const decoder = new JSONStream((e) => { 27 | this.emit('event', e); 28 | }); 29 | this.journalctl.stdout.on('data', (chunk) => { 30 | decoder.decode(chunk.toString()); 31 | }); 32 | } 33 | 34 | util.inherits(Journalctl, EventEmitter); 35 | 36 | Journalctl.prototype.stop = function (cb) { 37 | // Kill the process 38 | if (cb) this.journalctl.on('exit', cb); 39 | this.journalctl.kill(); 40 | }; 41 | 42 | module.exports = Journalctl; 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Journalctl 2 | 3 | This is a module for accessing the all mighty Systemd Journal and its handy dandy key-value store hidden behind every log line. In the background it spawns a journalctl process with the output format `json` and parses the serialised object stream. 4 | 5 | ## API 6 | 7 | Require the module and create a new instance: 8 | 9 | ```js 10 | const Journalctl = require('journalctl'); 11 | const journalctl = new Journalctl([opts]); 12 | ``` 13 | 14 | The optional object `opts` can have the following properties: 15 | * `identifier`: Just output logs of the given syslog identifier (cf. man journalctl, option '-t') 16 | * `unit`: Just output logs originated from the given unit file (cf. man journalctl, option '-u') 17 | * `filter`: An array of matches to filter by (cf. man journalctl, matches) 18 | * `all`: Show all fields in full, even if they include unprintable characters or are very long. (cf. man journalctl, option '-a') 19 | * `lines`: Show the most recent journal events and limit the number of events shown (cf. man journalctl, option '-n') 20 | * `since`: Start showing entries on or newer than the specified date (cf. man journalctl, option '-S') 21 | 22 | ### Event: 'event' 23 | 24 | ```js 25 | journalctl.on('event', (event) => {}); 26 | ``` 27 | 28 | Is fired on every log event and hands over the object `event` describing the event. *(Oh boy ... so many events in one sentence ...)* 29 | 30 | ### Method: stop 31 | 32 | ```js 33 | journalctl.stop([callback]); 34 | ``` 35 | 36 | Stops journalctl and calls the optional `callback` once everything has been killed. 37 | -------------------------------------------------------------------------------- /__tests__/json-stream.js: -------------------------------------------------------------------------------- 1 | /* eslint no-useless-escape: "off" */ 2 | const JSONStream = require('../json-stream.js'); 3 | 4 | test('parse json object', () => { 5 | const cb = jest.fn(); 6 | const stream = new JSONStream(cb); 7 | stream.decode('{"test":true}'); 8 | expect(cb.mock.calls.length).toBe(1); 9 | expect(cb.mock.calls[0][0]).toEqual({ 10 | 'test': true 11 | }); 12 | }); 13 | 14 | test('parse json string in two parts', () => { 15 | const cb = jest.fn(); 16 | const stream = new JSONStream(cb); 17 | stream.decode('{"test":'); 18 | expect(cb.mock.calls.length).toBe(0); 19 | stream.decode('true}'); 20 | expect(cb.mock.calls.length).toBe(1); 21 | expect(cb.mock.calls[0][0]).toEqual({ 22 | 'test': true 23 | }); 24 | }); 25 | 26 | test('ignore closing brackets in strings', () => { 27 | const cb = jest.fn(); 28 | const stream = new JSONStream(cb); 29 | stream.decode('{"test":"}"}'); 30 | expect(cb.mock.calls.length).toBe(1); 31 | expect(cb.mock.calls[0][0]).toEqual({ 32 | 'test': '}' 33 | }); 34 | }); 35 | 36 | test('ignore opening brackets in strings', () => { 37 | const cb = jest.fn(); 38 | const stream = new JSONStream(cb); 39 | stream.decode('{"test":"{"}'); 40 | expect(cb.mock.calls.length).toBe(1); 41 | expect(cb.mock.calls[0][0]).toEqual({ 42 | 'test': '{' 43 | }); 44 | }); 45 | 46 | test('ignore escaped quotes in strings', () => { 47 | const cb = jest.fn(); 48 | const stream = new JSONStream(cb); 49 | stream.decode('{"test":"\\"}"}'); 50 | expect(cb.mock.calls.length).toBe(1); 51 | expect(cb.mock.calls[0][0]).toEqual({ 52 | 'test': '\"}' 53 | }); 54 | }); 55 | 56 | test('decode many objects', () => { 57 | const cb = jest.fn(); 58 | const stream = new JSONStream(cb); 59 | stream.decode('{"test":true}{"test":false}'); 60 | expect(cb.mock.calls.length).toBe(2); 61 | expect(cb.mock.calls[0][0]).toEqual({ 62 | 'test': true 63 | }); 64 | expect(cb.mock.calls[1][0]).toEqual({ 65 | 'test': false 66 | }); 67 | }); 68 | 69 | test('decode nested objects', () => { 70 | const cb = jest.fn(); 71 | const stream = new JSONStream(cb); 72 | stream.decode('{"test":{"test":true}}'); 73 | expect(cb.mock.calls.length).toBe(1); 74 | expect(cb.mock.calls[0][0]).toEqual({ 75 | 'test': { 'test': true } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /__tests__/journalctl.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | const Journalctl = require('../journalctl.js'); 4 | 5 | jest.mock('child_process'); 6 | const childProcess = require('child_process'); 7 | 8 | jest.mock('../json-stream.js'); 9 | const JSONStream = require('../json-stream.js'); 10 | 11 | test('start journalctl', () => { 12 | const j = new Journalctl(); 13 | expect(childProcess.spawn.mock.calls.length).toBe(1); 14 | expect(childProcess.spawn.mock.calls[0]).toEqual([ 15 | 'journalctl', 16 | ['-f', '-o', 'json'] 17 | ]); 18 | expect(j).toBeInstanceOf(EventEmitter); 19 | }); 20 | 21 | test('decode incoming data', () => { 22 | const j = new Journalctl(); 23 | const json = '{"TEST": "1"}'; 24 | childProcess.__spawn.stdout.emit('data', Buffer.from(json)); 25 | expect(JSONStream.prototype.decode.mock.calls.length).toBe(1); 26 | expect(JSONStream.prototype.decode.mock.calls[0][0]).toEqual(json); 27 | expect(j).toBeInstanceOf(EventEmitter); 28 | }); 29 | 30 | test('emit events', () => { 31 | const j = new Journalctl(); 32 | const json = { 'TEST': null }; 33 | const cb = jest.fn(); 34 | j.on('event', cb); 35 | JSONStream.mock.calls[0][0](json); 36 | expect(cb.mock.calls[0][0]).toBe(json); 37 | }); 38 | 39 | test('kill journalctl', () => { 40 | const j = new Journalctl(); 41 | const cb = jest.fn(); 42 | j.stop(cb); 43 | expect(childProcess.__spawn.kill.mock.calls.length).toBe(1); 44 | childProcess.__spawn.emit('exit'); 45 | expect(cb.mock.calls.length).toBe(1); 46 | }); 47 | 48 | test('specify identifier', () => { 49 | const IDENTIFIER = 'test'; 50 | const j = new Journalctl({ 51 | identifier: IDENTIFIER 52 | }); 53 | expect(childProcess.spawn.mock.calls.length).toBe(1); 54 | expect(childProcess.spawn.mock.calls[0]).toEqual([ 55 | 'journalctl', 56 | ['-f', '-o', 'json', '-t', IDENTIFIER] 57 | ]); 58 | expect(j).toBeInstanceOf(EventEmitter); 59 | }); 60 | 61 | test('specify unit', () => { 62 | const UNIT = 'test'; 63 | const j = new Journalctl({ 64 | unit: UNIT 65 | }); 66 | expect(childProcess.spawn.mock.calls.length).toBe(1); 67 | expect(childProcess.spawn.mock.calls[0]).toEqual([ 68 | 'journalctl', 69 | ['-f', '-o', 'json', '-u', UNIT] 70 | ]); 71 | expect(j).toBeInstanceOf(EventEmitter); 72 | }); 73 | 74 | test('specify filter', () => { 75 | const FILTER = 'MESSAGE_ID=test'; 76 | const j = new Journalctl({ 77 | filter: FILTER 78 | }); 79 | expect(childProcess.spawn.mock.calls.length).toBe(1); 80 | expect(childProcess.spawn.mock.calls[0]).toEqual([ 81 | 'journalctl', 82 | ['-f', '-o', 'json', FILTER] 83 | ]); 84 | expect(j).toBeInstanceOf(EventEmitter); 85 | }); 86 | 87 | test('specify multiple filter', () => { 88 | const FILTER1 = 'MESSAGE_ID=test'; 89 | const FILTER2 = '_HOSTNAME=test'; 90 | const j = new Journalctl({ 91 | filter: [FILTER1, FILTER2] 92 | }); 93 | expect(childProcess.spawn.mock.calls.length).toBe(1); 94 | expect(childProcess.spawn.mock.calls[0]).toEqual([ 95 | 'journalctl', 96 | ['-f', '-o', 'json', FILTER1, FILTER2] 97 | ]); 98 | expect(j).toBeInstanceOf(EventEmitter); 99 | }); 100 | --------------------------------------------------------------------------------