KPI "Hooks heute": nur die heute empfangenen Hooks zaehlen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
266432c7df
commit
fb0baff3df
7 changed files with 59 additions and 37 deletions
8
API.md
8
API.md
|
|
@ -646,11 +646,9 @@ Auth: keine (oeffentlich wie die anderen `GET`-Endpunkte).
|
||||||
```
|
```
|
||||||
|
|
||||||
`count` zaehlt jede angenommene Zustellung an `POST /api/v1/webhooks/codeberg` (auch
|
`count` zaehlt jede angenommene Zustellung an `POST /api/v1/webhooks/codeberg` (auch
|
||||||
`ping`) **seit dem letzten Start des `api`-Containers** - nicht seit Projektbeginn: die
|
`ping`) **am heutigen Tag** (Serverdatum); ab Mitternacht beginnt der Zaehler bei 0. Die
|
||||||
Zaehldatei wird bei jedem Start dieses Containers zurueckgesetzt (`docker/entrypoint.sh`,
|
Zaehldatei (`MONITOR_HOOKS_COUNTER_FILE`) enthaelt Datum und Anzahl. Das Frontend zeigt den
|
||||||
`MONITOR_HOOKS_COUNTER_FILE`). Ein Neustart des `sync`-Containers alleine setzt den Zaehler
|
Wert als eigene, immer sichtbare Kachel "Hooks heute" ganz rechts im KPI-Monitor (Issue #38).
|
||||||
nicht zurueck, da nur der `api`-Container Webhooks empfaengt. Das Frontend zeigt den Wert als
|
|
||||||
eigene, immer sichtbare Kachel ganz rechts im KPI-Monitor (Issue #38).
|
|
||||||
|
|
||||||
## Fehlerformat
|
## Fehlerformat
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,4 @@ if [ ! -d /app/vendor ]; then
|
||||||
composer install --no-interaction --no-progress --working-dir=/app
|
composer install --no-interaction --no-progress --working-dir=/app
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Hooks-KPI (Issue #38): die Zaehldatei liegt im geteilten ./api-Volume und ueberlebt einen
|
|
||||||
# Container-Neustart daher von sich aus - "seit dem letzten Restart" muss also hier aktiv
|
|
||||||
# hergestellt werden. Nur beim Standard-Kommando des api-Containers (php -S ..., siehe
|
|
||||||
# Dockerfile CMD) zuruecksetzen: der sync-Container teilt sich zwar dasselbe Image/Entrypoint,
|
|
||||||
# startet aber mit einem anderen Kommando und empfaengt selbst keine Webhooks - ein eigener
|
|
||||||
# Neustart von ihm darf den Zaehler des api-Containers nicht faelschlich auf 0 setzen.
|
|
||||||
if [ "$1" = "php" ] && [ "$2" = "-S" ]; then
|
|
||||||
hooks_counter_file="${MONITOR_HOOKS_COUNTER_FILE:-/app/data/hooks-count}"
|
|
||||||
mkdir -p "$(dirname "$hooks_counter_file")"
|
|
||||||
echo 0 > "$hooks_counter_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,25 @@ declare(strict_types=1);
|
||||||
namespace Monitor\Api\Metrics;
|
namespace Monitor\Api\Metrics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zaehlt eingehende Webhooks seit dem letzten Start des api-Containers (Issue #38,
|
* Zaehlt die heute eingegangenen Webhooks (Issue #38, "Hooks heute"-KPI). Dateibasiert statt
|
||||||
* "Hooks"-KPI). Dateibasiert statt ein einfaches Objektfeld: der eingebaute PHP-Server
|
* ein einfaches Objektfeld: der eingebaute PHP-Server (docker-compose.yml,
|
||||||
* (docker-compose.yml, PHP_CLI_SERVER_WORKERS) fuehrt jede Anfrage als eigene
|
* PHP_CLI_SERVER_WORKERS) fuehrt jede Anfrage als eigene Skriptausfuehrung aus, ein
|
||||||
* Skriptausfuehrung aus, ein Klassenzustand ueberlebt also keine zwei Anfragen - nur eine
|
* Klassenzustand ueberlebt also keine zwei Anfragen - nur eine Datei tut das. Die Datei
|
||||||
* Datei tut das. "Seit dem letzten Restart" stellt nicht diese Klasse sicher, sondern
|
* enthaelt "<Datum> <Anzahl>"; ein anderes Datum (neuer Tag, alter Dateiinhalt) zaehlt als 0,
|
||||||
* docker/entrypoint.sh, das die Datei beim Start des api-Containers auf 0 zuruecksetzt (der
|
* ein Reset beim Containerstart ist daher nicht noetig.
|
||||||
* sync-Container teilt sich zwar dasselbe Image, darf die Zaehldatei bei einem eigenen
|
|
||||||
* Neustart aber nicht anfassen, siehe Kommentar dort).
|
|
||||||
*/
|
*/
|
||||||
final class HookCounter
|
final class HookCounter
|
||||||
{
|
{
|
||||||
public function __construct(private readonly string $path)
|
/** @var \Closure(): string */
|
||||||
|
private readonly \Closure $today;
|
||||||
|
|
||||||
|
/** @param (callable(): string)|null $today liefert das aktuelle Datum (Y-m-d), fuer Tests ersetzbar */
|
||||||
|
public function __construct(private readonly string $path, ?callable $today = null)
|
||||||
{
|
{
|
||||||
|
$this->today = $today !== null ? \Closure::fromCallable($today) : static fn (): string => date('Y-m-d');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Erhoeht den Zaehler atomar (mehrere PHP_CLI_SERVER_WORKERS koennen gleichzeitig schreiben). */
|
/** Erhoeht den heutigen Zaehler atomar (mehrere PHP_CLI_SERVER_WORKERS koennen gleichzeitig schreiben). */
|
||||||
public function increment(): int
|
public function increment(): int
|
||||||
{
|
{
|
||||||
$directory = dirname($this->path);
|
$directory = dirname($this->path);
|
||||||
|
|
@ -34,11 +37,10 @@ final class HookCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
flock($handle, LOCK_EX);
|
flock($handle, LOCK_EX);
|
||||||
$current = (int) trim((string) stream_get_contents($handle));
|
$next = $this->parse((string) stream_get_contents($handle)) + 1;
|
||||||
$next = $current + 1;
|
|
||||||
ftruncate($handle, 0);
|
ftruncate($handle, 0);
|
||||||
rewind($handle);
|
rewind($handle);
|
||||||
fwrite($handle, (string) $next);
|
fwrite($handle, ($this->today)() . ' ' . $next);
|
||||||
fflush($handle);
|
fflush($handle);
|
||||||
flock($handle, LOCK_UN);
|
flock($handle, LOCK_UN);
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
|
|
@ -54,7 +56,7 @@ final class HookCounter
|
||||||
|
|
||||||
$contents = @file_get_contents($this->path);
|
$contents = @file_get_contents($this->path);
|
||||||
|
|
||||||
return $contents === false ? 0 : (int) trim($contents);
|
return $contents === false ? 0 : $this->parse($contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fingerabdruck fuer den SSE-Endpoint (Realtime\ChangeFeed, F13). */
|
/** Fingerabdruck fuer den SSE-Endpoint (Realtime\ChangeFeed, F13). */
|
||||||
|
|
@ -62,4 +64,14 @@ final class HookCounter
|
||||||
{
|
{
|
||||||
return (string) $this->count();
|
return (string) $this->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Anzahl aus dem Dateiinhalt - 0, wenn sie nicht von heute stammt oder unlesbar ist. */
|
||||||
|
private function parse(string $contents): int
|
||||||
|
{
|
||||||
|
if (preg_match('/^(\d{4}-\d{2}-\d{2}) (\d+)$/', trim($contents), $m) !== 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $m[1] === ($this->today)() ? (int) $m[2] : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,28 @@ final class HookCounterTest extends TestCase
|
||||||
$counter->increment();
|
$counter->increment();
|
||||||
self::assertSame('1', $counter->fingerprint());
|
self::assertSame('1', $counter->fingerprint());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCountResetsOnANewDay(): void
|
||||||
|
{
|
||||||
|
$day = '2026-10-01';
|
||||||
|
$counter = new HookCounter($this->path, static function () use (&$day): string {
|
||||||
|
return $day;
|
||||||
|
});
|
||||||
|
|
||||||
|
$counter->increment();
|
||||||
|
$counter->increment();
|
||||||
|
self::assertSame(2, $counter->count());
|
||||||
|
|
||||||
|
$day = '2026-10-02';
|
||||||
|
self::assertSame(0, $counter->count());
|
||||||
|
self::assertSame(1, $counter->increment());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLegacyFileContentCountsAsZero(): void
|
||||||
|
{
|
||||||
|
mkdir(dirname($this->path), 0o775, true);
|
||||||
|
file_put_contents($this->path, '12');
|
||||||
|
|
||||||
|
self::assertSame(0, (new HookCounter($this->path))->count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ services:
|
||||||
# jede Anfrage mit 401 ab - der Endpunkt ist aus dem Internet erreichbar und die
|
# jede Anfrage mit 401 ab - der Endpunkt ist aus dem Internet erreichbar und die
|
||||||
# Signatur ist sein einziger Schutz.
|
# Signatur ist sein einziger Schutz.
|
||||||
CODEBERG_WEBHOOK_SECRET: ${CODEBERG_WEBHOOK_SECRET:-}
|
CODEBERG_WEBHOOK_SECRET: ${CODEBERG_WEBHOOK_SECRET:-}
|
||||||
# Zaehldatei fuer den Hooks-KPI (Issue #38, GET /api/v1/webhooks/stats): wird bei jedem
|
# Zaehldatei fuer den Hooks-KPI (Issue #38, GET /api/v1/webhooks/stats): speichert Datum + Anzahl der
|
||||||
# Start dieses Containers von entrypoint.sh auf 0 zurueckgesetzt.
|
# heute empfangenen Webhooks (ein neuer Tag beginnt bei 0).
|
||||||
MONITOR_HOOKS_COUNTER_FILE: /app/data/hooks-count
|
MONITOR_HOOKS_COUNTER_FILE: /app/data/hooks-count
|
||||||
# Sprachaufnahmen (Issue #85): Ablageort der Rohaufnahmen aus record.html und
|
# Sprachaufnahmen (Issue #85): Ablageort der Rohaufnahmen aus record.html und
|
||||||
# Obergrenze je Aufnahme. Die Groessengrenze muss zur Dockerfile-CMD passen
|
# Obergrenze je Aufnahme. Die Groessengrenze muss zur Dockerfile-CMD passen
|
||||||
|
|
|
||||||
|
|
@ -485,8 +485,8 @@ export function buildKpiTiles(kpis = []) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KPI-foermiger Eintrag fuer den Hooks-KPI (Issue #38): Anzahl der seit dem letzten Start des
|
* KPI-foermiger Eintrag fuer den Hooks-KPI (Issue #38): Anzahl der heute
|
||||||
* api-Containers empfangenen Webhooks (GET /api/v1/webhooks/stats). Anders als
|
* empfangenen Webhooks (GET /api/v1/webhooks/stats). Anders als
|
||||||
* buildAgentCountKpis() nicht aus bereits ohnehin geladenen Daten ableitbar, sondern ein
|
* buildAgentCountKpis() nicht aus bereits ohnehin geladenen Daten ableitbar, sondern ein
|
||||||
* eigener serverseitiger Zaehler - daher eine eigene, injizierbare Funktion statt Teil davon.
|
* eigener serverseitiger Zaehler - daher eine eigene, injizierbare Funktion statt Teil davon.
|
||||||
* Soll laut Issue #38 immer als letzte (rechte) Kachel im KPI-Monitor erscheinen, siehe
|
* Soll laut Issue #38 immer als letzte (rechte) Kachel im KPI-Monitor erscheinen, siehe
|
||||||
|
|
@ -497,7 +497,7 @@ export function buildKpiTiles(kpis = []) {
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
export function buildHooksKpi(count) {
|
export function buildHooksKpi(count) {
|
||||||
return { key: "hooks_received", label: "Hooks", unit: "", current_value: count, history: [] };
|
return { key: "hooks_received", label: "Hooks heute", unit: "", current_value: count, history: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@ test("buildAgentCountKpis liefert 0/0 ohne Agenten", () => {
|
||||||
test("buildHooksKpi liefert einen KPI-foermigen Eintrag mit der uebergebenen Anzahl (Issue #38)", () => {
|
test("buildHooksKpi liefert einen KPI-foermigen Eintrag mit der uebergebenen Anzahl (Issue #38)", () => {
|
||||||
assert.deepEqual(buildHooksKpi(12), {
|
assert.deepEqual(buildHooksKpi(12), {
|
||||||
key: "hooks_received",
|
key: "hooks_received",
|
||||||
label: "Hooks",
|
label: "Hooks heute",
|
||||||
unit: "",
|
unit: "",
|
||||||
current_value: 12,
|
current_value: 12,
|
||||||
history: [],
|
history: [],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue