Add option to send Miniserv errors to the systemd journal

Add a Logging page to the Usermin Configuration module for the access-log settings, guarded by a new "log" ACL.

Also, Webmin and Usermin logging pages add new an "Error log destination" choice on systemd systems. Selecting the journal sets errorlog=- and installs a local StandardError=journal drop-in, leaving the packaged unit untouched.

https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562/10?u=ilia
This commit is contained in:
Ilia Ross
2026-08-21 21:02:58 +02:00
parent 3c26b2be3b
commit 523853963e
12 changed files with 208 additions and 4 deletions
+4
View File
@@ -1,4 +1,8 @@
## Changelog
#### 2.661 (September, 2026)
* Add options to send Webmin and Usermin errors to the systemd journal [forum.virtualmin.com/t/136562](https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562)
* Add webserver logging controls to Usermin Configuration module
#### 2.660 (August 20, 2026)
* Add support for creating `vfsv1` Linux quota files for limits above 4 TiB, while preserving existing quota file formats
* Add Btrfs subvolume quota management to the Disk Quotas module, with full and simple accounting modes
+1 -1
View File
@@ -43,6 +43,6 @@ $o->{'bootup'} = $in{'bootup'};
sub get_icons
{
return ( "access" ,"bind" ,"ui" ,"umods" ,"os" ,"lang" ,"upgrade" ,"session" ,"assignment" ,"categories" ,"themes", "referers", "anon", "ssl" ,"configs" ,"acl" ,"restrict" ,"users" ,"defacl", "sessions", "blocked", "advanced" );
return ( "access" ,"bind" ,"log" ,"ui" ,"umods" ,"os" ,"lang" ,"upgrade" ,"session" ,"assignment" ,"categories" ,"themes", "referers", "anon", "ssl" ,"configs" ,"acl" ,"restrict" ,"users" ,"defacl", "sessions", "blocked", "advanced" );
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/local/bin/perl
# Save Usermin webserver logging options
require './usermin-lib.pl';
&ReadParse();
$access{'log'} || &error($text{'acl_ecannot'});
&error_setup($text{'log_err'});
&get_usermin_miniserv_config(\%miniserv);
# Only a change of error destination needs the systemd drop-in and restart.
my $journal_changed = defined($in{'error_journal'}) &&
($miniserv{'errorlog'} eq '-' ? 1 : 0) != ($in{'error_journal'} ? 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{'logtime'} = $in{'logtime'};
# Save the error destination and matching systemd drop-in when supported.
&lock_file($usermin_miniserv_config);
if ($journal_changed) {
&webmin::set_miniserv_error_destination(\%miniserv,
$usermin_miniserv_config, "usermin.service",
$in{'error_journal'});
}
else {
&put_usermin_miniserv_config(\%miniserv);
}
&unlock_file($usermin_miniserv_config);
# Restart through systemd when stderr needs to be re-attached.
if ($journal_changed) {
&webmin::restart_miniserv_systemd_service("usermin.service", 2);
}
else {
&restart_usermin_miniserv();
}
&webmin_log("log");
&redirect("");
+1
View File
@@ -29,3 +29,4 @@ advanced=1
mobile=1
twofactor=1
web=1
log=1
+40
View File
@@ -0,0 +1,40 @@
#!/usr/local/bin/perl
# Display Usermin webserver logging options
require './usermin-lib.pl';
$access{'log'} || &error($text{'acl_ecannot'});
&ui_print_header(undef, $text{'log_title'}, "");
&get_usermin_miniserv_config(\%miniserv);
print &text('log_desc', "<tt>$miniserv{'logfile'}</tt>"),"<p>\n";
print &ui_form_start("change_log.cgi", "post");
print &ui_table_start($text{'log_header'}, undef, 2);
# Control the Usermin access log and its built-in expiry mechanism.
print &ui_table_row($text{'log_status'},
&ui_radio("log", $miniserv{'log'} ? 1 : 0,
[ [ 1, $text{'log_enable'} ],
[ 0, $text{'log_disable'} ] ]));
print &ui_table_row($text{'log_resolv'},
&ui_yesno_radio("loghost", int($miniserv{'loghost'})));
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'})));
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'} ] ]));
# Only systemd services can safely inherit stderr into the journal.
if (&webmin::miniserv_systemd_journal_available("usermin.service")) {
print &ui_table_row($text{'log_error'},
&ui_radio("error_journal", $miniserv{'errorlog'} eq '-' ? 1 : 0,
[ [ 0, $text{'log_error_file'} ],
[ 1, $text{'log_error_journal'} ] ]));
}
print &ui_table_end();
print &ui_form_end([ [ "save", $text{'save'} ] ]);
&ui_print_footer("", $text{'index_return'});
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

+1
View File
@@ -55,6 +55,7 @@ if (!$miniserv{'root'} && !-d $miniserv{'root'}) {
@links = ( "edit_access.cgi",
"edit_bind.cgi",
"edit_log.cgi",
"edit_ui.cgi",
"edit_mods.cgi",
"edit_os.cgi",
+16
View File
@@ -1,4 +1,20 @@
index_title=Usermin Configuration
log_title=Logging
log_desc=Usermin can be configured to write a log of web server hits, in the standard CLF log file format. If logging is enabled, you can also choose whether IP addresses or hostnames are recorded, and how often the log file is cleared. When enabled, logs are written to the file $1.
log_header=Webserver logging options
log_status=Logging active?
log_enable=Enable logging
log_disable=Disable logging
log_resolv=Log resolved hostnames?
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_error=Error log destination
log_error_file=Write to <tt>miniserv.error</tt>
log_error_journal=Write to the <tt>systemd journal</tt>
log_err=Failed to save logging options
log_ehours='$1' is not a valid number of hours
index_econfig=The directory $1 either does not exist on your system, or is not the Usermin configuration directory. Maybe Usermin is not installed, or your <a href='$2'>module configuration</a> is incorrect.
index_return=Usermin configuration
index_stop=Stop Usermin
+22 -2
View File
@@ -9,6 +9,11 @@ require './webmin-lib.pl';
&lock_file($ENV{'MINISERV_CONFIG'});
&get_miniserv_config(\%miniserv);
my ($miniserv_log, $in_log) = ($miniserv{'log'}, $in{'log'});
# Only a change of error destination needs the systemd drop-in and restart.
my $journal_changed = defined($in{'error_journal'}) &&
($miniserv{'errorlog'} eq '-' ? 1 : 0) != ($in{'error_journal'} ? 1 : 0);
$miniserv{'log'} = $in{'log'};
$miniserv{'loghost'} = $in{'loghost'};
$miniserv{'logtrust'} = $in{'logtrust'};
@@ -42,7 +47,15 @@ if (defined($in{'login'})) {
delete($miniserv{'failed_script'});
}
}
&put_miniserv_config(\%miniserv);
# Save the error destination and matching systemd drop-in when supported.
if ($journal_changed) {
&set_miniserv_error_destination(\%miniserv,
$ENV{'MINISERV_CONFIG'}, "webmin.service",
$in{'error_journal'});
}
else {
&put_miniserv_config(\%miniserv);
}
&unlock_file($ENV{'MINISERV_CONFIG'});
$gconfig{'log'} = $in{'log'};
@@ -71,6 +84,13 @@ if ($miniserv_log != $in_log) {
}
}
&show_restart_page();
# Restart through systemd when stderr needs to be re-attached.
if ($journal_changed) {
&restart_miniserv_systemd_service("webmin.service", 2);
&redirect("");
}
else {
&show_restart_page();
}
&webmin_log("log", undef, undef, \%in);
+8 -1
View File
@@ -38,6 +38,14 @@ print &ui_table_row($text{'log_clear2'},
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ],
[ 0, $text{'no'} ] ]));
# A systemd service can keep Miniserv errors in the journal instead.
if (&miniserv_systemd_journal_available("webmin.service")) {
print &ui_table_row($text{'log_error'},
&ui_radio("error_journal", $miniserv{'errorlog'} eq '-' ? 1 : 0,
[ [ 0, $text{'log_error_file'} ],
[ 1, $text{'log_error_journal'} ] ]));
}
print &ui_table_hr();
# Webmin users to log for
@@ -101,4 +109,3 @@ print &ui_table_end();
print &ui_form_end([ [ "save", $text{'save'} ] ]);
&ui_print_footer("", $text{'index_return'});
+3
View File
@@ -93,6 +93,9 @@ 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_error=Error log destination
log_error_file=Write to <tt>miniserv.error</tt>
log_error_journal=Write to the <tt>systemd journal</tt>
log_forusers=Users to log Webmin actions for
log_uall=Log actions by all users
log_users=Only log actions by ..
+67
View File
@@ -43,6 +43,73 @@ if (!-r $update_cache) {
our $primary_host = "webmin.com";
our $primary_ssl = $can_http_ssl;
=head2 miniserv_systemd_journal_available(unit)
Returns 1 if a systemd service is available for receiving Miniserv errors.
=cut
sub miniserv_systemd_journal_available
{
my ($unit) = @_;
return &has_command("systemctl") &&
(-r "/etc/systemd/system/$unit" ||
-r "/usr/lib/systemd/system/$unit" ||
-r "/lib/systemd/system/$unit");
}
=head2 set_miniserv_error_destination(&miniserv, config, unit, journal)
Selects the Miniserv error file or the systemd journal, using a local systemd
drop-in so that the packaged service unit remains unchanged.
=cut
sub set_miniserv_error_destination
{
my ($miniserv, $config_file, $unit, $journal) = @_;
$unit =~ /^(webmin|usermin)\.service$/ || return 0;
my $dropin_dir = "/etc/systemd/system/$unit.d";
my $dropin = "$dropin_dir/10-miniserv-error-log.conf";
# The dash keeps Miniserv's stderr attached to the service manager.
if ($journal) {
$miniserv->{'errorlog'} = '-';
&make_dir($dropin_dir, 0755) if (!-d $dropin_dir);
&write_file_contents($dropin,
"[Service]\nStandardError=journal\n");
}
else {
my $logfile = $miniserv->{'logfile'};
$logfile =~ s![^/]+$!miniserv.error!;
$miniserv->{'errorlog'} = $logfile;
&unlink_file($dropin) if (-e $dropin);
}
# Apply the Miniserv and service-manager settings as one UI operation.
&write_file($config_file, $miniserv);
&system_logged("systemctl daemon-reload >/dev/null 2>&1");
}
=head2 restart_miniserv_systemd_service(unit, [delay])
Fully restarts a Miniserv systemd service so that stderr is re-attached to the
selected destination. A delay allows the current Webmin response to finish.
=cut
sub restart_miniserv_systemd_service
{
my ($unit, $delay) = @_;
$unit =~ /^(webmin|usermin)\.service$/ || return 0;
$delay = int($delay);
if ($delay) {
&system_logged("(sleep $delay; systemctl --no-block restart $unit) " .
">/dev/null 2>&1 </dev/null &");
}
else {
&system_logged("systemctl restart $unit >/dev/null 2>&1 </dev/null");
}
return 1;
}
our $primary_port = $primary_ssl ? 443 : 80;
our $webmin_key_email = "jcameron\@webmin.com";