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-03 07:31:16

            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          165 : journal.build_cmd = function build_cmd(/* ... */) {
      55          165 :     const matches = [];
      56          165 :     const options = { follow: true };
      57          165 :     for (let i = 0; i < arguments.length; i++) {
      58          165 :         const arg = arguments[i];
      59           15 :         if (typeof arg == "string") {
      60           15 :             matches.push(arg);
      61           15 :         } else if (typeof arg == "object") {
      62          165 :             if (arg instanceof Array) {
      63          165 :                 matches.push.apply(matches, arg);
      64          165 :             } else {
      65          165 :                 Object.assign(options, arg);
      66          165 :                 break;
      67          165 :             }
      68           15 :         } else {
      69           15 :             console.warn("journal.journalctl called with invalid argument:", arg);
      70           15 :         }
      71          165 :     }
      72              : 
      73           24 :     if (options.count === undefined) {
      74           24 :         if (options.follow)
      75           15 :             options.count = 10;
      76              :         else
      77           24 :             options.count = null;
      78           24 :     }
      79              : 
      80          165 :     const cmd = ["journalctl", "-q"];
      81          165 :     if (!options.count)
      82           20 :         cmd.push("--no-tail");
      83              :     else
      84          161 :         cmd.push("--lines=" + options.count);
      85              : 
      86          165 :     cmd.push("--output=" + (options.output || "json"));
      87              : 
      88          165 :     if (options.directory)
      89           15 :         cmd.push("--directory=" + options.directory);
      90          165 :     if (options.boot)
      91           16 :         cmd.push("--boot=" + options.boot);
      92          165 :     else if (options.boot !== undefined)
      93           15 :         cmd.push("--boot");
      94          165 :     if (options.since)
      95           24 :         cmd.push("--since=" + options.since);
      96          165 :     if (options.until)
      97           17 :         cmd.push("--until=" + options.until);
      98          165 :     if (options.cursor)
      99           24 :         cmd.push("--cursor=" + options.cursor);
     100          165 :     if (options.after)
     101           15 :         cmd.push("--after=" + options.after);
     102          165 :     if (options.priority)
     103           24 :         cmd.push("--priority=" + options.priority);
     104          165 :     if (options.grep)
     105           16 :         cmd.push("--grep=" + options.grep);
     106              : 
     107              :     /* journalctl doesn't allow reverse and follow together */
     108          165 :     if (options.reverse)
     109           24 :         cmd.push("--reverse");
     110          165 :     else if (options.follow)
     111          165 :         cmd.push("--follow");
     112              : 
     113          165 :     cmd.push("--");
     114          165 :     cmd.push.apply(cmd, matches);
     115          165 :     return cmd;
     116          165 : };
     117              : 
     118          165 : journal.journalctl = function journalctl(/* ... */) {
     119          165 :     const cmd = journal.build_cmd.apply(null, arguments);
     120              : 
     121          165 :     const dfd = cockpit.defer();
     122          165 :     const promise = dfd.promise();
     123          165 :     let buffer = "";
     124          165 :     let entries = [];
     125          165 :     let streamers = [];
     126          165 :     let interval = null;
     127              : 
     128          159 :     function fire_streamers() {
     129          159 :         let ents;
     130          159 :         let i;
     131          123 :         if (streamers.length && entries.length > 0) {
     132          123 :             ents = entries;
     133          123 :             entries = [];
     134          123 :             for (i = 0; i < streamers.length; i++)
     135          123 :                 streamers[i].apply(promise, [ents]);
     136          122 :         } else {
     137          158 :             window.clearInterval(interval);
     138          158 :             interval = null;
     139          158 :         }
     140          159 :     }
     141              : 
     142          165 :     const proc = cockpit.spawn(cmd, { batch: 8192, latency: 300, superuser: "try" })
     143          161 :             .stream(function(data) {
     144          161 :                 if (buffer)
     145          112 :                     data = buffer + data;
     146          161 :                 buffer = "";
     147              : 
     148          161 :                 const lines = data.split("\n");
     149          161 :                 const last = lines.length - 1;
     150          161 :                 lines.forEach(function(line, i) {
     151          161 :                     if (i == last) {
     152          161 :                         buffer = line;
     153          161 :                     } else if (line && line.indexOf("-- ") !== 0) {
     154          161 :                         try {
     155          161 :                             entries.push(JSON.parse(line));
     156           14 :                         } catch (e) {
     157           14 :                             console.warn(e, line);
     158           14 :                         }
     159          161 :                     }
     160          161 :                 });
     161              : 
     162          161 :                 if (streamers.length && interval === null)
     163          161 :                     interval = window.setInterval(fire_streamers, 300);
     164          161 :             })
     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          165 :     promise.stream = function stream(callback) {
     185          165 :         streamers.push(callback);
     186          165 :         return this;
     187          165 :     };
     188          130 :     promise.stop = function stop() {
     189          130 :         streamers = [];
     190          130 :         promise.stopped = true;
     191          130 :         proc.close("cancelled");
     192          130 :     };
     193          165 :     return promise;
     194          165 : };
     195              : 
     196          123 : journal.printable = function printable(value, key) {
     197          123 :     if (value === undefined || value === null)
     198           13 :         return _("[no data]");
     199          123 :     else if (typeof (value) == "string")
     200           13 :         return value;
     201           13 :     else if (value.length !== undefined && value.length <= 1000 && key == "MESSAGE")
     202           13 :         return new TextDecoder().decode(new Uint8Array(value));
     203           13 :     else {
     204           13 :         return _("[binary data]");
     205           13 :     }
     206          123 : };
     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          165 : journal.renderer = function renderer(output_funcs) {
     271          165 :     if (!output_funcs.render_line)
     272           15 :         console.error("Invalid renderer provided");
     273              : 
     274          116 :     function copy_object(o) {
     275          116 :         const c = { }; for (const p in o) c[p] = o[p]; return c;
     276          116 :     }
     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          123 :     function format_entry(journal_entry) {
     283          123 :         const d = journal_entry.__REALTIME_TIMESTAMP / 1000; // timestamps are in µs
     284          123 :         return {
     285          123 :             cursor: journal_entry.__CURSOR,
     286          123 :             full: journal_entry,
     287          123 :             day: timeformat.date(d),
     288          123 :             time: timeformat.time(d),
     289          123 :             bootid: journal_entry._BOOT_ID,
     290           71 :             ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM,
     291          123 :             prio: journal_entry.PRIORITY,
     292          123 :             message: journal.printable(journal_entry.MESSAGE, "MESSAGE")
     293          123 :         };
     294          123 :     }
     295              : 
     296          123 :     function entry_is_equal(a, b) {
     297          116 :         return (a && b &&
     298          116 :                 a.day == b.day &&
     299          116 :                 a.bootid == b.bootid &&
     300          116 :                 a.ident == b.ident &&
     301          115 :                 a.prio == b.prio &&
     302          113 :                 a.message == b.message);
     303          123 :     }
     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          123 :     function render_state_line(state) {
     321          123 :         return output_funcs.render_line(state.entry.ident,
     322          123 :                                         state.entry.prio,
     323          123 :                                         state.entry.message,
     324          123 :                                         state.count,
     325          123 :                                         state.last_time,
     326          123 :                                         state.entry.full);
     327          123 :     }
     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          165 :     let top_state;
     338          165 :     let bottom_state;
     339              : 
     340          165 :     top_state = bottom_state = { };
     341              : 
     342          123 :     function start_new_line() {
     343              :         // If we now have two lines, split the state
     344          116 :         if (top_state === bottom_state && top_state.entry) {
     345          116 :             top_state = copy_object(bottom_state);
     346          116 :         }
     347          123 :     }
     348              : 
     349          121 :     function top_output() {
     350           50 :         if (top_state.header_present) {
     351           50 :             output_funcs.remove_first();
     352           50 :             top_state.header_present = false;
     353           50 :         }
     354           50 :         if (top_state.line_present) {
     355           50 :             output_funcs.remove_first();
     356           50 :             top_state.line_present = false;
     357           50 :         }
     358          121 :         if (top_state.entry) {
     359          121 :             output_funcs.prepend(render_state_line(top_state));
     360          121 :             top_state.line_present = true;
     361          121 :         }
     362          121 :     }
     363              : 
     364          115 :     function prepend(journal_entry) {
     365          115 :         const entry = format_entry(journal_entry);
     366              : 
     367           19 :         if (entry_is_equal(top_state.entry, entry)) {
     368           19 :             top_state.count += 1;
     369           19 :             top_state.first_time = entry.time;
     370           19 :         } else {
     371          115 :             top_output();
     372              : 
     373          108 :             if (top_state.entry) {
     374          108 :                 if (entry.bootid != top_state.entry.bootid)
     375           10 :                     output_funcs.prepend(output_funcs.render_reboot_separator());
     376          108 :                 if (entry.day != top_state.entry.day)
     377           10 :                     output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
     378          108 :             }
     379              : 
     380          115 :             start_new_line();
     381          115 :             top_state.entry = entry;
     382          115 :             top_state.count = 1;
     383          115 :             top_state.first_time = top_state.last_time = entry.time;
     384          115 :             top_state.line_present = false;
     385          115 :         }
     386          115 :     }
     387              : 
     388          121 :     function prepend_flush() {
     389          121 :         top_output();
     390          121 :         if (top_state.entry) {
     391          121 :             output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
     392          121 :             top_state.header_present = true;
     393          121 :         }
     394          121 :     }
     395              : 
     396           11 :     function bottom_output() {
     397            5 :         if (bottom_state.line_present) {
     398            5 :             output_funcs.remove_last();
     399            5 :             bottom_state.line_present = false;
     400            5 :         }
     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            5 :         if (entry_is_equal(bottom_state.entry, entry)) {
     411            5 :             bottom_state.count += 1;
     412            5 :             bottom_state.last_time = entry.time;
     413            5 :         } 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            2 :                 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          165 :     return {
     436          165 :         prepend,
     437          165 :         prepend_flush,
     438          165 :         append,
     439          165 :         append_flush
     440          165 :     };
     441          165 : };
        

Generated by: LCOV version 2.0-1