Jallarhorn documentation
Install Jallarhorn, organize devices by site, configure sensors, wire up routed alerting and escalations, watch uptime and maps, federate across sites, and integrate with your own tooling. Everything is on this page; use the left sidebar to jump.
Downloads
Every download is hosted on jallarhorn.com, signed with our Cosign release key, and ships with an SPDX SBOM. Each archive is the full suite — jallarhorn-control, jallarhorn-sensor, and jallarhorn-farm in one bundle.
| Platform | Download | Notes |
|---|---|---|
| Linux (x86-64) | jallarhorn_0.2.3_linux_amd64.tar.gz · sig | Any glibc-2.31+ distro. systemd units in the archive. |
| Linux (arm64) | jallarhorn_0.2.3_linux_arm64.tar.gz · sig | Raspberry Pi, AWS Graviton, etc. |
| macOS (Apple Silicon) | jallarhorn_0.2.3_darwin_arm64.tar.gz · sig | macOS 12+. M-series. |
| macOS (Intel) | jallarhorn_0.2.3_darwin_amd64.tar.gz · sig | macOS 12+. Intel. |
| Windows (x86-64) | jallarhorn-setup-0.2.3.exe · sig jallarhorn_0.2.3_windows_amd64.zip · sig |
Windows 10/11, Server 2016+. Run the signed jallarhorn-setup-0.2.3.exe to install control + sensor as services, or use the manual ZIP + NSSM path (see Install → Windows). |
| Docker | docker pull jallarhorn.com/jallarhorn-control:0.2.3 |
Also -sensor, -farm, -license. docker compose up -d brings up the full stack. |
| Kubernetes | Helm chart in helm/jallarhorn/ |
Deployments for control + sensor, DaemonSet for per-node sensors. |
Checksums: SHA256SUMS · signing key: cosign.pub · SBOMs are the *.spdx.sbom.json files alongside each artifact.
Verifying a release
Each artifact has a detached .sig signed with our Cosign release key (public key at jallarhorn.com/cosign.pub — key-based, no third-party transparency log):
curl -O https://jallarhorn.com/cosign.pub cosign verify-blob \ --key cosign.pub \ --signature jallarhorn_0.2.3_linux_amd64.tar.gz.sig \ --insecure-ignore-tlog \ jallarhorn_0.2.3_linux_amd64.tar.gz # Container images: cosign verify --key cosign.pub jallarhorn.com/jallarhorn-control:0.2.3
Install & configure
Three executables — jallarhorn-control (REST + dashboard + alert engine, port 28090), jallarhorn-sensor (polling agent), and jallarhorn-farm (multi-site federation, port 28092). All three ship in a single archive.
Database requirement
Jallarhorn stores metrics in TimescaleDB, so PostgreSQL 16 with the timescaledb extension is required (the pgcrypto extension is also required — it ships with stock PostgreSQL). Install TimescaleDB per the official guide, then, as a database superuser:
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS timescaledb;
The control binary attempts these itself on boot and applies its own schema migrations automatically — there is nothing to run by hand. On a stock PostgreSQL without TimescaleDB it exits immediately with the exact commands to run.
Option A — Linux one-liner (recommended)
Downloads the latest signed Linux bundle, verifies its SHA-256 checksum, installs the binaries to /opt/jallarhorn, and writes systemd units (the sensor unit is granted CAP_NET_RAW so ICMP ping works). It does not start anything until you supply the database + secrets — the script prints the exact next steps.
curl -fsSL https://jallarhorn.com/install.sh | sudo sh
Option B — Manual Linux tarball
Prefer to inspect first? Grab the archive from the Downloads table (each is the full suite — jallarhorn-control, jallarhorn-sensor, jallarhorn-farm — plus docker-compose.yml and .env.production.example). Verify the signature with cosign (install from sigstore.dev), then extract:
curl -fsSLO https://jallarhorn.com/download/0.2.3/jallarhorn_0.2.3_linux_amd64.tar.gz curl -fsSLO https://jallarhorn.com/download/0.2.3/jallarhorn_0.2.3_linux_amd64.tar.gz.sig curl -fsSLO https://jallarhorn.com/cosign.pub cosign verify-blob --key cosign.pub \ --signature jallarhorn_0.2.3_linux_amd64.tar.gz.sig \ --insecure-ignore-tlog jallarhorn_0.2.3_linux_amd64.tar.gz sudo mkdir -p /opt/jallarhorn sudo tar -xzf jallarhorn_0.2.3_linux_amd64.tar.gz -C /opt/jallarhorn
Then create /etc/jallarhorn/control.env (see Environment variables) and a systemd unit pointing ExecStart at /opt/jallarhorn/jallarhorn-control. The one-liner above writes these units for you.
Option C — Docker
Images are served from jallarhorn.com and pull anonymously:
docker pull jallarhorn.com/jallarhorn-control:0.2.3 # also: jallarhorn.com/jallarhorn-sensor, -farm, -license
The release archive ships a docker-compose.yml that wires the control plane to a TimescaleDB container. Extract any bundle from the Downloads table, fill in .env from the included .env.production.example, then docker compose up -d.
Option D — Windows
Two ways to run on Windows 10/11 or Server 2016+. Either way, install PostgreSQL 16 + TimescaleDB first (see Database requirement) and create the jallarhorn database.
Graphical installer (control + sensor as Windows services). A signed jallarhorn-setup-0.2.3.exe is published in the Downloads table. It installs jallarhorn-control and jallarhorn-sensor to %ProgramFiles%\Jallarhorn, registers both as auto-starting services via the bundled NSSM, generates unique secrets, writes config to %ProgramData%\Jallarhorn\jallarhorn.env and sensor.env, and opens the dashboard at http://localhost:28090. Uninstalling stops and removes both services and leaves your database untouched. Verify its signature with cosign the same way as any other artifact.
Manual ZIP + NSSM (works with the current release). Download and verify the Windows bundle, extract it, and register the services yourself:
Expand-Archive jallarhorn_0.2.3_windows_amd64.zip -DestinationPath "C:\Program Files\Jallarhorn" # Control server + dashboard (port 28090) nssm install JallarhornControl "C:\Program Files\Jallarhorn\jallarhorn-control.exe" nssm set JallarhornControl AppEnvironmentExtra DATABASE_URL=postgres://postgres:PW@localhost:5432/jallarhorn JWT_SECRET=<32-hex> CONTROL_SECRET=<32-hex> HTTP_LISTEN=:28090 GIN_MODE=release nssm start JallarhornControl # Sensor agent nssm install JallarhornSensor "C:\Program Files\Jallarhorn\jallarhorn-sensor.exe" nssm set JallarhornSensor AppEnvironmentExtra SENSOR_ID=windows-1 DATABASE_URL=postgres://postgres:PW@localhost:5432/jallarhorn nssm start JallarhornSensor
Get the signed bundle and its .sig from the Downloads table. NSSM is a free, public-domain service wrapper from nssm.cc; the graphical installer bundles it so you don't need it separately.
First-run bootstrap
On a fresh database the control service auto-creates a single default tenant, so the very first registration needs no tenant id and that first user is automatically promoted to admin:
curl -fsS -X POST http://localhost:28090/api/v2/auth/register \
-H 'Content-Type: application/json' \
-d '{"username":"admin","email":"you@example.com","password":"CHANGE-ME"}'
After that, registration is invite-only (JALLARHORN_INVITE_ONLY=true by default). Generate further invites from Settings → Users → Invite; tokens are single-use and expire in 7 days. Set JALLARHORN_INVITE_ONLY=false to disable the gate for open self-hosted installs.
First 15 minutes — guided setup
The first time you sign in with an empty account, the dashboard shows a Set up monitoring hero instead of blank widgets. It launches a five-step wizard (also reachable any time from the Setup item in the sidebar while you have few devices) that takes you from nothing to a monitored site with alerts routed to your inbox:
- Name your first site. Sites are groups — one per client or location. This creates the root group everything below lands in.
- Find devices. Either scan a network range in CIDR form (the field is pre-filled with a guess derived from the control host's own interface — change it if your devices live elsewhere), or add a single device by hand with the same fields as Add Device.
- Adopt & monitor. Adopting a discovered device auto-creates the right sensors from what it answered — Ping always, HTTP and an SSL-certificate check if it serves web, SNMP basics if it replied — each with a sensible interval and a default alert rule, all assigned to your site group. Nothing is ever adopted “dead.” A manually-added device gets a Ping monitor and alert rule the same way. The Create monitors automatically checkbox (on by default) is also available on the standalone Discovery flow.
- Get notified. Add a notification channel. For email, guided presets (Gmail app-password, Microsoft 365, or a generic SMTP server) pre-fill the host and port and spell out exactly what to paste. Jallarhorn sends alerts through your own mail server — it never emails on your behalf — so you enter your mailbox's SMTP settings once and test them. Slack, Discord, and webhook channels take just a URL.
- Prove it. Fire a real test alert through the real engine and channels — respecting your severity and quiet-hours gating — and watch it arrive per channel before you rely on it. The drill alert auto-resolves itself. Test alerts are rate-limited to 3 per minute per account (a further attempt returns
429), so you can't accidentally storm your own inbox.
You can skip any step and finish later; the setup hero reappears until you actually have devices monitored.
Environment variables
| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL + TimescaleDB connection string. The control binary migrates the schema and seeds the default tenant on first run. |
JWT_SECRET | HMAC secret for session JWTs. 32+ hex bytes. |
CONTROL_SECRET | Shared secret between control and sensor over /api/*. |
JALLARHORN_ENCRYPTION_KEY | 32-byte symmetric key for at-rest credential encryption (SSH passwords, SNMP communities, etc.). Rotation procedure in the security page. |
HTTP_LISTEN | Control HTTP listen, default 127.0.0.1:28090. Bind to 0.0.0.0 to expose. |
JALLARHORN_LICENSE_URL | License server. Defaults to https://license.jallarhorn.com. |
JALLARHORN_LICENSE_MODE | fail-open (default) keeps collecting if the license server is unreachable; fail-closed halts after the grace period. |
JALLARHORN_INVITE_ONLY | true (default) requires an invite token after the bootstrap admin; false opens registration. |
JALLARHORN_SMTP_HOST / _PORT / _FROM | SMTP relay for outbound alert email. STARTTLS expected. |
Sensor variables
Set these in the sensor's environment (e.g. /etc/jallarhorn/sensor.env). The sensor pulls its check config from the control plane:
| Variable | Purpose |
|---|---|
CONTROL_URL | Base URL of the control plane, e.g. https://app.example.com. |
CONTROL_API_KEY | The credential the sensor sends to authenticate to control. Recommended: mint a probe key in the dashboard (Settings → Probe / API keys → Create probe key) and paste it here — it is shown only once. The shared CONTROL_SECRET also works as this value. |
SENSOR_NODE_ID | A UUID identifying this probe (e.g. from uuidgen); control assigns it checks bound to this id in the UI. Unbound sensors run on every probe. Leave unset to run only unbound sensors. (Not to be confused with SENSOR_ID, which is any label string.) |
SENSOR_ID / DATABASE_URL | Single-box mode only: point the sensor straight at the same TimescaleDB instead of a control plane. |
JALLARHORN_PING_MODE | privileged (default) uses raw ICMP sockets (needs CAP_NET_RAW — the shipped systemd unit grants it); unprivileged uses UDP ping. |
Sensor catalog
Jallarhorn ships 17 native sensor plugins covering the protocols a typical per-sensor tool covers, plus five generic frameworks (below) that close the long tail. Every sensor has a 60-second (1-minute) default poll, a per-sensor override, a warn threshold, an error threshold, and structured channel output for graphing.
Network probes
| Plugin | Channels |
|---|---|
| ICMP / Ping | RTT, jitter, packet loss |
| TCP port | reachability, banner read, latency |
| DNS | A / AAAA / MX / CNAME / TXT / DNSSEC |
| SSL cert | expiry days, chain validation, hostname mismatch |
| HTTP | status, content match, latency, redirect handling |
Device polling
| Plugin | Channels |
|---|---|
| SNMP (v1/v2c/v3) | get, walk, counter wrap, delta rates |
| SSH | disk, load, memory, uptime, CPU, custom script |
| WinRM | CPU, memory, disk, service, process, event log, perf counter, uptime, custom WQL / PowerShell (NTLM auth) |
System & services
| Plugin | Channels |
|---|---|
| Process | match by name + arg pattern |
| Disk | mountpoint fan-out, % full, inode pressure |
| PostgreSQL | replication lag, connections, custom SQL |
| Redis | INFO parse, key count, master/replica |
| HTTP-JSON | JSONPath extraction (see custom sensors) |
| Prom-scrape | Prometheus exposition (see custom sensors) |
| Docker | wraps any image as a one-shot probe (see custom sensors) |
What's intentionally not native
Out of scope on purpose: full packet capture (use ntopng or zeek), NetFlow collection (use the dedicated nfdump pipeline, then ingest via webhook-push), Active Directory introspection (PowerShell remoting via WinRM custom_script handles this). The custom-sensor frameworks cover roughly 70% of the long tail without writing bespoke code. Need a fully-custom plugin? Contact support@jallarhorn.com — bespoke plugin integration is available for Business and Enterprise tiers.
Sensor wizard & presets
Every sensor is created through the same guided wizard — from Add sensor on any device, or the Adopt & monitor step of first-run setup. It has three modes.
Device-type presets
One click builds a working bundle of sensors for a common device type, each with a sensible poll interval and default alert rules already attached. Shared credentials (a WinRM/SSH login or an SNMP community) are entered once and applied to every check in the bundle.
| Preset | Creates |
|---|---|
| Web server | Ping + HTTP (expects 200) + SSL certificate (warn 30 days / error 7 days to expiry) |
| Windows server | WinRM CPU (warn >90%, error >95%), memory, disk C:, uptime, and system event-log errors |
| Linux server | Ping + SSH 1-minute CPU load (warn >4) + root-disk usage (warn >90%) |
| Network gear | Ping + SNMP uptime + interface in-octets (traffic) |
| Database | Ping + TCP port check (PostgreSQL 5432) |
Single sensor + threshold builder
Pick one plugin and configure it, with inline help on the jargon fields (SNMP community, OID, WQL). Thresholds are set with a threshold builder, not two free-text boxes: each rule is a condition (above / below / equals) + value + unit + severity (warning / error / critical / info), saved as a visible alert rule you can see and edit later.
Every device detail page has an Alert rules panel per sensor — list, add, edit condition/value/severity, enable/disable, and delete rules directly, all backed by the same rules the engine evaluates.
Probe binding
A check can be pinned to a specific sensor node (probe) with the optional Run check from field — enter the sensor node's id and that node runs the check. This is how an MSP runs a client's checks from a probe sitting on the client's own network. Leave it blank to run from the default node.
Custom sensors
Five generic frameworks for anything not natively covered. All five share the same output schema, so a custom sensor's data graphs, alerts, and reports identically to a native one.
The channel format
Every custom sensor emits JSON shaped like this:
{
"channels": [
{ "name": "cpu", "value": 42.7, "unit": "%" },
{ "name": "memory", "value": 1.8, "unit": "GiB" }
]
}
Each channel becomes a separate metric track on the device, with its own warn/error threshold and its own graph line.
exec-script
Run any local command — shell script, executable, language runtime — and parse its stdout as the channel JSON above. Per-sensor timeout, working directory, env vars, and stdin. Bounded stdout/stderr buffers. SIGKILL via process group on timeout.
http-json
GET (or POST) any HTTP endpoint, parse the response as JSON, and extract channels via JSONPath. Supports Bearer tokens, custom headers, mTLS client certs, and JSON-body POSTs. Handles missing keys and type coercion (string → float).
webhook-push
Inverts the polling model: the integration POSTs metrics to /api/v2/sensors/:id/push on its own schedule, authenticated by a SHA-256-hashed per-sensor ingest token. Useful when the source is push-only (FaaS, CI runners, scheduled jobs).
prom-scrape
Polls a Prometheus exposition endpoint (/metrics) and turns selected metrics into Jallarhorn channels. Label filtering and metric rename rules. Pair this with any existing Prometheus exporter (node_exporter, mysqld_exporter, blackbox_exporter, …) to cover the long tail.
docker
Runs docker run --rm <image> <args> on a schedule and parses stdout as channel JSON. Defaults to --network none; opt-in for network access. SIGKILL via process group on timeout. Bounded stdout/stderr. Useful for vendor-supplied CLIs, Playwright scrapers, OPC UA / Modbus / SIP / DICOM / HL7 probes via public images.
Choosing the right framework
| Source shape | Framework |
|---|---|
| Local CLI or script | exec-script |
| HTTP JSON endpoint | http-json |
| Source pushes on its own schedule | webhook-push |
| Prometheus exposition | prom-scrape |
| Vendor binary / container image | docker |
| A vendor-specific binary protocol | contact support — bespoke plugin integration available |
Discovery
Discovery scans a network range, fingerprints whatever answers, and lets you adopt hosts into monitoring with the right sensors already attached (see first-run setup for the guided path). Adopting auto-creates sensors from the fingerprint — Ping always, HTTP + SSL if 80/443 answered, SNMP basics if the community responded — each with a default interval and alert rule, assigned to the site group you chose. The Create monitors automatically toggle (on by default) controls this.
The limits you will meet, enforced before any scanning starts:
| Limit | Behaviour |
|---|---|
| IPv4 only | IPv6 ranges are rejected — a single IPv6 /64 is 1.8×1019 addresses, never enumerable. |
| /16 maximum | At most 65,536 addresses per scan. A broader CIDR (e.g. /8) returns 400 with "network too large — use a /16 or smaller". |
| One scan at a time | Only one discovery scan runs per account. Starting a second while one is in flight returns 409 ("a discovery scan is already running"). Each scan is also hard-capped at 10 minutes. |
| Adopt is idempotent | Re-adopting a host that is already a monitored device creates nothing — it is reported as skipped, so a double-click never produces a duplicate device or duplicate sensors. |
Groups & site tree
Devices live in a tree of groups — one group per client site or location — shown in the left pane of the Devices page. Selecting a group scopes the device table on the right to that group's subtree, which gains search, filter, and sort.
- Create, rename, and nest groups inline. Move a device to a group with the per-device group field, or bulk-move several with the Move to group action from the table.
- Each node shows a rollup status dot that reflects the worst status of everything beneath it, so a red site is visible at the top of the tree without expanding it.
- A group can carry a location (label + latitude/longitude) used by the geographic map, and an escalation policy that every device in its subtree inherits.
Groups are also how notification routing, the per-site health tiles, and reports segment your data. Devices left ungrouped still monitor normally — they simply aren't scoped to any site (and only reach unscoped notification channels).
Maps — topology & geo
The Maps page has two tabs:
- Topology — an interactive graph of devices and their subnet links. Auto-inferred
/24links are labelled inferred subnet (so the map is never mistaken for verified L2/L3 discovery); add manual parent links to make it reflect reality. - Geo — a self-hosted map (no third-party geocoding) with a pin per group, positioned from the group's latitude/longitude and coloured by its rollup status. Click a pin to jump to that node in the tree. Set a group's coordinates in the group editor (a location label plus decimal-degree lat/long).
Dashboard & widgets
The dashboard is a grid of widgets you pick and arrange; your choice and layout are saved in the browser. Add or remove any of these from the widget picker:
| Widget | Shows |
|---|---|
| Status Summary | Overall up / warning / down counts |
| Live Alert Feed | Open alerts as they fire |
| Sensor Chart | A metric chart, titled by the sensor's name |
| Device Status | Per-device health table |
| Per-Site Health | One tile per group, coloured by rollup — the MSP's morning glance |
| Gauge | A single-value dial for one channel |
| Top-N Sensors | A compact ranking (see Reports) |
The separate /monitor route is a viewport-sized status board for wall-mounted displays.
Uptime & status history
Every device detail page shows uptime and a status-history strip, both derived honestly from the presence of metric samples: a poll that ran and stored a sample counts as up; a sample that should have arrived but didn't counts as down. There is no separate "availability" bookkeeping to drift out of sync.
- Uptime % and the strip are one model, so the badge can never disagree with the picture next to it. The window is sliced into time buckets and uptime is the share of buckets that had at least one sample — up buckets ÷ (up + down) buckets. Because a bucket is simply hit-or-not, a multi-channel poll can't push the number past 100%, and the cap is structural rather than a truncation that could hide an outage. The last 24 hours uses 30-minute buckets read from raw metrics; the last 30 days uses 1-hour buckets read from an hourly rollup, so it stays fast at scale.
- Buckets before the sensor existed don't count — a sensor installed an hour ago is measured only over that hour, not shown as ~0% for the rest of the window.
- Device uptime is the mean of its sensors' uptime, excluding paused sensors and sensors that have never returned a sample (so they can't drag a healthy device toward 0%). The badge's tooltip shows how many of the device's sensors are in the average (e.g. "3 of 5 sensors"). A device with no measurable sensors shows "—", never a misleading 0% or 100%.
- The status-history strip splits a window into cells (24h → 30-minute cells, 7d → 2-hour cells). Each cell is up (had samples), down (a real gap — the check was expected to run but no sample arrived), or no data (the cell predates the sensor, so nothing was ever expected).
That last distinction is deliberate: a brand-new or never-polled sensor reads "No data" (waiting for its first check), which is different from a sensor whose checks are failing. An erroring sensor gets its own status and a reason message, plus a link to its last alert.
Reports — SLA & Top-N
Two report types, each exportable as PDF, CSV, or HTML from the Reports page or the API. Both are built on the same metric-sample-presence idea as the device uptime model — a present sample is up, a missing one is down.
| Report | Contents |
|---|---|
| SLA | Uptime % per device or group over a period, with the underlying downtime intervals. GET /api/v2/reports/sla?device_id=…&from=…&to=…&format=pdf|csv|html |
| Top-N | Rank sensors by downtime, average latency, or alerts over a time range (default top 10, up to 100). GET /api/v2/reports/topn?metric=downtime|latency|alerts&n=…&format=pdf|csv|html |
Add /preview to either path (e.g. /reports/sla/preview) to get the same data as JSON for your own rendering.
Alert rules
An alert rule has a sensor, a comparison operator (>, <, ==, etc.), and two thresholds: warn and error. The alert engine runs every 30 seconds; when a sample crosses a threshold, an alert opens with that severity. Alerts auto-resolve when the underlying value crosses back. Duplicate suppression keeps a flapping sensor from generating storms.
Availability alerting is on by default — no rule required. A device that goes down produces no reading at all, so a numeric threshold (e.g. "latency above 1000 ms") can never trip on it. Every sensor therefore also has built-in availability alerting: when a sensor's status becomes down (unreachable / no response), the engine opens an error-severity alert — "Device X / sensor Y is down (no response)" — and auto-resolves it the moment the sensor answers again. This applies to every sensor created any way (setup wizard, adopt, discovery, or by hand), respects dependency and maintenance suppression and channel gating like any alert, and never double-fires with a latency rule. To opt a sensor out of availability alerting, pause it.
Operators can acknowledge an alert (silences further notifications without closing it) or resolve it (closes immediately, even if the underlying value is still over threshold). Auto-resolve fires when the sensor returns to OK for the configured cooldown period. Every notification (webhook, Slack, Teams, Discord, email, push) names the affected device and sensor; the outbound webhook payload additionally carries the device address, sensor status, severity, alert id, and timestamp.
Dependency suppression
Mark a sensor as depending on another (e.g. an HTTP check on a web app depends on the ICMP check of its host). When the parent goes red, child alerts are recorded with status: suppressed — visible in the audit log but no notifications are sent. When the parent recovers, child alerts that are still real fire normally.
Set this with the Depends on picker on each sensor row of the device detail page — choose the sensor this one depends on, and the engine handles the suppression from there.
Maintenance windows
Schedule planned downtime per device or per sensor. While the window is active, alerts are auto-suppressed; they still appear in the audit log labeled status: maintenance. Recurring windows are supported via cron-style expressions.
Notification channels
| Channel | Transport / Notes |
|---|---|
Sends through your own mail server over SMTP with STARTTLS — Jallarhorn never emails on your behalf. Per-channel guided presets (Gmail app-password, Microsoft 365, generic SMTP) pre-fill host/port and spell out what to paste, with a test-send to validate. A global JALLARHORN_SMTP_* default is also available. | |
| Slack | Incoming webhook. Severity-coloured blocks with sensor, device, current value, and deep-link to the alert. |
| Microsoft Teams | Adaptive Card 1.4 via incoming webhook. Same payload shape as Slack. |
| Discord | Incoming webhook. Useful for community-run installs. |
| PagerDuty | Events API v2. Maps Jallarhorn severities to PagerDuty's warning / error / critical. |
| Opsgenie | Alert API. Auto-resolve when the Jallarhorn alert clears (closes the Opsgenie incident). |
| Web Push (VAPID) | Browser + PWA push notifications with min-severity, quiet-hours, and timezone awareness. |
| Generic webhook | Outbound JSON POST to any URL. Configurable headers, retry-with-backoff, HMAC-SHA-256 request signing. |
Every channel has a Send test button in the dashboard. The test payload identifies itself as a test so it can't be mistaken for a real incident.
Notification routing
By default every alert goes to every channel — zero-config and safe. To send a client's alerts only to that client's queue, give a channel a site scope (one or more groups) and a minimum severity. The channel then receives an alert only when both are true: the alert's device sits in one of the scoped groups' subtrees, and the alert meets the severity floor.
An unscoped channel keeps receiving everything (the pre-existing behaviour). A device with no group only reaches unscoped channels — an unfiled device's alert is never silently routed to a client-specific queue.
Quiet hours & timezone
Each channel can set a quiet-hours window (start / end in HH:MM) and a timezone. An alert that arrives inside the window is suppressed and logged as such with the reason (see the notification log). Windows may span midnight (e.g. 22:00–07:00).
Escalations bypass quiet hours. An escalation step fires even inside a quiet window — the whole point of an escalation is that an unacknowledged problem must eventually break through. Escalation pages still respect the channel's minimum severity; only the quiet-hours gate is bypassed, and any send that is still suppressed (below severity) is recorded in the log.
Escalation chains
An escalation policy is an ordered list of steps: after N minutes of an alert staying unacknowledged, notify a chosen set of channels; then the next step after its own delay; optionally repeat the final step on a cadence. For example: "Slack at T+0, page on-call at T+5 min if not acknowledged, page secondary at T+15 min."
- Normal routing fires first (the alert reaches your routed channels immediately); each step then targets its own explicit set of channels, picked in the builder's per-step channel multi-select. So a step can page a distinct second channel — e.g. Slack routes the alert now, and if it's still unacked one minute later, step 1 pages PagerDuty and nobody else.
- Step channels are paged regardless of their own site routing. A step dispatches to the channels you list directly — attaching the policy to a site is the routing decision. That makes an "escalation-only" channel possible: give a channel no site routing (so it never receives the initial page) and list it in a step, and it is paged only on escalation.
- Steps fire on the alert's age alone, so a control restart never double-pages a step already sent and never skips a boundary it crossed while down.
- Escalation halts the moment the alert is acknowledged or resolved — an acked or resolved alert drops out of the escalation scan immediately.
- Which policy applies is resolved from the tree: the nearest group above the alert's device that carries a policy wins; failing that, the account's default policy. Attaching a policy to a site therefore covers every device in that site.
- Escalation pages are recorded in the notification log like any other send, and honour each channel's minimum severity. They bypass quiet hours — and, unlike the initial page, they are not filtered by group routing (a step pages its channels directly).
Notification log
The bell in the top bar and the Notification log page show every send the engine attempted, so a bad SMTP password can't fail silently. Each row is one of:
| Status | Meaning |
|---|---|
| Delivered | The channel accepted the notification. |
| Failed | Delivery errored (e.g. wrong SMTP credentials) — the error message is shown. The bell badges unseen failures. |
| Suppressed | Intentionally not sent, with the reason: quiet hours, below the channel's minimum severity, or dependency/maintenance suppression. Suppressed is not a failure. |
Filter the log by channel, status, and time period.
Federation with jallarhorn-farm
Run multiple Jallarhorn control instances (one per site, one per tenant, one per regulatory boundary) and view them through a single aggregated UI. Each control keeps its own database; the farm executable is a stateless aggregator that fans queries out, merges results, and serves a unified dashboard at port 28092.
Seeding the farm
Start jallarhorn-farm with --seeds host1:28090,host2:28090,host3:28090. The farm GETs /api/v2/farm/announce on each seed, learns its peers, and continues discovering via SWIM-style gossip on UDP. New control instances joining the farm auto-announce.
Aggregated endpoints
| Endpoint | Behaviour |
|---|---|
GET /api/v2/farm/stats | Sum of stats across instances |
GET /api/v2/farm/devices | Union of devices with origin annotation |
GET /api/v2/farm/alerts | Union of open alerts |
GET /api/v2/farm/sensors | Union of sensors |
Partial failures (one control unreachable) return the merged result with X-Partial-Results: true and the failing instance(s) listed in the response body. Per-instance timeout 5s.
Write-proxy via ?instance=
Writes (create device, ack alert, edit sensor) need to land on the specific control that owns the record. Add ?instance=<id> to the request — the farm proxies it. Without that parameter, writes return 400 rather than silently writing to a random instance.
REST API
Jallarhorn-control exposes a REST API at /api/v2/* (JWT-authenticated, for humans and integrations) and a simpler shared-secret surface at /api/* (used by the sensor binary to publish metrics). All responses are JSON. Per-IP rate limit on /api/v2/auth/login: 10/min then exponential backoff.
Authentication
# Login
curl -sS https://app.example.com/api/v2/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"..."}'
# → { "access_token": "eyJ...", "refresh_token": "eyJ...",
# "expires_in": 900, "token_type": "Bearer" }
# Subsequent calls — send the access_token as a Bearer token
curl -sS https://app.example.com/api/v2/devices \
-H 'Authorization: Bearer eyJ...'
JWTs land in an httpOnly Strict cookie when issued via the web flow. Programmatic clients should use the Authorization: Bearer header.
Resources
| Path | Operations |
|---|---|
/api/v2/users | List, create, update, delete (admin only) |
/api/v2/devices | List, create, get, update, delete; tagged + grouped |
/api/v2/devices/:id/sensors | List + create sensors on a device (create = POST); see Creating a sensor |
/api/v2/sensors/:sensorId | Get, update, delete a sensor; set dependency + probe node |
/api/v2/groups | CRUD device groups; /tree returns the hierarchy with rollup status per node |
/api/v2/metrics/:sensorID | Time-series query — ?from=…&to=…&channel=…&agg=avg|max|min|count |
/api/v2/devices/:id/uptime | Per-sensor + device-rollup uptime for 24h and 30d |
/api/v2/sensors/:id/history | Status-history timeline — ?window=24h|7d |
/api/v2/alerts | List, ack, resolve; filter by severity / device / sensor |
/api/v2/alert-rules | CRUD on threshold rules + dependency suppression |
/api/v2/discovery/scan | Kick off a CIDR scan; poll /jobs/:id and adopt with /adopt |
/api/v2/reports/sla | SLA report (?format=pdf|csv|html); /sla/preview returns JSON |
/api/v2/reports/topn | Top-N by downtime / latency / alerts (?format=pdf|csv|html); /topn/preview returns JSON |
/api/v2/notifications | Configure channels (site-scope + min-severity), send tests, view delivery log |
/api/v2/escalation-policies | CRUD escalation chains (ordered steps, repeat-last) |
/api/v2/webhooks | Outbound webhook subscriptions with HMAC signing |
/api/v2/maintenance | Schedule + manage windows |
/api/v2/status | Component health rollup; GET /api/v2/health for liveness |
/api/v2/ws | WebSocket subscription for real-time metric + alert pushes |
/api/v2/audit | Audit log query |
Creating a sensor
Sensors are created on a device: POST /api/v2/devices/:id/sensors. The body is { name, sensor_type, interval, config }. The check runs against the device's address; config carries the per-plugin parameters in the canonical shape:
{
"target": "10.0.0.5", // optional — overrides the device address
"params": { /* plugin-specific key/values (strings) */ }
}
Put every plugin parameter under params. (Credential params — password, community, auth_pass, priv_pass, secret — are encrypted at rest.) One example per common plugin:
# TCP port check
curl -sS -X POST https://app.example.com/api/v2/devices/$DEV/sensors \
-H 'Authorization: Bearer eyJ...' -H 'Content-Type: application/json' \
-d '{"name":"https-port","sensor_type":"tcp","interval":"60 seconds",
"config":{"params":{"port":"443","timeout_ms":"2000"}}}'
# HTTP with content match
# config.params: { "url", "method", "expected_status", "content_match" }
# {"params":{"url":"https://example.com/health","expected_status":"200","content_match":"OK"}}
# SNMP OID poll
# config.params: { "community", "oid", "version" }
# {"params":{"community":"public","oid":"1.3.6.1.2.1.1.3.0","version":"v2c"}}
# SSL certificate expiry
# config.params: { "hostname", "warn_days", "err_days" }
# {"params":{"hostname":"example.com","warn_days":"30","err_days":"7"}}
# DNS record
# config.params: { "record_type", "expected_value" }
# {"params":{"record_type":"A","expected_value":"1.2.3.4"}}
# SSH command (returns a single number)
# config.params: { "username", "password", "command" }
# {"params":{"username":"root","password":"...","command":"uptime | ..."}}
# ICMP ping — no params
# {"params":{}}
WebSocket
Connect with the same Bearer token (sent as a ?token= query param). Server pushes { "type": "metric", … } or { "type": "alert", … } messages per tenant; the dashboard uses this for live tiles.
Node / TypeScript SDK
npm install @jallarhorn/client
import { Jallarhorn } from "@jallarhorn/client";
const j = new Jallarhorn({
baseURL: "https://app.example.com",
token: process.env.JALLARHORN_TOKEN!,
});
const devices = await j.devices.list();
await j.alerts.ack(alertId);
Python SDK
pip install jallarhorn
from jallarhorn import Jallarhorn
j = Jallarhorn(base_url="https://app.example.com",
token=os.environ["JALLARHORN_TOKEN"])
for d in j.devices.list():
print(d.name, d.status)
j.alerts.ack(alert_id)
Both SDKs share the same method names + paginated-iterator shape, so a script translated between languages reads near-identically. A /api/v3/* migration — if it ever happens — would ship as @jallarhorn/client@2.x / jallarhorn-client>=2.0.
Mobile / PWA
The Jallarhorn dashboard is a Progressive Web App — installable to the home screen on iOS and Android, with offline-aware loading for cached views. There is no separate native app; the PWA covers the use cases that warrant one (alerts, ack, drill-down). The /monitor route renders a viewport-sized dashboard with safe-area awareness for the iPhone notch — useful as a status-board page on a wall-mounted tablet or as the on-call engineer's lock-screen shortcut.
Web Push
VAPID-authenticated push notifications. Users opt in from Settings → Notifications → Browser push. Per-user preferences: minimum severity, quiet hours, timezone. Operators can send a test push from the same screen.
Web Push is per-person, not one of the group-routed channels. Each user subscribes their own browser or installed PWA and sets their own severity floor and quiet hours; a push follows that individual's preferences rather than a channel's site-scope routing policy.
Migrate from per-sensor monitoring
A staged migration where Jallarhorn and the legacy tool run side-by-side; sensors move group-by-group; the legacy install gets retired only after the new one is proven.
Phase 1 — Stand Jallarhorn up alongside
Install per Install & configure. Point Jallarhorn at the same network segments your existing tool already monitors. Don’t disable any of the legacy sensors yet.
Phase 2 — Auto-discover
Run POST /api/v2/discovery/scan against each CIDR. The discovery engine sweeps ICMP, walks SNMP sysDescr / sysName / sysOID, reverse-DNS-looks-up everything, and produces a candidate list. Adopt with one click per device — Jallarhorn then auto-creates sensors based on the device type fingerprint.
Phase 3 — Sensor mapping
Most legacy per-sensor types map 1:1 to a Jallarhorn native plugin (ping, SNMP, HTTP, SSL, SSH, WinRM, DNS, TCP, disk, process, PostgreSQL, Redis, Docker). For the long tail, a custom-sensor framework usually covers it.
Phase 4 — Alert rules
Recreate threshold rules in Jallarhorn (UI or API). Run both alert engines in parallel for a week, watching for divergence. The Jallarhorn alert log includes a source_sensor field so cross-system comparison is mechanical.
Phase 5 — Cutover
Disable the legacy sensors group-by-group as confidence builds. Keep the old install read-only for ~30 days as a rollback option, then decommission. Save the legacy Probe Devices XML export — Jallarhorn can ingest it into a tagged group called imported-legacy.
For a side-by-side feature + pricing comparison, see how Jallarhorn compares.
Docs version: 2026-07-06. Product version: see /health on your control instance. Something missing or wrong? Email support@jallarhorn.com.