Add support for tailing logs in real time

This commit is contained in:
Ilia Ross
2024-05-27 21:52:24 +03:00
parent 8164480b48
commit bb7938a0f5
5 changed files with 234 additions and 105 deletions

View File

@@ -1,5 +1,5 @@
skip_index=1
lines=1000
lines=100
others=0
reverse=1
log_any=0

View File

@@ -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

View File

@@ -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

View File

@@ -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("<tt>".&html_escape($file || $cmd_unpacked)."</tt>",
@@ -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 "<pre>";
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 "<pre>";
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 "<i>$text{'view_empty'}</i>\n"
if (!$got || $safe_proc_out =~ /-- No entries --/m);
print "</pre>\n";
}
# Progressive output
else {
print "<pre id='logdata' data-reversed='$reverse'>";
print "</pre>\n";
print <<EOF;
<script>
// 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);
});
})();
</script>
EOF
}
print "<i>$text{'view_empty'}</i>\n"
if (!$got || $safe_proc_out =~ /-- No entries --/m);
print "</pre>\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&nbsp; " .
my $since_label = $follow ? $text{'journal_sincefollow'} :
$text{'journal_since'};
$sel .= "$since_label&nbsp; " .
&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, "&nbsp;" . &ui_textbox("lines", $lines, 3), "&nbsp;$sel"),"\n";
if ($follow) {
print &text('view_header3', "&nbsp;$sel"),"\n";
}
else {
print &text($text_view_header, "&nbsp;" . &ui_textbox("lines", $lines, 3), "&nbsp;$sel"),"\n";
}
print "&nbsp;&nbsp;&nbsp;&nbsp;\n";
print &text('view_filter', "&nbsp;" . &ui_textbox("filter", $in{'filter'}, 12)),"\n";
print "&nbsp;&nbsp;\n";
print &ui_submit($text{'view_refresh'});
print &ui_submit($follow ? $text{'view_filter2'} : $text{'view_refresh'});
print &ui_form_end(),"<br>\n";
}

View File

@@ -0,0 +1,47 @@
#!/usr/local/bin/perl
# view_log_progress.cgi
# Returns progressive output for some system log
require './logviewer-lib.pl';
&ReadParse();
&foreign_require("proc", "proc-lib.pl");
# System log to follow
my @systemctl_cmds = &get_systemctl_cmds(1);
my ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds;
my $cmd = $log->{'cmd'};
# Disable output buffering
print "Content-Type: text/plain\n\n";
$| = 1;
# Access check
if (!$cmd || $cmd !~ /^journalctl/ ||
!(&can_edit_log($log) && $access{'syslog'})) {
print $text{'save_ecannot3'};
exit;
}
# No lines for real time logs
$cmd =~ s/\s+\-n\s+\d+//;
# Show real time logs
$cmd .= " -f";
# Add filter to the command if present
my $filter = $in{'filter'} ? quotemeta($in{'filter'}) : "";
if ($filter) {
$cmd .= " -g $filter";
}
# Open a pipe to the journalctl command
my $pid = open(my $fh, '-|', "$cmd") ||
print &text('save_ecannot4', $cmd).": $!";
# Read and output the log
while (my $line = <$fh>) {
print $line;
}
# Clean up when done
close($fh);