mirror of
https://github.com/webmin/webmin.git
synced 2026-02-03 14:13:29 +00:00
Add support for tailing logs in real time
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
skip_index=1
|
||||
lines=1000
|
||||
lines=100
|
||||
others=0
|
||||
reverse=1
|
||||
log_any=0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 " .
|
||||
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(),"<br>\n";
|
||||
}
|
||||
|
||||
|
||||
47
logviewer/view_log_progress.cgi
Normal file
47
logviewer/view_log_progress.cgi
Normal 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);
|
||||
Reference in New Issue
Block a user