Markus Michalski FAQ

Watchdog: Eigene Checks schreiben

Eigenen Check-Typ für Watchdog in PHP implementieren: CheckInterface, Konfigurationsschema, Runner-Modus, Statuslogik, Auslieferung per Image-Rebuild und Unit-Test.

English Version

Zurück zur Übersicht

Watchdog: Eigene Checks schreiben

Das Check-System ist erweiterbar. Jede PHP-Klasse, die CheckInterface implementiert und das Attribut #[AutoconfigureTag('watchdog.check')] trägt, findet die CheckRegistry automatisch. Der Typ erscheint danach im Auswahlfeld Check type, ohne YAML-Konfiguration.

Eigene Checks sind Code im Image. Nach dem Anlegen der Klasse musst Du das Image neu bauen und ausrollen, zum Beispiel mit make stage-update oder make live-update. Betreibst Du Agenten, brauchen sie ebenfalls ein neues Image, wenn der Typ dort laufen soll.

Der Vertrag

interface CheckInterface
{
    public function getType(): string;               // eindeutige Kennung, z. B. "my_custom"
    public function getLabel(): string;              // Anzeigename in Auswahl, Dashboard, E-Mail
    public function getDefaultConfig(): array;       // Standardwerte, werden mit der Konfiguration gemischt
    public function getConfigSchema(): array;        // Formularfelder
    public function runnerMode(): RunnerMode;        // AgentOnly, DashboardOnly oder Both
    public function run(SiteCheck $check): CheckResult;
    public function getEmailTargetLabel(): ?string;  // Spaltenname für das Ziel in der E-Mail
    public function resolveEmailTarget(array $config): ?string;
}

Schritt 1: Klasse anlegen

Lege die Klasse in src/Check/ an:

<?php

declare(strict_types=1);

namespace App\Check;

use App\Entity\CheckResult;
use App\Entity\SiteCheck;
use App\Enum\CheckStatus;
use App\Enum\RunnerMode;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('watchdog.check')]
final class MyCustomCheck implements CheckInterface
{
    public function getType(): string
    {
        return 'my_custom';
    }

    public function getLabel(): string
    {
        return 'My Custom Check';
    }

    public function runnerMode(): RunnerMode
    {
        return RunnerMode::Both;
    }

    public function getDefaultConfig(): array
    {
        return ['threshold' => 100];
    }

    public function getConfigSchema(): array
    {
        return [
            [
                'name' => 'threshold',
                'label' => 'Threshold',
                'type' => 'number',
                'required' => false,
                'default' => 100,
                'placeholder' => '100',
                'help' => 'Fail if the measured value exceeds this threshold.',
            ],
        ];
    }

    public function getEmailTargetLabel(): ?string
    {
        return 'Threshold';
    }

    public function resolveEmailTarget(array $config): ?string
    {
        return isset($config['threshold']) ? (string) $config['threshold'] : null;
    }

    public function run(SiteCheck $check): CheckResult
    {
        $config = array_merge($this->getDefaultConfig(), $check->getConfig());
        $result = new CheckResult();
        $result->setCheck($check);

        $value = 42; // hier Deine Messung
        $threshold = (int) $config['threshold'];

        if ($value > $threshold) {
            $result->setStatus(CheckStatus::Fail);
            $result->setMessage(sprintf('Value %d exceeds threshold %d', $value, $threshold));
        } else {
            $result->setStatus(CheckStatus::Ok);
            $result->setMessage(sprintf('Value %d (threshold: %d)', $value, $threshold));
        }

        return $result;
    }
}

Schritt 2: Runner-Modus wählen

Modus Wann
RunnerMode::DashboardOnly Der Check braucht eine URL aus dem Client-Datensatz (wie HTTP oder SSL). Agenten überspringen ihn
RunnerMode::AgentOnly Der Check braucht direkten Zugriff auf die Maschine (Dateisystem, /proc, Prozesse)
RunnerMode::Both Netzwerk-Check, der von jeder Seite laufen kann

Läuft der Check auf einem Agenten, übergibt Watchdog ihm ein leeres SiteCheck-Objekt, das nur Typ und Konfiguration enthält. Greife in run() bei AgentOnly und Both daher nur auf getConfig() zu, nicht auf Client oder ID.

Schritt 3: Konfigurationsschema

Jeder Eintrag von getConfigSchema() ist ein Formularfeld:

Schlüssel Bedeutung
name Schlüssel in der gespeicherten Konfiguration
label Beschriftung im Formular
type Feldtyp, siehe Tabelle
required Markiert das Feld mit * und setzt im Browser required
default Vorbelegung
placeholder Platzhaltertext
help Hilfetext unter dem Feld
Feldtyp Darstellung Gespeichert als
--------- ------------- -----------------
text Textfeld Text
number Zahlenfeld ganze Zahl
float Zahlenfeld mit Dezimalstellen Kommazahl
password Passwortfeld Text, unverschlüsselt
duration Tage, Stunden, Minuten Minuten als ganze Zahl
client_url_select Auswahl einer URL des Clients ID der URL als ganze Zahl
client_url_multiselect Mehrfachauswahl von URLs des Clients Liste der Hostnamen
checkbox Kontrollkästchen siehe Hinweis

required wird nur im Browser erzwungen, der Server prüft die Konfiguration nicht. Beim Speichern überspringt Watchdog leere Felder. Nur wenn alle Felder leer sind, speichert es getDefaultConfig() komplett. Fehlende einzelne Werte ergänzt der Check selbst, indem run() die gespeicherte Konfiguration per array_merge mit getDefaultConfig() mischt. Prüfe Werte deshalb in run() und gib bei fehlender Konfiguration CheckStatus::Unknown zurück. Ein leerer Wert eines Kontrollkästchens wird nicht gespeichert, die Checkbox ist daher nicht zuverlässig nutzbar. Verwende stattdessen ein number-Feld mit 0 und 1.

Ein leeres Schema ([]) bedeutet, dass der Check keine Konfiguration braucht.

Schritt 4: Status und Ergebnis

run() gibt ein nicht gespeichertes CheckResult zurück. Watchdog speichert es und wertet es aus.

Status Bedeutung E-Mail
CheckStatus::Ok In Ordnung Wiederherstellungs-Mail nach WARN oder FAIL
CheckStatus::Warn Schwelle erreicht Fehler-Mail beim Wechsel aus OK oder UNKNOWN
CheckStatus::Fail Fehlgeschlagen Fehler-Mail beim Wechsel aus OK oder UNKNOWN
CheckStatus::Unknown Kann nicht laufen (fehlende Konfiguration, nicht verfügbare Ressource) keine

Die Meldung (setMessage, bis 1024 Zeichen sinnvoll) erscheint im Dashboard, im Verlauf und in der E-Mail. Mit setResponseTimeMs() setzt Du eine Antwortzeit. Läuft der Check auf einem Agenten und setzt keine, trägt der Agent die gemessene Laufzeit ein. Auf dem Dashboard bleibt das Feld ohne eigenen Wert leer.

Schritt 5: Abhängigkeiten

Checks sind normale Symfony-Services mit Autowiring. Verpacke Zugriffe auf System und Netzwerk in Interfaces, damit Du sie im Test ersetzen kannst. Alle eingebauten Checks folgen diesem Muster.

public function __construct(
    private readonly HttpClientInterface $httpClient,
) {
}

Schritt 6: Test

Tests liegen in tests/Unit/Check/ und laufen mit make test:

final class MyCustomCheckTest extends TestCase
{
    public function testReturnsOkBelowThreshold(): void
    {
        $siteCheck = new SiteCheck();
        $siteCheck->setType('my_custom');
        $siteCheck->setConfig(['threshold' => 100]);

        $result = (new MyCustomCheck())->run($siteCheck);

        self::assertSame(CheckStatus::Ok, $result->getStatus());
    }
}

Beitragen

Ist Dein Check allgemein nützlich, kannst Du einen Pull Request öffnen. Ablauf und Code-Stil stehen in CONTRIBUTING.md. Für Beiträge ist das Contributor License Agreement nötig.