SSR alert mail recipients maintainable in the site settings
New site setting vitec.ssr.alertEmails (Site Management, Settings, VITEC; one address per entry). vitec:ssr-health reads the recipients from there, invalid entries are skipped and logged, SSR_ALERT_EMAIL in the env file stays as fallback. The cockpit shows the recipients in the live band. Pre-filled with the address that was in the env file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
994f9f2a1b
commit
90cb2faaf9
6 files changed
+81
-6
No files matched your search
@@ -23,3 +23,5 @@ vitec.ssr.enabled: true
|
|||||||
# Shell-Seiten (Produkt-/News-/Story-Detail-Container) nicht in die XML-Sitemap
|
# Shell-Seiten (Produkt-/News-/Story-Detail-Container) nicht in die XML-Sitemap
|
||||||
# aufnehmen; 106 = alter /en/news-Ast (noindex-Duplikate). SEO-Audit 2026-11-09.
|
# aufnehmen; 106 = alter /en/news-Ast (noindex-Duplikate). SEO-Audit 2026-11-09.
|
||||||
seo.sitemap.pages.excludePagesRecursive: '10,21,26,106'
|
seo.sitemap.pages.excludePagesRecursive: '10,21,26,106'
|
||||||
|
vitec.ssr.alertEmails:
|
||||||
|
- info@evomedien.de
|
||||||
@@ -206,15 +206,20 @@ final class SsrHealthSampleCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mails are best effort: a mail problem must never break the sampler
|
* Mails are best effort: a mail problem must never break the sampler
|
||||||
* (the cron would stop logging history). Recipients come from
|
* (the cron would stop logging history). Recipients come from the site
|
||||||
* SSR_ALERT_EMAIL in var/vitec-ssr-purge.env - unset means "alerts
|
* setting vitec.ssr.alertEmails (Site Management › Settings › VITEC),
|
||||||
* off", which the output states so the cron log shows why.
|
* fallback SSR_ALERT_EMAIL in var/vitec-ssr-purge.env - none at all
|
||||||
|
* means "alerts off", which the output states so the cron log shows why.
|
||||||
*/
|
*/
|
||||||
private function sendAlert(string $subject, string $body, OutputInterface $output): void
|
private function sendAlert(string $subject, string $body, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$recipients = GeneralUtility::trimExplode(',', $this->purgeService->getAlertEmail(), true);
|
$alert = $this->purgeService->getAlertRecipients();
|
||||||
|
if ($alert['invalid'] !== []) {
|
||||||
|
$output->writeln('<comment>skipped invalid alert addresses: ' . implode(', ', $alert['invalid']) . '</comment>');
|
||||||
|
}
|
||||||
|
$recipients = $alert['recipients'];
|
||||||
if ($recipients === []) {
|
if ($recipients === []) {
|
||||||
$output->writeln('<comment>transition detected but SSR_ALERT_EMAIL is not configured - no mail sent</comment>');
|
$output->writeln('<comment>transition detected but no alert recipients are configured (Site settings › VITEC or SSR_ALERT_EMAIL) - no mail sent</comment>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ final class SsrController
|
|||||||
$lastSample = $healthLog[0] ?? null;
|
$lastSample = $healthLog[0] ?? null;
|
||||||
$clientErrors = $this->clientErrors(50);
|
$clientErrors = $this->clientErrors(50);
|
||||||
$cockpit = $this->cockpitData($health, $lastSample);
|
$cockpit = $this->cockpitData($health, $lastSample);
|
||||||
|
$alert = $this->purgeService->getAlertRecipients();
|
||||||
|
|
||||||
$settingsUri = '';
|
$settingsUri = '';
|
||||||
if ($GLOBALS['BE_USER']->isAdmin()) {
|
if ($GLOBALS['BE_USER']->isAdmin()) {
|
||||||
@@ -92,6 +93,10 @@ final class SsrController
|
|||||||
'cockpitJson' => json_encode($cockpit, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
'cockpitJson' => json_encode($cockpit, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
||||||
'settingsUri' => $settingsUri,
|
'settingsUri' => $settingsUri,
|
||||||
'failoverAfter' => $this->purgeService->getFailoverAfter(),
|
'failoverAfter' => $this->purgeService->getFailoverAfter(),
|
||||||
|
'alert' => $alert,
|
||||||
|
'alertList' => implode(', ', $alert['recipients']),
|
||||||
|
'alertCount' => count($alert['recipients']),
|
||||||
|
'alertInvalid' => implode(', ', $alert['invalid']),
|
||||||
'chunkCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => $r['is_chunk_reload'])),
|
'chunkCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => $r['is_chunk_reload'])),
|
||||||
'jsErrorCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => !$r['is_chunk_reload'])),
|
'jsErrorCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => !$r['is_chunk_reload'])),
|
||||||
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
|
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace Evomedien\Vitec\Service;
|
|||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Core\Environment;
|
use TYPO3\CMS\Core\Core\Environment;
|
||||||
use TYPO3\CMS\Core\Http\RequestFactory;
|
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||||
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Talks to the SSR node's POST /__purge endpoint (contract 0054-FE) and
|
* Talks to the SSR node's POST /__purge endpoint (contract 0054-FE) and
|
||||||
@@ -24,6 +26,7 @@ final class SsrPurgeService
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RequestFactory $requestFactory,
|
private readonly RequestFactory $requestFactory,
|
||||||
private readonly ConnectionPool $connectionPool,
|
private readonly ConnectionPool $connectionPool,
|
||||||
|
private readonly SiteFinder $siteFinder,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static function getConfigPath(): string
|
public static function getConfigPath(): string
|
||||||
@@ -66,6 +69,50 @@ final class SsrPurgeService
|
|||||||
return trim($this->getConfig()['SSR_ALERT_EMAIL'] ?? '');
|
return trim($this->getConfig()['SSR_ALERT_EMAIL'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Who gets the vitec:ssr-health alert mails. Maintained by editors in
|
||||||
|
* Site Management › Settings › VITEC (vitec.ssr.alertEmails, one
|
||||||
|
* address per entry); the first site with at least one valid address
|
||||||
|
* wins. Without such a list, SSR_ALERT_EMAIL from the env file is the
|
||||||
|
* fallback. Invalid entries are skipped and reported back.
|
||||||
|
*
|
||||||
|
* @return array{recipients: list<string>, source: string, invalid: list<string>}
|
||||||
|
*/
|
||||||
|
public function getAlertRecipients(): array
|
||||||
|
{
|
||||||
|
$invalid = [];
|
||||||
|
try {
|
||||||
|
foreach ($this->siteFinder->getAllSites() as $site) {
|
||||||
|
$list = $site->getSettings()->get('vitec.ssr.alertEmails', []);
|
||||||
|
if (is_string($list)) {
|
||||||
|
$list = GeneralUtility::trimExplode(',', $list, true);
|
||||||
|
}
|
||||||
|
$valid = [];
|
||||||
|
foreach ((array)$list as $address) {
|
||||||
|
$address = trim((string)$address);
|
||||||
|
if ($address === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (GeneralUtility::validEmail($address)) {
|
||||||
|
$valid[] = $address;
|
||||||
|
} else {
|
||||||
|
$invalid[] = $address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($valid !== []) {
|
||||||
|
return ['recipients' => array_values(array_unique($valid)), 'source' => 'site settings', 'invalid' => $invalid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// No site configuration readable (e.g. broken YAML): use the env fallback.
|
||||||
|
}
|
||||||
|
$env = array_values(array_filter(
|
||||||
|
GeneralUtility::trimExplode(',', $this->getAlertEmail(), true),
|
||||||
|
static fn(string $a): bool => GeneralUtility::validEmail($a)
|
||||||
|
));
|
||||||
|
return ['recipients' => $env, 'source' => $env !== [] ? 'var/vitec-ssr-purge.env' : '', 'invalid' => $invalid];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consecutive DOWN samples after which vitec:ssr-health fails over to
|
* Consecutive DOWN samples after which vitec:ssr-health fails over to
|
||||||
* the SPA (SSR_FAILOVER_AFTER, default 2 = ~10 minutes at the 5-minute
|
* the SPA (SSR_FAILOVER_AFTER, default 2 = ~10 minutes at the 5-minute
|
||||||
|
|||||||
@@ -134,3 +134,10 @@ settings:
|
|||||||
category: VITEC
|
category: VITEC
|
||||||
type: bool
|
type: bool
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
vitec.ssr.alertEmails:
|
||||||
|
label: 'SSR alert mail recipients'
|
||||||
|
description: 'E-mail addresses that get the alert mails of the SSR health check: node down, node back, automatic fallback to the SPA. One address per entry; invalid addresses are skipped. Empty: the addresses from SSR_ALERT_EMAIL in var/vitec-ssr-purge.env are used.'
|
||||||
|
category: VITEC
|
||||||
|
type: stringlist
|
||||||
|
default: []
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<!-- Live band: node state, release, delivery switch, 24 h availability -->
|
<!-- Live band: node state, release, delivery switch, 24 h availability -->
|
||||||
<section class="vc-card vc-hero vc-s12" aria-label="Live status">
|
<section class="vc-card vc-hero vc-s12" aria-label="Live status">
|
||||||
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-live">?</button><span class="vc-tip" role="tooltip" id="vc-tip-live">Live state of the SSR node on the VPS. The dot pulses green while the node answers its health check. Delivery shows whether visitors get server-rendered pages; the switch is in the Site settings. The strip shows the last 24 hours in 15-minute windows: green ok, yellow slower than 400 ms, red down, grey no sample. After {failoverAfter} failed checks in a row the cron switches back to the SPA and sends an alert mail.</span></span>
|
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-live">?</button><span class="vc-tip" role="tooltip" id="vc-tip-live">Live state of the SSR node on the VPS. The dot pulses green while the node answers its health check. Delivery shows whether visitors get server-rendered pages; the switch is in the Site settings. The strip shows the last 24 hours in 15-minute windows: green ok, yellow slower than 400 ms, red down, grey no sample. After {failoverAfter} failed checks in a row the cron switches back to the SPA. Alert mails go to the addresses under Site settings › VITEC › SSR alert mail recipients; hover “Alert mail” to see them.</span></span>
|
||||||
<div class="vc-hero-top">
|
<div class="vc-hero-top">
|
||||||
<div class="vc-live">
|
<div class="vc-live">
|
||||||
<f:if condition="{health}">
|
<f:if condition="{health}">
|
||||||
@@ -67,6 +67,15 @@
|
|||||||
</f:if>
|
</f:if>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="vc-fact">
|
||||||
|
<dt>Alert mail</dt>
|
||||||
|
<dd>
|
||||||
|
<f:if condition="{alertCount}">
|
||||||
|
<f:then><span class="vc-pill {f:if(condition: alertInvalid, then: 'is-warn', else: 'is-ok')}" title="{alertList} (from {alert.source}){f:if(condition: alertInvalid, then: ' · skipped, not valid: {alertInvalid}')}">{alertCount} recipient<f:if condition="{alertCount} != 1">s</f:if></span></f:then>
|
||||||
|
<f:else><span class="vc-pill is-warn" title="No alert mails are sent. Add addresses in Site Management › Settings › VITEC › SSR alert mail recipients.">none</span></f:else>
|
||||||
|
</f:if>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div class="vc-fact">
|
<div class="vc-fact">
|
||||||
<dt>Auto failover</dt>
|
<dt>Auto failover</dt>
|
||||||
<dd><span class="vc-pill is-navy" title="Cron vitec:ssr-health every 5 minutes: alert mail and fallback to the SPA after {failoverAfter} failed checks in a row">after {failoverAfter} failed checks</span></dd>
|
<dd><span class="vc-pill is-navy" title="Cron vitec:ssr-health every 5 minutes: alert mail and fallback to the SPA after {failoverAfter} failed checks in a row">after {failoverAfter} failed checks</span></dd>
|
||||||
|
|||||||
Reference in new issue
Block a user