UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

461 lines (416 loc) 14.5 kB
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib'; import { KarpenterDisruptionChart } from './components/Chart/KarpenterDisruptionChart/KarpenterDisruptionChart'; import { NodeClaimCreationChart } from './components/Chart/KarpenterNodeClaimCreationChart/KarpenterNodeClaimCreationChart'; import { KarpenterNodeClaimsProvisionChart } from './components/Chart/KarpenterNodeClaimProvisionChart/KarpenterNodeClaimProvisionChart'; import { KarpenterNodePoolResourceChart } from './components/Chart/KarpenterNodePoolResourceChart/KarpenterNodePoolResourceChart'; import { KarpenterPendingPods } from './components/Chart/KarpenterPendingPods/KarpenterPendingPods'; import { isPrometheusInstalled, KubernetesType } from './request'; export const PLUGIN_NAME = 'prometheus'; /** * ClusterData type represents the configuration data for a cluster. * @property {boolean} autoDetect - Whether to auto-detect Prometheus metrics. * @property {boolean} isMetricsEnabled - Whether metrics are enabled for the cluster. * @property {string} address - The address of the Prometheus service. * @property {string} defaultTimespan - The default timespan for metrics. * @property {string} defaultResolution - The default resolution for metrics. */ type ClusterData = { autoDetect?: boolean; isMetricsEnabled?: boolean; address?: string; subPath?: string; defaultTimespan?: string; defaultResolution?: string; }; /** * Conf type represents the configuration data for the prometheus plugin. * @property {[cluster: string]: ClusterData} - The configuration data for each cluster. */ type Conf = { [cluster: string]: ClusterData; }; /** * getConfigStore returns the config store for the prometheus plugin. * @returns {ConfigStore<Conf>} The config store. */ export function getConfigStore(): ConfigStore<Conf> { return new ConfigStore<Conf>(PLUGIN_NAME); } /** * getClusterConfig returns the configuration for a specific cluster. * @param {string} cluster - The name of the cluster. * @returns {ClusterData | null} The configuration for the cluster, or null if not found. */ function getClusterConfig(cluster: string): ClusterData | null { const configStore = getConfigStore(); const conf = configStore.get(); if (!cluster || !conf) { return null; } return conf[cluster] || null; } /** * enableMetrics enables metrics for a specific cluster. * @param {string} cluster - The name of the cluster. */ export function enableMetrics(cluster: string) { const store = getConfigStore(); const config = store.get() || {}; const clusterConfig = config[cluster] || { autoDetect: true }; store.update({ ...config, [cluster]: { ...clusterConfig, isMetricsEnabled: true, }, }); } /** * disableMetrics disables metrics for a specific cluster. * @param {string} cluster - The name of the cluster. */ export function disableMetrics(cluster: string) { const store = getConfigStore(); const config = store.get() || {}; const clusterConfig = config[cluster] || { autoDetect: true }; store.update({ ...config, [cluster]: { ...clusterConfig, isMetricsEnabled: false, }, }); } /** * getPrometheusPrefix returns the prefix for the Prometheus metrics. * @param {string} cluster - The name of the cluster. * @returns {Promise<string | null>} The prefix for the Prometheus metrics, or null if not found. */ export async function getPrometheusPrefix(cluster: string): Promise<string | null> { // check if cluster has autoDetect enabled // if so return the prometheus pod address const clusterData = getClusterConfig(cluster); if (clusterData?.autoDetect) { const prometheusEndpoint = await isPrometheusInstalled(); if (prometheusEndpoint.type === KubernetesType.none) { return null; } const prometheusPortStr = prometheusEndpoint.port ? `:${prometheusEndpoint.port}` : ''; return `${prometheusEndpoint.namespace}/${prometheusEndpoint.type}/${prometheusEndpoint.name}${prometheusPortStr}`; } if (clusterData?.address) { const [namespace, service] = clusterData?.address.split('/'); return `${namespace}/services/${service}`; } return null; } /** * getPrometheusSubPath returns the subpath for the Prometheus metrics. * @param {string} cluster - The name of the cluster. * @returns {string | null} The subpath for the Prometheus metrics, or null if not found. */ export function getPrometheusSubPath(cluster: string): string | null { const clusterData = getClusterConfig(cluster); return !clusterData?.subPath || clusterData.subPath === '' ? null : clusterData.subPath; } /** * getPrometheusInterval returns the default timespan for the Prometheus metrics. * @param {string} cluster - The name of the cluster. * @returns {string} The default timespan for the Prometheus metrics. */ export function getPrometheusInterval(cluster: string): string { const clusterData = getClusterConfig(cluster); return clusterData?.defaultTimespan ?? '24h'; } /** * getPrometheusResolution returns the default resolution for the Prometheus metrics. * @param {string} cluster - The name of the cluster. * @returns {string} The default resolution for the Prometheus metrics. */ export function getPrometheusResolution(cluster: string): string { const clusterData = getClusterConfig(cluster); return clusterData?.defaultResolution ?? 'medium'; } type ResourceIdentity = { kind?: string; apiVersion?: string; jsonData?: { kind?: string; apiVersion?: string; }; }; function getResourceKind(resource?: ResourceIdentity): string | undefined { return resource?.jsonData?.kind ?? resource?.kind; } function getResourceApiVersion(resource?: ResourceIdentity): string | undefined { return resource?.jsonData?.apiVersion ?? resource?.apiVersion; } const resourceApiVersionRules: Record<string, string> = { Job: 'batch/v1', }; export function supportsPrometheusMetrics(resource?: ResourceIdentity): boolean { const kind = getResourceKind(resource); if (!kind || !ChartEnabledKinds.includes(kind)) { return false; } const requiredApiVersion = resourceApiVersionRules[kind]; if (requiredApiVersion) { return getResourceApiVersion(resource) === requiredApiVersion; } return true; } const ChartEnabledKinds = [ 'Pod', 'Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'Job', 'CronJob', 'PersistentVolumeClaim', 'ScaledObject', 'ScaledJob', 'NodePool', 'NodeClaim', ]; /** * Creates a formatter function that formats timestamps based on the given interval. * The formatter maintains state to avoid rendering duplicate timestamps. * * @param {string} interval - The time interval to format for (e.g. '10m', '1h', '24h', 'week') * @returns {function(number): string} A formatter function that takes a timestamp and returns a formatted string * * @example * const formatter = createTickTimestampFormatter('24h'); * formatter(1698321684); // Returns "10:30" * formatter(1698321744); // Returns "" (if same formatted time as previous) * * Formats: * - For intervals <= 48h: "HH:MM" (e.g. "14:30") * - For today/yesterday: "HH:00" (e.g. "14:00") * - For week/lastweek/7d/14d: "M/D" (e.g. "10/26") */ export function createTickTimestampFormatter(interval: string) { let prevRenderedTimestamp = null; return function (timestamp) { const date = new Date(timestamp * 1000); let format: string; // Determine format based on interval switch (interval) { case '10m': case '30m': case '1h': case '3h': case '6h': case '12h': case '24h': case '48h': format = `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; break; case 'today': case 'yesterday': format = `${date.getHours()}:00`; break; case 'week': case 'lastweek': case '7d': case '14d': format = `${date.getMonth() + 1}/${date.getDate()}`; break; default: format = `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; } // Check if the current timestamp is different from the previously rendered one const shouldRenderDate = format !== prevRenderedTimestamp; // Update the previous timestamp prevRenderedTimestamp = format; return shouldRenderDate ? format : ''; }; } export type PrometheusValue = [number, string]; export type PrometheusResult = { metric?: Record<string, string>; values: PrometheusValue[]; }; export type PrometheusResponse = { data?: { result?: PrometheusResult[]; }; }; export type ChartDataPoint = { timestamp: number; y: number }; /** * Extracts chart data from a Prometheus time series result. * @param {PrometheusValue[]} values - The values array from a Prometheus result item. * @returns {ChartDataPoint[]} - Parsed array of { timestamp, y } objects. */ function extractChartData(values: PrometheusValue[] = []): ChartDataPoint[] { return values.map(([timestamp, value]) => ({ timestamp, y: Number(value), })); } /** * Processes the first time series in a Prometheus response into chart data. * @param {PrometheusResponse} response - The raw Prometheus response. * @returns {ChartDataPoint[]} - Parsed chart data for the first series. */ export function dataProcessor(response: PrometheusResponse): ChartDataPoint[] { const values = response?.data?.result?.[0]?.values; return extractChartData(values); } /** * Creates a data processor for a selected time series index in the Prometheus response. * @param {number} selectedIndex - Index of the desired series to process. * @returns {(response: PrometheusResponse) => ChartDataPoint[]} - Data processor function for that index. */ export function createDataProcessor( selectedIndex: number ): (response: PrometheusResponse) => ChartDataPoint[] { return function (response: PrometheusResponse): ChartDataPoint[] { const values = response?.data?.result?.[selectedIndex]?.values; return extractChartData(values); }; } /** * Formats a number of bytes into a human-readable string with appropriate units. * @param {number} bytes - The number of bytes to format * @returns {string} A formatted string with the bytes value and appropriate unit (B, KB, MB, GB, or TB) * @example * formatBytes(1024) // Returns "1.00KB" * formatBytes(1234567) // Returns "1.18MB" */ export function formatBytes(bytes: number) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = bytes === 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, i)).toFixed(2) + units[i]; } // Number of seconds in a day (24h) const DAY_IN_SECONDS = 86400; // Time intervals mapped to seconds const TIME_INTERVALS: Record<string, number> = { '1m': 60, '10m': 600, '30m': 1800, '1h': 3600, '3h': 10800, '6h': 21600, '12h': 43200, '24h': DAY_IN_SECONDS, '48h': 2 * DAY_IN_SECONDS, '7d': 7 * DAY_IN_SECONDS, '14d': 14 * DAY_IN_SECONDS, }; // Fixed step sizes in seconds const FIXED_STEPS: Record<string, number> = { '10s': 10, '30s': 30, '1m': 60, '5m': 300, '15m': 900, '1h': 3600, }; // Resolution factors for dynamic calculation const RESOLUTION_FACTORS: Record<string, number> = { low: 100, medium: 250, high: 750, }; /** * Calculates the time range and step size based on the given interval and resolution. * @param {string} interval - The time interval (e.g., '10m', '1h', '24h', 'week'). * @param {string} resolution - The resolution level or fixed interval (e.g., 'low', 'medium', 'high', '10s', '1m', '1h'). * @returns {Object} An object containing the 'from' timestamp, 'to' timestamp, and 'step' in seconds. */ export function getTimeRangeAndStepSize( interval: string, resolution: string ): { from: number; to: number; step: number } { const now = Math.floor(Date.now() / 1000); // Calculate time range let from: number; let to: number = now; switch (interval) { case 'today': from = now - (now % DAY_IN_SECONDS); break; case 'yesterday': from = now - (now % DAY_IN_SECONDS) - DAY_IN_SECONDS; to = now - (now % DAY_IN_SECONDS); break; case 'week': from = now - 7 * DAY_IN_SECONDS; break; case 'lastweek': from = now - 14 * DAY_IN_SECONDS; to = now - 7 * DAY_IN_SECONDS; break; default: { const duration = TIME_INTERVALS[interval] || 600; // Default to 10 minutes interval from = now - duration; break; } } // Calculate step size let step: number; if (resolution in FIXED_STEPS) { step = FIXED_STEPS[resolution]; } else { const rangeMs = (to - from) * 1000; const factor = RESOLUTION_FACTORS[resolution] || RESOLUTION_FACTORS.medium; // Default to medium resolution step = Math.max(Math.floor(rangeMs / factor / 1000), 1); } return { from, to, step }; } export const getNodePoolChartConfigs = (name: string) => [ { key: 'usage', label: 'Resource Usage', icon: 'mdi:chart-bar', queries: { usageQuery: `karpenter_nodepools_usage{nodepool='${name}'}`, limitQuery: `karpenter_nodepools_limit{nodepool='${name}'}`, }, component: KarpenterNodePoolResourceChart, }, { key: 'nodes', label: 'Allowed Disruptions', icon: 'mdi:chip', queries: { activeNodesQuery: `karpenter_nodepools_allowed_disruptions{nodepool='${name}'}`, }, component: KarpenterDisruptionChart, }, { key: 'pending-pods', label: 'Pending Pods', icon: 'mdi:clock-outline', queries: { pendingPodsQuery: `sum(karpenter_pods_state{phase='Pending'}) by (reason)`, }, component: KarpenterPendingPods, }, ]; export const getNodeClaimChartConfigs = (name: string, nodepool?: string) => [ { key: 'creation-rate', label: 'Creation Rate', icon: 'mdi:chart-line-variant', queries: { nodeClaimCreationQuery: `sum(rate(karpenter_nodeclaims_created_total{nodepool="${ nodepool || 'all' }"}[5m]))`, }, component: NodeClaimCreationChart, }, { key: 'provisioning-duration', label: 'Provisioning Duration', icon: 'mdi:clock-time-four', queries: { provisioningDurationQuery: `avg(rate(operator_nodeclaim_status_condition_transition_seconds_sum[5m])) / avg(rate(operator_nodeclaim_status_condition_transition_seconds_count[5m]))`, }, component: KarpenterNodeClaimsProvisionChart, }, ];