diff --git a/logviewer/config b/logviewer/config index 3bf7bf31f..13952b3a2 100644 --- a/logviewer/config +++ b/logviewer/config @@ -1,5 +1,5 @@ skip_index=1 -lines=1000 +lines=100 others=0 reverse=1 log_any=0 diff --git a/logviewer/lang/en b/logviewer/lang/en index 90938c829..66975e5cc 100644 --- a/logviewer/lang/en +++ b/logviewer/lang/en @@ -17,25 +17,34 @@ journal_journalctl_notice_warning=Notice and warning messages journal_journalctl_err_crit=Error and critical messages journal_journalctl_alert_emerg=Alert and emergency messages journal_journalctl_unit=Messages for specific unit -journal_since0=Current boot -journal_since1=7 days ago -journal_since2=24 hours ago -journal_since3=8 hours ago -journal_since4=1 hour ago -journal_since5=30 minutes ago -journal_since6=10 minutes ago -journal_since7=2 minute ago +journal_since0=Latest available +journal_since1=Real-time follow +journal_since2=Current boot +journal_since3=7 days ago +journal_since4=24 hours ago +journal_since5=8 hours ago +journal_since6=1 hour ago +journal_since7=30 minutes ago +journal_since8=10 minutes ago +journal_since9=3 minutes ago +journal_since10=1 minute ago +journal_sincefollow=in +journal_since=since view_title=View Logfile view_titlejournal=View Journal view_header=Last $1 lines of $2 view_header2=Last $1 lines +view_header3=Lines of $1 view_empty=Log file is empty view_refresh=Refresh view_filter=Filter lines with text $1 +view_filter2=Filter save_efile='$1' is not a valid filename : $2 save_ecannot2=You are not allowed to view this log +save_ecannot3=Error: You are not allowed to view this log +save_ecannot4=Error: Could not open '$1' save_ecannot6=You are not allowed to view arbitrary logs save_ecannot7=You are not allowed to view this extra log save_emissing=Missing log file to view diff --git a/logviewer/logviewer-lib.pl b/logviewer/logviewer-lib.pl index a6bb79045..4bd810c14 100755 --- a/logviewer/logviewer-lib.pl +++ b/logviewer/logviewer-lib.pl @@ -27,6 +27,21 @@ foreach $f (@files) { return 0; } +# get_journal_since +# Returns a list of journalctl commands to get logs since various times, +# which should correspond with language strings journal_since0, +# journal_since1, journal_since2, etc. +sub get_journal_since +{ +return + ("", "-f", + "-b", "-S '7 days ago'", + "-S '24 hours ago'", "-S '8 hours ago'", + "-S '1 hour ago'", "-S '30 minutes ago'", + "-S '10 minutes ago'", "-S '3 minutes ago'", + "-S '1 minute ago'"); +} + # get_systemctl_cmds([force-select]) # Returns logs for journalctl sub get_systemctl_cmds diff --git a/logviewer/view_log.cgi b/logviewer/view_log.cgi index 04aa1407f..7d586a2f6 100755 --- a/logviewer/view_log.cgi +++ b/logviewer/view_log.cgi @@ -7,7 +7,7 @@ require './logviewer-lib.pl'; &foreign_require("proc", "proc-lib.pl"); # Viewing a log file -@extras = &extra_log_files(); +my @extras = &extra_log_files(); if ($in{'idx'} =~ /^\//) { # The drop-down selector on this page has chosen a file if (&indexof($in{'idx'}, (map { $_->{'file'} } @extras)) >= 0) { @@ -21,25 +21,23 @@ if ($in{'idx'} =~ /^\//) { delete($in{'idx'}); delete($in{'oidx'}); } -my @journal_since = - ("-b", "-S '7 days ago'", - "-S '24 hours ago'", "-S '8 hours ago'", - "-S '1 hour ago'", "-S '30 minutes ago'", - "-S '10 minutes ago'", "-S '1 minute ago'"); +my @journal_since = &get_journal_since(); if ($in{'idx'} ne '') { # From systemctl commands if ($in{'idx'} =~ /^journal-/) { my @systemctl_cmds = &get_systemctl_cmds(1); my ($log); if ($in{'idx'} eq 'journal-u') { - ($log) = grep { $_->{'cmd'} =~ /-u\s+\w+/ } @systemctl_cmds; + ($log) = grep { $_->{'cmd'} =~ /-u\s+\w+/ } + @systemctl_cmds; $in{'idx'} = $log->{'id'}; } else { - ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds; - } + ($log) = grep { $_->{'id'} eq $in{'idx'} } + @systemctl_cmds; + } # If reverse is set, add it to the command - if ($config{'reverse'}) { + if ($reverse) { $log->{'cmd'} .= " -r"; } # If since is set and allowed, add it to the command @@ -114,6 +112,11 @@ else { } print "Refresh: $config{'refresh'}\r\n" if ($config{'refresh'}); +my $lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'}); +my $jfilter = $in{'filter'} ? $in{'filter'} : ""; +my $filter = $jfilter ? quotemeta($jfilter) : ""; +my $reverse = $config{'reverse'} ? 1 : 0; +my $follow = $in{'since'} eq '-f' ? 1 : 0; my $no_navlinks = $in{'nonavlinks'} == 1 ? 1 : undef; my $skip_index = $config{'skip_index'} == 1 ? 1 : undef; my $help_link = (!$no_navlinks && $skip_index) ? @@ -121,6 +124,9 @@ my $help_link = (!$no_navlinks && $skip_index) ? my $no_links = $no_navlinks || $skip_index; my $cmd_unpacked = $cmd; $cmd_unpacked =~ s/\\x([0-9A-Fa-f]{2})/pack('H2', $1)/eg; +$cmd_unpacked =~ s/\s+\-r// if ($follow); +$cmd_unpacked =~ s/\s+\-n\s+\d+// if ($follow); +$cmd_unpacked .= " -g \"@{[&html_escape($jfilter)]}\"" if ($filter); my $view_title = $in{'idx'} =~ /^journal/ ? $text{'view_titlejournal'} : $text{'view_title'}; &ui_print_header("".&html_escape($file || $cmd_unpacked)."", @@ -129,99 +135,144 @@ my $view_title = $in{'idx'} =~ /^journal/ ? ($no_navlinks || $skip_index) ? 1 : undef, 0, $help_link); -$lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'}); -$filter = $in{'filter'} ? quotemeta($in{'filter'}) : ""; - &filter_form(); -$| = 1; -print "
";
-local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
-$tailcmd =~ s/LINES/$lines/g;
-my $safe_proc_out;
-if ($filter ne "") {
- # Are we supposed to filter anything? Then use grep.
- local @cats;
- if ($cmd) {
- # Getting output from a command
- push(@cats, $cmd);
- }
- elsif ($config{'compressed'}) {
- # All compressed versions
- foreach $l (&all_log_files($file)) {
- $c = &catter_command($l);
- push(@cats, $c) if ($c);
- }
- }
- else {
- # Just the one log
- @cats = ( "cat ".quotemeta($file) );
- }
- $cat = "(".join(" ; ", @cats).")";
- if ($config{'reverse'}) {
- $tailcmd .= " | tac" if ($fullcmd !~ /journalctl/);
- }
- $eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
- $dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
- if (@cats) {
- $got = &proc::safe_process_exec(
- "$cat | grep -i -a $eflag $dashflag $filter ".
- "| $tailcmd",
- 0, 0, STDOUT, undef, 1, 0, undef, 1);
- }
- else {
- $got = undef;
- }
-} else {
- # Not filtering .. so cat the most recent non-empty file
- if ($cmd) {
- # Getting output from a command
- $fullcmd = $cmd.($fullcmd !~ /journalctl/ ? "" : " | ".$tailcmd);
- }
- elsif ($config{'compressed'}) {
- # Cat all compressed files
+# Standard output
+if (!$follow) {
+ $| = 1;
+ print "";
+ local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
+ $tailcmd =~ s/LINES/$lines/g;
+ my $safe_proc_out;
+ if ($filter ne "") {
+ # Are we supposed to filter anything? Then use grep.
local @cats;
- $total = 0;
- foreach $l (reverse(&all_log_files($file))) {
- next if (!-s $l);
- $c = &catter_command($l);
- if ($c) {
- $len = int(&backquote_command(
- "$c | wc -l"));
- $total += $len;
- push(@cats, $c);
- last if ($total > $in{'lines'});
+ if ($cmd) {
+ # Getting output from a command
+ push(@cats, $cmd);
+ }
+ elsif ($config{'compressed'}) {
+ # All compressed versions
+ foreach $l (&all_log_files($file)) {
+ $c = &catter_command($l);
+ push(@cats, $c) if ($c);
}
}
+ else {
+ # Just the one log
+ @cats = ( "cat ".quotemeta($file) );
+ }
+ $cat = "(".join(" ; ", @cats).")";
+ if ($reverse) {
+ $tailcmd .= " | tac" if ($cmd !~ /journalctl/);
+ }
+ $eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
+ $dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
if (@cats) {
- $cat = "(".join(" ; ", reverse(@cats)).")";
- $fullcmd = $cat." | ".$tailcmd;
+ my $fcmd;
+ if ($cmd =~ /journalctl/) {
+ $fcmd = "$cmd -g $filter";
+ }
+ else {
+ $fcmd = "$cat | grep -i -a $eflag $dashflag $filter ".
+ "| $tailcmd";
+ }
+ $got = &proc::safe_process_exec($fcmd,
+ 0, 0, STDOUT, undef, 1, 0, undef, 1);
}
else {
- $fullcmd = undef;
+ $got = undef;
+ }
+ } else {
+ # Not filtering .. so cat the most recent non-empty file
+ if ($cmd) {
+ # Getting output from a command
+ $fullcmd = $cmd.($cmd =~ /journalctl/ ? "" : (" | ".$tailcmd));
+ }
+ elsif ($config{'compressed'}) {
+ # Cat all compressed files
+ local @cats;
+ $total = 0;
+ foreach $l (reverse(&all_log_files($file))) {
+ next if (!-s $l);
+ $c = &catter_command($l);
+ if ($c) {
+ $len = int(&backquote_command(
+ "$c | wc -l"));
+ $total += $len;
+ push(@cats, $c);
+ last if ($total > $in{'lines'});
+ }
+ }
+ if (@cats) {
+ $cat = "(".join(" ; ", reverse(@cats)).")";
+ $fullcmd = $cat." | ".$tailcmd;
+ }
+ else {
+ $fullcmd = undef;
+ }
+ }
+ else {
+ # Just run tail on the file
+ $fullcmd = $tailcmd." ".quotemeta($file);
+ }
+ if ($reverse && $fullcmd) {
+ $fullcmd .= " | tac" if ($fullcmd !~ /journalctl/);
+ }
+ if ($fullcmd) {
+ open(my $output_fh, '>', \$safe_proc_out);
+ $got = &proc::safe_process_exec(
+ $fullcmd, 0, 0, $output_fh, undef, 1, 0, undef, 1);
+ close($output_fh);
+ print $safe_proc_out if ($safe_proc_out !~ /-- No entries --/m);
+ }
+ else {
+ $got = undef;
}
}
- else {
- # Just run tail on the file
- $fullcmd = $tailcmd." ".quotemeta($file);
- }
- if ($config{'reverse'} && $fullcmd) {
- $fullcmd .= " | tac" if ($fullcmd !~ /journalctl/);
- }
- if ($fullcmd) {
- open(my $output_fh, '>', \$safe_proc_out);
- $got = &proc::safe_process_exec(
- $fullcmd, 0, 0, $output_fh, undef, 1, 0, undef, 1);
- close($output_fh);
- print $safe_proc_out if ($safe_proc_out !~ /-- No entries --/m);
- }
- else {
- $got = undef;
- }
+ print "$text{'view_empty'}\n"
+ if (!$got || $safe_proc_out =~ /-- No entries --/m);
+ print "\n";
+ }
+# Progressive output
+else {
+ print "";
+ print "
\n";
+ print <
+ // Update log viewer with new data from the server
+ (async function () {
+ const logDataElement = document.getElementById("logdata"),
+ response = await fetch("view_log_progress.cgi?idx=$in{'idx'}&filter=$jfilter"),
+ reader = response.body.getReader(),
+ decoder = new TextDecoder("utf-8"),
+ processText = async function () {
+ let { done, value } = await reader.read();
+ while (!done) {
+ const chunk = decoder.decode(value, { stream: true }).trim(),
+ dataReversed = logDataElement.getAttribute("data-reversed");
+ let lines = chunk.split("\\n");
+ if (dataReversed === "1") {
+ lines = lines.reverse();
+ logDataElement.textContent =
+ lines.join("\\n") + "\\n" + logDataElement.textContent;
+ }
+ else {
+ logDataElement.textContent += lines.join("\\n") + "\\n";
+ }
+ if (typeof logviewer_progress_update === 'function') {
+ logviewer_progress_update(chunk, dataReversed);
+ }
+ ({ done, value } = await reader.read());
+ }
+ };
+ processText().catch((error) => {
+ console.error("Failed to fetch log progress:", error);
+ });
+ })();
+
+EOF
}
-print "$text{'view_empty'}\n"
- if (!$got || $safe_proc_out =~ /-- No entries --/m);
-print " \n";
&filter_form();
if ($no_links) {
&ui_print_footer();
@@ -315,7 +366,9 @@ if (@logfiles && $found) {
push(@$selots, [ $journal_since[$i],
$text{'journal_since'.$i} ]);
}
- $sel .= "since " .
+ my $since_label = $follow ? $text{'journal_sincefollow'} :
+ $text{'journal_since'};
+ $sel .= "$since_label " .
&ui_select("since", $in{'since'}, $selots, undef,
undef, undef, undef, "onChange='form.submit()'");
}
@@ -324,12 +377,17 @@ else {
$text_view_header = 'view_header2';
print &ui_hidden("idx", $in{'idx'}),"\n";
}
-
-print &text($text_view_header, " " . &ui_textbox("lines", $lines, 3), " $sel"),"\n";
+if ($follow) {
+ print &text('view_header3', " $sel"),"\n";
+ }
+else {
+ print &text($text_view_header, " " . &ui_textbox("lines", $lines, 3), " $sel"),"\n";
+ }
print " \n";
print &text('view_filter', " " . &ui_textbox("filter", $in{'filter'}, 12)),"\n";
+
print " \n";
-print &ui_submit($text{'view_refresh'});
+print &ui_submit($follow ? $text{'view_filter2'} : $text{'view_refresh'});
print &ui_form_end(),"