Feature: true ping, rename old ping to siteMonitor (#2215)

pull/2222/head
shamoon 7 months ago committed by GitHub
parent 0c8c759f8a
commit 792f768a7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -101,30 +101,50 @@ To use a local icon, first create a Docker mount to `/app/public/icons` and then
## Ping
Services may have an optional `ping` property that allows you to monitor the availability of an endpoint you chose and have the response time displayed. You do not need to set your ping URL equal to your href URL.
!!! note
The ping feature works by making an http `HEAD` request to the URL, and falls back to `GET` in case that fails. It will not, for example, login if the URL requires auth or is behind e.g. Authelia. In the case of a reverse proxy and/or auth this usually requires the use of an 'internal' URL to make the ping feature correctly display status.
Services may have an optional `ping` property that allows you to monitor the availability of an external host. As of v0.7.5, the ping feature uses the true ping command on the underlying host.
```yaml
- Group A:
- Sonarr:
icon: sonarr.png
href: http://sonarr.host/
ping: http://sonarr.host/
ping: sonarr.host
- Group B:
- Radarr:
icon: radarr.png
href: http://radarr.host/
ping: http://some.other.host/
ping: some.other.host
```
<img width="1038" alt="Ping" src="https://github.com/gethomepage/homepage/assets/88257202/7bc13bd3-0d0b-44e3-888c-a20e069a3233">
You can also apply different styles to the ping indicator by using the `statusStyle` property, see [settings](settings.md#status-style).
## Site Monitor
Services may have an optional `siteMonitor` property (formerly `ping`) that allows you to monitor the availability of a URL you chose and have the response time displayed. You do not need to set your monitor URL equal to your href or ping URL.
!!! note
The site monitor feature works by making an http `HEAD` request to the URL, and falls back to `GET` in case that fails. It will not, for example, login if the URL requires auth or is behind e.g. Authelia. In the case of a reverse proxy and/or auth this usually requires the use of an 'internal' URL to make the site monitor feature correctly display status.
```yaml
- Group A:
- Sonarr:
icon: sonarr.png
href: http://sonarr.host/
siteMonitor: http://sonarr.host/
- Group B:
- Radarr:
icon: radarr.png
href: http://radarr.host/
siteMonitor: http://some.other.host/
```
You can also apply different styles to the site monitor indicator by using the `statusStyle` property, see [settings](settings.md#status-style).
## Docker Integration
Services may be connected to a Docker container, either running on the local machine, or a remote machine.

@ -382,11 +382,11 @@ If you have both set the per-service settings take precedence.
## Status Style
You can choose from the following styles for docker or k8s status and ping: `dot` or `basic`
You can choose from the following styles for docker or k8s status, site monitor and ping: `dot` or `basic`
- The default is no value, and displays the ping response time in ms and the docker / k8s container status
- `dot` shows a green dot for a successful ping or healthy status.
- `basic` shows either UP or DOWN for ping
- The default is no value, and displays the montior and ping response time in ms and the docker / k8s container status
- `dot` shows a green dot for a successful monitor ping or healthy status.
- `basic` shows either UP or DOWN for monitor & ping
For example:

9
package-lock.json generated

@ -23,6 +23,7 @@
"minecraft-ping-js": "^1.0.2",
"next": "^12.3.1",
"next-i18next": "^12.0.1",
"ping": "^0.4.4",
"pretty-bytes": "^6.0.0",
"raw-body": "^2.5.1",
"react": "^18.2.0",
@ -4861,6 +4862,14 @@
"node": ">=0.10.0"
}
},
"node_modules/ping": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz",
"integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pirates": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",

@ -25,6 +25,7 @@
"minecraft-ping-js": "^1.0.2",
"next": "^12.3.1",
"next-i18next": "^12.0.1",
"ping": "^0.4.4",
"pretty-bytes": "^6.0.0",
"raw-body": "^2.5.1",
"react": "^18.2.0",

@ -50,6 +50,9 @@ dependencies:
next-i18next:
specifier: ^12.0.1
version: 12.1.0(next@12.3.4)(react-dom@18.2.0)(react@18.2.0)
ping:
specifier: ^0.4.4
version: 0.4.4
pretty-bytes:
specifier: ^6.0.0
version: 6.1.0
@ -3103,6 +3106,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/ping@0.4.4:
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
engines: {node: '>=4.0.0'}
dev: false
/pirates@4.0.5:
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
engines: {node: '>= 6'}

@ -79,13 +79,20 @@
"partial": "Partial"
},
"ping": {
"http_status": "HTTP status",
"error": "Error",
"ping": "Ping",
"down": "Down",
"up": "Up",
"not_available": "Not Available"
},
"siteMonitor": {
"http_status": "HTTP status",
"error": "Error",
"response": "Response",
"down": "Down",
"up": "Up",
"not_available": "Not Available"
},
"emby": {
"playing": "Playing",
"transcoding": "Transcoding",

@ -4,6 +4,7 @@ import { useContext, useState } from "react";
import Status from "./status";
import Widget from "./widget";
import Ping from "./ping";
import SiteMonitor from "./site-monitor";
import KubernetesStatus from "./kubernetes-status";
import Docker from "widgets/docker/component";
@ -93,6 +94,13 @@ export default function Item({ service, group }) {
</div>
)}
{service.siteMonitor && (
<div className="flex-shrink-0 flex items-center justify-center service-tag service-site-monitor">
<SiteMonitor group={group} service={service.name} style={statusStyle} />
<span className="sr-only">Site monitor status</span>
</div>
)}
{service.container && (
<button
type="button"

@ -9,7 +9,7 @@ export default function Ping({ group, service, style }) {
let colorClass = "text-black/20 dark:text-white/40 opacity-20";
let backgroundClass = "bg-theme-500/10 dark:bg-theme-900/50 px-1.5 py-0.5";
let statusTitle = t("ping.http_status");
let statusTitle = t("ping.ping");
let statusText = "";
if (error) {
@ -19,18 +19,13 @@ export default function Ping({ group, service, style }) {
} else if (!data) {
statusText = t("ping.ping");
statusTitle += ` ${t("ping.not_available")}`;
} else if (data.status > 403) {
} else if (!data.alive) {
colorClass = "text-rose-500/80";
statusTitle += ` ${data.status}`;
if (style === "basic") {
statusText = t("ping.down");
} else {
statusText = data.status;
}
} else if (data) {
const ping = t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", maximumFractionDigits: 0 });
statusTitle += ` ${data.status} (${ping})`;
statusTitle += ` ${t("ping.down")}`;
statusText = t("ping.down");
} else if (data.alive) {
const ping = t("common.ms", { value: data.time, style: "unit", unit: "millisecond", maximumFractionDigits: 0 });
statusTitle += ` ${t("ping.up")} (${ping})`;
colorClass = "text-emerald-500/80";
if (style === "basic") {

@ -0,0 +1,63 @@
import { useTranslation } from "react-i18next";
import useSWR from "swr";
export default function SiteMonitor({ group, service, style }) {
const { t } = useTranslation();
const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ group, service }).toString()}`, {
refreshInterval: 30000,
});
let colorClass = "text-black/20 dark:text-white/40 opacity-20";
let backgroundClass = "bg-theme-500/10 dark:bg-theme-900/50 px-1.5 py-0.5";
let statusTitle = t("siteMonitor.http_status");
let statusText = "";
if (error) {
colorClass = "text-rose-500";
statusText = t("siteMonitor.error");
statusTitle += ` ${t("siteMonitor.error")}`;
} else if (!data) {
statusText = t("siteMonitor.response");
statusTitle += ` ${t("siteMonitor.not_available")}`;
} else if (data.status > 403) {
colorClass = "text-rose-500/80";
statusTitle += ` ${data.status}`;
if (style === "basic") {
statusText = t("siteMonitor.down");
} else {
statusText = data.status;
}
} else if (data) {
const responseTime = t("common.ms", {
value: data.latency,
style: "unit",
unit: "millisecond",
maximumFractionDigits: 0,
});
statusTitle += ` ${data.status} (${responseTime})`;
colorClass = "text-emerald-500/80";
if (style === "basic") {
statusText = t("siteMonitor.up");
} else {
statusText = responseTime;
colorClass += " lowercase";
}
}
if (style === "dot") {
backgroundClass = "p-4";
colorClass = colorClass.replace(/text-/g, "bg-").replace(/\/\d\d/g, "");
}
return (
<div
className={`w-auto text-center rounded-b-[3px] overflow-hidden site-monitor-status ${backgroundClass}`}
title={statusTitle}
>
{style !== "dot" && <div className={`font-bold uppercase text-[8px] ${colorClass}`}>{statusText}</div>}
{style === "dot" && <div className={`rounded-full h-3 w-3 ${colorClass}`} />}
</div>
);
}

@ -1,8 +1,7 @@
import { performance } from "perf_hooks";
import { promise as ping } from "ping";
import { getServiceItem } from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { httpProxy } from "utils/proxy/http";
const logger = createLogger("ping");
@ -16,35 +15,28 @@ export default async function handler(req, res) {
});
}
const { ping: pingURL } = serviceItem;
const { ping: pingHostOrURL } = serviceItem;
if (!pingURL) {
logger.debug("No ping URL specified");
if (!pingHostOrURL) {
logger.debug("No ping host specified");
return res.status(400).send({
error: "No ping URL given",
error: "No ping host given",
});
}
let hostname = pingHostOrURL;
try {
let startTime = performance.now();
let [status] = await httpProxy(pingURL, {
method: "HEAD",
});
let endTime = performance.now();
if (status > 403) {
// try one more time as a GET in case HEAD is rejected for whatever reason
startTime = performance.now();
[status] = await httpProxy(pingURL);
endTime = performance.now();
}
// maintain backwards compatibility with old ping where may be http://...
hostname = new URL(pingHostOrURL).hostname;
} catch (e) {
// eslint-disable-line no-empty
}
return res.status(200).json({
status,
latency: endTime - startTime,
});
try {
const response = await ping.probe(hostname);
return res.status(200).json(response);
} catch (e) {
logger.debug("Error attempting ping: %s", JSON.stringify(e));
logger.debug("Error attempting ping: %s", e);
return res.status(400).send({
error: "Error attempting ping, see logs.",
});

@ -0,0 +1,52 @@
import { performance } from "perf_hooks";
import { getServiceItem } from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { httpProxy } from "utils/proxy/http";
const logger = createLogger("siteMonitor");
export default async function handler(req, res) {
const { group, service } = req.query;
const serviceItem = await getServiceItem(group, service);
if (!serviceItem) {
logger.debug(`No service item found for group ${group} named ${service}`);
return res.status(400).send({
error: "Unable to find service, see log for details.",
});
}
const { siteMonitor: monitorURL } = serviceItem;
if (!monitorURL) {
logger.debug("No http monitor URL specified");
return res.status(400).send({
error: "No http monitor URL given",
});
}
try {
let startTime = performance.now();
let [status] = await httpProxy(monitorURL, {
method: "HEAD",
});
let endTime = performance.now();
if (status > 403) {
// try one more time as a GET in case HEAD is rejected for whatever reason
startTime = performance.now();
[status] = await httpProxy(monitorURL);
endTime = performance.now();
}
return res.status(200).json({
status,
latency: endTime - startTime,
});
} catch (e) {
logger.debug("Error attempting http monitor: %s", e);
return res.status(400).send({
error: "Error attempting http monitor, see logs.",
});
}
}

@ -258,6 +258,9 @@ export async function servicesFromKubernetes() {
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
constructedService.ping = ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`];
}
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`]) {
constructedService.siteMonitor = ingress.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`];
}
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
constructedService.statusStyle = ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
}

Loading…
Cancel
Save