Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions prometheus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This plugin adds advanced charts to the details view of workload resources.

It also shows optional Argo CD Application charts for sync activity, average sync duration, and
orphaned resources. These charts appear only when Prometheus is enabled for the selected cluster
and the resource is `argoproj.io/v1alpha1` `Application`. Missing Argo CD metrics simply show the
normal no-data state.

## Enabling Charts

For the charts to be shown, Prometheus must be installed in the cluster.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getArgoCDApplicationChartConfigs } from './ArgoCDApplicationChart';

describe('Argo CD Application chart configuration', () => {
test('creates the three Application-specific charts', () => {
const configs = getArgoCDApplicationChartConfigs('argocd', 'guestbook');

expect(configs.map(config => config.key)).toEqual([
'sync-activity',
'sync-duration',
'orphaned-resources',
]);
expect(configs[0].queries.query).toContain('argocd_app_sync_total');
expect(configs[1].queries.query).toContain('argocd_app_sync_duration_seconds_total');
expect(configs[1].queries.query).toContain('argocd_app_sync_total');
expect(configs[1].queries.query).not.toContain('argocd_app_sync_duration_seconds_sum');
expect(configs[1].queries.query).not.toContain('argocd_app_sync_duration_seconds_count');
expect(configs[2].queries.query).toContain('argocd_app_orphaned_resources_count');
});

test('escapes Application labels before embedding them in PromQL', () => {
const configs = getArgoCDApplicationChartConfigs('team"one', 'app\\name');

expect(configs[0].queries.query).toContain('namespace="team\\"one"');
expect(configs[0].queries.query).toContain('name="app\\\\name"');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useTranslation } from '@kinvolk/headlamp-plugin/lib';
import { blue } from '@mui/material/colors';
import { alpha, useTheme } from '@mui/material/styles';
import { fetchMetrics } from '../../../request';
import { createDataProcessor, createTickTimestampFormatter } from '../../../util';
import Chart from '../Chart/Chart';

interface ArgoCDApplicationMetricChartProps {
refresh: boolean;
prometheusPrefix: string;
resolution: string;
subPath?: string;
timespan: string;
query: string;
metricLabel: string;
yAxisLabel: string;
CustomTooltip: (props: any) => JSX.Element | null;
}

/** Renders one Application-specific Argo CD metric using the shared chart behaviour. */
export function ArgoCDApplicationMetricChart(props: ArgoCDApplicationMetricChartProps) {
const { t } = useTranslation();
const theme = useTheme();
const xTickFormatter = createTickTimestampFormatter(props.timespan);

return (
<Chart
plots={[
{
query: props.query,
name: t(props.metricLabel),
strokeColor: alpha(blue[600], 0.8),
fillColor: alpha(blue[400], 0.1),
stackId: null,
dataProcessor: createDataProcessor(0),
},
]}
xAxisProps={{
dataKey: 'timestamp',
tickLine: false,
tick: tickProps => {
const value = xTickFormatter(tickProps.payload.value);
if (!value) return null;

return (
<g
transform={`translate(${tickProps.x},${tickProps.y})`}
fill={theme.palette.chartStyles.labelColor}
>
<text x={0} y={10} dy={0} textAnchor="middle">
{value}
</text>
</g>
);
},
}}
yAxisProps={{
domain: [0, 'auto'],
width: 80,
label: {
value: t(props.yAxisLabel),
angle: -90,
position: 'insideLeft',
style: { textAnchor: 'middle' },
},
}}
CustomTooltip={props.CustomTooltip}
fetchMetrics={fetchMetrics}
autoRefresh={props.refresh}
prometheusPrefix={props.prometheusPrefix}
interval={props.timespan}
resolution={props.resolution}
subPath={props.subPath}
/>
);
}

function escapePrometheusLabelValue(value: string) {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
}

function applicationSelector(namespace: string, name: string) {
return `namespace="${escapePrometheusLabelValue(namespace)}",name="${escapePrometheusLabelValue(
name
)}"`;
}

export function getArgoCDApplicationChartConfigs(namespace: string, name: string) {
const selector = applicationSelector(namespace, name);

return [
{
key: 'sync-activity',
label: 'Sync activity',
icon: 'mdi:sync',
queries: {
query: `sum(increase(argocd_app_sync_total{${selector}}[5m]))`,
metricLabel: 'Sync operations',
yAxisLabel: 'Operations',
},
component: ArgoCDApplicationMetricChart,
},
{
key: 'sync-duration',
label: 'Sync duration',
icon: 'mdi:timer-outline',
queries: {
query: `sum(rate(argocd_app_sync_duration_seconds_total{${selector}}[5m])) / sum(rate(argocd_app_sync_total{${selector}}[5m]))`,
metricLabel: 'Average sync duration',
yAxisLabel: 'Seconds',
},
component: ArgoCDApplicationMetricChart,
},
{
key: 'orphaned-resources',
label: 'Orphaned resources',
icon: 'mdi:alert-outline',
queries: {
query: `argocd_app_orphaned_resources_count{${selector}}`,
metricLabel: 'Orphaned resources',
yAxisLabel: 'Resources',
},
component: ArgoCDApplicationMetricChart,
},
];
}
13 changes: 13 additions & 0 deletions prometheus/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@kinvolk/headlamp-plugin/lib';
import type { KubeObject } from '@kinvolk/headlamp-plugin/lib/lib/k8s/KubeObject';
import { KarpenterChart } from '../src/components/Chart/KarpenterChart/KarpenterChart';
import { getArgoCDApplicationChartConfigs } from './components/Chart/ArgoCDApplicationChart/ArgoCDApplicationChart';
import { CapiChart } from './components/Chart/CapiChart/CapiChart';
import { DiskMetricsChart } from './components/Chart/DiskMetricsChart/DiskMetricsChart';
import { GenericMetricsChart } from './components/Chart/GenericMetricsChart/GenericMetricsChart';
Expand Down Expand Up @@ -182,6 +183,18 @@ function PrometheusMetrics(resource: KubeObject) {
return <VolcanoChart chartConfigs={getVolcanoQueueChartConfigs(name)} defaultChart="cpu" />;
}

if (resourceKind === 'Application' && resource.jsonData?.apiVersion === 'argoproj.io/v1alpha1') {
return (
<VolcanoChart
chartConfigs={getArgoCDApplicationChartConfigs(
resource.jsonData.metadata.namespace,
resource.jsonData.metadata.name
)}
defaultChart="sync-activity"
/>
);
}

// cluster api resources
if (resource.kind === 'Cluster') {
const name = resource.jsonData.metadata.name;
Expand Down
4 changes: 2 additions & 2 deletions prometheus/src/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@ describe('supportsPrometheusMetrics', () => {
],
['rejects Queues without apiVersion', { kind: 'Queue', jsonData: { kind: 'Queue' } }, false],
[
'does not show Application metrics before the chart UI is available',
'supports Argo CD Applications when the chart UI is available',
{
kind: 'Application',
jsonData: { kind: 'Application', apiVersion: 'argoproj.io/v1alpha1' },
},
false,
true,
],
[
'rejects non-Argo Applications with the same kind',
Expand Down
2 changes: 2 additions & 0 deletions prometheus/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ function getResourceApiVersion(resource?: ResourceIdentity): string | undefined
const resourceApiVersionRules: Record<string, RegExp> = {
Job: /^batch\/v1$/,
Queue: /^scheduling\.volcano\.sh\/v1beta1$/,
Application: /^argoproj\.io\/v1alpha1$/,
Cluster: /^cluster\.x-k8s\.io\//,
Machine: /^cluster\.x-k8s\.io\//,
MachineDeployment: /^cluster\.x-k8s\.io\//,
Expand Down Expand Up @@ -219,6 +220,7 @@ const ChartEnabledKinds = [
'Service',
'Revision',
'Queue',
'Application',
'Cluster',
'MachineDeployment',
'MachineSet',
Expand Down
Loading