Feature: Prometheus Metric service widget (#4269)

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
pull/4278/head
Felix Cornelius 1 month ago committed by GitHub
parent 794ec127cd
commit e938c3ac1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -98,6 +98,7 @@ You can also find a list of all available service widgets in the sidebar navigat
- [Plex](plex.md) - [Plex](plex.md)
- [Portainer](portainer.md) - [Portainer](portainer.md)
- [Prometheus](prometheus.md) - [Prometheus](prometheus.md)
- [Prometheus Metric](prometheusmetric.md)
- [Prowlarr](prowlarr.md) - [Prowlarr](prowlarr.md)
- [Proxmox](proxmox.md) - [Proxmox](proxmox.md)
- [Proxmox Backup Server](proxmoxbackupserver.md) - [Proxmox Backup Server](proxmoxbackupserver.md)

@ -0,0 +1,67 @@
---
title: Prometheus Metric
description: Prometheus Metric Widget Configuration
---
Learn more about [Querying Prometheus](https://prometheus.io/docs/prometheus/latest/querying/basics/).
This widget can show metrics for your service defined by PromQL queries which are requested from a running Prometheus instance.
Quries can be defined in the `metrics` array of the widget along with a label to be used to present the metric value. You can optionally specify a global `refreshInterval` in milliseconds and/or define the `refreshInterval` per metric. Inside the optional `format` object of a metric various formatting styles and transformations can be applied (see below).
```yaml
widget:
type: prometheusmetric
url: https://prometheus.host.or.ip
refreshInterval: 10000 # optional - in milliseconds, defaults to 10s
metrics:
- label: Metric 1
query: alertmanager_alerts{state="active"}
- label: Metric 2
query: apiserver_storage_size_bytes{node="mynode"}
format:
type: bytes
- label: Metric 3
query: avg(prometheus_notifications_latency_seconds)
format:
type: number
suffix: s
options:
maximumFractionDigits: 4
- label: Metric 4
query: time()
refreshInterval: 1000 # will override global refreshInterval
format:
type: date
scale: 1000
options:
timeStyle: medium
```
## Formatting
Supported values for `format.type` are `text`, `number`, `percent`, `bytes`, `bits`, `bbytes`, `bbits`, `byterate`, `bibyterate`, `bitrate`, `bibitrate`, `date`, `duration`, `relativeDate`, and `text` which is the default.
The `dateStyle` and `timeStyle` options of the `date` format are passed directly to [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) and the `style` and `numeric` options of `relativeDate` are passed to [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat). For the `number` format, options of [Intl.NumberFormat](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) can be used, e.g. `maximumFractionDigits` or `minimumFractionDigits`.
### Data Transformation
You can manipulate your metric value with the following tools: `scale`, `prefix` and `suffix`, for example:
```yaml
- query: my_custom_metric{}
label: Metric 1
format:
type: number
scale: 1000 # multiplies value by a number or fraction string e.g. 1/16
- query: my_custom_metric{}
label: Metric 2
format:
type: number
prefix: "$" # prefixes value with given string
- query: my_custom_metric{}
label: Metric 3
format:
type: number
suffix: "€" # suffixes value with given string
```

@ -121,6 +121,7 @@ nav:
- widgets/services/plex.md - widgets/services/plex.md
- widgets/services/portainer.md - widgets/services/portainer.md
- widgets/services/prometheus.md - widgets/services/prometheus.md
- widgets/services/prometheusmetric.md
- widgets/services/prowlarr.md - widgets/services/prowlarr.md
- widgets/services/proxmox.md - widgets/services/proxmox.md
- widgets/services/proxmoxbackupserver.md - widgets/services/proxmoxbackupserver.md

@ -418,7 +418,7 @@ export function cleanServiceGroups(groups) {
pointsLimit, pointsLimit,
diskUnits, diskUnits,
// glances, customapi, iframe // glances, customapi, iframe, prometheusmetric
refreshInterval, refreshInterval,
// hdhomerun // hdhomerun
@ -461,6 +461,9 @@ export function cleanServiceGroups(groups) {
// opnsense, pfsense // opnsense, pfsense
wan, wan,
// prometheusmetric
metrics,
// proxmox // proxmox
node, node,
@ -646,6 +649,10 @@ export function cleanServiceGroups(groups) {
if (type === "vikunja") { if (type === "vikunja") {
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList; if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
} }
if (type === "prometheusmetric") {
if (metrics) cleanedService.widget.metrics = metrics;
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
}
} }
return cleanedService; return cleanedService;

@ -95,6 +95,7 @@ const components = {
plex: dynamic(() => import("./plex/component")), plex: dynamic(() => import("./plex/component")),
portainer: dynamic(() => import("./portainer/component")), portainer: dynamic(() => import("./portainer/component")),
prometheus: dynamic(() => import("./prometheus/component")), prometheus: dynamic(() => import("./prometheus/component")),
prometheusmetric: dynamic(() => import("./prometheusmetric/component")),
prowlarr: dynamic(() => import("./prowlarr/component")), prowlarr: dynamic(() => import("./prowlarr/component")),
proxmox: dynamic(() => import("./proxmox/component")), proxmox: dynamic(() => import("./proxmox/component")),
pterodactyl: dynamic(() => import("./pterodactyl/component")), pterodactyl: dynamic(() => import("./pterodactyl/component")),

@ -0,0 +1,115 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
function formatValue(t, metric, rawValue) {
if (!rawValue) return "-";
let value = rawValue;
// Scale the value. Accepts either a number to multiply by or a string
// like "12/345".
const scale = metric?.format?.scale;
if (typeof scale === "number") {
value *= scale;
} else if (typeof scale === "string" && scale.includes("/")) {
const parts = scale.split("/");
const numerator = parts[0] ? parseFloat(parts[0]) : 1;
const denominator = parts[1] ? parseFloat(parts[1]) : 1;
value = (value * numerator) / denominator;
} else {
value = parseFloat(value);
}
// Format the value using a known type and optional options.
switch (metric?.format?.type) {
case "text":
break;
default:
value = t(`common.${metric.format.type}`, { value, ...metric.format?.options });
}
// Apply fixed prefix.
const prefix = metric?.format?.prefix;
if (prefix) {
value = `${prefix}${value}`;
}
// Apply fixed suffix.
const suffix = metric?.format?.suffix;
if (suffix) {
value = `${value}${suffix}`;
}
return value;
}
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { metrics = [], refreshInterval = 10000 } = widget;
let prometheusmetricError;
const prometheusmetricData = new Map(
metrics.slice(0, 4).map((metric) => {
// disable the rule that hooks should not be called from a callback,
// because we don't need a strong guarantee of hook execution order here.
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: resultData, error: resultError } = useWidgetAPI(widget, "query", {
query: metric.query,
refreshInterval: Math.max(1000, metric.refreshInterval ?? refreshInterval),
});
if (resultError) {
prometheusmetricError = resultError;
}
return [metric.key ?? metric.label, resultData];
}),
);
if (prometheusmetricError) {
return <Container service={service} error={prometheusmetricError} />;
}
if (!prometheusmetricData) {
return (
<Container service={service}>
{metrics.slice(0, 4).map((item) => (
<Block label={item.label} key={item.label} />
))}
</Container>
);
}
function getResultValue(data) {
// Fetches the first metric result from the Prometheus query result data.
// The first element in the result value is the timestamp which is ignored here.
const resultType = data?.data?.resultType;
const result = data?.data?.result;
switch (resultType) {
case "vector":
return result?.[0]?.value?.[1];
case "scalar":
return result?.[1];
default:
return "";
}
}
return (
<Container service={service}>
{metrics.map((metric) => (
<Block
label={metric.label}
key={metric.key ?? metric.label}
value={formatValue(t, metric, getResultValue(prometheusmetricData.get(metric.key ?? metric.label)))}
/>
))}
</Container>
);
}

@ -0,0 +1,16 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
query: {
method: "GET",
endpoint: "query",
params: ["query"],
},
},
};
export default widget;

@ -87,6 +87,7 @@ import plantit from "./plantit/widget";
import plex from "./plex/widget"; import plex from "./plex/widget";
import portainer from "./portainer/widget"; import portainer from "./portainer/widget";
import prometheus from "./prometheus/widget"; import prometheus from "./prometheus/widget";
import prometheusmetric from "./prometheusmetric/widget";
import prowlarr from "./prowlarr/widget"; import prowlarr from "./prowlarr/widget";
import proxmox from "./proxmox/widget"; import proxmox from "./proxmox/widget";
import pterodactyl from "./pterodactyl/widget"; import pterodactyl from "./pterodactyl/widget";
@ -218,6 +219,7 @@ const widgets = {
plex, plex,
portainer, portainer,
prometheus, prometheus,
prometheusmetric,
prowlarr, prowlarr,
proxmox, proxmox,
pterodactyl, pterodactyl,

Loading…
Cancel
Save