132 —Joomla
Joomla 4 geeft 500 na Akeeba-restore: action_logs-fix
Een gewone Akeeba-restore. Een blanco 500. Veertig minuten grep, schema-reconstructie en één stille INSERT later draaide de Joomla 4-site weer.
23:41 op een vrijdag. Een Nederlands bureau waar we mee samenwerken had net een routinematige restore van een Joomla 4-site afgerond, van Akeeba Backup naar een nieuwe staging-server. De DB-import liep schoon. De bestandsextractie eindigde zonder waarschuwingen. Toen ze de frontend-URL aanriepen, kregen ze een leeg wit scherm met het ene karakter dat de meeste Joomla-adminweekenden afsluit: 0.
De site was een Joomla 4.4.3-nieuwsplatform met meerdere auteurs, ongeveer 12.000 artikelen, en migreerde van PHP 8.1 op productie naar PHP 8.2 op staging vooruitlopend op een php.net version cliff. Het Akeeba-profiel was geërfd van een eerdere ontwikkelaar die het project twee jaar eerder had verlaten. De overdrachtsdocumentatie was één PDF met het woord back-ups en een vinkje ernaast. Verder niets.
Dit bericht is het volledige incidentlog: de eerste foutregel, de diagnose, de SQL die de legacy site terugbracht, en de kleine aanpassing aan het Akeeba-profiel die ervoor zorgt dat het bij de volgende restore niet opnieuw gebeurt. Draai je Joomla 4 in productie, dan is dit een van de beter te vermijden vrijdagavondtelefoontjes.
De eerste foutregel
De lead van het bureau opende een Loom en deelde zijn scherm met de staging-server. De error.log van Apache schreef sinds de restore klaar was om de paar seconden dezelfde regel:
[Fri Jun 06 23:42:11.218443 2026] [proxy_fcgi:error] [pid 8141]
[client 10.0.0.4:54231] AH01071: Got error 'PHP message:
1146 Table \'staging_jdb.jos_action_logs\' doesn\'t exist'De eigen log van Joomla onder administrator/logs/error.php was uitgebreider, maar vertelde hetzelfde verhaal:
PHP Fatal error: Uncaught Joomla\Database\Exception\ExecutionFailureException:
Table 'staging_jdb.jos_action_logs' doesn't exist
SQL=INSERT INTO `jos_action_logs` (`message_language_key`, `message`,
`log_date`, `extension`, `user_id`, `item_id`, `ip_address`) VALUES ...Eén ontbrekende tabel. Vier kolommen stack trace. Volledige uitval op elke URL, inclusief /administrator. De reden is niet subtiel zodra je het ziet: de actionlog system plugin staat in elke Joomla 4-installatie standaard aan, luistert naar events zoals onUserLogin, onContentAfterSave en onExtensionAfterInstall, en op Joomla 4 raakt zelfs een ongeauthenticeerde frontend-hit uiteindelijk een codepad dat een logregel wil schrijven. Mislukt die schrijfactie, dan 500't het request. Mislukt elk request, dan ligt de site eruit.
Om te bevestigen dat er verder niets ontbrak, draaiden we eerst een one-liner over de error.log van die dag:
grep -i "doesn't exist" /var/log/apache2/error.log \
| grep -oE "'[^']+'" \
| sort -uDe output bestond uit vier regels, allemaal uit dezelfde familie:
'staging_jdb.jos_action_logs'
'staging_jdb.jos_action_logs_config'
'staging_jdb.jos_action_logs_extensions'
'staging_jdb.jos_action_logs_users'Verder niets. De rest van het schema, alle 184 andere tabellen, was schoon geïmporteerd. De blast radius was precies de component User Actions Log, en die component trok de site onderuit.
Wat Akeeba stilletjes had uitgesloten
De Akeeba Backup-engine heeft in elk profiel een tabblad Database table filters. Het accepteert twee soorten regels: sla de data van de tabel over (exporteer het schema, laat de rijen weg) en sla de tabel volledig over (geen schema, geen rijen). Beide zijn krachtig op een live, groeiende site. Vooral #__action_logs groeit hard op drukke installaties. Elke login, elke artikel-save, elke plugin-toggle is één INSERT. Op een nieuwssite met 12.000 artikelen en acht redacteuren had die op productie 412MB bereikt. De nachtelijke archieven duurden 14 minuten langer dan ze hadden moeten duren, en iemand had ergens onderweg de action-logs-familie aan de skip-entirely-lijst toegevoegd om de backuptijd terug te dringen.
Die keuze was redelijk voor incrementele rolling backups. De ontwikkelaar die hem maakte, wist vrijwel zeker dat een verse Joomla-installatie deze tabellen aanmaakt vanuit installation/sql/mysql/base.sql tijdens de install, en dat de tabellen op elke nieuwe omgeving aanwezig zouden zijn. Wat hij of zij niet meewoog: een Akeeba-restore draait de Joomla-installer niet. Hij zet precies terug wat in het archief zit. Bevat het archief geen schema voor die tabellen, dan heeft de teruggezette database geen schema voor die tabellen, en elk request dat een actie probeert te loggen, loopt tegen een missing-table-exception aan.
De Joomla-documentatie behandelt de component, maar zwijgt over de schema-afhankelijkheid van de core plugin op die tabellen. De docs van Akeeba zelf over database-filters waarschuwen om voorzichtig te zijn, maar een waarschuwing in een configuratietabblad is 18 maanden later onzichtbaar wanneer iemand anders op Restore klikt.
De fix van 40 minuten
De volledige reparatie kostte 40 minuten. Het meeste daarvan ging op aan het vinden van de canonieke CREATE TABLE-statements voor een verse Joomla 4.4.3-installatie. De snelste route is de bijbehorende Joomla-release-ZIP downloaden, lokaal uitpakken en installation/sql/mysql/base.sql openen. Zoek op action_logs en kopieer de vier CREATE TABLE-blokken letterlijk. Ze zijn stabiel over de hele Joomla 4.x-lijn, maar match voor de zekerheid altijd met de versie van de draaiende site.
Vervang de placeholder #__ met je werkelijke prefix (hier jos_) en draai het geheel door de staging-database. Het volledige reparatie-script van het bureau zag er zo uit, met de luide stukken weggesnoeid:
CREATE TABLE IF NOT EXISTS `jos_action_logs` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`message_language_key` varchar(255) NOT NULL DEFAULT '',
`message` text NOT NULL,
`log_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`extension` varchar(50) NOT NULL DEFAULT '',
`user_id` int NOT NULL DEFAULT 0,
`item_id` int unsigned NOT NULL DEFAULT 0,
`ip_address` varchar(40) NOT NULL DEFAULT '0.0.0.0',
PRIMARY KEY (`id`),
KEY `idx_user_id_logdate` (`user_id`,`log_date`),
KEY `idx_user_id_extension` (`user_id`,`extension`),
KEY `idx_extension` (`extension`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `jos_action_logs_extensions` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`extension` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `jos_action_logs_users` (
`user_id` int unsigned NOT NULL,
`notify` tinyint unsigned NOT NULL,
`extensions` text NOT NULL,
PRIMARY KEY (`user_id`),
KEY `idx_notify` (`notify`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `jos_action_logs_config` (
`id` tinyint unsigned NOT NULL AUTO_INCREMENT,
`type_title` varchar(255) NOT NULL DEFAULT '',
`type_alias` varchar(255) NOT NULL DEFAULT '',
`id_holder` varchar(255) DEFAULT NULL,
`title_holder` varchar(255) DEFAULT NULL,
`table_name` varchar(255) DEFAULT NULL,
`text_prefix` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;Het schema alleen is genoeg om de 500's te stoppen. Om te voorkomen dat de adminview van User Actions Log zelf alsnog crasht op een lege extensions-lijst, vul jos_action_logs_extensions ook met de standaardrijen die Joomla verwacht:
INSERT INTO `jos_action_logs_extensions` (`extension`) VALUES
('com_banners'), ('com_cache'), ('com_categories'),
('com_config'), ('com_contact'), ('com_content'),
('com_installer'), ('com_media'), ('com_menus'),
('com_messages'), ('com_modules'), ('com_newsfeeds'),
('com_plugins'), ('com_redirect'), ('com_tags'),
('com_templates'), ('com_users'), ('com_checkin'),
('com_scheduler'), ('com_workflow');De site kwam terug op de eerste reload. Backend-login werkte. De Action Logs-adminview laadde schoon en leeg, wat de juiste toestand is na een vers schema zonder historische events.
De klok van 40 minuten, opgesplitst
Voor wie dit soort incident voor het eerst behandelt, ongeveer zo verdeelt de tijd zich:
- 0 tot 5 minuten. Tail
error.log. Bevestig dat één foutklasse zich herhaalt. Bevestig dat het ontbrekende object een tabel is, geen kolom, bestand of class. De goedkope one-liner hierboven geeft een complete lijst. - 5 tot 15 minuten. Identificeer de gebruikte Joomla-release (
cat administrator/manifests/files/joomla.xml | grep version). Download de bijbehorende release-ZIP uit het officiële archief. Pak lokaal uit. Openinstallation/sql/mysql/base.sqlin je editor. - 15 tot 30 minuten. Haal de relevante CREATE TABLE-statements eruit, vervang de
#__prefix met de live prefix, en draai ze. Tools zoals de mysql-client zijn prima; een GUI ook. Draai daarna de seed-INSERTs. - 30 tot 40 minuten. Laad de site opnieuw, log in, loop de drukst bezochte URL's na, controleer of de error-log stil blijft. Verifieer dat de User Actions Log-component in de admin laadt. Maak een vers Akeeba-archief van de nu correcte DB als nieuwe baseline.
De langste subtaak is bijna altijd het vinden van de juiste CREATE TABLE-statements voor de juiste Joomla-versie. Onderhoud je meer dan twee Joomla 4-sites, houd dan ergens een map met de uitgepakte installation/sql/mysql/-tree van elke minor release die je ondersteunt. Dat alleen scheelt 10 minuten bij elk volgend incident.
Audit de restore, niet alleen de backup
De aanpassing die het bureau de maandag erop deed, kostte 90 seconden. Ze openden het Akeeba-profiel, haalden de action-logs-filters uit het nachtelijke profiel en zetten ze in een nieuw nightly-fast-profiel, en maakten een weekly-full-profiel zonder uitsluitingen. Beide profielen werden in een README van één pagina in de repo gedocumenteerd. Ze schreven een zondag-cron die een dry-run-restore doet van het weekly-full-archief naar een scratch-database, en daarna SHOW TABLES draait tegen een referentielijst van verwachte Joomla 4-tabellen. Elke diff stuurt een mail.
Die laatste stap is degene die de meeste teams overslaan, en degene die zich het snelst terugverdient. Een backup die je nooit hebt teruggezet, is geen backup. Het is een bestand dat bestaat.
Toen we Pier bouwden, kwamen we precies hetzelfde tegen op de legacy site van een klant, twee keer binnen een maand, op twee verschillende Joomla-projecten. Zo hebben we het uiteindelijk aangepakt: de ingebouwde MySQL editor doet een schema-diff tussen de gekoppelde database en een referentie-Joomla 4-schema dat met de app meekomt, dus ontbrekende tabellen markeren zichzelf in de sidebar nog voordat je code pusht die ervan afhankelijk is. De volledige version history van elke SQL-wijziging die je draait, blijft lokaal bewaard, dus het reparatiescript hierboven was bij de volgende site één klik om opnieuw af te spelen.
Het kleinste wat je vandaag kunt doen: open je actieve Akeeba-profiel, klik op het tabblad Database table filters, en maak er een screenshot van. Komen #__action_logs, #__action_logs_extensions, #__action_logs_users of #__action_logs_config ergens in die lijst voor, dan zit er een restore-nachtprobleem aan te komen. Verplaats ze naar een snel nachtprofiel, houd een weekprofiel zonder uitsluitingen, en slaap rustiger op vrijdagen.
— Vragen —
Maakt Joomla de ontbrekende action_logs-tabellen vanzelf opnieuw aan?
Nee. Joomla schrijft core-schema alleen weg tijdens een verse installatie of een update-run. Een restore speelt het archief letterlijk terug, dus elke tabel die door het backup-filter is uitgesloten, blijft ontbreken totdat je hem zelf aanmaakt.
Is het veilig om action_logs uit te sluiten van nachtelijke backups?
Ja, zolang minstens één wekelijks profiel de volledige action_logs-familie wel meeneemt. Behandel het snelle profiel als routinematige ops en het volledige profiel als je disaster-recovery-baseline.
Wat als mijn tabelprefix niet jos_ is?
Vervang jos_ door je werkelijke prefix in elk CREATE TABLE-statement. De prefix vind je in configuration.php als de public $dbprefix-waarde, of onder #__ in de oorspronkelijke base.sql van Joomla.