mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
refactor: update heartbeat URL format to use path segments and implement legacy URL rewriting
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { Reroute } from "@sveltejs/kit";
|
||||
|
||||
// Back-compat for issue #759: heartbeat URLs used to be `/ext/heartbeat/<tag>:<secret>`,
|
||||
// one path segment joined by a colon. A `:` is illegal in Windows file paths, so the
|
||||
// route is now `/ext/heartbeat/<tag>/<secret>` (two segments). Legacy colon-form URLs
|
||||
// live forever in external cron jobs / uptime pingers, so rewrite them internally to
|
||||
// the new path. Returns a 200 (no redirect) — heartbeat clients often don't follow 3xx.
|
||||
//
|
||||
// `reroute` is a *universal* hook: it MUST be in src/hooks.ts. A `reroute` exported from
|
||||
// src/hooks.server.ts is silently ignored by SvelteKit. Keep this file free of
|
||||
// server-only imports — it is bundled for the client too. Must stay pure/side-effect-free.
|
||||
//
|
||||
// The transform is in-place (no path reconstruction), so any KENER_BASE_PATH prefix is
|
||||
// preserved automatically. `[^/:]+` matches the validated tag charset; only the first
|
||||
// colon after `/ext/heartbeat/<tag>` is rewritten.
|
||||
const LEGACY_HEARTBEAT = /(\/ext\/heartbeat\/[^/:]+):/;
|
||||
|
||||
export const reroute: Reroute = ({ url }) => {
|
||||
if (LEGACY_HEARTBEAT.test(url.pathname)) {
|
||||
return url.pathname.replace(LEGACY_HEARTBEAT, "$1/");
|
||||
}
|
||||
};
|
||||
@@ -10,11 +10,13 @@ Heartbeat monitors are push-based: your job calls a URL, and Kener measures how
|
||||
URL format:
|
||||
|
||||
```
|
||||
/ext/heartbeat/{tag}:{secret}
|
||||
/ext/heartbeat/{tag}/{secret}
|
||||
```
|
||||
|
||||
Accepted methods: `GET` and `POST`.
|
||||
|
||||
> Older heartbeat URLs used a colon — `/ext/heartbeat/{tag}:{secret}`. Those still work; they are rewritten to the path-separated form automatically, so existing cron jobs need no changes.
|
||||
|
||||
## Minimum setup {#minimum-setup}
|
||||
|
||||
Set:
|
||||
@@ -53,11 +55,11 @@ Latency is recorded as elapsed time since the last heartbeat (ms).
|
||||
Minimal cron usage pattern:
|
||||
|
||||
```bash
|
||||
*/5 * * * * /path/to/job.sh && curl -s "https://your-kener-host/ext/heartbeat/my-job:my-secret"
|
||||
*/5 * * * * /path/to/job.sh && curl -s "https://your-kener-host/ext/heartbeat/my-job/my-secret"
|
||||
```
|
||||
|
||||
## Troubleshooting {#troubleshooting}
|
||||
|
||||
- **Always NO_DATA**: endpoint never called or wrong `tag:secret`
|
||||
- **Always NO_DATA**: endpoint never called or wrong `tag`/`secret`
|
||||
- **Always DOWN/DEGRADED**: thresholds too low for actual job interval
|
||||
- **Signal accepted but stale**: ensure heartbeat is sent only after successful completion
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import * as Select from "$lib/components/ui/select/index.js";
|
||||
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import ExternalLinkIcon from "@lucide/svelte/icons/external-link";
|
||||
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||
import SirenIcon from "@lucide/svelte/icons/siren";
|
||||
import { goto } from "$app/navigation";
|
||||
import { onMount } from "svelte";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import GC from "$lib/global-constants";
|
||||
import { resolve } from "$app/paths";
|
||||
@@ -104,12 +102,12 @@
|
||||
|
||||
// Navigate to incident
|
||||
function openIncident(id: number) {
|
||||
goto(clientResolver(resolve, `/manage/app/incidents/${id}`));
|
||||
goto(resolve(`/manage/app/incidents/${id}`));
|
||||
}
|
||||
|
||||
// Create new incident
|
||||
function createNewIncident() {
|
||||
goto(clientResolver(resolve, "/manage/app/incidents/new"));
|
||||
goto(resolve("/manage/app/incidents/new"));
|
||||
}
|
||||
|
||||
// Handle state filter change
|
||||
@@ -127,7 +125,7 @@
|
||||
fetchData();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
@@ -142,7 +140,7 @@
|
||||
{stateOptions.find((o) => o.value === stateFilter)?.label || "All States"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each stateOptions as option}
|
||||
{#each stateOptions as option (option.value)}
|
||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
@@ -166,6 +164,7 @@
|
||||
<Table.Row>
|
||||
<Table.Head class="w-16">ID</Table.Head>
|
||||
<Table.Head>Title</Table.Head>
|
||||
<Table.Head class="w-40">Started</Table.Head>
|
||||
<Table.Head class="w-32">Duration</Table.Head>
|
||||
<Table.Head class="w-36">State</Table.Head>
|
||||
<Table.Head class="w-40">Affects</Table.Head>
|
||||
@@ -175,10 +174,10 @@
|
||||
<Table.Body>
|
||||
{#if incidents.length === 0 && !loading}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="text-muted-foreground py-8 text-center">No incidents found</Table.Cell>
|
||||
<Table.Cell colspan={7} class="text-muted-foreground py-8 text-center">No incidents found</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each incidents as incident}
|
||||
{#each incidents as incident (incident.id)}
|
||||
<Table.Row class="hover:bg-muted/50 cursor-pointer" onclick={() => openIncident(incident.id)}>
|
||||
<Table.Cell class="font-medium">{incident.id}</Table.Cell>
|
||||
<Table.Cell>
|
||||
@@ -191,6 +190,11 @@
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span class="text-muted-foreground text-sm whitespace-nowrap">
|
||||
{format(new Date(incident.start_date_time * 1000), "yyyy-MM-dd HH:mm")}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
@@ -227,7 +231,7 @@
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div class="space-y-1">
|
||||
{#each incident.monitors as monitor}
|
||||
{#each incident.monitors as monitor (monitor.tag || monitor.monitor_tag)}
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">{monitor.tag || monitor.monitor_tag}</span>
|
||||
<span class="text-muted-foreground ml-1"
|
||||
@@ -274,7 +278,7 @@
|
||||
</Button>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if totalPages <= 7}
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page (page)}
|
||||
<Button variant={page === pageNo ? "default" : "ghost"} size="sm" onclick={() => goToPage(page)}>
|
||||
{page}
|
||||
</Button>
|
||||
@@ -288,7 +292,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- Middle pages -->
|
||||
{#each Array.from({ length: 3 }, (_, i) => pageNo - 1 + i).filter((p) => p > 1 && p < totalPages) as page}
|
||||
{#each Array.from({ length: 3 }, (_, i) => pageNo - 1 + i).filter((p) => p > 1 && p < totalPages) as page (page)}
|
||||
<Button variant={page === pageNo ? "default" : "ghost"} size="sm" onclick={() => goToPage(page)}>
|
||||
{page}
|
||||
</Button>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
// Generate heartbeat URL
|
||||
let heartbeatUrl = $derived(
|
||||
tag
|
||||
? window.location.origin + clientResolve(resolve, `/ext/heartbeat/${tag}:${data.secretString}`)
|
||||
? window.location.origin + clientResolve(resolve, `/ext/heartbeat/${tag}/${data.secretString}`)
|
||||
: "Save the monitor first to get the heartbeat URL"
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user