Problem/Motivation
It's useful to know when a site went into and came back out of maintenance mode, so it would be nice if this information was added to the admin log when it happened.
Steps to reproduce
NA
Proposed resolution
Add a message in the system log when the site is switched to or from maintenance mode.
Remaining tasks
- [✓] - Fix & MR
- [✓] - Tests
User interface changes
Now, when the site is switched to or from maintenance mode, a message is added to the log.
Introduced terminology
None
API changes
None
Data model changes
None
Release notes snippet
NA
Original report by @geodaniel
It's useful to know when a site went into and came back out of maintenance mode, so it would be nice if this information was added to the admin log when it happened.
| Comment | File | Size | Author |
|---|
Issue fork drupal-229778
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #1
Race.it commentedThis would require a override of the submit hook for the form, I think that is the safest way of handling this request.
Comment #2
Race.it commentedCreated a patch to address this request, please review.
Comment #3
Race.it commentedComment #4
cburschkaAdd a period there.
site_maintenance_... is an unnecessary name quirk. This function should start with system_ to reflect the module it is in.
Also, if you use
system_site_maintenance_mode_submit(mode rather than admin_settings) you get consistency with the form's name as well as shortening the monster a bit.---
I need to read how submit handlers and auto-discovery are handled on system settings forms. There are several possibilities here, including a) that you need to add the submit handler after calling system_settings_form() or b) that if you pick the standard FORM_submit() name (in this case the one I suggested) the submit handler will be automatically discovered without having to be registered. I'm just speculating though.
Comment #5
cburschkaAddendum:
There's a space inside the left parenthesis on the first line, which should go.
Also, if you compare a variable that can only be 1 or 0, it is usually more readable to check it as a boolean - and also to put the truth (1) branch before the falsehood (0). In this case, the condition determines the state of a single variable and therefore should probably be a trinary.
Finally, splitting translateable strings when it's not necessary costs an extra string lookup and makes life difficult for the translators. It may look as though the strings are more "normalized", but be aware that by hard-coding this split, the separating colon and the strings' order, linguistic flexibility is lost. A language with more inflections than English can translate "offline" differently whether its stand-alone or part of a sentence.
In summary, a good choice here might be
Although I would favor a clearer sentence personally, such as "Site was taken offline" or "Site was put online".
(Naturally, all of this also applies to the "reset to defaults" string).
---
Also, it seems I was wrong on both of my earlier speculations:
a) system_settings_form() adds a #submit handler, but doesn't strip the ones that are there (good, because that'd be pretty silly)
b) drupal_prepare_form() adds FORM_ID_submit() only if no #submit handler has been explicitly defined, which system_settings_form() has done by then. So regardless of the name, it has to be explicitly added. It still makes sense to use that name though.
Comment #6
pfahlr commentedhttp://drupal.org/sandbox/pfahlr/1425554
Comment #7
mmedrano commentedSubmitting a new patch for feature request.
Comment #8
mmedrano commentedComment #9
panshulk commentedHi @mmedrano
Thanks for working on this.
I tested the latest 229778-log-changes-to-offline-mode.patch, the patch works as expected.
Following is the screenshot of the result in "Recent log messages" :
Hence Moving this to RTBC
Thanks :)
Comment #10
panshulk commentedComment #11
David_Rothstein commentedThis seems like a good idea but needs to go in Drupal 8 first.
Also a few issues:
Why not check $form_state['values']['maintenance_mode'] instead? That is usually the standard way to look at submitted form values.
It should not be necessary to include the username here - as the above screenshot shows, watchdog already records it automatically. So I think just something like "Site was put in maintenance mode." would be fine here.
Comment #12
David_Rothstein commentedEh, especially because that's what the issue title says this issue is about :) (And what the earlier patch in this issue did...)
Comment #13
cburschkaNote that in D8, maintenance mode can be triggered non-interactively, such as with drupal-console. Right now, these all simply use \Drupal::service('state')->set('system.maintenance_mode', true) just like the form submit function. That's okay as long as the submit function doesn't do anything special.
If we want to do more, the whole code needs to be moved into a separate function (perhaps in the system.manager service) to allow it to be called from modules.
(Unfortunately, as maintenance mode is now a state variable and not a configuration variable, this message cannot be added by listening to a config change event.)
Comment #14
David_Rothstein commentedThis is true in Drupal 7 also - you can do
drush vset maintenance_mode 1. However, I think it's not that big of a deal to only log it when maintenance mode is turned on via the admin UI. (Unless we're setting an expectation for people that it will always be logged unconditionally or something?)Comment #15
xjmComment #17
diamondseaAttached is a D8 implementation and screenshot.

Note: This will only log changes made through the UI, not through Drupal Console / Drush.
Comment #18
diamondseaComment #20
mmcintosh commentedReviewing patch now
Comment #21
john cook commentedI've used the message from the patch in #17 and moved it into the
CacheCollector:set()method.This allows the message to be logged when being set using Drush, although it is logged as being changed by 'Anonymous'.
Example log entries:

I won't create an interdiff as it's essentially a new patch.
Comment #22
dawehnerMaybe an alternative approach: we could add a method to
\Drupal\Core\Site\MaintenanceModeand let this logging happen there?Comment #23
joachim commented> Maybe an alternative approach: we could add a method to \Drupal\Core\Site\MaintenanceMode and let this logging happen there?
+1 to this. The cache collector seems an odd place to put logging.
Nitpick:
Comment is wrong - it logs when entering and leaving maintenance mode.
Comment #24
dinesh18 commentedI tried putting logger inside this function with if..else condition but it runs multiple times and print multiple log message for the same event.
Comment #25
dinesh18 commentedHere is the attached screenshot .
In -- > Maintenance Mode Enabled
Out --> Maintenance Mode Disabled
Comment #26
joachim commented> \Drupal\Core\Site\MaintenanceMode and let this logging happen there?
Ah. That service is about *reporting* whether the site is in maintenance mode. It doesn't allow you to change the status. So that's not the right place to put it.
Comment #27
dinesh18 commentedI am new to D8. So I should declare my own method in MaintenanceModeInterface.php.
I should create the method in MaintenanceMode.php.
How should I access the Enabled /disabled boolean in my new method ?
Comment #28
joachim commentedI don't think it's that simple.
Adding a method to set the status in MaintenanceModeInterface is an API change. I think this one should wait for the maintainer to look at.
Comment #38
o'briatHi
I just crossed the perfect example on why this issue should be taken much seriously: a admin user saw the "outdated message", so thinking he's doing the right thing he follows the link and the update process putting the site in maintenance mode... The update failed but he didn't notice the site was still in maintenance mode because he was already logged in.
He do it twice over several months...
It take a while to debug it since there is no Drupal log, and the web server log didn't show any visit to maintenance page.
Ok, the fault is mainly on the dev team side (update module enabled on production site and bad permissions) but still I think this issue should get a higher priority (or the first patch should be accepted)
See also: https://github.com/drush-ops/drush/issues/5365
Comment #39
damienmckennaFWIW the last patch still applies cleanly against 9.5.x.
Comment #40
o'briatThanks, I just add it to my `composer.json`.
So the patch number 17 could be marked as RTBTC
Comment #41
markdorisonThe patch still applies cleanly. I don't see any outstanding requests for changes. Switching this to 'needs review.'
Comment #42
smustgrave commentedThe issue summary is pretty bare.
Also could use some kind of test coverage. Even simply coverage.
Comment #44
heikkiy commentedI am running core version 9.5.11 I tested the patch from #21. I was not able to get anything in logs when setting the maintenance mode through the UI. I tried clearing caches with Drush a few times but it didn't have any effect.
Patch from #17 also applies cleanly and now I got log messages when it was set. But I think the solution in #21 would be better if it worked because we also set maintenance mode with Drush and would be good to catch those also.
Comment #45
sumit saini commentedAs maintenance mode is stored in state, it is good to log message in Drupal\Core\State::set() method. So, moving the fix given in #21 to State.php. This way, there will be a log for both cases - drush and form save. Verified the attached patch with Drupal 10.1.4
Comment #46
poker10 commented@sumit_saini Thanks for the patch - tests are still needed, so moving back to Needs Work.
Comment #48
aditi saraf commentedComment #49
poker10 commentedThe last patch is still missing the test(s).
And it would be great to update to update the issue summary (see #42). We should also include the summary of possible solutions (
State::set()vsCacheCollector:set()vsSiteMaintenanceModeForm..), as there are multiple patches with different approaches, so better to have this summarized. Thanks!Comment #50
damienmckennaRerolled.
Comment #51
smustgrave commentedFor the issue summary update and tests.
Comment #52
heikkiy commentedWe encountered an error when using this patch in core with Redirect 404 submodule of Redirect.
After upgrading to Drupal 10.2, I get the following error when trying to disable the maintenance mode in en/admin/config/development/maintenance:
Comment #55
vidorado commentedI’m not convinced by the solution proposed in #21. While it’s nice that the message is logged when the site is put into maintenance mode using Drush, I don’t believe the
Stateclass should be responsible for that. Perhaps we could handle it inDrupal\Core\EventSubscriber\MaintenanceModeSubscriberby introducing a second state variable to store the last logged action, which would help prevent log flooding.In the meantime, I’ve refined the
SiteMaintenanceModeFormsolution slightly and added a kernel test.Do you think we should invest effort into covering the Drush case?
Comment #56
smustgrave commentedLeft some small comments on the MR.
Comment #57
vidorado commentedThanks for the review @smustgrave :)
All threads resolved!
Comment #58
alexpottI think we should consider an architecture that allows us to log state keys set during a request / cli operation. We could add an property to store the keys that are set during the request to
\Drupal\Core\State\Stateand then add a public method to get this information. Then add a service then subscribes to the terminate even and gets this value and logs any state changes for a list of keys defined in a container parameter. This container parameter should contain the valuesystem.maintenance_modebe default.This way we'll get consistent logging across Drush / admin UI and any other way of doing this and we'll have a generic system to solve this if we want logging for any other state changes.
Comment #59
godotislateIn the case of state, and then perhaps generally, would there be a need to track the previous stored value for the key so it can be compared with the new value?
Comment #60
vidorado commentedUmm, this is getting interesting. Some thoughts:
system.maintenance_modeis set multiple times within the same request or CLI operation, the last change could be from 1 to 1, which wouldn’t be useful. However, that’s very unlikely to happen, though.Comment #61
alexpottI think we could just log the current value. However I also think there is a way around #60.1 If we want to also report the initial value then we could easily do something like
And then the method to get the keys set during the request could return the initial value too. I think we should only consider doing that if getting the initial value does not cost anything. I think it is okay to ignore multiple changes to a value during a request and only care about the initial and value value for the purposes of logging.
Comment #62
vidorado commentedSounds good. I'll work into this :)
Comment #63
godotislateI think one (small, probably) concern with not getting the initial stored value in the case of maintenance mode is that if the site was already put into maintenance in a prior request or CLI op, then the log will show successive messages indicating maintenance mode was enabled. This might be confusing to someone expecting to see a message about maintenance mode being disabled somewhere in between.
Maybe the log message for maintenance mode can be worded to acknowledge that maintenance mode state was set but not necessarily changed?
Comment #64
vidorado commentedI'm not feeling as confident with naming as I usually do while writing this code… maybe it's just late, and I'm not thinking clearly. 🙂
StateKeysSetLoggerSubscribersubscriber name.state.keys_set_logger.Nonetheless, I believe this approach addresses #60 and the concerns discussed earlier.
Any feedback would be greatly appreciated!
Edit: I'm also not sure if retrieving the original value would have a significant performance impact. Since the value is cached, fetching it a second time should be quick, but the initial retrieval might not be, and that would happen for every key set.
Comment #65
godotislateI have no opinion on the class name, but made some comments on the MR.
Also:
Detecting what the value has changed from isn't possible without
get()call, which I presumed was to be avoided. Though I'm also not sure about the performance impact.Comment #66
vidorado commentedI have another idea, although it's not exactly in line with the idea @alexpott suggested:
What if we fire a dedicated event for state changes, such as
StateKeySetEvent, to which aMaintenanceModeEventSubscribercould listen and log a more specific message?We could dispatch the event before the key is updated, allowing subscribers to retrieve the original value directly from the state instead of passing it within the event, only in case they need it. This would introduce a performance impact, but only for subscribers that actually need to access the original value. Given that there would only be a few subscribers, the impact wouldn’t be as significant as retrieving the original value for every key set in a request.
What do you think?
Comment #67
alexpott@vidorado I'm hesitant to have an event being triggered because this is during termination and only limited things will work as expected - for example you can't use the messenger service and expect something to make it to the page. That said this can be documented so maybe that is okay.
Oh... writing this makes me realise we can completely remove the generic logger and just have a logger for the maintenance mode. And we can change
public function getKeysSetDuringRequest(): array;topublic function getValueSetDuringRequest(string $key): ?array;and that can return the new and original value in an array or NULL if the key was not changed.Comment #68
vidorado commentedThanks @alexpott,
I hadn't realized that firing another event during the termination event could be problematic. Thanks for pointing it out.
However, the termination event we were subscribing to was intended to collect all the state keys set during the request, since at that stage, it's very unlikely that any more keys would be set. In contrast, the
StateKeySetEventI suggested would be fired each time a state key is set, rather than at termination.With this approach, a
MaintenanceModeEventSubscribercould listen forsystem.maintenance_modebeing set to 1 and log a message if the original value was 0. This might generate a log entry each time maintenance mode is set during a request, but it's highly unlikely that it would be set more than once.Would this event subscriber align with the "maintenance mode logger" you suggested? Were you referring to a polling strategy rather than a publisher-subscriber approach?
Comment #70
alexpott@vidorado I don't think we should fire an event every time a state key is set. I think the solution suggested in #67 is the way to go. The maintenance mode subscriber can subscribe to the terminate event and do everything it needs. See new MR - https://git.drupalcode.org/project/drupal/-/merge_requests/11220
Comment #71
alexpottWith the new MR you can see the logging in action with Drush if you are in verbose mode...
Comment #72
vidorado commentedThat's elegant! 🙂 Thanks for helping out!
Sorry, I wasn’t getting it at first when I read #67, but after viewing it as code, I got it! :)
I've also learned from it that a complete class constructor parameter promotion can be in the scope of an issue that modifies the class (which I think is cool), and also learned how to do a proper "autowire service promotion".
Overall, I think the code has turned out to be a very lean and clean change.
It works fine, either with Drush or the UI.
Only a question remains:
Should we be concerned about the performance impact of
$this->registerKeySetDuringRequest($key, $value, parent::get($key));being executed for each key set in State? (for theparent::get($key), which will have a cost).Comment #73
godotislateA couple small typos and a question. One test failure as well, presumably unrelated.
Comment #76
quietone commentedComment #77
alexpottI'm not concern because State is a cache collector and it has to get all the values before a set... - see the call to $this->lazyLoadCache(); in both get and set in the parent cache collector class.
Comment #78
alexpottI added a test case for this.
Comment #79
godotislateThis looks good now. There is one failing test, but I think it is unrelated and just needs re-running.
Comment #80
vidorado commentedI see. State data is cached in-memory in the
CacheCollector::$storagevariable, so it's cheap to retrieve the value with aget():)Since all threads and suggestions are resolved, the test are passing (I've launched another pipeline and it passed, the failing tests were the common ones that randomly fail and you must re-run), I believe we can switch to RTBC.
Thanks to all for your work! I've learned a lot from this ^^
Comment #81
alexpottWe need a change record to detail the addition of
StateInterface::getValuesSetDuringRequest()- https://www.drupal.org/node/add/changenotice?field_project=3060&field_is...Comment #82
godotislateAdded CR: https://www.drupal.org/node/3519307
I added a couple minor comments on the MR after looking again while writing the CR.
Comment #83
vidorado commentedSince the changes look good to me and the tests pass, I believe it's safe to switch back to RTBC.
Thanks @godotislate!
Comment #84
penyaskitoNW per https://git.drupalcode.org/project/drupal/-/merge_requests/11220#note_49.... We should accept that suggestion, then IMHO we should be good to go.
Comment #85
penyaskitoOK, I'll stop messing around. It's already fixed, just we can't resolve threads.
Comment #86
vidorado commentedNo worries @penyaskito 😁 you're not the only one who has been misleaded by unresolved threads. I don't really know what it depends on. Sometimes I see the "Resolve thread" button at GitLab, but sometimes not.
Comment #87
quietone commentedEverything looks in order here. I also updated credit.
I also tested the MR and it works as expected, it just needs a cache clear.
Comment #88
xjmI did a first pass at reviewing this. I had mostly small formatting feedback, plus a couple coding standards questions I need to look up. Next I'm going to fire up some manual testing!
Effusive aside: Friends, this issue is from March of 2008! Back then, I hadn't even submitted my first security hardening (which would trigger a sequence of events that led to me eventually becoming a core release manager), nor ever logged into IRC. I had never attended a single Drupal user group meeting nor Drupal Camp. I only posted in the forums, really. Remember the forums? Anyway. Very excited to finally fix this.
Starting manual testing now.
Comment #89
xjmManually tested with the following steps:
/admin/config/development/maintenanceand toggled the site into maintenance mode.http://127.0.0.1:8888/admin/reports/dblogand verified that it showed a message for admin putting the site into maintenance mode.Beautiful! This does make me think, though: Should our functional test include assertions about who is listed as triggering the maintenance mode change? Back in the day this would involve some complicated CSS assertions, but I think we might have API already for asserting dblog messages in our test suite.
Not NWing yet, because I still need to:
Comment #90
xjmOh, regarding:
You will see the button if you are the author of the merge request or a core committer. Most people are only ever one of those things. 😀
Comment #91
xjmOK, finally done with my review.
Overall, this looks great. The new functionality for state is elegant, as is the implementation of the message setting and unsetting. It's excellent that it works both in the browser and on the CLI. The CR is good. I've also reviewed all the issue comments to verify that nothing from them is outstanding.
Most of my points and suggestions on the merge request are code style and documentation improvements. There are two point for additional test coverage (the status code and the user from whom the action is logged; compare with my manual testing in #89). There is also sadly one point for a code style and documentation quality reduction WRT the array PHPDoc hinting. Tagging "Needs followup" to file (a) an issue in the coding standards issue queue for this, which would lead to getting api.d.o fixed once we adopt the standard etc. and (b) a followup postponed on that to re-add the nice phpdoc hint here.
Thanks everyone!
Comment #92
godotislateCoding standards were updated to allow all documented PHPStan PHPdoc types per https://www.drupal.org/node/3505429.
Is fixing api.d.o to work with these types correctly a blocker?
Comment #93
xjm@godotislate, thanks. There is no mention in that issue or the updated docs of the more complex array data typehinting, aside from (apparently) one comment from @donquixote saying that a certain example of it is "too much".
In the past, we definitely have blocked adopting a standard in core on api.d.o, Coder, and all related tooling supporting it.
I'll reach out to the committers who are on the coding standards committee for more feedback.
Comment #94
godotislate@xjm The problem/motivation in #3468236: Adopt phpstan phpdoc types as canonical reference of allowed types has "I propose that anything included in https://phpstan.org/writing-php-code/phpdoc-types should be allowed", but I agree that the updates made to the CS docs are not as definitive.
I bring it up because the use of array shapes and similar has been begun to be adopted in pending MRs across quite a few core issues, so if tools needing to be in place is a blocker, there might need to be more general awareness of that.
Comment #95
xjmOK, quick update. I discussed this with the release managers and so far we didn't really reach a conclusive decision about what's a hard blocker vs. soft blocker here in terms of adopting the array and object shapes.
However, this morning we also spoke with the DA, and we determined that api.d.o formatting is not a blocker. Upon further research:
@return, but there are also a few places the array shapes are used for@paramin newer or recently improved code (particularly Recipes, the mysqli driver and related DB-layer improvements, something about SVGs, and the new Hook API).@param typesare formatted correctly, unlike return types. (Example: Block attribute.)@vartypes are not the prettiest, but at least they are parsed correctly. (Example: UnpackOptions::$options.)So, based on all that, I've agreed with the DA to file an issue in api.d.o for the return type formatting (and a couple other type formatting bugs I found in the process of researching this). Since most of it already works and this issue is not using the nested angle bracket shape that seems to break parsing, we won't block this issue's doc formatting on fixing api.d.o.
We should still also address the documentation deficiency, and possibly adopt some standards about formatting and just how far we go in human- vs machine-readable here, but again, the use is wider than I thought in core and so we can descope that from being blocking. Just need to get the various followup tasks filed or commented on in the appropriate places.
NW for various other things that were still outstanding from my review, I believe. Thanks!
Comment #96
xjmComment #97
xjmFinally finished writing up the issue for the API module.
Comment #98
xjmComment #99
godotislateI think open MR comments have been addressed.
Comment #102
xjmForgot to credit the Slack discussion with the release managers.
Comment #103
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #104
godotislateFixed PHPStan annotations.
Comment #105
smustgrave commentedResolved what threads I could. But don't want to bypass @xjm review. xjm do you mind checking your open threads please?
Comment #107
nicxvan commentedPer 95, the phpstan docs are no longer a blocker and a follow up has been created for formatting. I resolved all other threads that had been addressed.
Besides that, the only question is the providers for the tests. Personally I think with the new comments it's clear enough using a provider would actually make it harder to follow since it will separate the data from the test.
However, we could use the new #[TestWith] attribute here and set up a new method for each group.
Code looks good, test coverage looks good. 89 reported a manual test which I did not attempt to replicate, but I think this is ready!
Comment #108
alexpottCommitted and pushed e7c745132a6 to 11.x and e2bebc04901 to 11.3.x. Thanks!