Add an option to rotate Miniserv logs using logrotate
This PR adds the ability for the logging pages to hand the Miniserv access and error logs, and the Webmin actions log, to logrotate instead of having Miniserv periodically delete them. The generated section uses copy-truncate rotation, since Miniserv only reopens its error log when the file disappears. The option only appears when the Log File Rotation module is available and configured correctly.
This commit is contained in:
+21
-3
@@ -13,14 +13,24 @@ my $journal_changed = defined($in{'error_journal'}) &&
|
||||
&webmin::miniserv_systemd_journal_available("usermin.service") &&
|
||||
($miniserv{'errorlog'} eq '-' ? 1 : 0) != ($in{'error_journal'} ? 1 : 0);
|
||||
|
||||
# Either Miniserv clears the logs itself, or logrotate takes them over. There
|
||||
# is no setting for the latter, so it is on when clearing is off and a section
|
||||
# already rotates the logs.
|
||||
my $logrotate = $in{'logclear'} == 2 ? 1 : 0;
|
||||
!$logrotate || &webmin::miniserv_logrotate_available() ||
|
||||
&error($text{'log_elogrotate'});
|
||||
my $was_logrotate = !int($miniserv{'logclear'}) &&
|
||||
&webmin::miniserv_logrotate_available() &&
|
||||
&webmin::get_miniserv_logrotate_section(\%miniserv) ? 1 : 0;
|
||||
|
||||
# Validate and save the access-log settings.
|
||||
!$in{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
||||
&error(&text('log_ehours', $in{'logtime'}));
|
||||
$miniserv{'log'} = $in{'log'};
|
||||
$miniserv{'loghost'} = $in{'loghost'};
|
||||
$miniserv{'logtrust'} = $in{'logtrust'};
|
||||
$miniserv{'logclf'} = $in{'logclf'};
|
||||
$miniserv{'logclear'} = $in{'logclear'};
|
||||
$miniserv{'logclear'} = $logrotate ? 0 : $in{'logclear'};
|
||||
!$miniserv{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
||||
&error(&text('log_ehours', $in{'logtime'}));
|
||||
$miniserv{'logtime'} = $in{'logtime'};
|
||||
|
||||
# Save the error destination and matching systemd drop-in when supported.
|
||||
@@ -35,6 +45,14 @@ else {
|
||||
}
|
||||
&unlock_file($usermin_miniserv_config);
|
||||
|
||||
# Hand the Miniserv logs to logrotate, or take them back from it
|
||||
if ($logrotate) {
|
||||
&webmin::setup_miniserv_logrotate(\%miniserv, "usermin");
|
||||
}
|
||||
elsif ($was_logrotate) {
|
||||
&webmin::remove_miniserv_logrotate(\%miniserv);
|
||||
}
|
||||
|
||||
# Restart through systemd when stderr needs to be re-attached.
|
||||
if ($journal_changed) {
|
||||
&webmin::restart_miniserv_systemd_service("usermin.service", 2);
|
||||
|
||||
+10
-4
@@ -21,11 +21,17 @@ print &ui_table_row($text{'log_trust'},
|
||||
&ui_yesno_radio("logtrust", int($miniserv{'logtrust'})));
|
||||
print &ui_table_row($text{'log_clf'},
|
||||
&ui_yesno_radio("logclf", int($miniserv{'logclf'})));
|
||||
my @clear_opts = ( [ 1, &text('log_period',
|
||||
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ] );
|
||||
my $logclear = int($miniserv{'logclear'});
|
||||
if (&webmin::miniserv_logrotate_available()) {
|
||||
push(@clear_opts, [ 2, $text{'log_logrotate'} ]);
|
||||
$logclear = 2 if (!$logclear &&
|
||||
&webmin::get_miniserv_logrotate_section(\%miniserv));
|
||||
}
|
||||
push(@clear_opts, [ 0, $text{'no'} ]);
|
||||
print &ui_table_row($text{'log_clear'},
|
||||
&ui_radio("logclear", int($miniserv{'logclear'}),
|
||||
[ [ 1, &text('log_period',
|
||||
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ],
|
||||
[ 0, $text{'no'} ] ]));
|
||||
&ui_radio("logclear", $logclear, \@clear_opts));
|
||||
|
||||
# Only systemd services can safely inherit stderr into the journal.
|
||||
if (&webmin::miniserv_systemd_journal_available("usermin.service")) {
|
||||
|
||||
@@ -10,6 +10,8 @@ log_trust=Log client IP address when behind proxy?
|
||||
log_clf=Use combined log format (including referrer and user agent)?
|
||||
log_clear=Periodically clear log files?
|
||||
log_period=Yes, every $1 hours
|
||||
log_logrotate=Yes, rotate using <tt>logrotate</tt>
|
||||
log_elogrotate=The <tt>logrotate</tt> configuration is not available on this system
|
||||
log_error=Error log destination
|
||||
log_error_file=Write to <tt>miniserv.error</tt>
|
||||
log_error_journal=Write to the <tt>systemd journal</tt>
|
||||
|
||||
@@ -7621,6 +7621,14 @@ my ($param_action,
|
||||
|
||||
my $m = $param_module ? $param_module : &get_module_name();
|
||||
|
||||
# When logrotate has emptied the actions log, throw away the file changes
|
||||
# and annotations that belonged to the rotated entries
|
||||
if ($gconfig{'logrotate'} && -e $webmin_logfile && !-s _) {
|
||||
&unlink_file("$ENV{'WEBMIN_VAR'}/diffs");
|
||||
&unlink_file("$ENV{'WEBMIN_VAR'}/files");
|
||||
&unlink_file("$ENV{'WEBMIN_VAR'}/annotations");
|
||||
}
|
||||
|
||||
if ($gconfig{'logclear'}) {
|
||||
# check if it is time to clear the log
|
||||
my @st = stat("$webmin_logfile.time");
|
||||
|
||||
+22
-4
@@ -20,8 +20,18 @@ $miniserv{'log'} = $in{'log'};
|
||||
$miniserv{'loghost'} = $in{'loghost'};
|
||||
$miniserv{'logtrust'} = $in{'logtrust'};
|
||||
$miniserv{'logclf'} = $in{'logclf'};
|
||||
$miniserv{'logclear'} = $in{'logclear'};
|
||||
!$in{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
||||
|
||||
# Either Miniserv clears the logs itself, or logrotate takes them over. There
|
||||
# is no setting for the latter, so it is on when clearing is off and a section
|
||||
# already rotates the logs.
|
||||
my $logrotate = $in{'logclear'} == 2 ? 1 : 0;
|
||||
!$logrotate || &miniserv_logrotate_available() ||
|
||||
&error($text{'log_elogrotate'});
|
||||
my $was_logrotate = !int($miniserv{'logclear'}) &&
|
||||
&miniserv_logrotate_available() &&
|
||||
&get_miniserv_logrotate_section(\%miniserv) ? 1 : 0;
|
||||
$miniserv{'logclear'} = $logrotate ? 0 : $in{'logclear'};
|
||||
!$miniserv{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
||||
&error(&text('log_ehours', $in{'logtime'}));
|
||||
$miniserv{'logtime'} = $in{'logtime'};
|
||||
if ($in{'perms_def'}) {
|
||||
@@ -60,9 +70,18 @@ else {
|
||||
}
|
||||
&unlock_file($ENV{'MINISERV_CONFIG'});
|
||||
|
||||
# Hand the Miniserv and actions logs to logrotate, or take them back from it
|
||||
if ($logrotate) {
|
||||
&setup_miniserv_logrotate(\%miniserv, "webmin", [ $webmin_logfile ]);
|
||||
}
|
||||
elsif ($was_logrotate) {
|
||||
&remove_miniserv_logrotate(\%miniserv, [ $webmin_logfile ]);
|
||||
}
|
||||
|
||||
$gconfig{'log'} = $in{'log'};
|
||||
$gconfig{'logtime'} = $in{'logtime'};
|
||||
$gconfig{'logclear'} = $in{'logclear'};
|
||||
$gconfig{'logclear'} = $miniserv{'logclear'};
|
||||
$gconfig{'logrotate'} = $logrotate;
|
||||
$gconfig{'logusers'} =
|
||||
$in{'uall'} ? '' : join(" ", split(/\0/, $in{'users'}));
|
||||
$gconfig{'logmodules'} =
|
||||
@@ -95,4 +114,3 @@ else {
|
||||
&show_restart_page();
|
||||
}
|
||||
&webmin_log("log", undef, undef, \%in);
|
||||
|
||||
|
||||
+11
-5
@@ -31,12 +31,18 @@ print &ui_table_row($text{'log_trust'},
|
||||
print &ui_table_row($text{'log_clf'},
|
||||
&ui_yesno_radio("logclf", int($miniserv{'logclf'})));
|
||||
|
||||
# Clear logs regularly
|
||||
# Clear logs regularly, or hand them over to logrotate when available
|
||||
my @clear_opts = ( [ 1, &text('log_period',
|
||||
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ] );
|
||||
my $logclear = int($miniserv{'logclear'});
|
||||
if (&miniserv_logrotate_available()) {
|
||||
push(@clear_opts, [ 2, $text{'log_logrotate'} ]);
|
||||
$logclear = 2 if (!$logclear &&
|
||||
&get_miniserv_logrotate_section(\%miniserv));
|
||||
}
|
||||
push(@clear_opts, [ 0, $text{'no'} ]);
|
||||
print &ui_table_row($text{'log_clear2'},
|
||||
&ui_radio("logclear", int($miniserv{'logclear'}),
|
||||
[ [ 1, &text('log_period',
|
||||
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ],
|
||||
[ 0, $text{'no'} ] ]));
|
||||
&ui_radio("logclear", $logclear, \@clear_opts));
|
||||
|
||||
# A systemd service can keep Miniserv errors in the journal instead.
|
||||
if (&miniserv_systemd_journal_available("webmin.service")) {
|
||||
|
||||
@@ -93,6 +93,8 @@ log_trust=Log client IP address when behind proxy?
|
||||
log_clf=Use combined log format (including referrer and user agent)?
|
||||
log_clear2=Periodically clear log files?
|
||||
log_period=Yes, every $1 hours
|
||||
log_logrotate=Yes, rotate using <tt>logrotate</tt>
|
||||
log_elogrotate=The <tt>logrotate</tt> configuration is not available on this system
|
||||
log_error=Error log destination
|
||||
log_error_file=Write to <tt>miniserv.error</tt>
|
||||
log_error_journal=Write to the <tt>systemd journal</tt>
|
||||
|
||||
@@ -83,6 +83,8 @@ else {
|
||||
$logfile =~ s![^/]+$!miniserv.error!;
|
||||
$miniserv->{'errorlog'} = $logfile;
|
||||
&unlink_file($dropin) if (-e $dropin);
|
||||
# Take the directory too, unless other drop-ins are still using it
|
||||
rmdir($dropin_dir) if (-d $dropin_dir);
|
||||
}
|
||||
|
||||
# Apply the Miniserv and service-manager settings as one UI operation.
|
||||
@@ -110,6 +112,142 @@ else {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
=head2 miniserv_logrotate_available()
|
||||
|
||||
Returns 1 if logrotate is installed and can be configured from Webmin.
|
||||
|
||||
=cut
|
||||
sub miniserv_logrotate_available
|
||||
{
|
||||
return &foreign_available("logrotate") && &foreign_installed("logrotate")
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
=head2 miniserv_log_files(&miniserv)
|
||||
|
||||
Returns the Miniserv access and error log paths that rotation applies to.
|
||||
|
||||
=cut
|
||||
sub miniserv_log_files
|
||||
{
|
||||
my ($miniserv) = @_;
|
||||
my $errorlog = $miniserv->{'logfile'};
|
||||
$errorlog =~ s![^/]+$!miniserv.error!;
|
||||
$errorlog = $miniserv->{'errorlog'} if ($miniserv->{'errorlog'} =~ /^\//);
|
||||
return &unique($miniserv->{'logfile'}, $errorlog);
|
||||
}
|
||||
|
||||
=head2 get_miniserv_logrotate_section(&miniserv)
|
||||
|
||||
Returns the logrotate section that covers the Miniserv access log, if any.
|
||||
|
||||
=cut
|
||||
sub get_miniserv_logrotate_section
|
||||
{
|
||||
my ($miniserv) = @_;
|
||||
&foreign_require("logrotate");
|
||||
my ($logfile) = &miniserv_log_files($miniserv);
|
||||
# Global directives like weekly and rotate share the list with log sections,
|
||||
# but their name is a plain string instead of a list of log files
|
||||
foreach my $c (@{&logrotate::get_config()}) {
|
||||
next if (ref($c->{'name'}) ne 'ARRAY');
|
||||
return $c if (&indexof($logfile, @{$c->{'name'}}) >= 0);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
=head2 setup_miniserv_logrotate(&miniserv, name, [&extra-logs])
|
||||
|
||||
Adds any missing Miniserv and extra logs to the access-log section, or creates
|
||||
a section when none exists. Copy-truncate rotation lets Miniserv keep its open
|
||||
error log handle, as it only re-opens that log when the file disappears.
|
||||
|
||||
=cut
|
||||
sub setup_miniserv_logrotate
|
||||
{
|
||||
my ($miniserv, $name, $extra) = @_;
|
||||
my @logs = &unique(&miniserv_log_files($miniserv), @{$extra || [ ]});
|
||||
my $lconf = &get_miniserv_logrotate_section($miniserv);
|
||||
|
||||
# Skip any log that some other section already rotates
|
||||
my %covered;
|
||||
foreach my $c (@{&logrotate::get_config()}) {
|
||||
next if (ref($c->{'name'}) ne 'ARRAY');
|
||||
foreach my $log (@{$c->{'name'}}) {
|
||||
$covered{$log} = 1;
|
||||
}
|
||||
}
|
||||
my @missing = grep { !$covered{$_} } @logs;
|
||||
return 0 if ($lconf && !@missing);
|
||||
|
||||
# Vendor files cannot be edited in place, so copy first on systems that
|
||||
# keep their logrotate config under /usr
|
||||
my $file = $lconf ? $lconf->{'file'} : &logrotate::get_add_file($name);
|
||||
&logrotate::ensure_writable_config_file($file);
|
||||
my $parent = &logrotate::get_config_parent();
|
||||
if ($lconf) {
|
||||
# Copying re-reads the config, so look the section up again
|
||||
$lconf = &get_miniserv_logrotate_section($miniserv);
|
||||
push(@{$lconf->{'name'}}, @missing);
|
||||
}
|
||||
else {
|
||||
$lconf = { 'file' => &logrotate::get_add_file($name),
|
||||
'name' => [ grep { !$covered{$_} } @logs ],
|
||||
'members' => [ { 'name' => 'weekly' },
|
||||
{ 'name' => 'rotate', 'value' => 7 },
|
||||
{ 'name' => 'missingok' },
|
||||
{ 'name' => 'notifempty' },
|
||||
{ 'name' => 'compress' },
|
||||
{ 'name' => 'copytruncate' } ] };
|
||||
}
|
||||
&lock_file($lconf->{'file'});
|
||||
&logrotate::save_directive($parent,
|
||||
defined($lconf->{'index'}) ? $lconf : undef,
|
||||
$lconf);
|
||||
&flush_file_lines($lconf->{'file'});
|
||||
&unlock_file($lconf->{'file'});
|
||||
&logrotate::flush_logrotate_config_cache();
|
||||
return 1;
|
||||
}
|
||||
|
||||
=head2 remove_miniserv_logrotate(&miniserv, [&extra-logs])
|
||||
|
||||
Removes the Miniserv logs and any extra logs from their logrotate section. The
|
||||
section and its file are deleted when they cover nothing else.
|
||||
|
||||
=cut
|
||||
sub remove_miniserv_logrotate
|
||||
{
|
||||
my ($miniserv, $extra) = @_;
|
||||
my $lconf = &get_miniserv_logrotate_section($miniserv);
|
||||
return 0 if (!$lconf);
|
||||
&logrotate::ensure_writable_config_file($lconf->{'file'});
|
||||
|
||||
# Copying re-reads the config, so look the section up again
|
||||
$lconf = &get_miniserv_logrotate_section($miniserv);
|
||||
my %logs = map { $_, 1 } (&miniserv_log_files($miniserv), @{$extra || [ ]});
|
||||
my @leftover = grep { !$logs{$_} } @{$lconf->{'name'}};
|
||||
my $parent = &logrotate::get_config_parent();
|
||||
&lock_file($lconf->{'file'});
|
||||
if (@leftover) {
|
||||
# Other logs share the section, so only drop ours
|
||||
$lconf->{'name'} = \@leftover;
|
||||
&logrotate::save_directive($parent, $lconf, $lconf);
|
||||
&flush_file_lines($lconf->{'file'});
|
||||
&unlock_file($lconf->{'file'});
|
||||
&logrotate::flush_logrotate_config_cache();
|
||||
}
|
||||
else {
|
||||
&logrotate::save_directive($parent, $lconf, undef);
|
||||
&flush_file_lines($lconf->{'file'});
|
||||
&unlock_file($lconf->{'file'});
|
||||
&logrotate::flush_logrotate_config_cache();
|
||||
&logrotate::delete_if_empty($lconf->{'file'});
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
our $primary_port = $primary_ssl ? 443 : 80;
|
||||
|
||||
our $webmin_key_email = "jcameron\@webmin.com";
|
||||
|
||||
Reference in New Issue
Block a user