* Copyright (C) 2015 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import * as timeformat from "timeformat";
const _ = cockpit.gettext;
export const journal = { };
* journalctl([match, ...], [options])
* @match: any number of journal match strings
* @options: an object containing further options
*
* Load and (by default) stream journal entries as
* json objects. This function returns a jQuery deferred
* object which delivers the various journal entries.
*
* The various @match strings are journalctl matches.
* Zero, one or more can be specified. They must be in
* string format, or arrays of strings.
*
* The optional @options object can contain the following:
* * "count": number of entries to load and/or pre-stream.
* Default is 10
* * "follow": if set to false just load entries and don't
* stream further journal data. Default is true.
* * "directory": optional directory to load journal files
* * "boot": when set only list entries from this specific
* boot id, or if null then the current boot.
* * "since": if specified list entries since the date/time
* * "until": if specified list entries until the date/time
* * "cursor": a cursor to start listing entries from
* * "after": a cursor to start listing entries after
* * "priority": if specified list entries below the specific priority, inclusive
*
* Returns a jQuery deferred promise. You can call these
* functions on the deferred to handle the responses. Note that
* there are additional non-jQuery methods.
*
* .done(function(entries) { }): Called when done, @entries is
* an array of all journal entries loaded. If .stream()
* has been invoked then @entries will be empty.
* .fail(function(ex) { }): called if the operation fails
* .stream(function(entries) { }): called when we receive entries
* entries. Called once per batch of journal @entries,
* whether following or not.
* .stop(): stop following or retrieving entries.
*/
journal.build_cmd = function build_cmd() {
const matches = [];
const options = { follow: true };
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];
if (typeof arg == "string") {
matches.push(arg);
} else if (typeof arg == "object") {
if (arg instanceof Array) {
matches.push.apply(matches, arg);
} else {
Object.assign(options, arg);
break;
}
} else {
console.warn("journal.journalctl called with invalid argument:", arg);
}
}
if (options.count === undefined) {
if (options.follow)
options.count = 10;
else
options.count = null;
}
const cmd = ["journalctl", "-q"];
if (!options.count)
cmd.push("--no-tail");
else
cmd.push("--lines=" + options.count);
cmd.push("--output=" + (options.output || "json"));
if (options.directory)
cmd.push("--directory=" + options.directory);
if (options.boot)
cmd.push("--boot=" + options.boot);
else if (options.boot !== undefined)
cmd.push("--boot");
if (options.since)
cmd.push("--since=" + options.since);
if (options.until)
cmd.push("--until=" + options.until);
if (options.cursor)
cmd.push("--cursor=" + options.cursor);
if (options.after)
cmd.push("--after=" + options.after);
if (options.priority)
cmd.push("--priority=" + options.priority);
if (options.grep)
cmd.push("--grep=" + options.grep);
if (options.reverse)
cmd.push("--reverse");
else if (options.follow)
cmd.push("--follow");
if (options['output-fields'])
cmd.push("--output-fields=" + options['output-fields']);
cmd.push("--");
cmd.push.apply(cmd, matches);
return cmd;
};
journal.journalctl = function journalctl() {
const cmd = journal.build_cmd.apply(null, arguments);
const dfd = cockpit.defer();
const promise = dfd.promise();
let buffer = "";
let entries = [];
let streamers = [];
let interval = null;
function fire_streamers() {
let ents;
let i;
if (streamers.length && entries.length > 0) {
ents = entries;
entries = [];
for (i = 0; i < streamers.length; i++)
streamers[i].apply(promise, [ents]);
} else {
window.clearInterval(interval);
interval = null;
}
}
const proc = cockpit.spawn(cmd, { batch: 8192, latency: 300, superuser: "try" })
.stream(function(data) {
if (buffer)
data = buffer + data;
buffer = "";
const lines = data.split("\n");
const last = lines.length - 1;
lines.forEach(function(line, i) {
if (i === last) {
buffer = line;
} else if (line && line.indexOf("-- ") !== 0) {
try {
entries.push(JSON.parse(line));
} catch (e) {
console.warn(e, line);
}
}
});
if (streamers.length && interval === null)
interval = window.setInterval(fire_streamers, 300);
})
.done(function() {
fire_streamers();
dfd.resolve(entries);
})
.fail(function(ex) {
* so we just ignore this status code */
if (ex.problem === "cancelled" ||
ex.exit_status === 1) {
fire_streamers();
dfd.resolve(entries);
} else {
dfd.reject(ex);
}
})
.always(function() {
window.clearInterval(interval);
});
promise.stream = function stream(callback) {
streamers.push(callback);
return this;
};
promise.stop = function stop() {
streamers = [];
promise.stopped = true;
proc.close("cancelled");
};
return promise;
};
journal.printable = function printable(value, key) {
if (value === undefined || value === null)
return _("[no data]");
else if (typeof (value) == "string")
return value;
else if (value.length !== undefined && value.length <= 1000 && key === "MESSAGE")
return new TextDecoder().decode(new Uint8Array(value));
else {
return _("[binary data]");
}
};
the caller via the 'output_funcs'.
Rendering is context aware. It will insert 'reboot' markers, for
example, and collapse repeated lines. You can extend the output at
the bottom and also at the top.
A new renderer is created by calling 'journal.renderer' like
so:
const renderer = journal.renderer(funcs);
You can feed new entries into the renderer by calling various
methods on the returned object:
- renderer.append(journal_entry)
- renderer.append_flush()
- renderer.prepend(journal_entry)
- renderer.prepend_flush()
A 'journal_entry' is one element of the result array returned by a
call to 'Query' with the 'cockpit.journal_fields' as the fields to
return.
Calling 'append' will append the given entry to the end of the
output, naturally, and 'prepend' will prepend it to the start.
The output might lag behind what has been input via 'append' and
'prepend', and you need to call 'append_flush' and 'prepend_flush'
respectively to ensure that the output is up-to-date. Flushing a
renderer does not introduce discontinuities into the output. You
can continue to feed entries into the renderer after flushing and
repeated lines will be correctly collapsed across the flush, for
example.
The renderer will call methods of the 'output_funcs' object to
produce the desired output:
- output_funcs.append(rendered)
- output_funcs.remove_last()
- output_funcs.prepend(rendered)
- output_funcs.remove_first()
The 'rendered' argument is the return value of one of the rendering
functions described below. The 'append' and 'prepend' methods
should add this element to the output, naturally, and 'remove_last'
and 'remove_first' should remove the indicated element.
If you never call 'prepend' on the renderer, 'output_func.prepend'
isn't called either. If you never call 'renderer.prepend' after
'renderer.prepend_flush', then 'output_func.remove_first' will
never be called. The same guarantees exist for the 'append' family
of functions.
The actual rendering is also done by calling methods on
'output_funcs':
- output_funcs.render_line(ident, prio, message, count, time, cursor)
- output_funcs.render_day_header(day)
- output_funcs.render_reboot_separator()
*/
journal.renderer = function renderer(output_funcs) {
if (!output_funcs.render_line)
console.error("Invalid renderer provided");
function copy_object(o) {
const c = { }; for (const p in o) c[p] = o[p]; return c;
}
function format_entry(journal_entry) {
const d = journal_entry.__REALTIME_TIMESTAMP / 1000;
return {
cursor: journal_entry.__CURSOR,
full: journal_entry,
day: timeformat.date(d),
time: timeformat.time(d),
bootid: journal_entry._BOOT_ID,
ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM,
prio: journal_entry.PRIORITY,
message: journal.printable(journal_entry.MESSAGE, "MESSAGE")
};
}
function entry_is_equal(a, b) {
return (a && b &&
a.day === b.day &&
a.bootid === b.bootid &&
a.ident === b.ident &&
a.prio === b.prio &&
a.message === b.message);
}
function render_state_line(state) {
return output_funcs.render_line(state.entry.ident,
state.entry.prio,
state.entry.message,
state.count,
state.last_time,
state.entry.full);
}
let top_state;
let bottom_state;
top_state = bottom_state = { };
function start_new_line() {
if (top_state === bottom_state && top_state.entry) {
top_state = copy_object(bottom_state);
}
}
function top_output() {
if (top_state.header_present) {
output_funcs.remove_first();
top_state.header_present = false;
}
if (top_state.line_present) {
output_funcs.remove_first();
top_state.line_present = false;
}
if (top_state.entry) {
output_funcs.prepend(render_state_line(top_state));
top_state.line_present = true;
}
}
function prepend(journal_entry) {
const entry = format_entry(journal_entry);
if (entry_is_equal(top_state.entry, entry)) {
top_state.count += 1;
top_state.first_time = entry.time;
} else {
top_output();
if (top_state.entry) {
if (entry.bootid !== top_state.entry.bootid)
output_funcs.prepend(output_funcs.render_reboot_separator());
if (entry.day !== top_state.entry.day)
output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
}
start_new_line();
top_state.entry = entry;
top_state.count = 1;
top_state.first_time = top_state.last_time = entry.time;
top_state.line_present = false;
}
}
function prepend_flush() {
top_output();
if (top_state.entry) {
output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
top_state.header_present = true;
}
}
function bottom_output() {
if (bottom_state.line_present) {
output_funcs.remove_last();
bottom_state.line_present = false;
}
if (bottom_state.entry) {
output_funcs.append(render_state_line(bottom_state));
bottom_state.line_present = true;
}
}
function append(journal_entry) {
const entry = format_entry(journal_entry);
if (entry_is_equal(bottom_state.entry, entry)) {
bottom_state.count += 1;
bottom_state.last_time = entry.time;
} else {
bottom_output();
if (!bottom_state.entry || entry.day !== bottom_state.entry.day) {
output_funcs.append(output_funcs.render_day_header(entry.day));
bottom_state.header_present = true;
}
if (bottom_state.entry && entry.bootid !== bottom_state.entry.bootid)
output_funcs.append(output_funcs.render_reboot_separator());
start_new_line();
bottom_state.entry = entry;
bottom_state.count = 1;
bottom_state.first_time = bottom_state.last_time = entry.time;
bottom_state.line_present = false;
}
}
function append_flush() {
bottom_output();
}
return {
prepend,
prepend_flush,
append,
append_flush
};
};