From 523853963e362cbc751b9f916ad12e0193563c23 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 21 Aug 2026 21:02:58 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 4 +++ usermin/acl_security.pl | 2 +- usermin/change_log.cgi | 45 +++++++++++++++++++++++++++ usermin/defaultacl | 1 + usermin/edit_log.cgi | 40 ++++++++++++++++++++++++ usermin/images/log.gif | Bin 0 -> 470 bytes usermin/index.cgi | 1 + usermin/lang/en | 16 ++++++++++ webmin/change_log.cgi | 24 ++++++++++++-- webmin/edit_log.cgi | 9 +++++- webmin/lang/en | 3 ++ webmin/webmin-lib.pl | 67 ++++++++++++++++++++++++++++++++++++++++ 12 files changed, 208 insertions(+), 4 deletions(-) create mode 100755 usermin/change_log.cgi create mode 100755 usermin/edit_log.cgi create mode 100644 usermin/images/log.gif 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 0000000000000000000000000000000000000000..7eebfc951f617c5dc02f55ffc470a492a1376476 GIT binary patch literal 470 zcmV;{0V)1RNk%w1VK4wN0KxzOnVFepW@gOH%>V!Y000000000000000A^8LW00093 zEC2ui05AYB000C22&%FEVhJ&-y*OLY+WVjcXe1VRsN;oTS$5pIh7&N=imSyZ7kW2Z zL}%g|J2Oe-7Eug|l@qcTb2M>~+LI`i9;HjAwrMLvyWL~?3Sw_v=(smbXRgg$d9Oz= zfBJ7EdT4iHYleO!I)iCWg(858aEyG3k&a<`l$Iixk3~^-Gz5?nyGu36yn|$sls#IWWcGX% zG$JRKE^Qu7sxz6-nTKi(g_;xTH=rDaUdn2c>OMp#>og@Ja>YkqWxO~_1Txb|XQev6 zy*M^wC8_*QrAdhIRFnV%s@d^d_O8^y#}1Cd5>t}m#R`9EFgtI-3(5e=;`#vPLW|Cz MLyI0w`a%K#J8&}0B>(^b literal 0 HcmV?d00001 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