ithilbert / alerts
Zentrale Störungsmeldungen per Mail und Telegram für alle Laravel-Projekte – mit Drosselung und Freigabe.
Requires
- php: ^8.5
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-23 09:08:27 UTC
README
Basis-Package ithilbert/alerts für Störungsmeldungen per Mail und Telegram. Es ist für
jedes Laravel-Projekt gedacht und wird dort einmal installiert. Andere Packages
melden Störungen über den Port AlertReporter, ohne die Kanäle zu kennen.
Das Package erfasst automatisch unbehandelte Exceptions, kritische Log-Einträge sowie 404- und 403-Antworten. Wiederholungen werden pro Fehlerursache gedrosselt. Nach der Behebung gibt der Entwickler die Sperre per Befehl oder signiertem Link wieder frei.
Installation
Das Package liegt auf GitHub (privates Repository). In der composer.json des Projekts
das VCS-Repository eintragen:
"repositories": [ { "type": "vcs", "url": "https://github.com/ITHilbert/alerts.git" } ]
Dann installieren:
composer require ithilbert/alerts
Solange noch kein Release-Tag existiert, mit composer require ithilbert/alerts:dev-main
installieren.
Da das Repository privat ist, braucht Composer Zugriff auf GitHub (z.B. über einen
GitHub-Token in auth.json oder COMPOSER_AUTH).
Die Konfiguration ist optional. Das Package bringt Standardwerte mit und bindet sie selbst ein.
Veröffentlichen nur nötig, wenn Werte in config/alerts.php angepasst werden sollen:
php artisan vendor:publish --tag=alerts-config
Die ServiceProvider-Registrierung erfolgt über Laravel-Auto-Discovery.
Voraussetzungen: PHP ^8.5, Laravel ^12.
Konfiguration (.env)
Alle Werte stammen aus src/Config/alerts.php. Die Spalte „Standard“ zeigt den Wert ohne
gesetzte Variable.
| Variable | Standard | Bedeutung |
|---|---|---|
ALERTS_ENABLED |
true |
false schaltet Erfassung und Versand komplett ab. Sinnvoll für lokale Entwicklung und Tests. |
ALERTS_CHANNELS |
mail,telegram |
Aktive Kanäle, kommagetrennt. Gültig: mail, telegram. |
ALERTS_MAIL_TO |
leer | Empfänger, kommagetrennt. Leer = Mail-Kanal inaktiv. |
ALERTS_MAILER |
leer | Name eines Mailers aus config/mail.php. Leer = MAIL_MAILER. |
ALERTS_MAIL_FEHLERTEXT |
true |
Fehlertext (Exception-Meldung bzw. Fehlertext des Ports) in der Mail. |
ALERTS_TELEGRAM_BOT_TOKEN |
leer | Token des Bots von BotFather. Leer = Telegram-Kanal inaktiv. |
ALERTS_TELEGRAM_CHAT_ID |
leer | Chat-ID des Empfängers (Person oder Gruppe). Leer = Telegram-Kanal inaktiv. |
ALERTS_TELEGRAM_FEHLERTEXT |
false |
Fehlertext in der Telegram-Nachricht. Standard aus Datenschutzgründen aus, siehe unten. |
ALERTS_TELEGRAM_TIMEOUT |
5 |
Gesamt-Timeout des Telegram-Aufrufs in Sekunden. |
ALERTS_TELEGRAM_CONNECT_TIMEOUT |
3 |
Verbindungs-Timeout in Sekunden. |
ALERTS_SPERRE_STUNDEN |
24 |
Sperrdauer pro Fingerabdruck in Stunden. Dezimalwerte sind erlaubt. |
ALERTS_CACHE_STORE |
leer | Cache-Store für Sperren. Leer = Standard-Store der App. |
ALERTS_EXCEPTIONS |
true |
Unbehandelte Exceptions erfassen. |
ALERTS_LOG |
true |
Log-Einträge ab Level critical erfassen. |
ALERTS_HTTP_404 |
true |
404 mit internem Referer erfassen. |
ALERTS_HTTP_403 |
true |
403 bei angemeldetem Nutzer erfassen. |
ALERTS_FREIGABE_TAGE |
7 |
Gültigkeit des Freigabe-Links in Tagen. |
Nicht über .env einstellbar (nur in config/alerts.php):
alerts.telegram.api_urlist fest aufhttps://api.telegram.orggesetzt. So kann kein Env-Fehler den Bot-Token an einen fremden Host senden.alerts.drosselung.prefix(alerts) undalerts.freigabe.prefix(alerts) bestimmen Cache-Präfix und URL-Präfix der Freigabe-Routen.alerts.freigabe.middleware(Standardweb,throttle:6,1) schützt die Freigabe-Route.
Ein aktiver Kanal ohne vollständige Konfiguration wird still übersprungen. Ist in
ALERTS_CHANNELS also telegram gesetzt, ohne Token oder Chat-ID, passiert nichts
und es erscheint kein Hinweis.
APP_NAME und APP_ENV stehen in jeder Nachricht. APP_URL muss das öffentliche Projekt-URL
sein, weil daraus der Freigabe-Link gebaut wird und die Erkennung interner 404-Referer darauf beruht.
Telegram einrichten
-
Bot anlegen: In Telegram den Kontakt
@BotFatheröffnen,/newbotsenden und Namen sowie Benutzernamen (Endungbot) vergeben. BotFather liefert den Token. Er gehört nur in die.env(ALERTS_TELEGRAM_BOT_TOKEN) und nie ins Repository. -
Chat öffnen: Den neuen Bot in Telegram anschreiben und
/startsenden. Bei einer Gruppe den Bot in die Gruppe aufnehmen und dort eine Nachricht schreiben. -
Chat-ID ermitteln: Im Browser aufrufen:
https://api.telegram.org/bot<TOKEN>/getUpdatesIn der Antwort steht bei
"chat":{"id": ...}die Chat-ID. Gruppen-IDs sind negativ und beginnen mit-. Die ID inALERTS_TELEGRAM_CHAT_IDeintragen.getUpdatesliefert nur Nachrichten, die nicht älter als etwa 24 Stunden sind und noch nicht per Webhook abgeholt wurden. Ist die Antwort leer, eine neue Nachricht schreiben und den Aufruf wiederholen. -
Test: Ein Aufruf über den Port (siehe unten) auslösen und prüfen, ob die Nachricht ankommt.
Was wird erfasst
| Quelle | Auslöser | Fingerabdruck | Titel |
|---|---|---|---|
Exception (Exception) |
Unbehandelte Exception, die Laravel meldet. dontReport-Exceptions (z.B. ValidationException, HttpException) bleiben außen vor. |
exception.<klasse>.<hash> |
Klassenname ohne Namespace |
Log (Log) |
Log::critical(), Log::alert(), Log::emergency(). error und darunter lösen nichts aus. |
In dieser Reihenfolge: gültiger alert_key im Kontext; Exception im Kontext (exception) → wie bei der Exception-Erfassung; sonst log.<meldung-normalisiert>.<hash> |
Log-Meldung |
404 (404) |
Antwort 404 und Referer gehört zur Domain aus APP_URL. Ohne Referer oder mit fremdem Referer keine Meldung. |
404.<pfad>.<hash> |
„Seite nicht gefunden (404)“ |
403 (403) |
Antwort 403 und ein angemeldeter Nutzer. Anonyme 403 werden ignoriert. | 403.<pfad>.<hash> |
„Zugriff verweigert (403)“ |
Port (Meldung) |
Aufruf von AlertReporter::melde() |
genau der übergebene Schlüssel | titel aus Meldung |
Bei 404 und 403 wird nur der Pfad für den Fingerabdruck verwendet. Der Query-String fehlt in allen Nachrichten. Die 403-Nachricht enthält keine Nutzerdaten (kein Name, keine ID, keine E-Mail).
Fallstricke bei der Erfassung
- Exception-Fingerabdruck: Er enthält Klasse, Datei und Zeile, aber nicht die Meldung. Verschiebt ein Deployment die Zeile, gilt das als neue Störung und löst erneut eine Meldung aus.
- Exception zusätzlich auf
criticalgeloggt: Wird die Exception im Log-Kontext alsexceptionübergeben (Log::critical('…', ['exception' => $e])), nutzt der Log-Kanal denselben Fingerabdruck wie die Exception-Erfassung. Es entsteht nur eine Meldung, unabhängig von der Reihenfolge. - Log ohne
alert_key: Der Fingerabdruck entsteht aus dem Meldungstext, wobei jede Ziffernfolge vorher durch#ersetzt wird. „Import 17 abgebrochen“ und „Import 18 abgebrochen“ ergeben also dieselbe Meldung. Andere variable Werte (Namen, Hex-IDs, UUIDs, Dateinamen) werden nicht normalisiert und erzeugen je eigene Meldungen. Für wiederkehrende Fehler mit solchen Wertenalert_keysetzen. - Ungültiger
alert_key: Entspricht der Schlüssel nicht dem Format (siehe unten), wird eine Warnung geloggt und der Fingerabdruck wie ohne Schlüssel abgeleitet. - 404 je Pfad: Jede betroffene URL hat einen eigenen Fingerabdruck. Viele tote URLs mit unterschiedlichen IDs erzeugen viele Meldungen.
Nutzung aus anderen Packages
Ein Package meldet Störungen ausschließlich über den Port AlertReporter. Der Port wird per
Dependency Injection aufgelöst. Kein new des Dienstes, keine Facade, keine direkte
Abhängigkeit auf Interna des Alert-Packages.
<?php declare(strict_types=1); namespace Vendor\Rechtstexte; use ITHilbert\Alerts\Contracts\AlertReporter; use ITHilbert\Alerts\Domain\Meldung; final class RechtstextLader { public function __construct(private readonly AlertReporter $alerts) {} public function lade(string $typ): ?string { // ... Abruf schlägt fehl und es gibt keinen Fallback ... $this->alerts->melde(new Meldung( schluessel: 'consentlaw.kein-fallback', titel: 'Rechtstext nicht lieferbar', fehlertext: 'API antwortete mit HTTP 503', details: ['typ' => $typ], )); return null; } }
Die Parameter von Meldung sind benannt:
| Parameter | Typ | Pflicht | Bedeutung |
|---|---|---|---|
schluessel |
string |
ja | Fingerabdruck der Störung, siehe Format unten |
titel |
string |
ja | Betreff und Überschrift der Nachricht |
fehlertext |
?string |
nein | Technischer Text. Wird je nach Kanal-Einstellung mitgesendet |
details |
array<string, scalar|null> |
nein | Zusatzfelder, z.B. ['typ' => 'impressum'] |
Schlüsselformat: /^[a-z0-9][a-z0-9._-]{0,99}$/. Empfohlen ist <paket>.<ursache>, z.B.
consentlaw.kein-fallback. Der Schlüssel muss fest und sprechend sein, nicht aus Werten
zusammengesetzt. Ein ungültiger Schlüssel wirft ITHilbert\Alerts\Exceptions\UngueltigerSchluessel
(eine \DomainException) schon beim Erzeugen von Meldung. Das ist ein Entwicklungsfehler
und sollte in den Tests des Packages auffallen.
melde() wirft keine Exception wegen Versandfehlern. Ein Package darf den Aufruf deshalb
ohne eigenes try/catch verwenden.
Das Package ist eine Pflicht-Abhängigkeit für jedes Package, das den Port nutzt. Ohne
installiertes ithilbert/alerts schlägt die DI-Auflösung fehl.
Drosselung und Freigabe
Sperre
- Die erste Störung eines Fingerabdrucks wird sofort gemeldet. Danach ist dieser Fingerabdruck
für
ALERTS_SPERRE_STUNDEN(Standard 24 h) gesperrt. - Weitere Auftreten innerhalb der Sperre werden nur gezählt und nicht verschickt.
- Nach Ablauf der Sperre kommt die nächste Störung als Sammelmeldung mit dem Hinweis „X× seit letzter Meldung“.
- Die Sperre gilt auch dann, wenn der Versand fehlschlägt. Ein Fehlschlag soll keine Meldungsflut auslösen.
- Die Sperre liegt im Cache (
ALERTS_CACHE_STORE), nicht in der Datenbank. Meldungen funktionieren so auch bei ausgefallener Datenbank.
Fallstrick: Ist der Cache nicht erreichbar, wird die Meldung ohne Sperre verschickt und der
Fehler auf warning geloggt. Bei einem Cache-Ausfall gibt es also keine Drosselung.
Freigabe per Befehl
# Eine bestimmte Sperre aufheben (Sperre und Zähler) php artisan alerts:reset consentlaw.kein-fallback # Alle Sperren aufheben php artisan alerts:reset --all
- Das nächste Auftreten nach einem Reset wird sofort gemeldet.
- Ist der Fingerabdruck unbekannt, gibt der Befehl einen Hinweis aus und endet mit Exit-Code
1. - Ohne Fingerabdruck und ohne
--allendet der Befehl ebenfalls mit Exit-Code1.
Freigabe per Link
Jede Mail und jede Telegram-Nachricht enthält einen signierten Link:
https://<APP_URL>/alerts/freigabe/<fingerabdruck>?expires=…&signature=…
Der Link ist standardmäßig 7 Tage gültig (ALERTS_FREIGABE_TAGE).
- GET öffnet nur eine Bestätigungsseite. Die Sperre bleibt bestehen.
- POST mit CSRF-Token gibt die Sperre frei. Erst dieser Schritt ändert etwas.
- Ungültige oder abgelaufene Signatur liefert HTTP 403.
- Die Route ist gedrosselt auf 6 Anfragen pro Minute.
Warum zweistufig: Mail-Scanner, Link-Vorschauen und Messenger rufen Links automatisch per GET auf. Eine Freigabe per GET würde Sperren ungewollt lösen, bevor jemand den Fehler gesehen hat.
Fallstrick: Der Link ist die Berechtigung. Wer ihn besitzt, kann die Sperre innerhalb der Gültigkeit ohne Anmeldung aufheben. Deshalb nur an eigene Empfänger senden.
Die Bestätigungsseite und die Erfolgsseite laden keine externen Assets, Schriften oder Skripte (DSGVO).
Fehlertext je Kanal und Datenschutz
Der Fehlertext ist die technische Meldung einer Exception oder der Parameter fehlertext aus
Meldung. Ob er mitgesendet wird, legt jeder Kanal für sich fest:
| Kanal | Variable | Standard | Begründung |
|---|---|---|---|
ALERTS_MAIL_FEHLERTEXT |
true |
Die Mail geht nur an die eigenen Adressen. Der Text hilft bei der Fehlersuche. | |
| Telegram | ALERTS_TELEGRAM_FEHLERTEXT |
false |
Telegram ist ein Drittanbieter außerhalb der EU. Fehlertexte können personenbezogene Daten enthalten. |
Der Ort (Datei und Zeile) bei Exceptions erscheint unabhängig davon immer.
Datenschutz-Hinweise:
- Es werden keine Nutzerdaten, keine Secrets und kein Bot-Token gesendet. Der Token wird auch im Log nicht ausgegeben.
- Fehlertexte von Exceptions können Werte enthalten, z.B. E-Mail-Adressen oder IDs. Mit
ALERTS_TELEGRAM_FEHLERTEXT=trueübernimmt der Betreiber die Verantwortung dafür, dass diese Daten an Telegram übertragen werden dürfen. detailsinMeldunglanden unverändert in Mail und Telegram. Dort keine personenbezogenen Daten übergeben.- Die URL in der Nachricht enthält keinen Query-String.
Vorlagen überschreiben
Die Vorlagen für Mail und Telegram sowie die Freigabe-Seiten lassen sich veröffentlichen:
php artisan vendor:publish --tag=alerts-views
Die Kopien liegen danach unter resources/views/vendor/alerts/ und haben Vorrang vor dem Package.
Die Vorlagen erhalten die Variablen $alert und $mitFehlertext.
Die Telegram-Vorlage wird als Klartext gesendet, ohne HTML- oder Markdown-Auszeichnung.
Tests
Das Package testet gegen eine echte Laravel-Anwendung (Orchestra Testbench):
composer install vendor/bin/phpunit
Hinweise
- Versand ist synchron. Mail und Telegram werden direkt im Request oder Befehl verschickt. Ein Queue-Worker ist nicht nötig. Der Grund: Fällt der Worker aus, bliebe sonst genau die Meldung aus, die den Ausfall anzeigen soll. Telegram hat einen knappen Timeout (5 s).
- Die Abschaltung über
ALERTS_ENABLED=falsewirkt auf Erfassung und Versand. - Eigene Kanäle (z.B. Slack) sind nicht enthalten. Sie lassen sich später als zusätzliche Notification-Kanäle ergänzen, ohne den Port zu ändern.