LCOV - code coverage report
Current view: top level - pkg/lib - journal.js Coverage Total Hit
Test: cockpit Lines: 100.0 % 261 261
Test Date: 2026-07-17 14:32:59

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2015 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          205 : import cockpit from "cockpit";
       7              : import * as timeformat from "timeformat";
       8              : 
       9          205 : const _ = cockpit.gettext;
      10              : 
      11          205 : export const journal = { };
      12              : 
      13              : /**
      14              :  * journalctl([match, ...], [options])
      15              :  * @match: any number of journal match strings
      16              :  * @options: an object containing further options
      17              :  *
      18              :  * Load and (by default) stream journal entries as
      19              :  * json objects. This function returns a jQuery deferred
      20              :  * object which delivers the various journal entries.
      21              :  *
      22              :  * The various @match strings are journalctl matches.
      23              :  * Zero, one or more can be specified. They must be in
      24              :  * string format, or arrays of strings.
      25              :  *
      26              :  * The optional @options object can contain the following:
      27              :  *  * "count": number of entries to load and/or pre-stream.
      28              :  *    Default is 10
      29              :  *  * "follow": if set to false just load entries and don't
      30              :  *    stream further journal data. Default is true.
      31              :  *  * "directory": optional directory to load journal files
      32              :  *  * "boot": when set only list entries from this specific
      33              :  *    boot id, or if null then the current boot.
      34              :  *  * "since": if specified list entries since the date/time
      35              :  *  * "until": if specified list entries until the date/time
      36              :  *  * "cursor": a cursor to start listing entries from
      37              :  *  * "after": a cursor to start listing entries after
      38              :  *  * "priority": if specified list entries below the specific priority, inclusive
      39              :  *
      40              :  * Returns a jQuery deferred promise. You can call these
      41              :  * functions on the deferred to handle the responses. Note that
      42              :  * there are additional non-jQuery methods.
      43              :  *
      44              :  *  .done(function(entries) { }): Called when done, @entries is
      45              :  *         an array of all journal entries loaded. If .stream()
      46              :  *         has been invoked then @entries will be empty.
      47              :  *  .fail(function(ex) { }): called if the operation fails
      48              :  *  .stream(function(entries) { }): called when we receive entries
      49              :  *         entries. Called once per batch of journal @entries,
      50              :  *         whether following or not.
      51              :  *  .stop(): stop following or retrieving entries.
      52              :  */
      53              : 
      54          167 : journal.build_cmd = function build_cmd(/* ... */) {
      55          167 :     const matches = [];
      56          167 :     const options = { follow: true };
      57          167 :     for (let i = 0; i < arguments.length; i++) {
      58          167 :         const arg = arguments[i];
      59           17 :         if (typeof arg == "string") {
      60           17 :             matches.push(arg);
      61           17 :         } else if (typeof arg == "object") {
      62          167 :             if (arg instanceof Array) {
      63          167 :                 matches.push.apply(matches, arg);
      64          167 :             } else {
      65          167 :                 Object.assign(options, arg);
      66          167 :                 break;
      67          167 :             }
      68           17 :         } else {
      69           17 :             console.warn("journal.journalctl called with invalid argument:", arg);
      70           17 :         }
      71          167 :     }
      72              : 
      73           26 :     if (options.count === undefined) {
      74           26 :         if (options.follow)
      75           17 :             options.count = 10;
      76              :         else
      77           26 :             options.count = null;
      78           26 :     }
      79              : 
      80          167 :     const cmd = ["journalctl", "-q"];
      81          167 :     if (!options.count)
      82           22 :         cmd.push("--no-tail");
      83              :     else
      84          163 :         cmd.push("--lines=" + options.count);
      85              : 
      86          167 :     cmd.push("--output=" + (options.output || "json"));
      87              : 
      88          167 :     if (options.directory)
      89           17 :         cmd.push("--directory=" + options.directory);
      90          167 :     if (options.boot)
      91           18 :         cmd.push("--boot=" + options.boot);
      92          167 :     else if (options.boot !== undefined)
      93           17 :         cmd.push("--boot");
      94          167 :     if (options.since)
      95           26 :         cmd.push("--since=" + options.since);
      96          167 :     if (options.until)
      97           19 :         cmd.push("--until=" + options.until);
      98          167 :     if (options.cursor)
      99           26 :         cmd.push("--cursor=" + options.cursor);
     100          167 :     if (options.after)
     101           17 :         cmd.push("--after=" + options.after);
     102          167 :     if (options.priority)
     103           26 :         cmd.push("--priority=" + options.priority);
     104          167 :     if (options.grep)
     105           18 :         cmd.push("--grep=" + options.grep);
     106              : 
     107              :     /* journalctl doesn't allow reverse and follow together */
     108          167 :     if (options.reverse)
     109           26 :         cmd.push("--reverse");
     110          167 :     else if (options.follow)
     111          167 :         cmd.push("--follow");
     112              : 
     113          167 :     cmd.push("--");
     114          167 :     cmd.push.apply(cmd, matches);
     115          167 :     return cmd;
     116          167 : };
     117              : 
     118          167 : journal.journalctl = function journalctl(/* ... */) {
     119          167 :     const cmd = journal.build_cmd.apply(null, arguments);
     120              : 
     121          167 :     const dfd = cockpit.defer();
     122          167 :     const promise = dfd.promise();
     123          167 :     let buffer = "";
     124          167 :     let entries = [];
     125          167 :     let streamers = [];
     126          167 :     let interval = null;
     127              : 
     128          158 :     function fire_streamers() {
     129          158 :         let ents;
     130          158 :         let i;
     131          126 :         if (streamers.length && entries.length > 0) {
     132          126 :             ents = entries;
     133          126 :             entries = [];
     134          126 :             for (i = 0; i < streamers.length; i++)
     135          126 :                 streamers[i].apply(promise, [ents]);
     136          126 :         } else {
     137          158 :             window.clearInterval(interval);
     138          158 :             interval = null;
     139          158 :         }
     140          158 :     }
     141              : 
     142          167 :     const proc = cockpit.spawn(cmd, { batch: 8192, latency: 300, superuser: "try" })
     143          163 :             .stream(function(data) {
     144          163 :                 if (buffer)
     145          118 :                     data = buffer + data;
     146          163 :                 buffer = "";
     147              : 
     148          163 :                 const lines = data.split("\n");
     149          163 :                 const last = lines.length - 1;
     150          163 :                 lines.forEach(function(line, i) {
     151          163 :                     if (i == last) {
     152          163 :                         buffer = line;
     153          163 :                     } else if (line && line.indexOf("-- ") !== 0) {
     154          163 :                         try {
     155          163 :                             entries.push(JSON.parse(line));
     156           16 :                         } catch (e) {
     157           16 :                             console.warn(e, line);
     158           16 :                         }
     159          163 :                     }
     160          163 :                 });
     161              : 
     162          163 :                 if (streamers.length && interval === null)
     163          163 :                     interval = window.setInterval(fire_streamers, 300);
     164          163 :             })
     165           12 :             .done(function() {
     166           12 :                 fire_streamers();
     167           12 :                 dfd.resolve(entries);
     168           12 :             })
     169          130 :             .fail(function(ex) {
     170              :             /* The journalctl command fails when no entries are matched
     171              :              * so we just ignore this status code */
     172          130 :                 if (ex.problem == "cancelled" ||
     173            4 :                 ex.exit_status === 1) {
     174          130 :                     fire_streamers();
     175          130 :                     dfd.resolve(entries);
     176            3 :                 } else {
     177            3 :                     dfd.reject(ex);
     178            3 :                 }
     179          130 :             })
     180          133 :             .always(function() {
     181          133 :                 window.clearInterval(interval);
     182          133 :             });
     183              : 
     184          167 :     promise.stream = function stream(callback) {
     185          167 :         streamers.push(callback);
     186          167 :         return this;
     187          167 :     };
     188          130 :     promise.stop = function stop() {
     189          130 :         streamers = [];
     190          130 :         promise.stopped = true;
     191          130 :         proc.close("cancelled");
     192          130 :     };
     193          167 :     return promise;
     194          167 : };
     195              : 
     196          126 : journal.printable = function printable(value, key) {
     197          126 :     if (value === undefined || value === null)
     198           14 :         return _("[no data]");
     199          126 :     else if (typeof (value) == "string")
     200           14 :         return value;
     201           14 :     else if (value.length !== undefined && value.length <= 1000 && key == "MESSAGE")
     202           14 :         return new TextDecoder().decode(new Uint8Array(value));
     203           14 :     else {
     204           14 :         return _("[binary data]");
     205           14 :     }
     206          126 : };
     207              : 
     208              : /* Render the journal entries by passing suitable DOM elements back to
     209              :    the caller via the 'output_funcs'.
     210              : 
     211              :    Rendering is context aware.  It will insert 'reboot' markers, for
     212              :    example, and collapse repeated lines.  You can extend the output at
     213              :    the bottom and also at the top.
     214              : 
     215              :    A new renderer is created by calling 'journal.renderer' like
     216              :    so:
     217              : 
     218              :       const renderer = journal.renderer(funcs);
     219              : 
     220              :    You can feed new entries into the renderer by calling various
     221              :    methods on the returned object:
     222              : 
     223              :       - renderer.append(journal_entry)
     224              :       - renderer.append_flush()
     225              :       - renderer.prepend(journal_entry)
     226              :       - renderer.prepend_flush()
     227              : 
     228              :    A 'journal_entry' is one element of the result array returned by a
     229              :    call to 'Query' with the 'cockpit.journal_fields' as the fields to
     230              :    return.
     231              : 
     232              :    Calling 'append' will append the given entry to the end of the
     233              :    output, naturally, and 'prepend' will prepend it to the start.
     234              : 
     235              :    The output might lag behind what has been input via 'append' and
     236              :    'prepend', and you need to call 'append_flush' and 'prepend_flush'
     237              :    respectively to ensure that the output is up-to-date.  Flushing a
     238              :    renderer does not introduce discontinuities into the output.  You
     239              :    can continue to feed entries into the renderer after flushing and
     240              :    repeated lines will be correctly collapsed across the flush, for
     241              :    example.
     242              : 
     243              :    The renderer will call methods of the 'output_funcs' object to
     244              :    produce the desired output:
     245              : 
     246              :       - output_funcs.append(rendered)
     247              :       - output_funcs.remove_last()
     248              :       - output_funcs.prepend(rendered)
     249              :       - output_funcs.remove_first()
     250              : 
     251              :    The 'rendered' argument is the return value of one of the rendering
     252              :    functions described below.  The 'append' and 'prepend' methods
     253              :    should add this element to the output, naturally, and 'remove_last'
     254              :    and 'remove_first' should remove the indicated element.
     255              : 
     256              :    If you never call 'prepend' on the renderer, 'output_func.prepend'
     257              :    isn't called either.  If you never call 'renderer.prepend' after
     258              :    'renderer.prepend_flush', then 'output_func.remove_first' will
     259              :    never be called.  The same guarantees exist for the 'append' family
     260              :    of functions.
     261              : 
     262              :    The actual rendering is also done by calling methods on
     263              :    'output_funcs':
     264              : 
     265              :       - output_funcs.render_line(ident, prio, message, count, time, cursor)
     266              :       - output_funcs.render_day_header(day)
     267              :       - output_funcs.render_reboot_separator()
     268              : */
     269              : 
     270          167 : journal.renderer = function renderer(output_funcs) {
     271          167 :     if (!output_funcs.render_line)
     272           17 :         console.error("Invalid renderer provided");
     273              : 
     274          119 :     function copy_object(o) {
     275          119 :         const c = { }; for (const p in o) c[p] = o[p]; return c;
     276          119 :     }
     277              : 
     278              :     // A 'entry' object describes a journal entry in formatted form.
     279              :     // It has fields 'bootid', 'ident', 'prio', 'message', 'time',
     280              :     // 'day', all of which are strings.
     281              : 
     282          126 :     function format_entry(journal_entry) {
     283          126 :         const d = journal_entry.__REALTIME_TIMESTAMP / 1000; // timestamps are in µs
     284          126 :         return {
     285          126 :             cursor: journal_entry.__CURSOR,
     286          126 :             full: journal_entry,
     287          126 :             day: timeformat.date(d),
     288          126 :             time: timeformat.time(d),
     289          126 :             bootid: journal_entry._BOOT_ID,
     290           74 :             ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM,
     291          126 :             prio: journal_entry.PRIORITY,
     292          126 :             message: journal.printable(journal_entry.MESSAGE, "MESSAGE")
     293          126 :         };
     294          126 :     }
     295              : 
     296          126 :     function entry_is_equal(a, b) {
     297          119 :         return (a && b &&
     298          119 :                 a.day == b.day &&
     299          119 :                 a.bootid == b.bootid &&
     300          119 :                 a.ident == b.ident &&
     301          118 :                 a.prio == b.prio &&
     302          116 :                 a.message == b.message);
     303          126 :     }
     304              : 
     305              :     // A state object describes a line that should be eventually
     306              :     // output.  It has an 'entry' field as per description above, and
     307              :     // also 'count', 'last_time', and 'first_time', which record
     308              :     // repeated entries.  Additionally:
     309              :     //
     310              :     // line_present: When true, the line has been output already with
     311              :     //     some preliminary data.  It needs to be removed before
     312              :     //     outputting more recent data.
     313              :     //
     314              :     // header_present: The day header has been output preliminarily
     315              :     //     before the actual log lines.  It needs to be removed before
     316              :     //     prepending more lines.  If both line_present and
     317              :     //     header_present are true, then the header comes first in the
     318              :     //     output, followed by the line.
     319              : 
     320          126 :     function render_state_line(state) {
     321          126 :         return output_funcs.render_line(state.entry.ident,
     322          126 :                                         state.entry.prio,
     323          126 :                                         state.entry.message,
     324          126 :                                         state.count,
     325          126 :                                         state.last_time,
     326          126 :                                         state.entry.full);
     327          126 :     }
     328              : 
     329              :     // We keep the state of the first and last journal lines,
     330              :     // respectively, in order to collapse repeated lines, and to
     331              :     // insert reboot markers and day headers.
     332              :     //
     333              :     // Normally, there are two state objects, but if only a single
     334              :     // line has been output so far, top_state and bottom_state point
     335              :     // to the same object.
     336              : 
     337          167 :     let top_state;
     338          167 :     let bottom_state;
     339              : 
     340          167 :     top_state = bottom_state = { };
     341              : 
     342          126 :     function start_new_line() {
     343              :         // If we now have two lines, split the state
     344          119 :         if (top_state === bottom_state && top_state.entry) {
     345          119 :             top_state = copy_object(bottom_state);
     346          119 :         }
     347          126 :     }
     348              : 
     349          124 :     function top_output() {
     350           49 :         if (top_state.header_present) {
     351           49 :             output_funcs.remove_first();
     352           49 :             top_state.header_present = false;
     353           49 :         }
     354           49 :         if (top_state.line_present) {
     355           49 :             output_funcs.remove_first();
     356           49 :             top_state.line_present = false;
     357           49 :         }
     358          124 :         if (top_state.entry) {
     359          124 :             output_funcs.prepend(render_state_line(top_state));
     360          124 :             top_state.line_present = true;
     361          124 :         }
     362          124 :     }
     363              : 
     364          117 :     function prepend(journal_entry) {
     365          117 :         const entry = format_entry(journal_entry);
     366              : 
     367           20 :         if (entry_is_equal(top_state.entry, entry)) {
     368           20 :             top_state.count += 1;
     369           20 :             top_state.first_time = entry.time;
     370           20 :         } else {
     371          117 :             top_output();
     372              : 
     373          110 :             if (top_state.entry) {
     374          110 :                 if (entry.bootid != top_state.entry.bootid)
     375           10 :                     output_funcs.prepend(output_funcs.render_reboot_separator());
     376          110 :                 if (entry.day != top_state.entry.day)
     377           10 :                     output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
     378          110 :             }
     379              : 
     380          117 :             start_new_line();
     381          117 :             top_state.entry = entry;
     382          117 :             top_state.count = 1;
     383          117 :             top_state.first_time = top_state.last_time = entry.time;
     384          117 :             top_state.line_present = false;
     385          117 :         }
     386          117 :     }
     387              : 
     388          124 :     function prepend_flush() {
     389          124 :         top_output();
     390          124 :         if (top_state.entry) {
     391          124 :             output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
     392          124 :             top_state.header_present = true;
     393          124 :         }
     394          124 :     }
     395              : 
     396           11 :     function bottom_output() {
     397            6 :         if (bottom_state.line_present) {
     398            6 :             output_funcs.remove_last();
     399            6 :             bottom_state.line_present = false;
     400            6 :         }
     401           11 :         if (bottom_state.entry) {
     402           11 :             output_funcs.append(render_state_line(bottom_state));
     403           11 :             bottom_state.line_present = true;
     404           11 :         }
     405           11 :     }
     406              : 
     407           11 :     function append(journal_entry) {
     408           11 :         const entry = format_entry(journal_entry);
     409              : 
     410            6 :         if (entry_is_equal(bottom_state.entry, entry)) {
     411            6 :             bottom_state.count += 1;
     412            6 :             bottom_state.last_time = entry.time;
     413            6 :         } else {
     414           11 :             bottom_output();
     415              : 
     416           10 :             if (!bottom_state.entry || entry.day != bottom_state.entry.day) {
     417           11 :                 output_funcs.append(output_funcs.render_day_header(entry.day));
     418           11 :                 bottom_state.header_present = true;
     419           11 :             }
     420           10 :             if (bottom_state.entry && entry.bootid != bottom_state.entry.bootid)
     421            5 :                 output_funcs.append(output_funcs.render_reboot_separator());
     422              : 
     423           11 :             start_new_line();
     424           11 :             bottom_state.entry = entry;
     425           11 :             bottom_state.count = 1;
     426           11 :             bottom_state.first_time = bottom_state.last_time = entry.time;
     427           11 :             bottom_state.line_present = false;
     428           11 :         }
     429           11 :     }
     430              : 
     431           11 :     function append_flush() {
     432           11 :         bottom_output();
     433           11 :     }
     434              : 
     435          167 :     return {
     436          167 :         prepend,
     437          167 :         prepend_flush,
     438          167 :         append,
     439          167 :         append_flush
     440          167 :     };
     441          167 : };
        

Generated by: LCOV version 2.0-1