mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add Kubernetes resource breakdown to MetricMonitorResponse
- Introduced KubernetesAffectedResource and KubernetesResourceBreakdown interfaces to enhance metric monitoring capabilities. - Updated MetricMonitorResponse to include kubernetesResourceBreakdown. - Enhanced AreaChart, BarChart, and LineChart components with onValueChange prop for better interactivity. - Added new Dashboard components: Gauge and Table, with respective configuration arguments. - Implemented DashboardVariableSelector for dynamic variable selection in dashboards. - Added warning and critical thresholds to DashboardValueComponent and DashboardChartComponent for improved data visualization. - Updated DashboardChartComponentUtil to support additional queries and chart types. - Enhanced error handling and loading states in Dashboard components.
This commit is contained in:
@@ -25,6 +25,7 @@ export interface ComponentProps {
|
||||
telemetryAttributes: string[];
|
||||
};
|
||||
dashboardStartAndEndDate: RangeStartAndEndDateTime;
|
||||
refreshTick?: number | undefined;
|
||||
}
|
||||
|
||||
const DashboardCanvas: FunctionComponent<ComponentProps> = (
|
||||
@@ -221,6 +222,7 @@ const DashboardCanvas: FunctionComponent<ComponentProps> = (
|
||||
updateComponent(updatedComponent);
|
||||
}}
|
||||
isSelected={isSelected}
|
||||
refreshTick={props.refreshTick}
|
||||
onClick={() => {
|
||||
// component is selected
|
||||
props.onComponentSelected(componentId);
|
||||
|
||||
@@ -2,10 +2,14 @@ import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
import DashboardTextComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardTextComponent";
|
||||
import DashboardChartComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardChartComponent";
|
||||
import DashboardValueComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardValueComponent";
|
||||
import DashboardTableComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardTableComponent";
|
||||
import DashboardGaugeComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardGaugeComponent";
|
||||
import DashboardBaseComponent from "Common/Types/Dashboard/DashboardComponents/DashboardBaseComponent";
|
||||
import DashboardChartComponent from "./DashboardChartComponent";
|
||||
import DashboardValueComponent from "./DashboardValueComponent";
|
||||
import DashboardTextComponent from "./DashboardTextComponent";
|
||||
import DashboardTableComponent from "./DashboardTableComponent";
|
||||
import DashboardGaugeComponent from "./DashboardGaugeComponent";
|
||||
import DefaultDashboardSize, {
|
||||
GetDashboardComponentHeightInDashboardUnits,
|
||||
GetDashboardComponentWidthInDashboardUnits,
|
||||
@@ -37,6 +41,7 @@ export interface DashboardBaseComponentProps {
|
||||
dashboardViewConfig: DashboardViewConfig;
|
||||
dashboardStartAndEndDate: RangeStartAndEndDateTime;
|
||||
metricTypes: Array<MetricType>;
|
||||
refreshTick?: number | undefined;
|
||||
}
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
@@ -404,6 +409,22 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
|
||||
component={component as DashboardValueComponentType}
|
||||
/>
|
||||
)}
|
||||
{component.componentType === DashboardComponentType.Table && (
|
||||
<DashboardTableComponent
|
||||
{...props}
|
||||
isEditMode={props.isEditMode}
|
||||
isSelected={props.isSelected}
|
||||
component={component as DashboardTableComponentType}
|
||||
/>
|
||||
)}
|
||||
{component.componentType === DashboardComponentType.Gauge && (
|
||||
<DashboardGaugeComponent
|
||||
{...props}
|
||||
isEditMode={props.isEditMode}
|
||||
isSelected={props.isSelected}
|
||||
component={component as DashboardGaugeComponentType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getResizeWidthElement()}
|
||||
{getResizeHeightElement()}
|
||||
|
||||
@@ -31,10 +31,24 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
// Resolve query configs - support both single and multi-query
|
||||
const resolveQueryConfigs: () => Array<MetricQueryConfigData> = () => {
|
||||
if (
|
||||
props.component.arguments.metricQueryConfigs &&
|
||||
props.component.arguments.metricQueryConfigs.length > 0
|
||||
) {
|
||||
return props.component.arguments.metricQueryConfigs;
|
||||
}
|
||||
if (props.component.arguments.metricQueryConfig) {
|
||||
return [props.component.arguments.metricQueryConfig];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const queryConfigs: Array<MetricQueryConfigData> = resolveQueryConfigs();
|
||||
|
||||
const metricViewData: MetricViewData = {
|
||||
queryConfigs: props.component.arguments.metricQueryConfig
|
||||
? [props.component.arguments.metricQueryConfig]
|
||||
: [],
|
||||
queryConfigs: queryConfigs,
|
||||
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
||||
props.dashboardStartAndEndDate,
|
||||
),
|
||||
@@ -97,24 +111,36 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
}, [props.dashboardStartAndEndDate, props.metricTypes]);
|
||||
}, [props.dashboardStartAndEndDate, props.metricTypes, props.refreshTick]);
|
||||
|
||||
const [metricQueryConfig, setMetricQueryConfig] = React.useState<
|
||||
MetricQueryConfigData | undefined
|
||||
>(props.component.arguments.metricQueryConfig);
|
||||
const [prevQueryConfigs, setPrevQueryConfigs] = React.useState<
|
||||
Array<MetricQueryConfigData> | MetricQueryConfigData | undefined
|
||||
>(
|
||||
props.component.arguments.metricQueryConfigs ||
|
||||
props.component.arguments.metricQueryConfig,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// set metricQueryConfig to the new value only if it is different from the previous value
|
||||
const currentConfigs:
|
||||
| Array<MetricQueryConfigData>
|
||||
| MetricQueryConfigData
|
||||
| undefined =
|
||||
props.component.arguments.metricQueryConfigs ||
|
||||
props.component.arguments.metricQueryConfig;
|
||||
|
||||
if (
|
||||
JSONFunctions.isJSONObjectDifferent(
|
||||
metricQueryConfig || {},
|
||||
props.component.arguments.metricQueryConfig || {},
|
||||
prevQueryConfigs || {},
|
||||
currentConfigs || {},
|
||||
)
|
||||
) {
|
||||
setMetricQueryConfig(props.component.arguments.metricQueryConfig);
|
||||
setPrevQueryConfigs(currentConfigs);
|
||||
fetchAggregatedResults();
|
||||
}
|
||||
}, [props.component.arguments.metricQueryConfig]);
|
||||
}, [
|
||||
props.component.arguments.metricQueryConfig,
|
||||
props.component.arguments.metricQueryConfigs,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
@@ -142,35 +168,57 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
|
||||
heightOfChart = undefined;
|
||||
}
|
||||
|
||||
// add title and description.
|
||||
|
||||
type GetMetricChartType = () => MetricChartType;
|
||||
|
||||
// Convert dashboard chart type to metric chart type
|
||||
const getMetricChartType: GetMetricChartType = (): MetricChartType => {
|
||||
if (props.component.arguments.chartType === DashboardChartType.Bar) {
|
||||
return MetricChartType.BAR;
|
||||
}
|
||||
if (
|
||||
props.component.arguments.chartType === DashboardChartType.Area ||
|
||||
props.component.arguments.chartType === DashboardChartType.StackedArea
|
||||
) {
|
||||
return MetricChartType.AREA;
|
||||
}
|
||||
return MetricChartType.LINE;
|
||||
};
|
||||
|
||||
const chartMetricViewData: MetricViewData = {
|
||||
queryConfigs: props.component.arguments.metricQueryConfig
|
||||
? [
|
||||
{
|
||||
...props.component.arguments.metricQueryConfig!,
|
||||
queryConfigs: queryConfigs.map(
|
||||
(config: MetricQueryConfigData, index: number) => {
|
||||
// For the first query, apply the chart-level title/description/legend
|
||||
if (index === 0) {
|
||||
return {
|
||||
...config,
|
||||
metricAliasData: {
|
||||
title: props.component.arguments.chartTitle || undefined,
|
||||
title:
|
||||
config.metricAliasData?.title ||
|
||||
props.component.arguments.chartTitle ||
|
||||
undefined,
|
||||
description:
|
||||
props.component.arguments.chartDescription || undefined,
|
||||
metricVariable: undefined,
|
||||
legend: props.component.arguments.legendText || undefined,
|
||||
legendUnit: props.component.arguments.legendUnit || undefined,
|
||||
config.metricAliasData?.description ||
|
||||
props.component.arguments.chartDescription ||
|
||||
undefined,
|
||||
metricVariable:
|
||||
config.metricAliasData?.metricVariable || undefined,
|
||||
legend:
|
||||
config.metricAliasData?.legend ||
|
||||
props.component.arguments.legendText ||
|
||||
undefined,
|
||||
legendUnit:
|
||||
config.metricAliasData?.legendUnit ||
|
||||
props.component.arguments.legendUnit ||
|
||||
undefined,
|
||||
},
|
||||
chartType: getMetricChartType(),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
chartType: config.chartType || getMetricChartType(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
chartType: config.chartType || getMetricChartType(),
|
||||
};
|
||||
},
|
||||
),
|
||||
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
||||
props.dashboardStartAndEndDate,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
import DashboardGaugeComponent from "Common/Types/Dashboard/DashboardComponents/DashboardGaugeComponent";
|
||||
import { DashboardBaseComponentProps } from "./DashboardBaseComponent";
|
||||
import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricUtil from "../../Metrics/Utils/Metrics";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
component: DashboardGaugeComponent;
|
||||
}
|
||||
|
||||
const DashboardGaugeComponentElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [metricResults, setMetricResults] = React.useState<
|
||||
Array<AggregatedResult>
|
||||
>([]);
|
||||
const [aggregationType, setAggregationType] =
|
||||
React.useState<AggregationType>(AggregationType.Avg);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const metricViewData: MetricViewData = {
|
||||
queryConfigs: props.component.arguments.metricQueryConfig
|
||||
? [props.component.arguments.metricQueryConfig]
|
||||
: [],
|
||||
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
||||
props.dashboardStartAndEndDate,
|
||||
),
|
||||
formulaConfigs: [],
|
||||
};
|
||||
|
||||
const fetchAggregatedResults: PromiseVoidFunction =
|
||||
async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (
|
||||
!metricViewData.startAndEndDate?.startValue ||
|
||||
!metricViewData.startAndEndDate?.endValue
|
||||
) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!metricViewData.queryConfigs ||
|
||||
metricViewData.queryConfigs.length === 0 ||
|
||||
!metricViewData.queryConfigs[0] ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData.filterData ||
|
||||
Object.keys(metricViewData.queryConfigs[0].metricQueryData.filterData)
|
||||
.length === 0
|
||||
) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!metricViewData.queryConfigs[0] ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData.filterData ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData.filterData
|
||||
?.aggegationType
|
||||
) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setAggregationType(
|
||||
(metricViewData.queryConfigs[0].metricQueryData.filterData
|
||||
?.aggegationType as AggregationType) || AggregationType.Avg,
|
||||
);
|
||||
|
||||
try {
|
||||
const results: Array<AggregatedResult> = await MetricUtil.fetchResults({
|
||||
metricViewData: metricViewData,
|
||||
});
|
||||
|
||||
setMetricResults(results);
|
||||
setError("");
|
||||
} catch (err: unknown) {
|
||||
setError(API.getFriendlyErrorMessage(err as Error));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const [metricQueryConfig, setMetricQueryConfig] = React.useState<
|
||||
MetricQueryConfigData | undefined
|
||||
>(props.component.arguments.metricQueryConfig);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
}, [props.dashboardStartAndEndDate, props.metricTypes, props.refreshTick]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
JSONFunctions.isJSONObjectDifferent(
|
||||
metricQueryConfig || {},
|
||||
props.component.arguments.metricQueryConfig || {},
|
||||
)
|
||||
) {
|
||||
setMetricQueryConfig(props.component.arguments.metricQueryConfig);
|
||||
fetchAggregatedResults();
|
||||
}
|
||||
}, [props.component.arguments.metricQueryConfig]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ComponentLoader />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
// Calculate aggregated value
|
||||
let aggregatedValue: number = 0;
|
||||
let avgCount: number = 0;
|
||||
|
||||
for (const result of metricResults) {
|
||||
for (const item of result.data) {
|
||||
const value: number = item.value;
|
||||
|
||||
if (aggregationType === AggregationType.Avg) {
|
||||
aggregatedValue += value;
|
||||
avgCount += 1;
|
||||
} else if (aggregationType === AggregationType.Sum) {
|
||||
aggregatedValue += value;
|
||||
} else if (aggregationType === AggregationType.Min) {
|
||||
aggregatedValue = Math.min(aggregatedValue, value);
|
||||
} else if (aggregationType === AggregationType.Max) {
|
||||
aggregatedValue = Math.max(aggregatedValue, value);
|
||||
} else if (aggregationType === AggregationType.Count) {
|
||||
aggregatedValue += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aggregationType === AggregationType.Avg && avgCount > 0) {
|
||||
aggregatedValue = aggregatedValue / avgCount;
|
||||
}
|
||||
|
||||
aggregatedValue = Math.round(aggregatedValue * 100) / 100;
|
||||
|
||||
const minValue: number = props.component.arguments.minValue ?? 0;
|
||||
const maxValue: number = props.component.arguments.maxValue ?? 100;
|
||||
const warningThreshold: number | undefined =
|
||||
props.component.arguments.warningThreshold;
|
||||
const criticalThreshold: number | undefined =
|
||||
props.component.arguments.criticalThreshold;
|
||||
|
||||
// Calculate percentage for the gauge arc
|
||||
const range: number = maxValue - minValue;
|
||||
const percentage: number =
|
||||
range > 0
|
||||
? Math.min(Math.max((aggregatedValue - minValue) / range, 0), 1)
|
||||
: 0;
|
||||
|
||||
// Determine color based on thresholds
|
||||
let gaugeColor: string = "#10b981"; // green
|
||||
if (
|
||||
criticalThreshold !== undefined &&
|
||||
aggregatedValue >= criticalThreshold
|
||||
) {
|
||||
gaugeColor = "#ef4444"; // red
|
||||
} else if (
|
||||
warningThreshold !== undefined &&
|
||||
aggregatedValue >= warningThreshold
|
||||
) {
|
||||
gaugeColor = "#f59e0b"; // yellow
|
||||
}
|
||||
|
||||
// SVG gauge rendering
|
||||
const size: number = Math.min(
|
||||
props.dashboardComponentWidthInPx - 20,
|
||||
props.dashboardComponentHeightInPx - 50,
|
||||
);
|
||||
const gaugeSize: number = Math.max(size, 60);
|
||||
const strokeWidth: number = Math.max(gaugeSize * 0.12, 8);
|
||||
const radius: number = (gaugeSize - strokeWidth) / 2;
|
||||
const centerX: number = gaugeSize / 2;
|
||||
const centerY: number = gaugeSize / 2;
|
||||
|
||||
// Semi-circle arc (180 degrees, from left to right)
|
||||
const startAngle: number = Math.PI;
|
||||
const endAngle: number = 0;
|
||||
const sweepAngle: number = startAngle - endAngle;
|
||||
const currentAngle: number = startAngle - sweepAngle * percentage;
|
||||
|
||||
const arcStartX: number = centerX + radius * Math.cos(startAngle);
|
||||
const arcStartY: number = centerY - radius * Math.sin(startAngle);
|
||||
const arcEndX: number = centerX + radius * Math.cos(endAngle);
|
||||
const arcEndY: number = centerY - radius * Math.sin(endAngle);
|
||||
const arcCurrentX: number = centerX + radius * Math.cos(currentAngle);
|
||||
const arcCurrentY: number = centerY - radius * Math.sin(currentAngle);
|
||||
|
||||
const backgroundPath: string = `M ${arcStartX} ${arcStartY} A ${radius} ${radius} 0 0 1 ${arcEndX} ${arcEndY}`;
|
||||
const valuePath: string = `M ${arcStartX} ${arcStartY} A ${radius} ${radius} 0 ${percentage > 0.5 ? 1 : 0} 1 ${arcCurrentX} ${arcCurrentY}`;
|
||||
|
||||
const titleHeightInPx: number = Math.max(
|
||||
props.dashboardComponentHeightInPx * 0.1,
|
||||
12,
|
||||
);
|
||||
const valueHeightInPx: number = Math.max(gaugeSize * 0.2, 14);
|
||||
|
||||
return (
|
||||
<div className="w-full text-center h-full flex flex-col items-center justify-center">
|
||||
{props.component.arguments.gaugeTitle && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: titleHeightInPx > 0 ? `${titleHeightInPx}px` : "",
|
||||
}}
|
||||
className="text-center font-semibold text-gray-700 mb-1 truncate"
|
||||
>
|
||||
{props.component.arguments.gaugeTitle}
|
||||
</div>
|
||||
)}
|
||||
<svg
|
||||
width={gaugeSize}
|
||||
height={gaugeSize / 2 + strokeWidth}
|
||||
viewBox={`0 0 ${gaugeSize} ${gaugeSize / 2 + strokeWidth}`}
|
||||
>
|
||||
<path
|
||||
d={backgroundPath}
|
||||
fill="none"
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{percentage > 0 && (
|
||||
<path
|
||||
d={valuePath}
|
||||
fill="none"
|
||||
stroke={gaugeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<div
|
||||
className="font-bold text-gray-800"
|
||||
style={{
|
||||
fontSize: valueHeightInPx > 0 ? `${valueHeightInPx}px` : "",
|
||||
marginTop: `-${gaugeSize * 0.15}px`,
|
||||
}}
|
||||
>
|
||||
{aggregatedValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardGaugeComponentElement;
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { FunctionComponent, ReactElement, useEffect } from "react";
|
||||
import DashboardTableComponent from "Common/Types/Dashboard/DashboardComponents/DashboardTableComponent";
|
||||
import { DashboardBaseComponentProps } from "./DashboardBaseComponent";
|
||||
import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult";
|
||||
import AggregatedModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricUtil from "../../Metrics/Utils/Metrics";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
|
||||
import JSONFunctions from "Common/Types/JSONFunctions";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import Icon from "Common/UI/Components/Icon/Icon";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
component: DashboardTableComponent;
|
||||
}
|
||||
|
||||
const DashboardTableComponentElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
const [metricResults, setMetricResults] = React.useState<
|
||||
Array<AggregatedResult>
|
||||
>([]);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
|
||||
const metricViewData: MetricViewData = {
|
||||
queryConfigs: props.component.arguments.metricQueryConfig
|
||||
? [props.component.arguments.metricQueryConfig]
|
||||
: [],
|
||||
startAndEndDate: RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
||||
props.dashboardStartAndEndDate,
|
||||
),
|
||||
formulaConfigs: [],
|
||||
};
|
||||
|
||||
const fetchAggregatedResults: PromiseVoidFunction =
|
||||
async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (
|
||||
!metricViewData.startAndEndDate?.startValue ||
|
||||
!metricViewData.startAndEndDate?.endValue
|
||||
) {
|
||||
setIsLoading(false);
|
||||
setError("Please select a valid start and end date.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!metricViewData.queryConfigs ||
|
||||
metricViewData.queryConfigs.length === 0 ||
|
||||
!metricViewData.queryConfigs[0] ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData.filterData ||
|
||||
Object.keys(metricViewData.queryConfigs[0].metricQueryData.filterData)
|
||||
.length === 0
|
||||
) {
|
||||
setIsLoading(false);
|
||||
setError("Please select a metric. Click here to add a metric.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!metricViewData.queryConfigs[0] ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData.filterData ||
|
||||
!metricViewData.queryConfigs[0].metricQueryData.filterData
|
||||
?.aggegationType
|
||||
) {
|
||||
setIsLoading(false);
|
||||
setError(
|
||||
"Please select an aggregation. Click here to add an aggregation.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const results: Array<AggregatedResult> = await MetricUtil.fetchResults({
|
||||
metricViewData: metricViewData,
|
||||
});
|
||||
|
||||
setMetricResults(results);
|
||||
setError("");
|
||||
} catch (err: unknown) {
|
||||
setError(API.getFriendlyErrorMessage(err as Error));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
}, [props.dashboardStartAndEndDate, props.metricTypes, props.refreshTick]);
|
||||
|
||||
const [metricQueryConfig, setMetricQueryConfig] = React.useState<
|
||||
MetricQueryConfigData | undefined
|
||||
>(props.component.arguments.metricQueryConfig);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
JSONFunctions.isJSONObjectDifferent(
|
||||
metricQueryConfig || {},
|
||||
props.component.arguments.metricQueryConfig || {},
|
||||
)
|
||||
) {
|
||||
setMetricQueryConfig(props.component.arguments.metricQueryConfig);
|
||||
fetchAggregatedResults();
|
||||
}
|
||||
}, [props.component.arguments.metricQueryConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ComponentLoader />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="m-auto flex flex-col justify-center w-full h-full">
|
||||
<div className="h-7 w-7 text-gray-400 w-full text-center mx-auto">
|
||||
<Icon icon={IconProp.TableCells} />
|
||||
</div>
|
||||
<ErrorMessage message={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const maxRows: number = props.component.arguments.maxRows || 20;
|
||||
|
||||
const allData: Array<AggregatedModel> = [];
|
||||
for (const result of metricResults) {
|
||||
for (const item of result.data) {
|
||||
allData.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const displayData: Array<AggregatedModel> = allData.slice(0, maxRows);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
{props.component.arguments.tableTitle && (
|
||||
<div className="text-sm font-semibold text-gray-700 mb-2 px-1">
|
||||
{props.component.arguments.tableTitle}
|
||||
</div>
|
||||
)}
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-gray-500 uppercase bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-3 py-2">Timestamp</th>
|
||||
<th className="px-3 py-2">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayData.map((item: AggregatedModel, index: number) => {
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b border-gray-100 hover:bg-gray-50"
|
||||
>
|
||||
<td className="px-3 py-1.5 text-gray-600">
|
||||
{OneUptimeDate.getDateAsLocalFormattedString(
|
||||
OneUptimeDate.fromString(item.timestamp),
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-1.5 font-medium">
|
||||
{Math.round(item.value * 100) / 100}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{displayData.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={2} className="px-3 py-4 text-center text-gray-400">
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardTableComponentElement;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import DashboardTextComponent from "Common/Types/Dashboard/DashboardComponents/DashboardTextComponent";
|
||||
import { DashboardBaseComponentProps } from "./DashboardBaseComponent";
|
||||
import LazyMarkdownViewer from "Common/UI/Components/Markdown.tsx/LazyMarkdownViewer";
|
||||
|
||||
export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
component: DashboardTextComponent;
|
||||
@@ -9,6 +10,14 @@ export interface ComponentProps extends DashboardBaseComponentProps {
|
||||
const DashboardTextComponentElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
if (props.component.arguments.isMarkdown) {
|
||||
return (
|
||||
<div className="h-full overflow-auto p-1">
|
||||
<LazyMarkdownViewer text={props.component.arguments.text || ""} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const textClassName: string = `m-auto truncate flex flex-col justify-center h-full ${props.component.arguments.isBold ? "font-medium" : ""} ${props.component.arguments.isItalic ? "italic" : ""} ${props.component.arguments.isUnderline ? "underline" : ""}`;
|
||||
const textHeightInxPx: number = props.dashboardComponentHeightInPx * 0.4;
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ const DashboardValueComponent: FunctionComponent<ComponentProps> = (
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
}, [props.dashboardStartAndEndDate, props.metricTypes]);
|
||||
}, [props.dashboardStartAndEndDate, props.metricTypes, props.refreshTick]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAggregatedResults();
|
||||
@@ -173,8 +173,30 @@ const DashboardValueComponent: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
})?.unit || "";
|
||||
|
||||
// Determine color based on thresholds
|
||||
let valueColorClass: string = "text-gray-800";
|
||||
let bgColorClass: string = "";
|
||||
const warningThreshold: number | undefined =
|
||||
props.component.arguments.warningThreshold;
|
||||
const criticalThreshold: number | undefined =
|
||||
props.component.arguments.criticalThreshold;
|
||||
|
||||
if (
|
||||
criticalThreshold !== undefined &&
|
||||
aggregatedValue >= criticalThreshold
|
||||
) {
|
||||
valueColorClass = "text-red-700";
|
||||
bgColorClass = "bg-red-50";
|
||||
} else if (
|
||||
warningThreshold !== undefined &&
|
||||
aggregatedValue >= warningThreshold
|
||||
) {
|
||||
valueColorClass = "text-yellow-700";
|
||||
bgColorClass = "bg-yellow-50";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full text-center h-full m-auto">
|
||||
<div className={`w-full text-center h-full m-auto rounded ${bgColorClass}`}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: titleHeightInPx > 0 ? `${titleHeightInPx}px` : "",
|
||||
@@ -184,7 +206,7 @@ const DashboardValueComponent: FunctionComponent<ComponentProps> = (
|
||||
{props.component.arguments.title || " "}
|
||||
</div>
|
||||
<div
|
||||
className="text-center text-semibold truncate"
|
||||
className={`text-center text-semibold truncate ${valueColorClass}`}
|
||||
style={{
|
||||
fontSize: valueHeightInPx > 0 ? `${valueHeightInPx}px` : "",
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -9,12 +10,17 @@ import DashboardToolbar from "./Toolbar/DashboardToolbar";
|
||||
import DashboardCanvas from "./Canvas/Index";
|
||||
import DashboardMode from "Common/Types/Dashboard/DashboardMode";
|
||||
import DashboardComponentType from "Common/Types/Dashboard/DashboardComponentType";
|
||||
import DashboardViewConfig from "Common/Types/Dashboard/DashboardViewConfig";
|
||||
import DashboardViewConfig, {
|
||||
AutoRefreshInterval,
|
||||
getAutoRefreshIntervalInMs,
|
||||
} from "Common/Types/Dashboard/DashboardViewConfig";
|
||||
import { ObjectType } from "Common/Types/JSON";
|
||||
import DashboardBaseComponent from "Common/Types/Dashboard/DashboardComponents/DashboardBaseComponent";
|
||||
import DashboardChartComponentUtil from "Common/Utils/Dashboard/Components/DashboardChartComponent";
|
||||
import DashboardValueComponentUtil from "Common/Utils/Dashboard/Components/DashboardValueComponent";
|
||||
import DashboardTextComponentUtil from "Common/Utils/Dashboard/Components/DashboardTextComponent";
|
||||
import DashboardTableComponentUtil from "Common/Utils/Dashboard/Components/DashboardTableComponent";
|
||||
import DashboardGaugeComponentUtil from "Common/Utils/Dashboard/Components/DashboardGaugeComponent";
|
||||
import BadDataException from "Common/Types/Exception/BadDataException";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
|
||||
@@ -30,6 +36,7 @@ import MetricUtil from "../Metrics/Utils/Metrics";
|
||||
import RangeStartAndEndDateTime from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
import TimeRange from "Common/Types/Time/TimeRange";
|
||||
import MetricType from "Common/Models/DatabaseModels/MetricType";
|
||||
import DashboardVariable from "Common/Types/Dashboard/DashboardVariable";
|
||||
|
||||
export interface ComponentProps {
|
||||
dashboardId: ObjectID;
|
||||
@@ -49,6 +56,23 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
|
||||
// Auto-refresh state
|
||||
const [autoRefreshInterval, setAutoRefreshInterval] =
|
||||
useState<AutoRefreshInterval>(AutoRefreshInterval.OFF);
|
||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||
const [dashboardVariables, setDashboardVariables] = useState<
|
||||
Array<DashboardVariable>
|
||||
>([]);
|
||||
|
||||
// Zoom stack for time range
|
||||
const [timeRangeStack, setTimeRangeStack] = useState<
|
||||
Array<RangeStartAndEndDateTime>
|
||||
>([]);
|
||||
const autoRefreshTimerRef: React.MutableRefObject<ReturnType<
|
||||
typeof setInterval
|
||||
> | null> = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [refreshTick, setRefreshTick] = useState<number>(0);
|
||||
|
||||
// ref for dashboard div.
|
||||
|
||||
const dashboardViewRef: React.RefObject<HTMLDivElement> =
|
||||
@@ -140,13 +164,23 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
return;
|
||||
}
|
||||
|
||||
setDashboardViewConfig(
|
||||
JSONFunctions.deserializeValue(
|
||||
dashboard.dashboardViewConfig ||
|
||||
DashboardViewConfigUtil.createDefaultDashboardViewConfig(),
|
||||
) as DashboardViewConfig,
|
||||
);
|
||||
const config: DashboardViewConfig = JSONFunctions.deserializeValue(
|
||||
dashboard.dashboardViewConfig ||
|
||||
DashboardViewConfigUtil.createDefaultDashboardViewConfig(),
|
||||
) as DashboardViewConfig;
|
||||
|
||||
setDashboardViewConfig(config);
|
||||
setDashboardName(dashboard.name || "Untitled Dashboard");
|
||||
|
||||
// Restore saved auto-refresh interval
|
||||
if (config.refreshInterval) {
|
||||
setAutoRefreshInterval(config.refreshInterval);
|
||||
}
|
||||
|
||||
// Restore saved variables
|
||||
if (config.variables) {
|
||||
setDashboardVariables(config.variables);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPage: PromiseVoidFunction = async (): Promise<void> => {
|
||||
@@ -169,6 +203,47 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Auto-refresh timer management
|
||||
const triggerRefresh: () => void = useCallback(() => {
|
||||
setIsRefreshing(true);
|
||||
setRefreshTick((prev: number) => {
|
||||
return prev + 1;
|
||||
});
|
||||
// Brief indicator
|
||||
setTimeout(() => {
|
||||
setIsRefreshing(false);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear existing timer
|
||||
if (autoRefreshTimerRef.current) {
|
||||
clearInterval(autoRefreshTimerRef.current);
|
||||
autoRefreshTimerRef.current = null;
|
||||
}
|
||||
|
||||
// Don't auto-refresh in edit mode
|
||||
if (dashboardMode === DashboardMode.Edit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalMs: number | null =
|
||||
getAutoRefreshIntervalInMs(autoRefreshInterval);
|
||||
|
||||
if (intervalMs !== null) {
|
||||
autoRefreshTimerRef.current = setInterval(() => {
|
||||
triggerRefresh();
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (autoRefreshTimerRef.current) {
|
||||
clearInterval(autoRefreshTimerRef.current);
|
||||
autoRefreshTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [autoRefreshInterval, dashboardMode, triggerRefresh]);
|
||||
|
||||
const isEditMode: boolean = dashboardMode === DashboardMode.Edit;
|
||||
|
||||
const sideBarWidth: number = isEditMode && selectedComponentId ? 650 : 0;
|
||||
@@ -219,15 +294,33 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
dashboardName={dashboardName}
|
||||
isSaving={isSaving}
|
||||
onSaveClick={() => {
|
||||
// Save auto-refresh interval with the config
|
||||
const configWithRefresh: DashboardViewConfig = {
|
||||
...dashboardViewConfig,
|
||||
refreshInterval: autoRefreshInterval,
|
||||
};
|
||||
setDashboardViewConfig(configWithRefresh);
|
||||
|
||||
saveDashboardViewConfig().catch((err: Error) => {
|
||||
setError(API.getFriendlyErrorMessage(err));
|
||||
});
|
||||
setDashboardMode(DashboardMode.View);
|
||||
}}
|
||||
startAndEndDate={startAndEndDate}
|
||||
canResetZoom={timeRangeStack.length > 0}
|
||||
onResetZoom={() => {
|
||||
if (timeRangeStack.length > 0) {
|
||||
const previousRange: RangeStartAndEndDateTime =
|
||||
timeRangeStack[timeRangeStack.length - 1]!;
|
||||
setStartAndEndDate(previousRange);
|
||||
setTimeRangeStack(timeRangeStack.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
onStartAndEndDateChange={(
|
||||
newStartAndEndDate: RangeStartAndEndDateTime,
|
||||
) => {
|
||||
// Push current range to zoom stack before changing
|
||||
setTimeRangeStack([...timeRangeStack, startAndEndDate]);
|
||||
setStartAndEndDate(newStartAndEndDate);
|
||||
}}
|
||||
onCancelEditClick={async () => {
|
||||
@@ -238,6 +331,26 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
onEditClick={() => {
|
||||
setDashboardMode(DashboardMode.Edit);
|
||||
}}
|
||||
autoRefreshInterval={autoRefreshInterval}
|
||||
onAutoRefreshIntervalChange={(interval: AutoRefreshInterval) => {
|
||||
setAutoRefreshInterval(interval);
|
||||
}}
|
||||
isRefreshing={isRefreshing}
|
||||
variables={dashboardVariables}
|
||||
onVariableValueChange={(variableId: string, value: string) => {
|
||||
const updatedVariables: Array<DashboardVariable> =
|
||||
dashboardVariables.map((v: DashboardVariable) => {
|
||||
if (v.id === variableId) {
|
||||
return { ...v, selectedValue: value };
|
||||
}
|
||||
return v;
|
||||
});
|
||||
setDashboardVariables(updatedVariables);
|
||||
// Trigger refresh when variable changes
|
||||
setRefreshTick((prev: number) => {
|
||||
return prev + 1;
|
||||
});
|
||||
}}
|
||||
onAddComponentClick={(componentType: DashboardComponentType) => {
|
||||
let newComponent: DashboardBaseComponent | null = null;
|
||||
|
||||
@@ -253,6 +366,14 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
newComponent = DashboardTextComponentUtil.getDefaultComponent();
|
||||
}
|
||||
|
||||
if (componentType === DashboardComponentType.Table) {
|
||||
newComponent = DashboardTableComponentUtil.getDefaultComponent();
|
||||
}
|
||||
|
||||
if (componentType === DashboardComponentType.Gauge) {
|
||||
newComponent = DashboardGaugeComponentUtil.getDefaultComponent();
|
||||
}
|
||||
|
||||
if (!newComponent) {
|
||||
throw new BadDataException(
|
||||
`Unknown component type: ${componentType}`,
|
||||
@@ -291,6 +412,7 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
|
||||
telemetryAttributes,
|
||||
metricTypes,
|
||||
}}
|
||||
refreshTick={refreshTick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,14 @@ import MoreMenuItem from "Common/UI/Components/MoreMenu/MoreMenuItem";
|
||||
import DashboardComponentType from "Common/Types/Dashboard/DashboardComponentType";
|
||||
import RangeStartAndEndDateTime from "Common/Types/Time/RangeStartAndEndDateTime";
|
||||
import RangeStartAndEndDateView from "Common/UI/Components/Date/RangeStartAndEndDateView";
|
||||
import DashboardViewConfig from "Common/Types/Dashboard/DashboardViewConfig";
|
||||
import DashboardViewConfig, {
|
||||
AutoRefreshInterval,
|
||||
getAutoRefreshIntervalLabel,
|
||||
} from "Common/Types/Dashboard/DashboardViewConfig";
|
||||
import DashboardVariable from "Common/Types/Dashboard/DashboardVariable";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
import Loader from "Common/UI/Components/Loader/Loader";
|
||||
import DashboardVariableSelector from "./DashboardVariableSelector";
|
||||
|
||||
export interface ComponentProps {
|
||||
onEditClick: () => void;
|
||||
@@ -23,6 +28,13 @@ export interface ComponentProps {
|
||||
startAndEndDate: RangeStartAndEndDateTime;
|
||||
onStartAndEndDateChange: (startAndEndDate: RangeStartAndEndDateTime) => void;
|
||||
dashboardViewConfig: DashboardViewConfig;
|
||||
autoRefreshInterval: AutoRefreshInterval;
|
||||
onAutoRefreshIntervalChange: (interval: AutoRefreshInterval) => void;
|
||||
isRefreshing?: boolean | undefined;
|
||||
variables?: Array<DashboardVariable> | undefined;
|
||||
onVariableValueChange?: ((variableId: string, value: string) => void) | undefined;
|
||||
canResetZoom?: boolean | undefined;
|
||||
onResetZoom?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
const DashboardToolbar: FunctionComponent<ComponentProps> = (
|
||||
@@ -58,6 +70,66 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template variables */}
|
||||
{props.variables &&
|
||||
props.variables.length > 0 &&
|
||||
props.onVariableValueChange && (
|
||||
<div className="mt-1.5 mr-2">
|
||||
<DashboardVariableSelector
|
||||
variables={props.variables}
|
||||
onVariableValueChange={props.onVariableValueChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reset Zoom button */}
|
||||
{props.canResetZoom && props.onResetZoom && !isEditMode && (
|
||||
<Button
|
||||
icon={IconProp.Refresh}
|
||||
title="Reset Zoom"
|
||||
buttonStyle={ButtonStyleType.HOVER_PRIMARY_OUTLINE}
|
||||
onClick={props.onResetZoom}
|
||||
tooltip="Reset to original time range"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Auto-refresh dropdown - only show in view mode */}
|
||||
{!isEditMode &&
|
||||
props.dashboardViewConfig &&
|
||||
props.dashboardViewConfig.components &&
|
||||
props.dashboardViewConfig.components.length > 0 && (
|
||||
<MoreMenu
|
||||
menuIcon={IconProp.Refresh}
|
||||
text={
|
||||
props.autoRefreshInterval !== AutoRefreshInterval.OFF
|
||||
? getAutoRefreshIntervalLabel(props.autoRefreshInterval)
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{Object.values(AutoRefreshInterval).map(
|
||||
(interval: AutoRefreshInterval) => {
|
||||
return (
|
||||
<MoreMenuItem
|
||||
key={interval}
|
||||
text={getAutoRefreshIntervalLabel(interval)}
|
||||
onClick={() => {
|
||||
props.onAutoRefreshIntervalChange(interval);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</MoreMenu>
|
||||
)}
|
||||
|
||||
{/* Refreshing indicator */}
|
||||
{props.isRefreshing &&
|
||||
props.autoRefreshInterval !== AutoRefreshInterval.OFF && (
|
||||
<div className="flex items-center ml-2">
|
||||
<div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditMode ? (
|
||||
<MoreMenu menuIcon={IconProp.Add} text="Add Component">
|
||||
<MoreMenuItem
|
||||
@@ -81,6 +153,20 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
|
||||
props.onAddComponentClick(DashboardComponentType.Text);
|
||||
}}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
text={"Add Table"}
|
||||
key={"add-table"}
|
||||
onClick={() => {
|
||||
props.onAddComponentClick(DashboardComponentType.Table);
|
||||
}}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
text={"Add Gauge"}
|
||||
key={"add-gauge"}
|
||||
onClick={() => {
|
||||
props.onAddComponentClick(DashboardComponentType.Gauge);
|
||||
}}
|
||||
/>
|
||||
</MoreMenu>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import DashboardVariable from "Common/Types/Dashboard/DashboardVariable";
|
||||
|
||||
export interface ComponentProps {
|
||||
variables: Array<DashboardVariable>;
|
||||
onVariableValueChange: (variableId: string, value: string) => void;
|
||||
}
|
||||
|
||||
const DashboardVariableSelector: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
if (!props.variables || props.variables.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{props.variables.map((variable: DashboardVariable) => {
|
||||
const options: Array<string> = variable.customListValues
|
||||
? variable.customListValues.split(",").map((v: string) => {
|
||||
return v.trim();
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div key={variable.id} className="flex items-center gap-1">
|
||||
<label className="text-xs font-medium text-gray-500">
|
||||
{variable.label || variable.name}:
|
||||
</label>
|
||||
{options.length > 0 ? (
|
||||
<select
|
||||
className="text-xs border border-gray-200 rounded px-2 py-1 bg-white"
|
||||
value={variable.selectedValue || variable.defaultValue || ""}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
props.onVariableValueChange(variable.id, e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
{options.map((option: string) => {
|
||||
return (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="text-xs border border-gray-200 rounded px-2 py-1 bg-white w-24"
|
||||
value={variable.selectedValue || variable.defaultValue || ""}
|
||||
placeholder={variable.name}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.onVariableValueChange(variable.id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardVariableSelector;
|
||||
@@ -35,6 +35,13 @@ export interface ComponentProps {
|
||||
monitorStep: MonitorStep;
|
||||
}
|
||||
|
||||
const isMetricOnlyMonitorType = (monitorType: MonitorType): boolean => {
|
||||
return (
|
||||
monitorType === MonitorType.Kubernetes ||
|
||||
monitorType === MonitorType.Metrics
|
||||
);
|
||||
};
|
||||
|
||||
const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
@@ -77,6 +84,18 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
);
|
||||
}, [criteriaFilter]);
|
||||
|
||||
const isMetricOnly: boolean = isMetricOnlyMonitorType(props.monitorType);
|
||||
|
||||
// Auto-select MetricValue for metric-only monitor types (Kubernetes, Metrics)
|
||||
useEffect(() => {
|
||||
if (isMetricOnly && criteriaFilter && criteriaFilter.checkOn !== CheckOn.MetricValue) {
|
||||
props.onChange?.({
|
||||
...criteriaFilter,
|
||||
checkOn: CheckOn.MetricValue,
|
||||
});
|
||||
}
|
||||
}, [isMetricOnly]);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
@@ -168,6 +187,8 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-md p-2 bg-gray-50 my-5 border-gray-200 border-solid border-2">
|
||||
{/* Hide Filter Type dropdown for metric-only monitors since MetricValue is the only option */}
|
||||
{!isMetricOnly && (
|
||||
<div className="">
|
||||
<FieldLabelElement title="Filter Type" />
|
||||
<Dropdown
|
||||
@@ -186,6 +207,7 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{criteriaFilter?.checkOn &&
|
||||
criteriaFilter?.checkOn === CheckOn.DiskUsagePercent && (
|
||||
@@ -210,7 +232,10 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
{criteriaFilter?.checkOn &&
|
||||
criteriaFilter?.checkOn === CheckOn.MetricValue && (
|
||||
<div className="mt-1">
|
||||
<FieldLabelElement title="Select Metric Variable" />
|
||||
<FieldLabelElement
|
||||
title={isMetricOnly ? "Metric" : "Select Metric Variable"}
|
||||
description={isMetricOnly ? "Which metric query should this alert rule check?" : undefined}
|
||||
/>
|
||||
<Dropdown
|
||||
value={selectedMetricVariableOption}
|
||||
options={metricVariableOptions}
|
||||
@@ -232,7 +257,10 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
{criteriaFilter?.checkOn &&
|
||||
criteriaFilter?.checkOn === CheckOn.MetricValue && (
|
||||
<div className="mt-1">
|
||||
<FieldLabelElement title="Select Aggregation" />
|
||||
<FieldLabelElement
|
||||
title={isMetricOnly ? "Aggregation" : "Select Aggregation"}
|
||||
description={isMetricOnly ? "How to combine multiple data points (e.g. Average, Max, Min)." : undefined}
|
||||
/>
|
||||
<Dropdown
|
||||
value={metricAggregationValue}
|
||||
options={metricAggregationOptions}
|
||||
@@ -350,7 +378,10 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
{!criteriaFilter?.checkOn ||
|
||||
(criteriaFilter?.checkOn && (
|
||||
<div className="mt-1">
|
||||
<FieldLabelElement title="Filter Condition" />
|
||||
<FieldLabelElement
|
||||
title={isMetricOnly ? "Condition" : "Filter Condition"}
|
||||
description={isMetricOnly ? "When should this alert trigger?" : undefined}
|
||||
/>
|
||||
<Dropdown
|
||||
value={filterConditionValue}
|
||||
options={filterTypeOptions}
|
||||
@@ -377,7 +408,10 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
checkOn: criteriaFilter?.checkOn,
|
||||
}) && (
|
||||
<div className="mt-1">
|
||||
<FieldLabelElement title="Value" />
|
||||
<FieldLabelElement
|
||||
title={isMetricOnly ? "Threshold" : "Value"}
|
||||
description={isMetricOnly ? "The value to compare against." : undefined}
|
||||
/>
|
||||
<Input
|
||||
placeholder={valuePlaceholder}
|
||||
value={criteriaFilter?.value?.toString()}
|
||||
@@ -425,7 +459,7 @@ const CriteriaFilterElement: FunctionComponent<ComponentProps> = (
|
||||
|
||||
<div className="mt-3 -mr-2 w-full flex justify-end">
|
||||
<Button
|
||||
title="Delete Filter"
|
||||
title={isMetricOnly ? "Delete Rule" : "Delete Filter"}
|
||||
buttonStyle={ButtonStyleType.DANGER_OUTLINE}
|
||||
icon={IconProp.Trash}
|
||||
buttonSize={ButtonSize.Small}
|
||||
|
||||
@@ -3,6 +3,7 @@ import IconProp from "Common/Types/Icon/IconProp";
|
||||
import {
|
||||
CheckOn,
|
||||
CriteriaFilter,
|
||||
EvaluateOverTimeType,
|
||||
FilterType,
|
||||
} from "Common/Types/Monitor/CriteriaFilter";
|
||||
import MonitorStep from "Common/Types/Monitor/MonitorStep";
|
||||
@@ -98,18 +99,39 @@ const CriteriaFilters: FunctionComponent<ComponentProps> = (
|
||||
})}
|
||||
<div className="mt-3 -ml-3">
|
||||
<Button
|
||||
title="Add Filter"
|
||||
title={
|
||||
props.monitorType === MonitorType.Kubernetes ||
|
||||
props.monitorType === MonitorType.Metrics
|
||||
? "Add Rule"
|
||||
: "Add Filter"
|
||||
}
|
||||
buttonSize={ButtonSize.Small}
|
||||
icon={IconProp.Add}
|
||||
onClick={() => {
|
||||
const newCriteriaFilters: Array<CriteriaFilter> = [
|
||||
...criteriaFilters,
|
||||
];
|
||||
newCriteriaFilters.push({
|
||||
checkOn: CheckOn.IsOnline,
|
||||
filterType: FilterType.EqualTo,
|
||||
value: "",
|
||||
});
|
||||
|
||||
const isMetricOnly: boolean =
|
||||
props.monitorType === MonitorType.Kubernetes ||
|
||||
props.monitorType === MonitorType.Metrics;
|
||||
|
||||
newCriteriaFilters.push(
|
||||
isMetricOnly
|
||||
? {
|
||||
checkOn: CheckOn.MetricValue,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: "",
|
||||
metricMonitorOptions: {
|
||||
metricAggregationType: EvaluateOverTimeType.AnyValue,
|
||||
},
|
||||
}
|
||||
: {
|
||||
checkOn: CheckOn.IsOnline,
|
||||
filterType: FilterType.EqualTo,
|
||||
value: "",
|
||||
},
|
||||
);
|
||||
|
||||
props.onChange?.(newCriteriaFilters);
|
||||
}}
|
||||
@@ -117,8 +139,18 @@ const CriteriaFilters: FunctionComponent<ComponentProps> = (
|
||||
</div>
|
||||
{showCantDeleteModal ? (
|
||||
<ConfirmModal
|
||||
description={`We need at least one filter for this criteria. We cant delete one remaining filter. If you don't need filters, please feel free to delete criteria instead.`}
|
||||
title={`Cannot delete last remaining filter.`}
|
||||
description={
|
||||
props.monitorType === MonitorType.Kubernetes ||
|
||||
props.monitorType === MonitorType.Metrics
|
||||
? `At least one alert rule is required. If you don't need rules, you can delete the entire criteria instead.`
|
||||
: `We need at least one filter for this criteria. We cant delete one remaining filter. If you don't need filters, please feel free to delete criteria instead.`
|
||||
}
|
||||
title={
|
||||
props.monitorType === MonitorType.Kubernetes ||
|
||||
props.monitorType === MonitorType.Metrics
|
||||
? `Cannot delete last remaining rule.`
|
||||
: `Cannot delete last remaining filter.`
|
||||
}
|
||||
onSubmit={() => {
|
||||
setShowCantDeleteModal(false);
|
||||
}}
|
||||
|
||||
@@ -247,8 +247,16 @@ const MonitorCriteriaInstanceElement: FunctionComponent<ComponentProps> = (
|
||||
|
||||
{/* Filters Section - Collapsible */}
|
||||
<CollapsibleSection
|
||||
title="Filters"
|
||||
description="Add criteria for different monitor properties."
|
||||
title={
|
||||
props.monitorType === MonitorType.Kubernetes || props.monitorType === MonitorType.Metrics
|
||||
? "Alert Rules"
|
||||
: "Filters"
|
||||
}
|
||||
description={
|
||||
props.monitorType === MonitorType.Kubernetes || props.monitorType === MonitorType.Metrics
|
||||
? "Define when this alert should trigger based on metric values."
|
||||
: "Add criteria for different monitor properties."
|
||||
}
|
||||
badge={filterSummary}
|
||||
variant="bordered"
|
||||
defaultCollapsed={false}
|
||||
@@ -257,8 +265,16 @@ const MonitorCriteriaInstanceElement: FunctionComponent<ComponentProps> = (
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
<FieldLabelElement
|
||||
title="Filter Condition"
|
||||
description="Select All if you want all the criteria to be met. Select any if you like any criteria to be met."
|
||||
title={
|
||||
props.monitorType === MonitorType.Kubernetes || props.monitorType === MonitorType.Metrics
|
||||
? "Match Condition"
|
||||
: "Filter Condition"
|
||||
}
|
||||
description={
|
||||
props.monitorType === MonitorType.Kubernetes || props.monitorType === MonitorType.Metrics
|
||||
? "Should all rules match, or just any one of them?"
|
||||
: "Select All if you want all the criteria to be met. Select any if you like any criteria to be met."
|
||||
}
|
||||
required={true}
|
||||
/>
|
||||
<Radio
|
||||
|
||||
@@ -159,11 +159,16 @@ const MetricCharts: FunctionComponent<ComponentProps> = (
|
||||
});
|
||||
}
|
||||
|
||||
// Determine chart type - use BAR for bar chart type, AREA for everything else
|
||||
const chartType: ChartType =
|
||||
queryConfig.chartType === MetricChartType.BAR
|
||||
? ChartType.BAR
|
||||
: ChartType.AREA;
|
||||
let chartType: ChartType;
|
||||
if (queryConfig.chartType === MetricChartType.BAR) {
|
||||
chartType = ChartType.BAR;
|
||||
} else if (queryConfig.chartType === MetricChartType.AREA) {
|
||||
chartType = ChartType.AREA;
|
||||
} else if (queryConfig.chartType === MetricChartType.LINE) {
|
||||
chartType = ChartType.LINE;
|
||||
} else {
|
||||
chartType = ChartType.AREA;
|
||||
}
|
||||
|
||||
const chart: Chart = {
|
||||
id: index.toString(),
|
||||
|
||||
@@ -43,6 +43,10 @@ import URL from "../../../Types/API/URL";
|
||||
import IP from "../../../Types/IP/IP";
|
||||
import Hostname from "../../../Types/API/Hostname";
|
||||
import Port from "../../../Types/Port";
|
||||
import MetricMonitorResponse, {
|
||||
KubernetesAffectedResource,
|
||||
KubernetesResourceBreakdown,
|
||||
} from "../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
||||
|
||||
export default class MonitorCriteriaEvaluator {
|
||||
public static async processMonitorStep(input: {
|
||||
@@ -545,6 +549,11 @@ ${contextBlock}
|
||||
monitorStep: MonitorStep;
|
||||
monitor: Monitor;
|
||||
}): string | null {
|
||||
// Handle Kubernetes monitors with rich resource context
|
||||
if (input.monitor.monitorType === MonitorType.Kubernetes) {
|
||||
return MonitorCriteriaEvaluator.buildKubernetesRootCauseContext(input);
|
||||
}
|
||||
|
||||
const requestDetails: Array<string> = [];
|
||||
const responseDetails: Array<string> = [];
|
||||
const failureDetails: Array<string> = [];
|
||||
@@ -653,6 +662,293 @@ ${contextBlock}
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
private static buildKubernetesRootCauseContext(input: {
|
||||
dataToProcess: DataToProcess;
|
||||
monitorStep: MonitorStep;
|
||||
monitor: Monitor;
|
||||
}): string | null {
|
||||
const metricResponse: MetricMonitorResponse =
|
||||
input.dataToProcess as MetricMonitorResponse;
|
||||
|
||||
const breakdown: KubernetesResourceBreakdown | undefined =
|
||||
metricResponse.kubernetesResourceBreakdown;
|
||||
|
||||
if (!breakdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sections: Array<string> = [];
|
||||
|
||||
// Cluster context
|
||||
const clusterDetails: Array<string> = [];
|
||||
clusterDetails.push(`- Cluster: ${breakdown.clusterName}`);
|
||||
clusterDetails.push(`- Metric: ${breakdown.metricFriendlyName} (\`${breakdown.metricName}\`)`);
|
||||
|
||||
if (breakdown.attributes["k8s.namespace.name"]) {
|
||||
clusterDetails.push(
|
||||
`- Namespace: ${breakdown.attributes["k8s.namespace.name"]}`,
|
||||
);
|
||||
}
|
||||
|
||||
sections.push(
|
||||
`**Kubernetes Cluster Details**\n${clusterDetails.join("\n")}`,
|
||||
);
|
||||
|
||||
// Affected resources
|
||||
if (
|
||||
breakdown.affectedResources &&
|
||||
breakdown.affectedResources.length > 0
|
||||
) {
|
||||
const resourceLines: Array<string> = [];
|
||||
|
||||
// Sort by metric value descending (worst first)
|
||||
const sortedResources: Array<KubernetesAffectedResource> = [
|
||||
...breakdown.affectedResources,
|
||||
].sort(
|
||||
(
|
||||
a: KubernetesAffectedResource,
|
||||
b: KubernetesAffectedResource,
|
||||
) => {
|
||||
return b.metricValue - a.metricValue;
|
||||
},
|
||||
);
|
||||
|
||||
// Show top 10 affected resources
|
||||
const resourcesToShow: Array<KubernetesAffectedResource> =
|
||||
sortedResources.slice(0, 10);
|
||||
|
||||
for (const resource of resourcesToShow) {
|
||||
const details: Array<string> = [];
|
||||
|
||||
if (resource.namespace) {
|
||||
details.push(`Namespace: \`${resource.namespace}\``);
|
||||
}
|
||||
if (resource.workloadType && resource.workloadName) {
|
||||
details.push(
|
||||
`${resource.workloadType}: \`${resource.workloadName}\``,
|
||||
);
|
||||
}
|
||||
if (resource.podName) {
|
||||
details.push(`Pod: \`${resource.podName}\``);
|
||||
}
|
||||
if (resource.containerName) {
|
||||
details.push(`Container: \`${resource.containerName}\``);
|
||||
}
|
||||
if (resource.nodeName) {
|
||||
details.push(`Node: \`${resource.nodeName}\``);
|
||||
}
|
||||
|
||||
details.push(`Value: **${resource.metricValue}**`);
|
||||
|
||||
resourceLines.push(`- ${details.join(" | ")}`);
|
||||
}
|
||||
|
||||
if (sortedResources.length > 10) {
|
||||
resourceLines.push(
|
||||
`- ... and ${sortedResources.length - 10} more affected resources`,
|
||||
);
|
||||
}
|
||||
|
||||
sections.push(
|
||||
`\n\n**Affected Resources** (${sortedResources.length} total)\n${resourceLines.join("\n")}`,
|
||||
);
|
||||
|
||||
// Add root cause analysis based on metric type
|
||||
const analysis: string | null =
|
||||
MonitorCriteriaEvaluator.buildKubernetesRootCauseAnalysis({
|
||||
breakdown: breakdown,
|
||||
topResource: resourcesToShow[0]!,
|
||||
});
|
||||
|
||||
if (analysis) {
|
||||
sections.push(`\n\n**Root Cause Analysis**\n${analysis}`);
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join("\n");
|
||||
}
|
||||
|
||||
private static buildKubernetesRootCauseAnalysis(input: {
|
||||
breakdown: KubernetesResourceBreakdown;
|
||||
topResource: KubernetesAffectedResource;
|
||||
}): string | null {
|
||||
const { breakdown, topResource } = input;
|
||||
const metricName: string = breakdown.metricName;
|
||||
const lines: Array<string> = [];
|
||||
|
||||
if (
|
||||
metricName === "k8s.container.restarts" ||
|
||||
metricName.includes("restart")
|
||||
) {
|
||||
lines.push(
|
||||
`Container restart count is elevated, indicating a potential CrashLoopBackOff condition.`,
|
||||
);
|
||||
if (topResource.containerName) {
|
||||
lines.push(
|
||||
`The container \`${topResource.containerName}\` in pod \`${topResource.podName || "unknown"}\` has restarted **${topResource.metricValue}** times.`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: application crash on startup, misconfigured environment variables, missing dependencies, OOM (Out of Memory) kills, failed health checks, or missing config maps/secrets.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Check container logs with \`kubectl logs ${topResource.podName || "<pod-name>"} -c ${topResource.containerName || "<container>"} --previous\` and inspect events with \`kubectl describe pod ${topResource.podName || "<pod-name>"}\`.`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.pod.phase" &&
|
||||
breakdown.attributes["k8s.pod.phase"] === "Pending"
|
||||
) {
|
||||
lines.push(
|
||||
`Pods are stuck in Pending phase and unable to be scheduled.`,
|
||||
);
|
||||
lines.push(
|
||||
`Common causes: insufficient CPU/memory resources on nodes, node affinity/taint restrictions preventing scheduling, PersistentVolumeClaim pending, or resource quota exceeded.`,
|
||||
);
|
||||
if (topResource.podName) {
|
||||
lines.push(
|
||||
`Recommended actions: Check scheduling events with \`kubectl describe pod ${topResource.podName}\` and verify node resources with \`kubectl describe nodes\`.`,
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
metricName === "k8s.node.condition_ready" ||
|
||||
metricName.includes("node") && metricName.includes("condition")
|
||||
) {
|
||||
lines.push(`One or more nodes have transitioned to a NotReady state.`);
|
||||
if (topResource.nodeName) {
|
||||
lines.push(
|
||||
`Node \`${topResource.nodeName}\` is reporting NotReady (value: ${topResource.metricValue}).`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: kubelet process failure, node resource exhaustion (disk pressure, memory pressure, PID pressure), network connectivity issues, or underlying VM/hardware failure.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Check node conditions with \`kubectl describe node ${topResource.nodeName || "<node-name>"}\` and verify kubelet status on the node.`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.node.cpu.utilization" ||
|
||||
metricName.includes("cpu") && metricName.includes("utilization")
|
||||
) {
|
||||
lines.push(`Node CPU utilization has exceeded the configured threshold.`);
|
||||
if (topResource.nodeName) {
|
||||
lines.push(
|
||||
`Node \`${topResource.nodeName}\` is at **${topResource.metricValue.toFixed(1)}%** CPU utilization.`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: resource-intensive workloads, insufficient resource limits on pods, noisy neighbor pods consuming excessive CPU, or insufficient cluster capacity.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Identify top CPU consumers with \`kubectl top pods --all-namespaces --sort-by=cpu\` and consider scaling the cluster or adjusting pod resource limits.`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.node.memory.usage" ||
|
||||
metricName.includes("memory") && metricName.includes("usage")
|
||||
) {
|
||||
lines.push(
|
||||
`Node memory utilization has exceeded the configured threshold.`,
|
||||
);
|
||||
if (topResource.nodeName) {
|
||||
lines.push(
|
||||
`Node \`${topResource.nodeName}\` memory usage is at **${topResource.metricValue.toFixed(1)}%**.`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: memory leaks in applications, insufficient memory limits on pods, too many pods scheduled on the node, or growing dataset sizes.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Check memory consumers with \`kubectl top pods --all-namespaces --sort-by=memory\` and review pod memory limits. Consider scaling the cluster or adding nodes with more memory.`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.deployment.unavailable_replicas" ||
|
||||
metricName.includes("unavailable")
|
||||
) {
|
||||
lines.push(
|
||||
`Deployment has unavailable replicas, indicating a mismatch between desired and available replicas.`,
|
||||
);
|
||||
if (topResource.workloadName) {
|
||||
lines.push(
|
||||
`${topResource.workloadType || "Deployment"} \`${topResource.workloadName}\` has **${topResource.metricValue}** unavailable replica(s).`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: failed rolling update, image pull errors (wrong image tag or missing registry credentials), pod crash loops, insufficient cluster resources to schedule new pods, or PodDisruptionBudget blocking updates.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Check deployment rollout status with \`kubectl rollout status deployment/${topResource.workloadName || "<deployment>"}\` and inspect pod events.`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.job.failed_pods" ||
|
||||
metricName.includes("job") && metricName.includes("fail")
|
||||
) {
|
||||
lines.push(`Kubernetes Job has failed pods.`);
|
||||
if (topResource.workloadName) {
|
||||
lines.push(
|
||||
`Job \`${topResource.workloadName}\` has **${topResource.metricValue}** failed pod(s).`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: application error or non-zero exit code, resource limits exceeded (OOMKilled), misconfigured command or arguments, missing environment variables, or timeout exceeded.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Check job status with \`kubectl describe job ${topResource.workloadName || "<job-name>"}\` and review pod logs for the failed pod(s).`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.node.filesystem.usage" ||
|
||||
metricName.includes("disk") ||
|
||||
metricName.includes("filesystem")
|
||||
) {
|
||||
lines.push(
|
||||
`Node disk/filesystem usage has exceeded the configured threshold.`,
|
||||
);
|
||||
if (topResource.nodeName) {
|
||||
lines.push(
|
||||
`Node \`${topResource.nodeName}\` filesystem usage is at **${topResource.metricValue.toFixed(1)}%**.`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: container image layers consuming disk space, excessive logging, large emptyDir volumes, or accumulation of unused container images.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Clean up unused images with \`docker system prune\` or \`crictl rmi --prune\`, check for large log files, and review PersistentVolumeClaim usage.`,
|
||||
);
|
||||
} else if (
|
||||
metricName === "k8s.daemonset.misscheduled_nodes" ||
|
||||
metricName.includes("daemonset")
|
||||
) {
|
||||
lines.push(
|
||||
`DaemonSet has misscheduled or unavailable nodes.`,
|
||||
);
|
||||
if (topResource.workloadName) {
|
||||
lines.push(
|
||||
`DaemonSet \`${topResource.workloadName}\` has **${topResource.metricValue}** misscheduled node(s).`,
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`Common causes: node taints preventing scheduling, incorrect node selectors, or node affinity rules excluding certain nodes.`,
|
||||
);
|
||||
lines.push(
|
||||
`Recommended actions: Check DaemonSet status with \`kubectl describe daemonset ${topResource.workloadName || "<daemonset>"}\` and verify node labels and taints.`,
|
||||
);
|
||||
} else {
|
||||
// Generic Kubernetes context
|
||||
lines.push(
|
||||
`Kubernetes metric \`${metricName}\` (${breakdown.metricFriendlyName}) has breached the configured threshold.`,
|
||||
);
|
||||
if (topResource.podName) {
|
||||
lines.push(`Most affected pod: \`${topResource.podName}\``);
|
||||
}
|
||||
if (topResource.nodeName) {
|
||||
lines.push(`Most affected node: \`${topResource.nodeName}\``);
|
||||
}
|
||||
lines.push(
|
||||
`Recommended actions: Investigate the affected resources using \`kubectl describe\` and \`kubectl logs\` commands.`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
private static getMonitorDestinationString(input: {
|
||||
monitorStep: MonitorStep;
|
||||
probeResponse: ProbeMonitorResponse | null;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
enum DashboardChartType {
|
||||
Line = "Line",
|
||||
Bar = "Bar",
|
||||
Area = "Area",
|
||||
StackedArea = "Stacked Area",
|
||||
Pie = "Pie",
|
||||
Heatmap = "Heatmap",
|
||||
Histogram = "Histogram",
|
||||
}
|
||||
|
||||
export default DashboardChartType;
|
||||
|
||||
@@ -2,6 +2,8 @@ enum DashboardComponentType {
|
||||
Chart = `Chart`,
|
||||
Value = `Value`,
|
||||
Text = `Text`,
|
||||
Table = `Table`,
|
||||
Gauge = `Gauge`,
|
||||
}
|
||||
|
||||
export default DashboardComponentType;
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum ComponentInputType {
|
||||
Number = "Number",
|
||||
Decimal = "Decimal",
|
||||
MetricsQueryConfig = "MetricsQueryConfig",
|
||||
MetricsQueryConfigs = "MetricsQueryConfigs",
|
||||
LongText = "Long Text",
|
||||
Dropdown = "Dropdown",
|
||||
}
|
||||
|
||||
@@ -4,15 +4,24 @@ import DashboardComponentType from "../DashboardComponentType";
|
||||
import DashboardChartType from "../Chart/ChartType";
|
||||
import BaseComponent from "./DashboardBaseComponent";
|
||||
|
||||
export interface ChartThreshold {
|
||||
value: number;
|
||||
label?: string | undefined;
|
||||
color?: string | undefined;
|
||||
}
|
||||
|
||||
export default interface DashboardChartComponent extends BaseComponent {
|
||||
componentType: DashboardComponentType.Chart;
|
||||
componentId: ObjectID;
|
||||
arguments: {
|
||||
metricQueryConfig?: MetricQueryConfigData | undefined;
|
||||
metricQueryConfigs?: Array<MetricQueryConfigData> | undefined;
|
||||
chartTitle?: string | undefined;
|
||||
chartDescription?: string | undefined;
|
||||
legendText?: string | undefined;
|
||||
legendUnit?: string | undefined;
|
||||
chartType?: DashboardChartType | undefined;
|
||||
warningThreshold?: number | undefined;
|
||||
criticalThreshold?: number | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import MetricQueryConfigData from "../../Metrics/MetricQueryConfigData";
|
||||
import ObjectID from "../../ObjectID";
|
||||
import DashboardComponentType from "../DashboardComponentType";
|
||||
import BaseComponent from "./DashboardBaseComponent";
|
||||
|
||||
export default interface DashboardGaugeComponent extends BaseComponent {
|
||||
componentType: DashboardComponentType.Gauge;
|
||||
componentId: ObjectID;
|
||||
arguments: {
|
||||
metricQueryConfig?: MetricQueryConfigData | undefined;
|
||||
gaugeTitle?: string | undefined;
|
||||
minValue?: number | undefined;
|
||||
maxValue?: number | undefined;
|
||||
warningThreshold?: number | undefined;
|
||||
criticalThreshold?: number | undefined;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetricQueryConfigData from "../../Metrics/MetricQueryConfigData";
|
||||
import ObjectID from "../../ObjectID";
|
||||
import DashboardComponentType from "../DashboardComponentType";
|
||||
import BaseComponent from "./DashboardBaseComponent";
|
||||
|
||||
export default interface DashboardTableComponent extends BaseComponent {
|
||||
componentType: DashboardComponentType.Table;
|
||||
componentId: ObjectID;
|
||||
arguments: {
|
||||
metricQueryConfig?: MetricQueryConfigData | undefined;
|
||||
tableTitle?: string | undefined;
|
||||
maxRows?: number | undefined;
|
||||
};
|
||||
}
|
||||
@@ -10,5 +10,6 @@ export default interface DashboardTextComponent extends BaseComponent {
|
||||
isBold: boolean;
|
||||
isItalic: boolean;
|
||||
isUnderline: boolean;
|
||||
isMarkdown?: boolean | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,5 +9,7 @@ export default interface DashboardValueComponent extends BaseComponent {
|
||||
arguments: {
|
||||
metricQueryConfig?: MetricQueryConfigData | undefined;
|
||||
title: string;
|
||||
warningThreshold?: number | undefined;
|
||||
criticalThreshold?: number | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
23
Common/Types/Dashboard/DashboardVariable.ts
Normal file
23
Common/Types/Dashboard/DashboardVariable.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export enum DashboardVariableType {
|
||||
CustomList = "Custom List",
|
||||
Query = "Query",
|
||||
TextInput = "Text Input",
|
||||
}
|
||||
|
||||
export default interface DashboardVariable {
|
||||
id: string;
|
||||
name: string;
|
||||
label?: string | undefined;
|
||||
type: DashboardVariableType;
|
||||
// For CustomList: comma-separated values
|
||||
customListValues?: string | undefined;
|
||||
// For Query: a ClickHouse query to populate options
|
||||
query?: string | undefined;
|
||||
// Current selected value(s)
|
||||
selectedValue?: string | undefined;
|
||||
selectedValues?: Array<string> | undefined;
|
||||
// Whether multi-select is enabled
|
||||
isMultiSelect?: boolean | undefined;
|
||||
// Default value
|
||||
defaultValue?: string | undefined;
|
||||
}
|
||||
@@ -1,8 +1,67 @@
|
||||
import { ObjectType } from "../JSON";
|
||||
import DashboardBaseComponent from "./DashboardComponents/DashboardBaseComponent";
|
||||
import DashboardVariable from "./DashboardVariable";
|
||||
|
||||
export enum AutoRefreshInterval {
|
||||
OFF = "off",
|
||||
FIVE_SECONDS = "5s",
|
||||
TEN_SECONDS = "10s",
|
||||
THIRTY_SECONDS = "30s",
|
||||
ONE_MINUTE = "1m",
|
||||
FIVE_MINUTES = "5m",
|
||||
FIFTEEN_MINUTES = "15m",
|
||||
}
|
||||
|
||||
export function getAutoRefreshIntervalInMs(
|
||||
interval: AutoRefreshInterval,
|
||||
): number | null {
|
||||
switch (interval) {
|
||||
case AutoRefreshInterval.OFF:
|
||||
return null;
|
||||
case AutoRefreshInterval.FIVE_SECONDS:
|
||||
return 5000;
|
||||
case AutoRefreshInterval.TEN_SECONDS:
|
||||
return 10000;
|
||||
case AutoRefreshInterval.THIRTY_SECONDS:
|
||||
return 30000;
|
||||
case AutoRefreshInterval.ONE_MINUTE:
|
||||
return 60000;
|
||||
case AutoRefreshInterval.FIVE_MINUTES:
|
||||
return 300000;
|
||||
case AutoRefreshInterval.FIFTEEN_MINUTES:
|
||||
return 900000;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoRefreshIntervalLabel(
|
||||
interval: AutoRefreshInterval,
|
||||
): string {
|
||||
switch (interval) {
|
||||
case AutoRefreshInterval.OFF:
|
||||
return "Off";
|
||||
case AutoRefreshInterval.FIVE_SECONDS:
|
||||
return "5s";
|
||||
case AutoRefreshInterval.TEN_SECONDS:
|
||||
return "10s";
|
||||
case AutoRefreshInterval.THIRTY_SECONDS:
|
||||
return "30s";
|
||||
case AutoRefreshInterval.ONE_MINUTE:
|
||||
return "1m";
|
||||
case AutoRefreshInterval.FIVE_MINUTES:
|
||||
return "5m";
|
||||
case AutoRefreshInterval.FIFTEEN_MINUTES:
|
||||
return "15m";
|
||||
default:
|
||||
return "Off";
|
||||
}
|
||||
}
|
||||
|
||||
export default interface DashboardViewConfig {
|
||||
_type: ObjectType.DashboardViewConfig;
|
||||
components: Array<DashboardBaseComponent>;
|
||||
heightInDashboardUnits: number;
|
||||
refreshInterval?: AutoRefreshInterval | undefined;
|
||||
variables?: Array<DashboardVariable> | undefined;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import MetricQueryData from "./MetricQueryData";
|
||||
export enum MetricChartType {
|
||||
LINE = "line",
|
||||
BAR = "bar",
|
||||
AREA = "area",
|
||||
}
|
||||
|
||||
export interface ChartSeries {
|
||||
|
||||
@@ -88,9 +88,19 @@ export function buildOfflineCriteriaInstance(args: {
|
||||
metricAlias: string;
|
||||
filterType: FilterType;
|
||||
value: number;
|
||||
incidentTitle?: string;
|
||||
incidentDescription?: string;
|
||||
criteriaName?: string;
|
||||
criteriaDescription?: string;
|
||||
}): MonitorCriteriaInstance {
|
||||
const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance();
|
||||
|
||||
const incidentTitle: string =
|
||||
args.incidentTitle || `${args.monitorName} - Alert Triggered`;
|
||||
const incidentDescription: string =
|
||||
args.incidentDescription ||
|
||||
`${args.monitorName} has triggered an alert condition. See root cause for detailed Kubernetes resource information.`;
|
||||
|
||||
instance.data = {
|
||||
id: ObjectID.generate().toString(),
|
||||
monitorStatusId: args.offlineMonitorStatusId,
|
||||
@@ -108,8 +118,8 @@ export function buildOfflineCriteriaInstance(args: {
|
||||
],
|
||||
incidents: [
|
||||
{
|
||||
title: `${args.monitorName} - Alert Triggered`,
|
||||
description: `${args.monitorName} has triggered an alert condition.`,
|
||||
title: incidentTitle,
|
||||
description: incidentDescription,
|
||||
incidentSeverityId: args.incidentSeverityId,
|
||||
autoResolveIncident: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
@@ -118,8 +128,8 @@ export function buildOfflineCriteriaInstance(args: {
|
||||
],
|
||||
alerts: [
|
||||
{
|
||||
title: `${args.monitorName} - Alert`,
|
||||
description: `${args.monitorName} has triggered an alert condition.`,
|
||||
title: incidentTitle,
|
||||
description: incidentDescription,
|
||||
alertSeverityId: args.alertSeverityId,
|
||||
autoResolveAlert: true,
|
||||
id: ObjectID.generate().toString(),
|
||||
@@ -129,8 +139,9 @@ export function buildOfflineCriteriaInstance(args: {
|
||||
changeMonitorStatus: true,
|
||||
createIncidents: true,
|
||||
createAlerts: true,
|
||||
name: `${args.monitorName} - Unhealthy`,
|
||||
description: `Criteria for detecting unhealthy state.`,
|
||||
name: args.criteriaName || `${args.monitorName} - Unhealthy`,
|
||||
description:
|
||||
args.criteriaDescription || `Criteria for detecting unhealthy state.`,
|
||||
};
|
||||
|
||||
return instance;
|
||||
@@ -239,6 +250,11 @@ const crashLoopBackOffTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 5,
|
||||
incidentTitle: `[K8s] CrashLoopBackOff Detected - ${args.monitorName}`,
|
||||
incidentDescription: `A container in the Kubernetes cluster is repeatedly crashing and restarting (CrashLoopBackOff). The container restart count has exceeded the threshold of 5 restarts. Check the root cause for the specific pod, container, and node details.`,
|
||||
criteriaName: "CrashLoopBackOff - Container Restarts > 5",
|
||||
criteriaDescription:
|
||||
"Triggers when any container restart count exceeds 5 in the monitoring window, indicating a CrashLoopBackOff condition.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -278,6 +294,11 @@ const podPendingTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] Pods Stuck in Pending - ${args.monitorName}`,
|
||||
incidentDescription: `One or more pods in the Kubernetes cluster are stuck in Pending phase and cannot be scheduled. This typically indicates insufficient cluster resources, node affinity constraints, or unbound PersistentVolumeClaims. Check the root cause for specific pod and scheduling details.`,
|
||||
criteriaName: "Pods Pending - Count > 0",
|
||||
criteriaDescription:
|
||||
"Triggers when any pods are in Pending phase, unable to be scheduled.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -316,6 +337,11 @@ const nodeNotReadyTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.EqualTo,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] Node Not Ready - ${args.monitorName}`,
|
||||
incidentDescription: `A Kubernetes node has transitioned to NotReady state. This is a critical condition that affects all pods scheduled on this node. Check the root cause for the specific node name, conditions, and recommended actions.`,
|
||||
criteriaName: "Node NotReady - Condition = 0",
|
||||
criteriaDescription:
|
||||
"Triggers when any node reports a NotReady condition (value 0).",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -353,6 +379,11 @@ const highCpuTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 90,
|
||||
incidentTitle: `[K8s] High CPU Utilization (>90%) - ${args.monitorName}`,
|
||||
incidentDescription: `Node CPU utilization has exceeded 90% in the Kubernetes cluster. Sustained high CPU usage can cause pod throttling, increased latency, and potential node instability. Check the root cause for the specific node and top CPU-consuming workloads.`,
|
||||
criteriaName: "High CPU - Utilization > 90%",
|
||||
criteriaDescription:
|
||||
"Triggers when average node CPU utilization exceeds 90% over the monitoring window.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -390,6 +421,11 @@ const highMemoryTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 85,
|
||||
incidentTitle: `[K8s] High Memory Utilization (>85%) - ${args.monitorName}`,
|
||||
incidentDescription: `Node memory utilization has exceeded 85% in the Kubernetes cluster. High memory usage can lead to OOMKilled pods, node instability, and potential evictions. Check the root cause for the specific node and top memory-consuming workloads.`,
|
||||
criteriaName: "High Memory - Utilization > 85%",
|
||||
criteriaDescription:
|
||||
"Triggers when average node memory utilization exceeds 85% over the monitoring window.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -428,6 +464,11 @@ const deploymentReplicaMismatchTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] Deployment Replica Mismatch - ${args.monitorName}`,
|
||||
incidentDescription: `A Kubernetes deployment has unavailable replicas — the desired replica count does not match the available count. This may indicate a failed rollout, image pull errors, insufficient resources, or pod crash loops. Check the root cause for the specific deployment and replica details.`,
|
||||
criteriaName: "Replica Mismatch - Unavailable > 0",
|
||||
criteriaDescription:
|
||||
"Triggers when any deployment has unavailable replicas.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -465,6 +506,11 @@ const jobFailuresTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] Job Failure Detected - ${args.monitorName}`,
|
||||
incidentDescription: `A Kubernetes Job has one or more failed pods. This indicates the job's workload is failing to complete successfully. Check the root cause for the specific job name, failed pod details, and error information.`,
|
||||
criteriaName: "Job Failures - Failed Pods > 0",
|
||||
criteriaDescription:
|
||||
"Triggers when any Kubernetes Job has failed pods.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -503,6 +549,11 @@ const etcdNoLeaderTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.EqualTo,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] CRITICAL: etcd No Leader - ${args.monitorName}`,
|
||||
incidentDescription: `The etcd cluster has no elected leader. This is a critical cluster health issue that can cause the Kubernetes API server to become unavailable. All cluster operations (scheduling, deployments, service discovery) will be affected.`,
|
||||
criteriaName: "etcd No Leader - Has Leader = 0",
|
||||
criteriaDescription:
|
||||
"Triggers immediately when etcd reports no elected leader.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -541,6 +592,11 @@ const apiServerThrottlingTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] CRITICAL: API Server Throttling - ${args.monitorName}`,
|
||||
incidentDescription: `The Kubernetes API server is dropping requests due to throttling. This indicates the API server is overloaded and cannot process all incoming requests, affecting cluster operations.`,
|
||||
criteriaName: "API Server Throttling - Dropped Requests > 0",
|
||||
criteriaDescription:
|
||||
"Triggers when the API server reports any dropped requests.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -579,6 +635,11 @@ const schedulerBacklogTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] Scheduler Backlog - ${args.monitorName}`,
|
||||
incidentDescription: `The Kubernetes scheduler has a backlog of pods waiting to be scheduled. This indicates the scheduler is unable to find suitable nodes for pending pods, possibly due to resource constraints or scheduling conflicts.`,
|
||||
criteriaName: "Scheduler Backlog - Pending Pods > 0",
|
||||
criteriaDescription:
|
||||
"Triggers when there are pods waiting to be scheduled for more than 5 minutes.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -616,6 +677,11 @@ const highDiskUsageTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 90,
|
||||
incidentTitle: `[K8s] High Disk Usage (>90%) - ${args.monitorName}`,
|
||||
incidentDescription: `Node disk/filesystem usage has exceeded 90% capacity. High disk usage can lead to pod evictions, inability to pull new container images, and node instability. Check the root cause for the specific node and disk usage details.`,
|
||||
criteriaName: "High Disk - Usage > 90%",
|
||||
criteriaDescription:
|
||||
"Triggers when average node filesystem usage exceeds 90% capacity.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
@@ -654,6 +720,11 @@ const daemonSetUnavailableTemplate: KubernetesAlertTemplate = {
|
||||
metricAlias,
|
||||
filterType: FilterType.GreaterThan,
|
||||
value: 0,
|
||||
incidentTitle: `[K8s] DaemonSet Unavailable Nodes - ${args.monitorName}`,
|
||||
incidentDescription: `A DaemonSet has nodes where the daemon pod is not running as expected. This indicates misscheduled or unavailable daemon pods, which may affect cluster-wide services like logging, monitoring, or networking.`,
|
||||
criteriaName: "DaemonSet Unavailable - Misscheduled > 0",
|
||||
criteriaDescription:
|
||||
"Triggers when a DaemonSet has nodes where daemon pods are not properly scheduled.",
|
||||
}),
|
||||
onlineCriteriaInstance: buildOnlineCriteriaInstance({
|
||||
onlineMonitorStatusId: args.onlineMonitorStatusId,
|
||||
|
||||
@@ -3,6 +3,25 @@ import InBetween from "../../BaseDatabase/InBetween";
|
||||
import MonitorEvaluationSummary from "../MonitorEvaluationSummary";
|
||||
import MetricsViewConfig from "../../Metrics/MetricsViewConfig";
|
||||
import ObjectID from "../../ObjectID";
|
||||
import Dictionary from "../../Dictionary";
|
||||
|
||||
export interface KubernetesAffectedResource {
|
||||
podName?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
nodeName?: string | undefined;
|
||||
containerName?: string | undefined;
|
||||
workloadType?: string | undefined;
|
||||
workloadName?: string | undefined;
|
||||
metricValue: number;
|
||||
}
|
||||
|
||||
export interface KubernetesResourceBreakdown {
|
||||
clusterName: string;
|
||||
metricName: string;
|
||||
metricFriendlyName: string;
|
||||
affectedResources: Array<KubernetesAffectedResource>;
|
||||
attributes: Dictionary<string>;
|
||||
}
|
||||
|
||||
export default interface MetricMonitorResponse {
|
||||
projectId: ObjectID;
|
||||
@@ -11,4 +30,5 @@ export default interface MetricMonitorResponse {
|
||||
metricViewConfig: MetricsViewConfig;
|
||||
monitorId: ObjectID;
|
||||
evaluationSummary?: MonitorEvaluationSummary | undefined;
|
||||
kubernetesResourceBreakdown?: KubernetesResourceBreakdown | undefined;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
|
||||
curve={props.curve || ChartCurve.MONOTONE}
|
||||
syncid={props.sync ? props.syncid : undefined}
|
||||
yAxisWidth={60}
|
||||
onValueChange={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,6 +69,7 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
|
||||
showTooltip={true}
|
||||
yAxisWidth={60}
|
||||
syncid={props.sync ? props.syncid : undefined}
|
||||
onValueChange={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
|
||||
curve={props.curve}
|
||||
syncid={props.sync ? props.syncid : undefined}
|
||||
yAxisWidth={60}
|
||||
onValueChange={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,6 +55,18 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
|
||||
label: "Bar Chart",
|
||||
value: DashboardChartType.Bar,
|
||||
},
|
||||
{
|
||||
label: "Area Chart",
|
||||
value: DashboardChartType.Area,
|
||||
},
|
||||
{
|
||||
label: "Stacked Area Chart",
|
||||
value: DashboardChartType.StackedArea,
|
||||
},
|
||||
{
|
||||
label: "Pie Chart",
|
||||
value: DashboardChartType.Pie,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -66,6 +78,16 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
|
||||
id: "metricQueryConfig",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Additional Queries",
|
||||
description:
|
||||
"Add multiple metric queries to overlay on the same chart",
|
||||
required: false,
|
||||
type: ComponentInputType.MetricsQueryConfigs,
|
||||
id: "metricQueryConfigs",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Chart Title",
|
||||
description: "The title of the chart",
|
||||
@@ -98,6 +120,26 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
|
||||
id: "legendUnit",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Warning Threshold",
|
||||
description:
|
||||
"A horizontal line will be drawn at this value in yellow to indicate a warning level",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "warningThreshold",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Critical Threshold",
|
||||
description:
|
||||
"A horizontal line will be drawn at this value in red to indicate a critical level",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "criticalThreshold",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
|
||||
99
Common/Utils/Dashboard/Components/DashboardGaugeComponent.ts
Normal file
99
Common/Utils/Dashboard/Components/DashboardGaugeComponent.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import DashboardGaugeComponent from "../../../Types/Dashboard/DashboardComponents/DashboardGaugeComponent";
|
||||
import { ObjectType } from "../../../Types/JSON";
|
||||
import ObjectID from "../../../Types/ObjectID";
|
||||
import DashboardBaseComponentUtil from "./DashboardBaseComponent";
|
||||
import {
|
||||
ComponentArgument,
|
||||
ComponentInputType,
|
||||
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
||||
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
||||
|
||||
export default class DashboardGaugeComponentUtil extends DashboardBaseComponentUtil {
|
||||
public static override getDefaultComponent(): DashboardGaugeComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.Gauge,
|
||||
widthInDashboardUnits: 3,
|
||||
heightInDashboardUnits: 3,
|
||||
topInDashboardUnits: 0,
|
||||
leftInDashboardUnits: 0,
|
||||
componentId: ObjectID.generate(),
|
||||
minHeightInDashboardUnits: 2,
|
||||
minWidthInDashboardUnits: 2,
|
||||
arguments: {
|
||||
metricQueryConfig: {
|
||||
metricQueryData: {
|
||||
filterData: {},
|
||||
groupBy: undefined,
|
||||
},
|
||||
},
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static override getComponentConfigArguments(): Array<
|
||||
ComponentArgument<DashboardGaugeComponent>
|
||||
> {
|
||||
const componentArguments: Array<
|
||||
ComponentArgument<DashboardGaugeComponent>
|
||||
> = [];
|
||||
|
||||
componentArguments.push({
|
||||
name: "Gauge Configuration",
|
||||
description: "Please select the metric to display on the gauge",
|
||||
required: true,
|
||||
type: ComponentInputType.MetricsQueryConfig,
|
||||
id: "metricQueryConfig",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Gauge Title",
|
||||
description: "The title of the gauge",
|
||||
required: false,
|
||||
type: ComponentInputType.Text,
|
||||
id: "gaugeTitle",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Min Value",
|
||||
description: "The minimum value of the gauge",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "minValue",
|
||||
placeholder: "0",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Max Value",
|
||||
description: "The maximum value of the gauge",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "maxValue",
|
||||
placeholder: "100",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Warning Threshold",
|
||||
description:
|
||||
"Values above this threshold will be shown in yellow",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "warningThreshold",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Critical Threshold",
|
||||
description:
|
||||
"Values above this threshold will be shown in red",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "criticalThreshold",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
69
Common/Utils/Dashboard/Components/DashboardTableComponent.ts
Normal file
69
Common/Utils/Dashboard/Components/DashboardTableComponent.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import DashboardTableComponent from "../../../Types/Dashboard/DashboardComponents/DashboardTableComponent";
|
||||
import { ObjectType } from "../../../Types/JSON";
|
||||
import ObjectID from "../../../Types/ObjectID";
|
||||
import DashboardBaseComponentUtil from "./DashboardBaseComponent";
|
||||
import {
|
||||
ComponentArgument,
|
||||
ComponentInputType,
|
||||
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
||||
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
||||
|
||||
export default class DashboardTableComponentUtil extends DashboardBaseComponentUtil {
|
||||
public static override getDefaultComponent(): DashboardTableComponent {
|
||||
return {
|
||||
_type: ObjectType.DashboardComponent,
|
||||
componentType: DashboardComponentType.Table,
|
||||
widthInDashboardUnits: 6,
|
||||
heightInDashboardUnits: 4,
|
||||
topInDashboardUnits: 0,
|
||||
leftInDashboardUnits: 0,
|
||||
componentId: ObjectID.generate(),
|
||||
minHeightInDashboardUnits: 3,
|
||||
minWidthInDashboardUnits: 4,
|
||||
arguments: {
|
||||
metricQueryConfig: {
|
||||
metricQueryData: {
|
||||
filterData: {},
|
||||
groupBy: undefined,
|
||||
},
|
||||
},
|
||||
maxRows: 20,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static override getComponentConfigArguments(): Array<
|
||||
ComponentArgument<DashboardTableComponent>
|
||||
> {
|
||||
const componentArguments: Array<
|
||||
ComponentArgument<DashboardTableComponent>
|
||||
> = [];
|
||||
|
||||
componentArguments.push({
|
||||
name: "Table Configuration",
|
||||
description: "Please select the metrics to display in the table",
|
||||
required: true,
|
||||
type: ComponentInputType.MetricsQueryConfig,
|
||||
id: "metricQueryConfig",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Table Title",
|
||||
description: "The title of the table",
|
||||
required: false,
|
||||
type: ComponentInputType.Text,
|
||||
id: "tableTitle",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Max Rows",
|
||||
description: "Maximum number of rows to display",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "maxRows",
|
||||
placeholder: "20",
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export default class DashboardTextComponentUtil extends DashboardBaseComponentUt
|
||||
isBold: false,
|
||||
isItalic: false,
|
||||
isUnderline: false,
|
||||
isMarkdown: false,
|
||||
},
|
||||
componentId: ObjectID.generate(),
|
||||
minHeightInDashboardUnits: 1,
|
||||
@@ -71,6 +72,16 @@ export default class DashboardTextComponentUtil extends DashboardBaseComponentUt
|
||||
placeholder: "false",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Markdown",
|
||||
description:
|
||||
"Enable markdown rendering (headers, links, lists, code blocks, tables, images)",
|
||||
required: false,
|
||||
type: ComponentInputType.Boolean,
|
||||
id: "isMarkdown",
|
||||
placeholder: "false",
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,26 @@ export default class DashboardValueComponentUtil extends DashboardBaseComponentU
|
||||
id: "metricQueryConfig",
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Warning Threshold",
|
||||
description:
|
||||
"Values above this threshold will be shown with a yellow background",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "warningThreshold",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
componentArguments.push({
|
||||
name: "Critical Threshold",
|
||||
description:
|
||||
"Values above this threshold will be shown with a red background",
|
||||
required: false,
|
||||
type: ComponentInputType.Number,
|
||||
id: "criticalThreshold",
|
||||
isAdvanced: true,
|
||||
});
|
||||
|
||||
return componentArguments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import DashboardBaseComponent from "../../../Types/Dashboard/DashboardComponents
|
||||
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
||||
import BadDataException from "../../../Types/Exception/BadDataException";
|
||||
import DashboardChartComponentUtil from "./DashboardChartComponent";
|
||||
import DashboardGaugeComponentUtil from "./DashboardGaugeComponent";
|
||||
import DashboardTableComponentUtil from "./DashboardTableComponent";
|
||||
import DashboardTextComponentUtil from "./DashboardTextComponent";
|
||||
import DashboardValueComponentUtil from "./DashboardValueComponent";
|
||||
|
||||
@@ -28,6 +30,18 @@ export default class DashboardComponentsUtil {
|
||||
>;
|
||||
}
|
||||
|
||||
if (dashboardComponentType === DashboardComponentType.Table) {
|
||||
return DashboardTableComponentUtil.getComponentConfigArguments() as Array<
|
||||
ComponentArgument<DashboardBaseComponent>
|
||||
>;
|
||||
}
|
||||
|
||||
if (dashboardComponentType === DashboardComponentType.Gauge) {
|
||||
return DashboardGaugeComponentUtil.getComponentConfigArguments() as Array<
|
||||
ComponentArgument<DashboardBaseComponent>
|
||||
>;
|
||||
}
|
||||
|
||||
throw new BadDataException(
|
||||
`Unknown dashboard component type: ${dashboardComponentType}`,
|
||||
);
|
||||
|
||||
@@ -45,6 +45,13 @@ import ExceptionInstance from "Common/Models/AnalyticsModels/ExceptionInstance";
|
||||
import MonitorStepKubernetesMonitor, {
|
||||
KubernetesResourceFilters,
|
||||
} from "Common/Types/Monitor/MonitorStepKubernetesMonitor";
|
||||
import {
|
||||
KubernetesResourceBreakdown,
|
||||
KubernetesAffectedResource,
|
||||
} from "Common/Types/Monitor/MetricMonitor/MetricMonitorResponse";
|
||||
import { getKubernetesMetricByMetricName } from "Common/Types/Monitor/KubernetesMetricCatalog";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
|
||||
RunCron(
|
||||
"TelemetryMonitor:MonitorTelemetryMonitor",
|
||||
@@ -428,13 +435,18 @@ const monitorKubernetes: MonitorKubernetesFunction = async (data: {
|
||||
);
|
||||
|
||||
const finalResult: Array<AggregatedResult> = [];
|
||||
let kubernetesResourceBreakdown: KubernetesResourceBreakdown | undefined =
|
||||
undefined;
|
||||
|
||||
for (const queryConfig of kubernetesMonitorConfig.metricViewConfig
|
||||
.queryConfigs) {
|
||||
const metricName: string =
|
||||
(queryConfig.metricQueryData.filterData.metricName as string) || "";
|
||||
|
||||
const query: Query<Metric> = {
|
||||
projectId: data.projectId,
|
||||
time: startAndEndDate,
|
||||
name: queryConfig.metricQueryData.filterData.metricName,
|
||||
name: metricName,
|
||||
};
|
||||
|
||||
// Start with any user-defined attribute filters
|
||||
@@ -452,9 +464,9 @@ const monitorKubernetes: MonitorKubernetesFunction = async (data: {
|
||||
);
|
||||
}
|
||||
|
||||
// Add Kubernetes-specific attribute filters
|
||||
// Add Kubernetes-specific attribute filters (ClickHouse stores these with "resource." prefix)
|
||||
if (kubernetesMonitorConfig.clusterIdentifier) {
|
||||
attributes["k8s.cluster.name"] =
|
||||
attributes["resource.k8s.cluster.name"] =
|
||||
kubernetesMonitorConfig.clusterIdentifier;
|
||||
}
|
||||
|
||||
@@ -463,20 +475,20 @@ const monitorKubernetes: MonitorKubernetesFunction = async (data: {
|
||||
kubernetesMonitorConfig.resourceFilters;
|
||||
|
||||
if (resourceFilters.namespace) {
|
||||
attributes["k8s.namespace.name"] = resourceFilters.namespace;
|
||||
attributes["resource.k8s.namespace.name"] = resourceFilters.namespace;
|
||||
}
|
||||
|
||||
if (resourceFilters.nodeName) {
|
||||
attributes["k8s.node.name"] = resourceFilters.nodeName;
|
||||
attributes["resource.k8s.node.name"] = resourceFilters.nodeName;
|
||||
}
|
||||
|
||||
if (resourceFilters.podName) {
|
||||
attributes["k8s.pod.name"] = resourceFilters.podName;
|
||||
attributes["resource.k8s.pod.name"] = resourceFilters.podName;
|
||||
}
|
||||
|
||||
if (resourceFilters.workloadName && resourceFilters.workloadType) {
|
||||
const workloadType: string = resourceFilters.workloadType.toLowerCase();
|
||||
attributes[`k8s.${workloadType}.name`] = resourceFilters.workloadName;
|
||||
attributes[`resource.k8s.${workloadType}.name`] = resourceFilters.workloadName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,6 +523,114 @@ const monitorKubernetes: MonitorKubernetesFunction = async (data: {
|
||||
logger.debug(aggregatedResults);
|
||||
|
||||
finalResult.push(aggregatedResults);
|
||||
|
||||
// Fetch raw metrics to extract per-resource Kubernetes context
|
||||
try {
|
||||
const rawMetrics: Array<Metric> = await MetricService.findBy({
|
||||
query: query,
|
||||
select: {
|
||||
attributes: true,
|
||||
value: true,
|
||||
time: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
limit: 100,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (rawMetrics.length > 0) {
|
||||
const affectedResourcesMap: Map<string, KubernetesAffectedResource> =
|
||||
new Map();
|
||||
|
||||
for (const metric of rawMetrics) {
|
||||
const metricAttrs: JSONObject =
|
||||
(metric.attributes as JSONObject) || {};
|
||||
const podName: string | undefined = metricAttrs[
|
||||
"resource.k8s.pod.name"
|
||||
] as string | undefined;
|
||||
const namespace: string | undefined = metricAttrs[
|
||||
"resource.k8s.namespace.name"
|
||||
] as string | undefined;
|
||||
const nodeName: string | undefined = metricAttrs[
|
||||
"resource.k8s.node.name"
|
||||
] as string | undefined;
|
||||
const containerName: string | undefined = metricAttrs[
|
||||
"resource.k8s.container.name"
|
||||
] as string | undefined;
|
||||
|
||||
// Detect workload type and name from attributes
|
||||
let workloadType: string | undefined = undefined;
|
||||
let workloadName: string | undefined = undefined;
|
||||
|
||||
if (metricAttrs["resource.k8s.deployment.name"]) {
|
||||
workloadType = "Deployment";
|
||||
workloadName = metricAttrs["resource.k8s.deployment.name"] as string;
|
||||
} else if (metricAttrs["resource.k8s.statefulset.name"]) {
|
||||
workloadType = "StatefulSet";
|
||||
workloadName = metricAttrs["resource.k8s.statefulset.name"] as string;
|
||||
} else if (metricAttrs["resource.k8s.daemonset.name"]) {
|
||||
workloadType = "DaemonSet";
|
||||
workloadName = metricAttrs["resource.k8s.daemonset.name"] as string;
|
||||
} else if (metricAttrs["resource.k8s.job.name"]) {
|
||||
workloadType = "Job";
|
||||
workloadName = metricAttrs["resource.k8s.job.name"] as string;
|
||||
} else if (metricAttrs["resource.k8s.cronjob.name"]) {
|
||||
workloadType = "CronJob";
|
||||
workloadName = metricAttrs["resource.k8s.cronjob.name"] as string;
|
||||
} else if (metricAttrs["resource.k8s.replicaset.name"]) {
|
||||
workloadType = "ReplicaSet";
|
||||
workloadName = metricAttrs["resource.k8s.replicaset.name"] as string;
|
||||
}
|
||||
|
||||
// Build unique key for deduplication
|
||||
const resourceKey: string = [
|
||||
podName || "",
|
||||
namespace || "",
|
||||
nodeName || "",
|
||||
containerName || "",
|
||||
workloadName || "",
|
||||
].join("|");
|
||||
|
||||
const metricValue: number =
|
||||
typeof metric.value === "number"
|
||||
? metric.value
|
||||
: Number(metric.value) || 0;
|
||||
|
||||
// Keep the highest value per resource
|
||||
const existing: KubernetesAffectedResource | undefined =
|
||||
affectedResourcesMap.get(resourceKey);
|
||||
if (!existing || metricValue > existing.metricValue) {
|
||||
affectedResourcesMap.set(resourceKey, {
|
||||
podName: podName || undefined,
|
||||
namespace: namespace || undefined,
|
||||
nodeName: nodeName || undefined,
|
||||
containerName: containerName || undefined,
|
||||
workloadType: workloadType || undefined,
|
||||
workloadName: workloadName || undefined,
|
||||
metricValue: metricValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const metricDef = getKubernetesMetricByMetricName(metricName);
|
||||
|
||||
kubernetesResourceBreakdown = {
|
||||
clusterName: kubernetesMonitorConfig.clusterIdentifier,
|
||||
metricName: metricName,
|
||||
metricFriendlyName: metricDef?.friendlyName || metricName,
|
||||
affectedResources: Array.from(affectedResourcesMap.values()),
|
||||
attributes: attributes,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to fetch Kubernetes resource breakdown");
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -519,6 +639,7 @@ const monitorKubernetes: MonitorKubernetesFunction = async (data: {
|
||||
startAndEndDate: startAndEndDate,
|
||||
metricResult: finalResult,
|
||||
monitorId: data.monitorId,
|
||||
kubernetesResourceBreakdown: kubernetesResourceBreakdown,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user