Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Alert, AlertStates, K8sResourceCommon } from '@openshift-console/dynami
import {
computeAlertFingerprint,
getAlertFingerprintPrefix,
matchesProposal,
matchesAgenticRun,
} from './alert-identifier';

const makeAlert = (labels: Record<string, string>): Alert =>
Expand All @@ -17,9 +17,9 @@ const makeAlert = (labels: Record<string, string>): Alert =>
rule: { id: '1', alerts: [], labels: {}, name: 'TestAlert', query: '', duration: 0 },
}) as unknown as Alert;

const makeProposal = (labels: Record<string, string>): K8sResourceCommon => ({
const makeAgenticRun = (labels: Record<string, string>): K8sResourceCommon => ({
apiVersion: 'agentic.openshift.io/v1alpha1',
kind: 'Proposal',
kind: 'AgenticRun',
metadata: {
name: 'test-proposal',
namespace: 'openshift-lightspeed',
Expand Down Expand Up @@ -72,28 +72,28 @@ describe('matchesProposal', () => {
it('matches by fingerprint prefix', () => {
const alert = makeAlert({ severity: 'critical' });
const fp8 = getAlertFingerprintPrefix(alert.labels);
const proposal = makeProposal({
const proposal = makeAgenticRun({
'agentic.openshift.io/alert-fingerprint': fp8,
'agentic.openshift.io/source': 'alertmanager',
});
expect(matchesProposal(alert, proposal)).toBe(true);
expect(matchesAgenticRun(alert, proposal)).toBe(true);
});

it('does not match when fingerprint differs', () => {
const alert = makeAlert({ severity: 'critical' });
const proposal = makeProposal({
const proposal = makeAgenticRun({
'agentic.openshift.io/alert-fingerprint': 'deadbeef',
'agentic.openshift.io/source': 'alertmanager',
});
expect(matchesProposal(alert, proposal)).toBe(false);
expect(matchesAgenticRun(alert, proposal)).toBe(false);
});

it('does not match when proposal has no fingerprint label', () => {
const alert = makeAlert({ severity: 'warning' });
const proposal = makeProposal({
const proposal = makeAgenticRun({
'agentic.openshift.io/alert-name': 'testalert',
'agentic.openshift.io/alert-severity': 'warning',
});
expect(matchesProposal(alert, proposal)).toBe(false);
expect(matchesAgenticRun(alert, proposal)).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
FINGERPRINT_PREFIX_LEN,
FNV_OFFSET_BASIS,
FNV_PRIME,
PROPOSAL_LABEL_FINGERPRINT,
AGENTIC_RUN_LABEL_FINGERPRINT,
SEPARATOR_BYTE,
UINT64_MASK,
} from './constants';
Expand Down Expand Up @@ -44,8 +44,8 @@ export const computeAlertFingerprint = (labels: Record<string, string>): string
export const getAlertFingerprintPrefix = (labels: Record<string, string>): string =>
computeAlertFingerprint(labels).slice(0, FINGERPRINT_PREFIX_LEN);

export const matchesProposal = (alert: Alert, proposal: K8sResourceCommon): boolean => {
const proposalFp = proposal.metadata?.labels?.[PROPOSAL_LABEL_FINGERPRINT];
export const matchesAgenticRun = (alert: Alert, proposal: K8sResourceCommon): boolean => {
const proposalFp = proposal.metadata?.labels?.[AGENTIC_RUN_LABEL_FINGERPRINT];
if (!proposalFp) {
return false;
}
Expand Down
15 changes: 15 additions & 0 deletions web/src/components/agentic-runs/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// FNV-1a 64-bit hash constants (Prometheus alerts fingerprint algorithm)
export const SEPARATOR_BYTE = 0xff;
export const FNV_OFFSET_BASIS = 14695981039346656037n;
export const FNV_PRIME = 1099511628211n;
export const UINT64_MASK = (1n << 64n) - 1n;
export const FINGERPRINT_PREFIX_LEN = 8;

// Proposal CR label keys
export const AGENTIC_RUN_LABEL_FINGERPRINT = 'agentic.openshift.io/alert-fingerprint';
export const AGENTIC_RUN_LABEL_SOURCE = 'agentic.openshift.io/source';

// Proposal CR values
export const AGENTIC_RUN_SOURCE_ALERTMANAGER = 'alertmanager';
export const AGENTIC_RUN_NAMESPACE = 'openshift-lightspeed';
export const AGENTIC_RUN_STALE_TIME = 30 * 1000;
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,45 @@ import { Alert, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'
import { k8sListResourceItems } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { ProposalModel } from '../console/models';
import { AgenticRunModel } from '../console/models';
import { getAlertFingerprintPrefix } from './alert-identifier';
import {
PROPOSAL_LABEL_FINGERPRINT,
PROPOSAL_LABEL_SOURCE,
PROPOSAL_NAMESPACE,
PROPOSAL_SOURCE_ALERTMANAGER,
PROPOSAL_STALE_TIME,
AGENTIC_RUN_LABEL_FINGERPRINT,
AGENTIC_RUN_LABEL_SOURCE,
AGENTIC_RUN_NAMESPACE,
AGENTIC_RUN_SOURCE_ALERTMANAGER,
AGENTIC_RUN_STALE_TIME,
} from './constants';

const buildQueryFn = (namespace: string, alertFingerprint: string) => () =>
k8sListResourceItems<K8sResourceCommon>({
model: ProposalModel,
model: AgenticRunModel,
queryParams: {
ns: namespace,
labelSelector: {
matchLabels: {
[PROPOSAL_LABEL_FINGERPRINT]: alertFingerprint,
[PROPOSAL_LABEL_SOURCE]: PROPOSAL_SOURCE_ALERTMANAGER,
[AGENTIC_RUN_LABEL_FINGERPRINT]: alertFingerprint,
[AGENTIC_RUN_LABEL_SOURCE]: AGENTIC_RUN_SOURCE_ALERTMANAGER,
},
},
},
});

export const useProposalCheck = (alert: Alert) => {
export const useAgenticRunCheck = (alert: Alert) => {
const alertFingerprint = useMemo(() => getAlertFingerprintPrefix(alert.labels), [alert.labels]);
const alertNamespace = alert.labels?.namespace;
const hasDistinctAlertNamespace = !!alertNamespace && alertNamespace !== PROPOSAL_NAMESPACE;
const hasDistinctAlertNamespace = !!alertNamespace && alertNamespace !== AGENTIC_RUN_NAMESPACE;
const [shouldFetch, setShouldFetch] = useState(false);

const {
data: defaultNsData,
isFetching: defaultNsFetching,
isError: defaultNsError,
} = useQuery({
queryKey: ['proposal-check', PROPOSAL_NAMESPACE, alertFingerprint],
queryFn: buildQueryFn(PROPOSAL_NAMESPACE, alertFingerprint),
queryKey: ['agentic-run-check', AGENTIC_RUN_NAMESPACE, alertFingerprint],
queryFn: buildQueryFn(AGENTIC_RUN_NAMESPACE, alertFingerprint),
enabled: shouldFetch,
staleTime: PROPOSAL_STALE_TIME,
staleTime: AGENTIC_RUN_STALE_TIME,
retry: false,
});

Expand All @@ -49,18 +49,18 @@ export const useProposalCheck = (alert: Alert) => {
isFetching: alertNsFetching,
isError: alertNsError,
} = useQuery({
queryKey: ['proposal-check', alertNamespace, alertFingerprint],
queryKey: ['agentic-run-check', alertNamespace, alertFingerprint],
queryFn: buildQueryFn(alertNamespace!, alertFingerprint),
enabled: shouldFetch && hasDistinctAlertNamespace,
staleTime: PROPOSAL_STALE_TIME,
staleTime: AGENTIC_RUN_STALE_TIME,
retry: false,
});

const prefetch = useCallback(() => setShouldFetch(true), []);

const proposals = useMemo(() => {
const agenticRuns = useMemo(() => {
const matchesFp = (p: K8sResourceCommon) =>
p.metadata?.labels?.[PROPOSAL_LABEL_FINGERPRINT] === alertFingerprint;
p.metadata?.labels?.[AGENTIC_RUN_LABEL_FINGERPRINT] === alertFingerprint;

return [...(defaultNsData ?? []).filter(matchesFp), ...(alertNsData ?? []).filter(matchesFp)];
}, [defaultNsData, alertNsData, alertFingerprint]);
Expand All @@ -69,8 +69,8 @@ export const useProposalCheck = (alert: Alert) => {
const isError = defaultNsError && (hasDistinctAlertNamespace ? alertNsError : true);

return {
proposals,
hasProposal: proposals.length > 0,
agenticRuns,
hasAgenticRun: agenticRuns.length > 0,
isFetching,
isError,
prefetch,
Expand Down
17 changes: 0 additions & 17 deletions web/src/components/ai-proposals/constants.ts

This file was deleted.

21 changes: 10 additions & 11 deletions web/src/components/alerting/AlertList/AlertTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,20 @@ import {
} from '../../../components/hooks/usePerspective';
import { useMonitoringNamespace } from '../../hooks/useMonitoringNamespace';
import { DataTestIDs } from '../../data-test';
import { useProposalCheck } from '../../ai-proposals/useProposalCheck';
import { useAgenticRunCheck } from '../../agentic-runs/useAgenticRunCheck';
import CustomIcon from '../../CustomIcon';

const getProposalUrl = (namespace: string, name: string): string => {
return `/lightspeed/proposals/${namespace}/${name}`;
const getAgenticRunUrl = (namespace: string, name: string): string => {
return `/lightspeed/runs/${namespace}/${name}`;
};

const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => {
const { t } = useTranslation(process.env.I18N_NAMESPACE);
const { perspective } = usePerspective();
const navigate = useNavigate();
const { namespace } = useMonitoringNamespace();

const state = alertState(alert);
const { proposals, hasProposal, prefetch, isFetching } = useProposalCheck(alert);
const { agenticRuns, hasAgenticRun, prefetch, isFetching } = useAgenticRunCheck(alert);

const title: string = alert.annotations?.description || alert.annotations?.message;

Expand All @@ -62,16 +61,16 @@ const AlertTableRow: FC<{ alert: Alert }> = ({ alert }) => {
);
}

if (hasProposal) {
const proposal = proposals[0];
const proposalName = proposal.metadata?.name;
if (proposalName) {
const proposalUrl = getProposalUrl(proposal.metadata.namespace, proposalName);
if (hasAgenticRun) {
const run = agenticRuns[0];
const runName = run.metadata?.name;
if (runName) {
const runUrl = getAgenticRunUrl(run.metadata.namespace, runName);
dropdownItems.push(
<DropdownItem
key="view-ai-investigation"
icon={<CustomIcon name="ai-experience" />}
onClick={() => navigate(proposalUrl)}
onClick={() => navigate(runUrl)}
data-test={DataTestIDs.ViewAIInvestigationDropdownItem}
>
{t('View AI Investigation')}
Expand Down
16 changes: 8 additions & 8 deletions web/src/components/console/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,16 @@ export const StatefulSetModel = {
kind: 'StatefulSet',
};

export const ProposalModel: K8sModel = {
kind: 'Proposal',
label: 'Proposal',
labelKey: 'Proposal',
labelPlural: 'Proposals',
labelPluralKey: 'Proposals',
export const AgenticRunModel: K8sModel = {
kind: 'AgenticRun',
label: 'AgenticRun',
labelKey: 'AgenticRun',
labelPlural: 'AgenticRuns',
labelPluralKey: 'AgenticRuns',
apiGroup: 'agentic.openshift.io',
apiVersion: 'v1alpha1',
abbr: 'PP',
abbr: 'AGR',
namespaced: true,
crd: true,
plural: 'proposals',
plural: 'agenticruns',
};