Feature: true ping, rename old ping to siteMonitor (#2215)
This commit is contained in:
parent
0c8c759f8a
commit
792f768a7f
12 changed files with 206 additions and 48 deletions
|
@ -101,30 +101,50 @@ To use a local icon, first create a Docker mount to `/app/public/icons` and then
|
||||||
|
|
||||||
## Ping
|
## 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.
|
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.
|
||||||
|
|
||||||
!!! 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.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- Group A:
|
- Group A:
|
||||||
- Sonarr:
|
- Sonarr:
|
||||||
icon: sonarr.png
|
icon: sonarr.png
|
||||||
href: http://sonarr.host/
|
href: http://sonarr.host/
|
||||||
ping: http://sonarr.host/
|
ping: sonarr.host
|
||||||
|
|
||||||
- Group B:
|
- Group B:
|
||||||
- Radarr:
|
- Radarr:
|
||||||
icon: radarr.png
|
icon: radarr.png
|
||||||
href: http://radarr.host/
|
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">
|
<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).
|
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
|
## Docker Integration
|
||||||
|
|
||||||
Services may be connected to a Docker container, either running on the local machine, or a remote machine.
|
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
|
## 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
|
- 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 ping or healthy status.
|
- `dot` shows a green dot for a successful monitor ping or healthy status.
|
||||||
- `basic` shows either UP or DOWN for ping
|
- `basic` shows either UP or DOWN for monitor & ping
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -23,6 +23,7 @@
|
||||||
"minecraft-ping-js": "^1.0.2",
|
"minecraft-ping-js": "^1.0.2",
|
||||||
"next": "^12.3.1",
|
"next": "^12.3.1",
|
||||||
"next-i18next": "^12.0.1",
|
"next-i18next": "^12.0.1",
|
||||||
|
"ping": "^0.4.4",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"raw-body": "^2.5.1",
|
"raw-body": "^2.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -4861,6 +4862,14 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"minecraft-ping-js": "^1.0.2",
|
"minecraft-ping-js": "^1.0.2",
|
||||||
"next": "^12.3.1",
|
"next": "^12.3.1",
|
||||||
"next-i18next": "^12.0.1",
|
"next-i18next": "^12.0.1",
|
||||||
|
"ping": "^0.4.4",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"raw-body": "^2.5.1",
|
"raw-body": "^2.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
@ -50,6 +50,9 @@ dependencies:
|
||||||
next-i18next:
|
next-i18next:
|
||||||
specifier: ^12.0.1
|
specifier: ^12.0.1
|
||||||
version: 12.1.0(next@12.3.4)(react-dom@18.2.0)(react@18.2.0)
|
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:
|
pretty-bytes:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
|
@ -3103,6 +3106,11 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ping@0.4.4:
|
||||||
|
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pirates@4.0.5:
|
/pirates@4.0.5:
|
||||||
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
|
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
|
@ -79,13 +79,20 @@
|
||||||
"partial": "Partial"
|
"partial": "Partial"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"http_status": "HTTP status",
|
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"ping": "Ping",
|
"ping": "Ping",
|
||||||
"down": "Down",
|
"down": "Down",
|
||||||
"up": "Up",
|
"up": "Up",
|
||||||
"not_available": "Not Available"
|
"not_available": "Not Available"
|
||||||
},
|
},
|
||||||
|
"siteMonitor": {
|
||||||
|
"http_status": "HTTP status",
|
||||||
|
"error": "Error",
|
||||||
|
"response": "Response",
|
||||||
|
"down": "Down",
|
||||||
|
"up": "Up",
|
||||||
|
"not_available": "Not Available"
|
||||||
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "Playing",
|
"playing": "Playing",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcoding",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useContext, useState } from "react";
|
||||||
import Status from "./status";
|
import Status from "./status";
|
||||||
import Widget from "./widget";
|
import Widget from "./widget";
|
||||||
import Ping from "./ping";
|
import Ping from "./ping";
|
||||||
|
import SiteMonitor from "./site-monitor";
|
||||||
import KubernetesStatus from "./kubernetes-status";
|
import KubernetesStatus from "./kubernetes-status";
|
||||||
|
|
||||||
import Docker from "widgets/docker/component";
|
import Docker from "widgets/docker/component";
|
||||||
|
@ -93,6 +94,13 @@ export default function Item({ service, group }) {
|
||||||
</div>
|
</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 && (
|
{service.container && (
|
||||||
<button
|
<button
|
||||||
type="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 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 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 = "";
|
let statusText = "";
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -19,18 +19,13 @@ export default function Ping({ group, service, style }) {
|
||||||
} else if (!data) {
|
} else if (!data) {
|
||||||
statusText = t("ping.ping");
|
statusText = t("ping.ping");
|
||||||
statusTitle += ` ${t("ping.not_available")}`;
|
statusTitle += ` ${t("ping.not_available")}`;
|
||||||
} else if (data.status > 403) {
|
} else if (!data.alive) {
|
||||||
colorClass = "text-rose-500/80";
|
colorClass = "text-rose-500/80";
|
||||||
statusTitle += ` ${data.status}`;
|
statusTitle += ` ${t("ping.down")}`;
|
||||||
|
statusText = t("ping.down");
|
||||||
if (style === "basic") {
|
} else if (data.alive) {
|
||||||
statusText = t("ping.down");
|
const ping = t("common.ms", { value: data.time, style: "unit", unit: "millisecond", maximumFractionDigits: 0 });
|
||||||
} else {
|
statusTitle += ` ${t("ping.up")} (${ping})`;
|
||||||
statusText = data.status;
|
|
||||||
}
|
|
||||||
} else if (data) {
|
|
||||||
const ping = t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", maximumFractionDigits: 0 });
|
|
||||||
statusTitle += ` ${data.status} (${ping})`;
|
|
||||||
colorClass = "text-emerald-500/80";
|
colorClass = "text-emerald-500/80";
|
||||||
|
|
||||||
if (style === "basic") {
|
if (style === "basic") {
|
||||||
|
|
63
src/components/services/site-monitor.jsx
Normal file
63
src/components/services/site-monitor.jsx
Normal file
|
@ -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 { getServiceItem } from "utils/config/service-helpers";
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import { httpProxy } from "utils/proxy/http";
|
|
||||||
|
|
||||||
const logger = createLogger("ping");
|
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) {
|
if (!pingHostOrURL) {
|
||||||
logger.debug("No ping URL specified");
|
logger.debug("No ping host specified");
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: "No ping URL given",
|
error: "No ping host given",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hostname = pingHostOrURL;
|
||||||
try {
|
try {
|
||||||
let startTime = performance.now();
|
// maintain backwards compatibility with old ping where may be http://...
|
||||||
let [status] = await httpProxy(pingURL, {
|
hostname = new URL(pingHostOrURL).hostname;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
status,
|
|
||||||
latency: endTime - startTime,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug("Error attempting ping: %s", JSON.stringify(e));
|
// eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await ping.probe(hostname);
|
||||||
|
return res.status(200).json(response);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug("Error attempting ping: %s", e);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
error: "Error attempting ping, see logs.",
|
error: "Error attempting ping, see logs.",
|
||||||
});
|
});
|
||||||
|
|
52
src/pages/api/siteMonitor.js
Normal file
52
src/pages/api/siteMonitor.js
Normal file
|
@ -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`]) {
|
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
|
||||||
constructedService.ping = 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`]) {
|
if (ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
|
||||||
constructedService.statusStyle = ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
|
constructedService.statusStyle = ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue