diff --git a/src/Wizard.tsx b/src/Wizard.tsx index f193cc7..1d7292f 100644 --- a/src/Wizard.tsx +++ b/src/Wizard.tsx @@ -9,7 +9,8 @@ import * as FeatureSLO from "./features/slo.tsx"; import * as FeatureLogs from "./features/logs.tsx"; import * as FeatureMetrics from "./features/metrics.tsx"; import * as FeatureNovus from "./features/novus.tsx"; -import * as FeatureEdge from "./features/edge.tsx"; +import * as FeatureRabbitMQ from "./features/rabbitmq.tsx"; +import * as FeatureValkey from "./features/valkey.tsx"; // Hard coded list of features that the users can tunr on/off const FEATURES = [ @@ -17,7 +18,8 @@ const FEATURES = [ FeatureSLO, FeatureMetrics, FeatureLogs, - FeatureEdge, + FeatureRabbitMQ, + FeatureValkey, ]; const DEFAULT_STEPS = [ diff --git a/src/features/edge.tsx b/src/features/edge.tsx deleted file mode 100644 index 22977b7..0000000 --- a/src/features/edge.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { useEffect, useState } from "react"; -import { queryPrometheus } from "../lib/prometheusQuerier.ts"; -import { useAuth } from "react-oidc-context"; -import { AutoComplete } from "primereact/autocomplete"; -import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; -import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; -import { useEnv } from '../components/env.tsx'; - -export const FeatureID = "edge"; -export const FeatureName = "Edge Services"; -export const FeatureIcon = "https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg"; - -type Rabbit = { - rabbitmq_cluster: string; - novus_region: string; -}; - -type Queue = { - queue: string; -}; - -export function Component({ goBack, goForward, setDashboardPanels }) { - - const auth = useAuth(); - const env = useEnv(); - const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; - - const [rabbits, setRabbits] = useState(); - const [selectedRegion, setSelectedRegion] = useState(); - const [queues, setQueues] = useState(); - const [queueFilter, setQueueFilter] = useState(".*"); - const [filteredQueues, setFilteredQueues] = useState(); - - const [selectedRabbitCluster, setSelectedRabbitCluster] = useState(); - const [filteredRabbitClusters, setFilteredRabbitClusters] = useState< - Rabbit[] - >([]); - - const getRabbitClusters = () => { - return queryPrometheus( - host, - "group(rabbitmq_identity_info{}) by (rabbitmq_cluster, novus_region)", - auth?.user?.id_token, - ).then((result) => { - return result.data.result.map((item: any) => { - return { - rabbitmq_cluster: item.metric.rabbitmq_cluster, - novus_region: item.metric.novus_region, - }; - }); - }); - }; - - const getRabbitQueues = () => { - let rabbitmq_cluster = selectedRabbitCluster?.rabbitmq_cluster; - if (rabbitmq_cluster === undefined) { - setQueues([]); - } - return queryPrometheus( - host, - `group(rabbitmq_detailed_queue_messages{service="${rabbitmq_cluster}"}) by (queue)`, - auth?.user?.id_token, - ).then((result) => { - return result.data.result.map((item: any) => { - return { - queue: item.metric.queue, - }; - }); - }); - }; - - const searchRabbitClusters = (event) => { - let _filtered: Rabbit[]; - if (!event.query.trim().length) { - _filtered = rabbits ?? []; - } else { - let terms = event.query.trim().split(/[-_\s]/); - _filtered = - rabbits?.filter((r) => { - return terms.every( - (t) => - r.rabbitmq_cluster.toLowerCase().includes(t) || - r.novus_region.toLowerCase().includes(t), - ); - }) ?? []; - } - - setFilteredRabbitClusters(_filtered); - }; - - useEffect(() => { - if (!auth?.isAuthenticated) return; - getRabbitClusters().then((res) => setRabbits(res)); - }, [auth?.isAuthenticated]); - - useEffect(() => { - if (!auth?.isAuthenticated || !selectedRabbitCluster) return; - getRabbitQueues().then((res) => setQueues(res)); - }, [auth?.isAuthenticated, selectedRabbitCluster]); - - useEffect(() => { - setFilteredQueues(queues?.filter((q) => q.queue.match(queueFilter))); - }, [queues, queueFilter]); - - const genRabbitOverview = () => { - return [new TimeSeriesPanelBuilder() - .title("Queues") - .height(8) - .span(8) - .withTarget( - new PrometheusDataqueryBuilder() - .datasource({ uid: "$prometheus" }) - .expr( - `sum(rabbitmq_detailed_queue_messages{service="${selectedRabbitCluster?.rabbitmq_cluster}", queue=~"${queueFilter}"}) by (queue)`, - ).legendFormat(`{{ queue }}`), - )]; - }; - - const onSubmit = () => { - setDashboardPanels(FeatureID, [], genRabbitOverview()); - goForward(); - }; - - const rabbitTemplate = (rabbit) => { - return ( -
- [{rabbit.novus_region.split("-")[0].toUpperCase()}]{" "} - {rabbit.rabbitmq_cluster} -
- ); - }; - - const selectedRabbitTemplate = (rabbit) => { - return rabbit.rabbitmq_cluster; - }; - - return ( - <> -
-

- Edge Configuration -

-

- Edge is our stateful services platform. This feature adds pre-built - panels scoped to edge service instances. -

-
- - - field="rabbitmq_cluster" - value={selectedRabbitCluster} - suggestions={filteredRabbitClusters} - completeMethod={searchRabbitClusters} - onChange={(e) => { - if (e?.value?.rabbitmq_cluster != "") { - setSelectedRabbitCluster(e.value); - } else { - setSelectedRabbitCluster(undefined); - } - }} - itemTemplate={rabbitTemplate} - selectedItemTemplate={selectedRabbitTemplate} - dropdown - > -
- {selectedRabbitCluster && ( -
-
- setQueueFilter(e.target.value)} - > -
queues: {filteredQueues?.length}
-
-
-
- {filteredQueues?.map((q) => { - return
{q.queue}
; - })} -
-
-
- )} -
- {/*
- -
    - {rabbits - ?.filter((rabbit) => rabbit.novus_region === selectedRegion) - .map((rabbit) => ( -
  • {rabbit.rabbitmq_cluster}
  • - ))} -
-
*/} - -
- {goBack && ( - - )} - {goForward && ( - - )} -
- - ); -} diff --git a/src/features/rabbitmq.tsx b/src/features/rabbitmq.tsx new file mode 100644 index 0000000..fb662b4 --- /dev/null +++ b/src/features/rabbitmq.tsx @@ -0,0 +1,202 @@ +import { useCallback, useEffect, useState } from "react"; +import { queryPrometheus } from "../lib/prometheusQuerier.ts"; +import { useAuth } from "react-oidc-context"; +import { AutoComplete } from "primereact/autocomplete"; +import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; +import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; +import { useEnv } from "../components/env.tsx"; + +export const FeatureID = "rabbitmq"; +export const FeatureName = "Edge RabbitMQ"; +export const FeatureIcon = "https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg"; + +// ---- Types ---- + +interface Rabbit { + rabbitmq_cluster: string; + novus_region: string; +} + +interface Queue { + queue: string; +} + +interface Props { + goBack?: () => void; + goForward?: () => void; + setDashboardPanels: (id: string, overview: any[], panels: any[]) => void; +} + +// ---- Data hook ---- + +function useRabbitData( + host: string | undefined, + token: string | undefined, + isAuthenticated: boolean, + selectedCluster: Rabbit | undefined, +) { + const [clusters, setClusters] = useState([]); + const [queues, setQueues] = useState([]); + + useEffect(() => { + if (!isAuthenticated) return; + queryPrometheus( + host, + "group(rabbitmq_identity_info{}) by (rabbitmq_cluster, novus_region)", + token, + ).then((result) => + setClusters( + result.data.result.map((item: any) => ({ + rabbitmq_cluster: item.metric.rabbitmq_cluster, + novus_region: item.metric.novus_region, + })), + ), + ); + }, [isAuthenticated]); + + useEffect(() => { + if (!isAuthenticated || !selectedCluster) { + setQueues([]); + return; + } + queryPrometheus( + host, + `group(rabbitmq_detailed_queue_messages{service="${selectedCluster.rabbitmq_cluster}"}) by (queue)`, + token, + ).then((result) => + setQueues( + result.data.result.map((item: any) => ({ queue: item.metric.queue })), + ), + ); + }, [isAuthenticated, selectedCluster]); + + return { clusters, queues }; +} + +// ---- Panel builders (pure, no React) ---- + +function buildPanels(cluster: string, queueFilter: string) { + return [ + new TimeSeriesPanelBuilder() + .title("Queues") + .height(8) + .span(8) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr( + `sum(rabbitmq_detailed_queue_messages{service="${cluster}", queue=~"${queueFilter}"}) by (queue)`, + ) + .legendFormat(`{{ queue }}`), + ), + ]; +} + +// ---- Component ---- + +export function Component({ goBack, goForward, setDashboardPanels }: Props) { + const auth = useAuth(); + const env = useEnv(); + const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; + const token = auth?.user?.id_token; + + const [clusterValue, setClusterValue] = useState(); + const [filteredClusters, setFilteredClusters] = useState([]); + const [queueFilter, setQueueFilter] = useState(".*"); + + const selectedCluster = typeof clusterValue === "object" ? clusterValue : undefined; + + const { clusters, queues } = useRabbitData(host, token, !!auth?.isAuthenticated, selectedCluster); + + const filteredQueues = queues.filter((q) => q.queue.match(queueFilter)); + + const searchClusters = useCallback( + (event: { query: string }) => { + if (!event.query.trim()) { + setFilteredClusters(clusters); + return; + } + const terms = event.query.trim().split(/[-_\s]/); + setFilteredClusters( + clusters.filter((r) => + terms.every( + (t) => + r.rabbitmq_cluster.toLowerCase().includes(t) || + r.novus_region.toLowerCase().includes(t), + ), + ), + ); + }, + [clusters], + ); + + const clusterTemplate = (rabbit: Rabbit) => ( +
+ [{rabbit.novus_region.split("-")[0]?.toUpperCase()}] {rabbit.rabbitmq_cluster} +
+ ); + + const onSubmit = () => { + if (!selectedCluster) return; + setDashboardPanels(FeatureID, [], buildPanels(selectedCluster.rabbitmq_cluster, queueFilter)); + goForward?.(); + }; + + return ( + <> +
+

RabbitMQ Configuration

+

+ RabbitMQ running on Edge, our stateful services platform. This feature + adds pre-built panels scoped to a RabbitMQ cluster instance. +

+
+ + + field="rabbitmq_cluster" + value={clusterValue} + suggestions={filteredClusters} + completeMethod={searchClusters} + onChange={(e) => setClusterValue(e.value)} + itemTemplate={clusterTemplate} + selectedItemTemplate={(rabbit: Rabbit) => rabbit.rabbitmq_cluster} + dropdown + /> +
+ {selectedCluster && ( +
+
+ setQueueFilter(e.target.value)} + /> +
queues: {filteredQueues.length}
+
+
+
+ {filteredQueues.map((q) => ( +
{q.queue}
+ ))} +
+
+
+ )} +
+ +
+ {goBack && ( + + )} + {goForward && ( + + )} +
+ + ); +} diff --git a/src/features/valkey.tsx b/src/features/valkey.tsx new file mode 100644 index 0000000..fb37ab9 --- /dev/null +++ b/src/features/valkey.tsx @@ -0,0 +1,169 @@ +import { useCallback, useEffect, useState } from "react"; +import { queryPrometheus } from "../lib/prometheusQuerier.ts"; +import { useAuth } from "react-oidc-context"; +import { AutoComplete } from "primereact/autocomplete"; +import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; +import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; +import { useEnv } from "../components/env.tsx"; + +export const FeatureID = "valkey"; +export const FeatureName = "Edge Valkey"; +export const FeatureIcon = "https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/valkey.svg" + +// ---- Types ---- + +interface Valkey { + valkey_cluster: string; + novus_region: string; +} + +interface Props { + goBack?: () => void; + goForward?: () => void; + setDashboardPanels: (id: string, overview: any[], panels: any[]) => void; +} + +// ---- Data hook ---- + +const valkeyRegex = /^(.*?)(?:-(?:valkey-)?metrics)$/; + +function useValkeyData( + host: string | undefined, + token: string | undefined, + isAuthenticated: boolean, +) { + const [clusters, setClusters] = useState([]); + + useEffect(() => { + if (!isAuthenticated) return; + queryPrometheus( + host, + "group(redis_up{}) by (service, novus_region)", + token, + ).then((result) => + setClusters( + result.data.result.map((item: any) => ({ + valkey_cluster: + item.metric.service.match(valkeyRegex)?.[1] ?? + item.metric.service, + novus_region: item.metric.novus_region, + })), + ), + ); + }, [isAuthenticated]); + + return { clusters }; +} + +// ---- Panel builders (pure, no React) ---- + +function buildPanels(cluster: string) { + return [ + new TimeSeriesPanelBuilder() + .title(`${cluster}: Commands`) + .height(8) + .span(12) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr(`sum(rate(redis_commands_total{service=~"${cluster}(-valkey)?-metrics"}[$__rate_interval])) by (cmd)`) + .legendFormat(`{{ cmd }}`), + ), + new TimeSeriesPanelBuilder() + .title(`${cluster}: Memory`) + .height(8) + .span(12) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr(`redis_memory_used_bytes{service=~"${cluster}(-valkey)?-metrics"}`) + .legendFormat(`{{ instance }}`), + ), + ]; +} + +// ---- Component ---- + +export function Component({ goBack, goForward, setDashboardPanels }: Props) { + const auth = useAuth(); + const env = useEnv(); + const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; + const token = auth?.user?.id_token; + + const [clusterValue, setClusterValue] = useState(); + const [filteredClusters, setFilteredClusters] = useState([]); + + const selectedCluster = typeof clusterValue === "object" ? clusterValue : undefined; + + const { clusters } = useValkeyData(host, token, !!auth?.isAuthenticated); + + const searchClusters = useCallback( + (event: { query: string }) => { + if (!event.query.trim()) { + setFilteredClusters(clusters); + return; + } + const terms = event.query.trim().split(/[-_\s]/); + setFilteredClusters( + clusters.filter((v) => + terms.every( + (t) => + v.valkey_cluster.toLowerCase().includes(t) || + v.novus_region.toLowerCase().includes(t), + ), + ), + ); + }, + [clusters], + ); + + const clusterTemplate = (valkey: Valkey) => ( +
+ [{valkey.novus_region.split("-")[0]?.toUpperCase()}] {valkey.valkey_cluster} +
+ ); + + const onSubmit = () => { + if (!selectedCluster) return; + setDashboardPanels(FeatureID, [], buildPanels(selectedCluster.valkey_cluster)); + goForward?.(); + }; + + return ( + <> +
+

Valkey Configuration

+

+ Valkey running on Edge, our stateful services platform. This feature + adds pre-built panels scoped to a Valkey cluster instance. +

+
+ + + field="valkey_cluster" + value={clusterValue} + suggestions={filteredClusters} + completeMethod={searchClusters} + onChange={(e) => setClusterValue(e.value)} + itemTemplate={clusterTemplate} + selectedItemTemplate={(valkey: Valkey) => valkey.valkey_cluster} + dropdown + /> +
+
+ +
+ {goBack && ( + + )} + {goForward && ( + + )} +
+ + ); +}