From d21b8dbf238419888a8e21c1f5f40feba1f793f6 Mon Sep 17 00:00:00 2001 From: Riccardo Forina Date: Wed, 24 Apr 2024 10:04:39 +0200 Subject: [PATCH] Change the required env variables Make some take an implicit default; make Prometheus optional showing fallbacks in the UI --- ui/api/kafka/actions.ts | 25 ++-- .../[kafkaId]/nodes/DistributionChart.tsx | 14 +-- .../kafka/[kafkaId]/nodes/NodesTable.tsx | 107 ++++++++++-------- .../[locale]/kafka/[kafkaId]/nodes/page.tsx | 55 ++++++--- .../kafka/[kafkaId]/overview/page.tsx | 21 +++- ui/app/api/auth/[...nextauth]/keycloak.ts | 26 +++-- ui/components/ClusterOverview/ClusterCard.tsx | 6 +- ui/environment.d.ts | 13 +-- ui/messages/en.json | 3 + ui/utils/authOptions.ts | 16 +-- ui/utils/logger.ts | 2 +- ui/utils/session.ts | 6 +- 12 files changed, 187 insertions(+), 107 deletions(-) diff --git a/ui/api/kafka/actions.ts b/ui/api/kafka/actions.ts index 1686e9c79..b446564b1 100644 --- a/ui/api/kafka/actions.ts +++ b/ui/api/kafka/actions.ts @@ -20,9 +20,11 @@ import * as topic from "./topic.promql"; export type ClusterMetric = keyof typeof cluster; export type TopicMetric = keyof typeof topic; -const prom = new PrometheusDriver({ - endpoint: process.env.CONSOLE_METRICS_PROMETHEUS_URL, -}); +const prom = process.env.CONSOLE_METRICS_PROMETHEUS_URL + ? new PrometheusDriver({ + endpoint: process.env.CONSOLE_METRICS_PROMETHEUS_URL, + }) + : undefined; const log = logger.child({ module: "kafka-api" }); @@ -71,13 +73,20 @@ export async function getKafkaCluster( export async function getKafkaClusterKpis( clusterId: string, -): Promise<{ cluster: ClusterDetail; kpis: ClusterKpis } | null> { +): Promise<{ cluster: ClusterDetail; kpis: ClusterKpis | null } | null> { try { const cluster = await getKafkaCluster(clusterId); if (!cluster) { return null; } + log.debug({ cluster, prom }, "????"); + + if (!prom) { + log.debug({ clusterId }, "getKafkaClusterKpis Prometheus unavailable"); + return { cluster, kpis: null }; + } + const valuesRes = await prom.instantQuery( values( cluster.attributes.namespace, @@ -208,7 +217,7 @@ export async function getKafkaClusterMetrics( const start = new Date().getTime() - 1 * 60 * 60 * 1000; const end = new Date(); const step = 60 * 1; - const seriesRes = await prom.rangeQuery( + const seriesRes = await prom!.rangeQuery( cluster[metric](namespace, name), start, end, @@ -227,7 +236,7 @@ export async function getKafkaClusterMetrics( try { const cluster = await getKafkaCluster(clusterId); - if (!cluster) { + if (!cluster || !prom) { return null; } @@ -271,7 +280,7 @@ export async function getKafkaTopicMetrics( const start = new Date().getTime() - 1 * 60 * 60 * 1000; const end = new Date(); const step = 60 * 1; - const seriesRes = await prom.rangeQuery( + const seriesRes = await prom!.rangeQuery( topic[metric](namespace, name), start, end, @@ -290,7 +299,7 @@ export async function getKafkaTopicMetrics( try { const cluster = await getKafkaCluster(clusterId); - if (!cluster) { + if (!cluster || !prom) { return null; } diff --git a/ui/app/[locale]/kafka/[kafkaId]/nodes/DistributionChart.tsx b/ui/app/[locale]/kafka/[kafkaId]/nodes/DistributionChart.tsx index 0eb3a9fe4..14774e380 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/nodes/DistributionChart.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/nodes/DistributionChart.tsx @@ -25,24 +25,24 @@ import { useState } from "react"; export function DistributionChart({ data, }: { - data: Record; + data: Record; }) { const t = useTranslations(); const [containerRef, width] = useChartWidth(); const [filter, setFilter] = useState<"all" | "leaders" | "followers">("all"); const allCount = Object.values(data).reduce( - (acc, v) => v.followers + v.leaders + acc, + (acc, v) => (v.followers ?? 0) + (v.leaders ?? 0) + acc, 0, ); const leadersCount = Object.values(data).reduce( - (acc, v) => v.leaders + acc, + (acc, v) => (v.leaders ?? 0) + acc, 0, ); const followersCount = Object.values(data).reduce( - (acc, v) => v.followers + acc, + (acc, v) => (v.followers ?? 0) + acc, 0, ); - return ( + return allCount > 0 ? ( @@ -172,7 +172,7 @@ export function DistributionChart({ name: `Broker ${node}`, x: "x", y: { - all: data.leaders + data.followers, + all: (data.leaders ?? 0) + (data.followers ?? 0), leaders: data.leaders, followers: data.followers, }[filter || "all"], @@ -185,5 +185,5 @@ export function DistributionChart({ - ); + ) : null; } diff --git a/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx b/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx index 9c889693b..281f7c147 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx @@ -1,5 +1,6 @@ "use client"; +import { Number } from "@/components/Format/Number"; import { ResponsiveTable } from "@/components/Table"; import { ChartDonutThreshold, @@ -29,12 +30,12 @@ export type Node = { id: number; isLeader: boolean; status: string; - followers: number; - leaders: number; + followers?: number; + leaders?: number; rack?: string; hostname?: string; - diskCapacity: number; - diskUsage: number; + diskCapacity?: number; + diskUsage?: number; }; export function NodesTable({ nodes }: { nodes: Node[] }) { @@ -109,7 +110,13 @@ export function NodesTable({ nodes }: { nodes: Node[] }) { case "replicas": return ( - {row.followers + row.leaders} + ); case "rack": @@ -147,49 +154,55 @@ export function NodesTable({ nodes }: { nodes: Node[] }) {
- (datum.x ? datum.x : null)} - padding={{ - bottom: 0, - left: 10, - right: 150, - top: 0, - }} - width={350} - > - - datum.x - ? `${datum.x}: ${format.number(datum.y / 100, { + {row.diskUsage !== undefined && + row.diskCapacity !== undefined && ( + (datum.x ? datum.x : null)} + padding={{ + bottom: 0, + left: 10, + right: 150, + top: 0, + }} + width={350} + > + + datum.x + ? `${datum.x}: ${format.number(datum.y / 100, { + style: "percent", + })}` + : null + } + legendData={[ + { name: `Capacity: 80%` }, + { name: "Warning at 60%" }, + { name: "Danger at 90%" }, + ]} + legendOrientation="vertical" + title={`${format.number( + row.diskUsage / row.diskCapacity, + { style: "percent", - })}` - : null - } - legendData={[ - { name: `Capacity: 80%` }, - { name: "Warning at 60%" }, - { name: "Danger at 90%" }, - ]} - legendOrientation="vertical" - title={`${format.number(row.diskUsage / row.diskCapacity, { - style: "percent", - })}`} - subTitle={`of ${formatBytes(row.diskCapacity)}`} - thresholds={[{ value: 60 }, { value: 90 }]} - /> - + }, + )}`} + subTitle={`of ${formatBytes(row.diskCapacity)}`} + thresholds={[{ value: 60 }, { value: 90 }]} + /> + + )}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx index 63c6fc913..4484b8d66 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx @@ -5,30 +5,53 @@ import { Node, NodesTable, } from "@/app/[locale]/kafka/[kafkaId]/nodes/NodesTable"; -import { PageSection } from "@/libs/patternfly/react-core"; +import { Alert, PageSection } from "@/libs/patternfly/react-core"; import { redirect } from "@/navigation"; +import { getTranslations } from "next-intl/server"; +import { Suspense } from "react"; -function nodeMetric(metrics: Record | undefined, nodeId: number): number { - return metrics ? (metrics[nodeId.toString()] ?? 0) : 0; +function nodeMetric( + metrics: Record | undefined, + nodeId: number, +): number { + return metrics ? metrics[nodeId.toString()] ?? 0 : 0; } -export default async function NodesPage({ params }: { params: KafkaParams }) { +export default function NodesPage({ params }: { params: KafkaParams }) { + return ( + + + + ); +} + +async function ConnectedNodes({ params }: { params: KafkaParams }) { + const t = await getTranslations(); const res = await getKafkaClusterKpis(params.kafkaId); if (!res) { return redirect("/"); } const { cluster, kpis } = res; - if (!cluster) { - redirect("/"); - return null; - } const nodes: Node[] = cluster.attributes.nodes.map((node) => { - const status = nodeMetric(kpis.broker_state, node.id) === 3 ? "Stable" : "Unstable"; - const leaders = nodeMetric(kpis.leader_count?.byNode, node.id); - const followers = nodeMetric(kpis.replica_count?.byNode, node.id) - leaders; - const diskCapacity = nodeMetric(kpis.volume_stats_capacity_bytes?.byNode, node.id); - const diskUsage = nodeMetric(kpis.volume_stats_used_bytes?.byNode, node.id); + const status = kpis + ? nodeMetric(kpis.broker_state, node.id) === 3 + ? "Stable" + : "Unstable" + : "Unknown"; + const leaders = kpis + ? nodeMetric(kpis.leader_count?.byNode, node.id) + : undefined; + const followers = + kpis && leaders + ? nodeMetric(kpis.replica_count?.byNode, node.id) - leaders + : undefined; + const diskCapacity = kpis + ? nodeMetric(kpis.volume_stats_capacity_bytes?.byNode, node.id) + : undefined; + const diskUsage = kpis + ? nodeMetric(kpis.volume_stats_used_bytes?.byNode, node.id) + : undefined; return { id: node.id, status, @@ -50,6 +73,12 @@ export default async function NodesPage({ params }: { params: KafkaParams }) { return ( <> + {!kpis && ( + + + + )} + diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx index f44c0e3eb..67ba06c75 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx @@ -44,10 +44,24 @@ async function ConnectedClusterCard({ data, consumerGroups, }: { - data: Promise<{ cluster: ClusterDetail; kpis: ClusterKpis } | null>; + data: Promise<{ cluster: ClusterDetail; kpis: ClusterKpis | null } | null>; consumerGroups: Promise; }) { const res = await data; + if (!res?.kpis) { + return ( + + ); + } const groupCount = await consumerGroups.then( (grpResp) => grpResp.meta.page.total ?? 0, ); @@ -86,9 +100,12 @@ async function ConnectedClusterCard({ async function ConnectedTopicsPartitionsCard({ data, }: { - data: Promise<{ cluster: ClusterDetail; kpis: ClusterKpis } | null>; + data: Promise<{ cluster: ClusterDetail; kpis: ClusterKpis | null } | null>; }) { const res = await data; + if (!res?.kpis) { + return null; + } const topicsTotal = res?.kpis.total_topics || 0; const topicsUnderreplicated = res?.kpis.underreplicated_topics || 0; return ( diff --git a/ui/app/api/auth/[...nextauth]/keycloak.ts b/ui/app/api/auth/[...nextauth]/keycloak.ts index 281e0ef06..ba6c00184 100644 --- a/ui/app/api/auth/[...nextauth]/keycloak.ts +++ b/ui/app/api/auth/[...nextauth]/keycloak.ts @@ -1,19 +1,25 @@ -import { Session, TokenSet } from "next-auth"; +import { logger } from "@/utils/logger"; +import { TokenSet } from "next-auth"; import { JWT } from "next-auth/jwt"; import KeycloakProvider from "next-auth/providers/keycloak"; -import { logger } from "@/utils/logger"; const log = logger.child({ module: "keycloak" }); -export const keycloak = KeycloakProvider({ - clientId: process.env.KEYCLOAK_CLIENTID, - clientSecret: process.env.KEYCLOAK_CLIENTSECRET, - issuer: process.env.NEXT_PUBLIC_KEYCLOAK_URL, -}); +const { KEYCLOAK_CLIENTID, KEYCLOAK_CLIENTSECRET, NEXT_PUBLIC_KEYCLOAK_URL } = + process.env; + +export const keycloak = + KEYCLOAK_CLIENTSECRET && KEYCLOAK_CLIENTID && NEXT_PUBLIC_KEYCLOAK_URL + ? KeycloakProvider({ + clientId: KEYCLOAK_CLIENTID, + clientSecret: KEYCLOAK_CLIENTSECRET, + issuer: NEXT_PUBLIC_KEYCLOAK_URL, + }) + : undefined; let _tokenEndpoint: string | undefined = undefined; async function getTokenEndpoint() { - if (keycloak.wellKnown) { + if (keycloak && keycloak.wellKnown) { const kc = await fetch(keycloak.wellKnown); const res = await kc.json(); _tokenEndpoint = res.token_endpoint; @@ -24,6 +30,10 @@ async function getTokenEndpoint() { export async function refreshToken(token: JWT): Promise { try { const tokenEndpoint = await getTokenEndpoint(); + if (!keycloak) { + log.error("Invalid Keycloak configuratio"); + throw token; + } if (!tokenEndpoint) { log.error("Invalid Keycloak wellKnow"); throw token; diff --git a/ui/components/ClusterOverview/ClusterCard.tsx b/ui/components/ClusterOverview/ClusterCard.tsx index ff6de5d2a..8632cf5a1 100644 --- a/ui/components/ClusterOverview/ClusterCard.tsx +++ b/ui/components/ClusterOverview/ClusterCard.tsx @@ -33,9 +33,9 @@ import { ErrorsAndWarnings } from "./components/ErrorsAndWarnings"; type ClusterCardProps = { name: string; status: string; - brokersOnline: number; - brokersTotal: number; - consumerGroups: number; + brokersOnline?: number; + brokersTotal?: number; + consumerGroups?: number; kafkaVersion: string; messages: Array<{ variant: "danger" | "warning"; diff --git a/ui/environment.d.ts b/ui/environment.d.ts index 86824cecd..f0cc47869 100644 --- a/ui/environment.d.ts +++ b/ui/environment.d.ts @@ -2,13 +2,12 @@ namespace NodeJS { interface ProcessEnv { NEXTAUTH_URL: string; NEXTAUTH_SECRET: string; - SESSION_SECRET: string; BACKEND_URL: string; - KEYCLOAK_CLIENTID: string; - KEYCLOAK_CLIENTSECRET: string; - NEXT_PUBLIC_KEYCLOAK_URL: string; - CONSOLE_METRICS_PROMETHEUS_URL: string; - LOG_LEVEL: "fatal" | "error" | "warn" | "info" | "debug" | "trace"; - CONSOLE_MODE: "read-only" | "read-write"; + KEYCLOAK_CLIENTID?: string; + KEYCLOAK_CLIENTSECRET?: string; + NEXT_PUBLIC_KEYCLOAK_URL?: string; + CONSOLE_METRICS_PROMETHEUS_URL?: string; + LOG_LEVEL?: "fatal" | "error" | "warn" | "info" | "debug" | "trace"; + CONSOLE_MODE?: "read-only" | "read-write"; } } diff --git a/ui/messages/en.json b/ui/messages/en.json index a00eb924f..de003207d 100644 --- a/ui/messages/en.json +++ b/ui/messages/en.json @@ -23,6 +23,9 @@ "overview": { "title": "Overview" }, + "nodes": { + "kpis_offline": "Prometheus can't be reached, information on the page will be incomplete. Please check your configuration." + }, "node-config-table": { "no_results_title": "No properties found", "no_results_body": "Adjust your selection criteria and try again.", diff --git a/ui/utils/authOptions.ts b/ui/utils/authOptions.ts index 714b930a5..965d80a28 100644 --- a/ui/utils/authOptions.ts +++ b/ui/utils/authOptions.ts @@ -1,31 +1,31 @@ import { keycloak, refreshToken } from "@/app/api/auth/[...nextauth]/keycloak"; -import CredentialsProvider from "next-auth/providers/credentials"; import { logger } from "@/utils/logger"; import NextAuth, { NextAuthOptions, Session } from "next-auth"; import { JWT } from "next-auth/jwt"; +import CredentialsProvider from "next-auth/providers/credentials"; const log = logger.child({ module: "auth" }); const anonymousProvider = CredentialsProvider({ // The name to display on the sign in form (e.g. 'Sign in with...') - name: 'Anonymous Session', + name: "Anonymous Session", - credentials: { }, + credentials: {}, async authorize() { - return { id: '1', name: 'Anonymous', email: 'anonymous@example.com' }; - } + return { id: "1", name: "Anonymous", email: "anonymous@example.com" }; + }, }); let _providers = []; let _callbacks = {}; -if (keycloak.options?.issuer) { +if (keycloak && keycloak.options?.issuer) { log.debug("Using keycloak provider"); _providers.push(keycloak); _callbacks = { - async jwt({ token, account }: { token: JWT, account: any }) { + async jwt({ token, account }: { token: JWT; account: any }) { // Persist the OAuth access_token and or the user id to the token right after signin if (account) { log.trace("account present, saving new token"); @@ -43,7 +43,7 @@ if (keycloak.options?.issuer) { return refreshToken(token); }, - async session({ session, token }: { session: Session, token: JWT }) { + async session({ session, token }: { session: Session; token: JWT }) { // Send properties to the client, like an access_token from a provider. log.trace(token, "Creating session from token"); return { diff --git a/ui/utils/logger.ts b/ui/utils/logger.ts index 5c894694c..6e052ac37 100644 --- a/ui/utils/logger.ts +++ b/ui/utils/logger.ts @@ -4,7 +4,7 @@ export const logger = pino({ browser: { asObject: true, }, - level: process.env.LOG_LEVEL, + level: process.env.LOG_LEVEL ?? "info", base: { env: process.env.NODE_ENV, }, diff --git a/ui/utils/session.ts b/ui/utils/session.ts index d21af4ffc..8eaeab3ea 100644 --- a/ui/utils/session.ts +++ b/ui/utils/session.ts @@ -1,10 +1,10 @@ "use server"; import { authOptions } from "@/utils/authOptions"; +import { logger } from "@/utils/logger"; import { sealData, unsealData } from "iron-session"; import { getServerSession } from "next-auth"; import { cookies } from "next/headers"; -import { logger } from "@/utils/logger"; const log = logger.child({ module: "session" }); @@ -23,7 +23,7 @@ export async function getSession>( } try { const rawSession = await unsealData(encryptedSession, { - password: process.env.SESSION_SECRET, + password: process.env.SESSION_SECRET ?? "strimziconsole", }); return rawSession as T; } catch { @@ -40,7 +40,7 @@ export async function setSession>( throw new Error("Can't set session for unauthenticated users"); } const encryptedSession = await sealData(session, { - password: process.env.SESSION_SECRET, + password: process.env.SESSION_SECRET ?? "strimziconsole", }); cookies().set({