Skip to content

feat: Health Monitoring widget unification with graphs #1959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 32 additions & 45 deletions packages/api/src/router/widgets/health-monitoring.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
import { observable } from "@trpc/server/observable";

import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema/sqlite";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { HealthMonitoring } from "@homarr/integrations";
import type { ProxmoxClusterInfo } from "@homarr/integrations/types";
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";

import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
import { z } from "zod";

export const healthMonitoringRouter = createTRPCRouter({
getSystemHealthStatus: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.query(async ({ ctx }) => {
getHealthStatus: publicProcedure
.input(z.object({ pointCount: z.number().optional(), maxElements: z.number().optional() }))
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "proxmox"))
.query(async ({ input: { pointCount = 1, maxElements = 32 }, ctx }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = systemInfoRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
ctx.integrations.map(async (integrationWithSecrets) => {
const innerHandler = systemInfoRequestHandler.handler(integrationWithSecrets, { maxElements, pointCount });
const healthInfo = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });

const { decryptedSecrets: _, ...integration } = integrationWithSecrets;

return {
integrationId: integration.id,
integrationName: integration.name,
healthInfo: data,
updatedAt: timestamp,
integration,
healthInfo,
};
}),
);
}),
subscribeSystemHealthStatus: publicProcedure
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {

subscribeHealthStatus: publicProcedure
.input(z.object({ maxElements: z.number().optional() }))
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "proxmox"))
.subscription(({ ctx, input: { maxElements = 32 } }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"healthMonitoring"> }>;
healthInfo: { data: HealthMonitoring; timestamp: Date };
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integration of ctx.integrations) {
const innerHandler = systemInfoRequestHandler.handler(integration, {});
const unsubscribe = innerHandler.subscribe((healthInfo) => {
for (const integrationWithSecrets of ctx.integrations) {
const innerHandler = systemInfoRequestHandler.handler(integrationWithSecrets, { maxElements, pointCount: 1 });
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const unsubscribe = innerHandler.subscribe((data) => {
emit.next({
integrationId: integration.id,
healthInfo,
timestamp: new Date(),
integration,
healthInfo: { data, timestamp: new Date() },
});
});
unsubscribes.push(unsubscribe);
Expand All @@ -48,26 +57,4 @@ export const healthMonitoringRouter = createTRPCRouter({
};
});
}),
getClusterHealthStatus: publicProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
.query(async ({ ctx }) => {
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data;
}),
subscribeClusterHealthStatus: publicProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "proxmox"))
.subscription(({ ctx }) => {
return observable<ProxmoxClusterInfo>((emit) => {
const unsubscribes: (() => void)[] = [];
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
const unsubscribe = innerHandler.subscribe((healthInfo) => {
emit.next(healthInfo);
});
unsubscribes.push(unsubscribe);
return () => {
unsubscribe();
};
});
}),
});
21 changes: 6 additions & 15 deletions packages/cron-jobs/src/jobs/integrations/health-monitoring.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";

import { createCronJob } from "../../lib";

export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SECONDS).withCallback(
createRequestIntegrationJobHandler(
(integration, itemOptions: Record<string, never>) => {
const { kind } = integration;
if (kind !== "proxmox") {
return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
}
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
createRequestIntegrationJobHandler(systemInfoRequestHandler.handler, {
widgetKinds: ["healthMonitoring"],
getInput: {
healthMonitoring: (options) => ({ maxElements: Number(options.pointDensity), pointCount: 1 }),
},
{
widgetKinds: ["healthMonitoring"],
getInput: {
healthMonitoring: () => ({}),
},
},
),
}),
);
5 changes: 3 additions & 2 deletions packages/definitions/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ export type IntegrationCategory =
| "miscellaneous"
| "smartHomeServer"
| "indexerManager"
| "healthMonitoring"
| "search"
| "mediaTranscoding"
| "networkController";
| "networkController"
| "healthMonitoring"
| "tempNone";
1 change: 1 addition & 0 deletions packages/integrations/src/base/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
import { EmbyIntegration } from "../emby/emby-integration";
import { DashDotIntegration } from "../health-monitoring/dashdot/dashdot-integration";
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
Expand Down Expand Up @@ -43,10 +44,10 @@

// factories are an array, to differentiate in js between class constructors and functions
if (Array.isArray(creator)) {
return (await creator[0](integration)) as IntegrationInstanceOfKind<TKind>;

Check failure on line 47 in packages/integrations/src/base/creator.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe call of a(n) `error` type typed value

Check failure on line 47 in packages/integrations/src/base/creator.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe return of type `any` from function with return type `Promise<IntegrationInstanceOfKind<TKind>>`
}

return new creator(integration) as IntegrationInstanceOfKind<TKind>;

Check failure on line 50 in packages/integrations/src/base/creator.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe construction of a(n) `error` type typed value

Check failure on line 50 in packages/integrations/src/base/creator.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe return of type `any` from function with return type `Promise<IntegrationInstanceOfKind<TKind>>`
};

export const createIntegrationAsyncFromSecrets = <TKind extends keyof typeof integrationCreators>(
Expand Down Expand Up @@ -86,7 +87,7 @@
openmediavault: OpenMediaVaultIntegration,
lidarr: LidarrIntegration,
readarr: ReadarrIntegration,
dashDot: DashDotIntegration,

Check failure on line 90 in packages/integrations/src/base/creator.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an error typed value
tdarr: TdarrIntegration,
proxmox: ProxmoxIntegration,
emby: EmbyIntegration,
Expand Down
149 changes: 0 additions & 149 deletions packages/integrations/src/dashdot/dashdot-integration.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { z, ZodType } from "zod";

import type { HealthMonitoring } from "../../interfaces/health-monitoring/healt-monitoring-data";
import { HealthMonitoringIntegration } from "../../interfaces/health-monitoring/health-monitoring-interface";
import {
configSchema,
cpuLoadSchema,
memoryLoadSchema,
networkLoadSchema,
serverInfoSchema,
storageLoadSchema,
} from "./dashdot-schema";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";

export class DashDotIntegration extends HealthMonitoringIntegration {
public async testConnectionAsync(): Promise<void> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/info"));
await response.json();
}

public async getSystemInfoAsync(): Promise<HealthMonitoring> {
const { config } = await this.dashDotApiCallAsync("/config", configSchema);
const info = await this.dashDotApiCallAsync("/info", serverInfoSchema);
const cpuLoad = await this.dashDotApiCallAsync("/load/cpu", cpuLoadSchema);
const memoryLoad = await this.dashDotApiCallAsync("/load/ram", memoryLoadSchema);
const storageLoad = await this.dashDotApiCallAsync("/load/storage", storageLoadSchema);
const networkLoad = await this.dashDotApiCallAsync("/load/network", networkLoadSchema);
return {
system: {
name: config.override.os ?? info.os.distro,
type: "single",
version: info.os.release,
uptime: info.os.uptime * 1000,
},
cpu: cpuLoad.map((cpu) => {
return {
id: `${cpu.core}`,
name: `${cpu.core}`,
temp: cpu.temp ? (config.use_imperial ? (cpu.temp - 32) / 1.8 : cpu.temp) : undefined,
maxValue: 100,
};
}),
memory: [
{
id: "unique",
name: `${config.override.ram_brand ?? info.ram.layout[0].brand} - ${config.override.ram_type ?? info.ram.layout[0].type}`,
maxValue: config.override.ram_size ?? info.ram.size,
},
],
storage: info.storage.map((storage, index) => {
return {
name: storage.disks.map((disk) => config.override.storage_brands?.[disk.brand] ?? disk.brand).join(", "),
used: storageLoad[index] ?? -1,
size: storage.size,
};
}),
network: [
{
id: "unique",
name: config.use_network_interface,
maxValue: ((config.override.network_interface_speed ?? info.network.interfaceSpeed) * 1000 ** 3) / 8,
},
],
history: [
{
timestamp: Date.now(),
cpu: cpuLoad.map(({ core, load }) => ({ id: `${core}`, value: load })),
memory: [{ id: "unique", value: memoryLoad.load }],
networkUp: [{ id: "unique", value: Math.round(networkLoad.up) }],
networkDown: [{ id: "unique", value: Math.round(networkLoad.down) }],
},
],
};
}

private async dashDotApiCallAsync<T extends ZodType>(path: `/${string}`, schema: T): Promise<z.infer<T>> {
const response = await fetchWithTrustedCertificatesAsync(this.url(path));
return await schema.parseAsync(await response.json()) as Promise<z.infer<T>>;
}
}
Loading
Loading