diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e005420..7b208f67b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/usermin/acl_security.pl b/usermin/acl_security.pl index 83b293ab8..ff510a3e9 100755 --- a/usermin/acl_security.pl +++ b/usermin/acl_security.pl @@ -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" ); } diff --git a/usermin/change_log.cgi b/usermin/change_log.cgi new file mode 100755 index 000000000..5b57386d0 --- /dev/null +++ b/usermin/change_log.cgi @@ -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(""); diff --git a/usermin/defaultacl b/usermin/defaultacl index 0ba26bd0f..ea7e4f8e0 100644 --- a/usermin/defaultacl +++ b/usermin/defaultacl @@ -29,3 +29,4 @@ advanced=1 mobile=1 twofactor=1 web=1 +log=1 diff --git a/usermin/edit_log.cgi b/usermin/edit_log.cgi new file mode 100755 index 000000000..746be0d46 --- /dev/null +++ b/usermin/edit_log.cgi @@ -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', "$miniserv{'logfile'}"),"

\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'}); diff --git a/usermin/images/log.gif b/usermin/images/log.gif new file mode 100644 index 000000000..7eebfc951 Binary files /dev/null and b/usermin/images/log.gif differ diff --git a/usermin/index.cgi b/usermin/index.cgi index c4b22266a..0221ee051 100755 --- a/usermin/index.cgi +++ b/usermin/index.cgi @@ -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", diff --git a/usermin/lang/en b/usermin/lang/en index e179a49f1..d19370253 100644 --- a/usermin/lang/en +++ b/usermin/lang/en @@ -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 miniserv.error +log_error_journal=Write to the systemd journal +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 module configuration is incorrect. index_return=Usermin configuration index_stop=Stop Usermin diff --git a/webmin/change_log.cgi b/webmin/change_log.cgi index de7d2dd90..75c9fa8e9 100755 --- a/webmin/change_log.cgi +++ b/webmin/change_log.cgi @@ -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); diff --git a/webmin/edit_log.cgi b/webmin/edit_log.cgi index 7eec306e4..107843594 100755 --- a/webmin/edit_log.cgi +++ b/webmin/edit_log.cgi @@ -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'}); - diff --git a/webmin/lang/en b/webmin/lang/en index 050b92089..da1cd4c6e 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -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 miniserv.error +log_error_journal=Write to the systemd journal log_forusers=Users to log Webmin actions for log_uall=Log actions by all users log_users=Only log actions by .. diff --git a/webmin/webmin-lib.pl b/webmin/webmin-lib.pl index f985d0f8a..e8e6ffe6c 100755 --- a/webmin/webmin-lib.pl +++ b/webmin/webmin-lib.pl @@ -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 2>&1