-
Notifications
You must be signed in to change notification settings - Fork 60
OU-1409: feat: fetch lazily ai proposal for each row in the alert list #1014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jgbernalp
wants to merge
1
commit into
openshift:main
Choose a base branch
from
jgbernalp:add-ai-investigation-action-to-alerts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+363
−22
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // SVG path data sourced from @rhds/icons (CC-BY-4.0 licensed) | ||
| import type { FC } from 'react'; | ||
|
|
||
| const icons = { | ||
| 'ai-experience': { | ||
| viewBox: '0 0 32 32', | ||
| d: 'M26.031 16.962a11.932 11.932 0 0 1-10.999-11c-.041-.52-.516-.961-1.038-.961s-.996.442-1.038.962A11.93 11.93 0 0 1 1.965 16.962c-.524.037-.97.514-.97 1.038 0 .521.442.997.962 1.038a11.933 11.933 0 0 1 11 11c.041.52.516.961 1.037.961.522 0 .997-.442 1.039-.962A11.931 11.931 0 0 1 26.03 19.038c.52-.042.962-.516.962-1.038 0-.521-.442-.997-.962-1.038Zm-12.037 8.803A13.888 13.888 0 0 0 6.228 18a13.898 13.898 0 0 0 7.767-7.766A13.888 13.888 0 0 0 21.76 18a13.899 13.899 0 0 0-7.767 7.766ZM30.502 7c0 .29-.209.536-.498.59a5.523 5.523 0 0 0-4.417 4.417c-.054.289-.3.498-.59.498s-.536-.21-.59-.498A5.52 5.52 0 0 0 19.99 7.59c-.289-.054-.498-.3-.498-.59s.21-.536.498-.59a5.52 5.52 0 0 0 4.417-4.417c.054-.289.301-.498.59-.498s.536.21.59.498a5.523 5.523 0 0 0 4.417 4.417c.289.054.498.3.498.59Z', | ||
| }, | ||
| } as const; | ||
|
|
||
| export type CustomIconName = keyof typeof icons; | ||
|
|
||
| type CustomIconProps = { | ||
| name: CustomIconName; | ||
| className?: string; | ||
| }; | ||
|
|
||
| const CustomIcon: FC<CustomIconProps> = ({ name, className }) => { | ||
| const icon = icons[name]; | ||
| return ( | ||
| <svg | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| viewBox={icon.viewBox} | ||
| fill="currentColor" | ||
| width="1em" | ||
| height="1em" | ||
| className={className} | ||
| > | ||
| <path d={icon.d} /> | ||
| </svg> | ||
| ); | ||
| }; | ||
|
|
||
| export default CustomIcon; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({ | ||
| ...jest.requireActual('@openshift-console/dynamic-plugin-sdk/lib/api/common-types'), | ||
| })); | ||
|
|
||
| import { Alert, AlertStates, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; | ||
| import { | ||
| computeAlertFingerprint, | ||
| getAlertFingerprintPrefix, | ||
| matchesProposal, | ||
| } from './alert-identifier'; | ||
|
|
||
| const makeAlert = (labels: Record<string, string>): Alert => | ||
| ({ | ||
| labels: { alertname: 'TestAlert', ...labels }, | ||
| annotations: {}, | ||
| state: AlertStates.Firing, | ||
| rule: { id: '1', alerts: [], labels: {}, name: 'TestAlert', query: '', duration: 0 }, | ||
| }) as unknown as Alert; | ||
|
|
||
| const makeProposal = (labels: Record<string, string>): K8sResourceCommon => ({ | ||
| apiVersion: 'agentic.openshift.io/v1alpha1', | ||
| kind: 'Proposal', | ||
| metadata: { | ||
| name: 'test-proposal', | ||
| namespace: 'openshift-lightspeed', | ||
| labels, | ||
| }, | ||
| }); | ||
|
|
||
| describe('computeAlertFingerprint', () => { | ||
| it('returns a 16-char hex string', () => { | ||
| const fp = computeAlertFingerprint({ alertname: 'TestAlert', severity: 'critical' }); | ||
| expect(fp).toHaveLength(16); | ||
| expect(fp).toMatch(/^[0-9a-f]{16}$/); | ||
| }); | ||
|
|
||
| it('is deterministic for the same labels', () => { | ||
| const labels = { alertname: 'HighMemory', severity: 'warning', namespace: 'default' }; | ||
| expect(computeAlertFingerprint(labels)).toBe(computeAlertFingerprint(labels)); | ||
| }); | ||
|
|
||
| it('produces the same result regardless of label insertion order', () => { | ||
| const fp1 = computeAlertFingerprint({ b: '2', a: '1' }); | ||
| const fp2 = computeAlertFingerprint({ a: '1', b: '2' }); | ||
| expect(fp1).toBe(fp2); | ||
| }); | ||
|
|
||
| it('produces different fingerprints for different labels', () => { | ||
| const fp1 = computeAlertFingerprint({ alertname: 'AlertA' }); | ||
| const fp2 = computeAlertFingerprint({ alertname: 'AlertB' }); | ||
| expect(fp1).not.toBe(fp2); | ||
| }); | ||
|
|
||
| it('returns empty label fingerprint for empty input', () => { | ||
| const fp = computeAlertFingerprint({}); | ||
| expect(fp).toHaveLength(16); | ||
| expect(fp).toMatch(/^[0-9a-f]{16}$/); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getAlertFingerprintPrefix', () => { | ||
| it('returns the first 8 characters of the fingerprint', () => { | ||
| const labels = { alertname: 'TestAlert' }; | ||
| const fp = computeAlertFingerprint(labels); | ||
| const prefix = getAlertFingerprintPrefix(labels); | ||
| expect(prefix).toHaveLength(8); | ||
| expect(fp.startsWith(prefix)).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('matchesProposal', () => { | ||
| it('matches by fingerprint prefix', () => { | ||
| const alert = makeAlert({ severity: 'critical' }); | ||
| const fp8 = getAlertFingerprintPrefix(alert.labels); | ||
| const proposal = makeProposal({ | ||
| 'agentic.openshift.io/alert-fingerprint': fp8, | ||
| 'agentic.openshift.io/source': 'alertmanager', | ||
| }); | ||
| expect(matchesProposal(alert, proposal)).toBe(true); | ||
| }); | ||
|
|
||
| it('does not match when fingerprint differs', () => { | ||
| const alert = makeAlert({ severity: 'critical' }); | ||
| const proposal = makeProposal({ | ||
| 'agentic.openshift.io/alert-fingerprint': 'deadbeef', | ||
| 'agentic.openshift.io/source': 'alertmanager', | ||
| }); | ||
| expect(matchesProposal(alert, proposal)).toBe(false); | ||
| }); | ||
|
|
||
| it('does not match when proposal has no fingerprint label', () => { | ||
| const alert = makeAlert({ severity: 'warning' }); | ||
| const proposal = makeProposal({ | ||
| 'agentic.openshift.io/alert-name': 'testalert', | ||
| 'agentic.openshift.io/alert-severity': 'warning', | ||
| }); | ||
| expect(matchesProposal(alert, proposal)).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { Alert, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; | ||
| import { | ||
| FINGERPRINT_PREFIX_LEN, | ||
| FNV_OFFSET_BASIS, | ||
| FNV_PRIME, | ||
| PROPOSAL_LABEL_FINGERPRINT, | ||
| SEPARATOR_BYTE, | ||
| UINT64_MASK, | ||
| } from './constants'; | ||
|
|
||
| /** | ||
| * Computes the Prometheus-compatible FNV-1a 64-bit fingerprint of a label set. | ||
| * Replicates the algorithm from prometheus/common/model/signature.go: | ||
| * - Sort label names lexicographically | ||
| * - For each pair: hash(name + 0xFF + value + 0xFF) | ||
| * - Returns 16-char zero-padded hex string | ||
| */ | ||
| export const computeAlertFingerprint = (labels: Record<string, string>): string => { | ||
| const names = Object.keys(labels).sort(); | ||
|
|
||
| let hash = FNV_OFFSET_BASIS; | ||
| for (const name of names) { | ||
| const value = labels[name]; | ||
| const bytes = new TextEncoder().encode(name); | ||
| for (const b of bytes) { | ||
| hash ^= BigInt(b); | ||
| hash = (hash * FNV_PRIME) & UINT64_MASK; | ||
| } | ||
| hash ^= BigInt(SEPARATOR_BYTE); | ||
| hash = (hash * FNV_PRIME) & UINT64_MASK; | ||
|
|
||
| const valueBytes = new TextEncoder().encode(value); | ||
| for (const b of valueBytes) { | ||
| hash ^= BigInt(b); | ||
| hash = (hash * FNV_PRIME) & UINT64_MASK; | ||
| } | ||
| hash ^= BigInt(SEPARATOR_BYTE); | ||
| hash = (hash * FNV_PRIME) & UINT64_MASK; | ||
| } | ||
|
|
||
| return hash.toString(16).padStart(16, '0'); | ||
| }; | ||
|
|
||
| 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]; | ||
| if (!proposalFp) { | ||
| return false; | ||
| } | ||
| return proposalFp === getAlertFingerprintPrefix(alert.labels); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // 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 PROPOSAL_LABEL_FINGERPRINT = 'agentic.openshift.io/alert-fingerprint'; | ||
| export const PROPOSAL_LABEL_SOURCE = 'agentic.openshift.io/source'; | ||
| export const PROPOSAL_LABEL_ALERT_NAME = 'agentic.openshift.io/alert-name'; | ||
| export const PROPOSAL_LABEL_ALERT_SEVERITY = 'agentic.openshift.io/alert-severity'; | ||
|
|
||
| // Proposal CR values | ||
| export const PROPOSAL_SOURCE_ALERTMANAGER = 'alertmanager'; | ||
| export const PROPOSAL_NAMESPACE = 'openshift-lightspeed'; | ||
| export const PROPOSAL_STALE_TIME = 30 * 1000; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| 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 { getAlertFingerprintPrefix, matchesProposal } from './alert-identifier'; | ||
| import { | ||
| PROPOSAL_LABEL_FINGERPRINT, | ||
| PROPOSAL_LABEL_SOURCE, | ||
| PROPOSAL_NAMESPACE, | ||
| PROPOSAL_SOURCE_ALERTMANAGER, | ||
| PROPOSAL_STALE_TIME, | ||
| } from './constants'; | ||
|
|
||
| const buildQueryFn = (alertFingerprint: string) => () => | ||
| k8sListResourceItems<K8sResourceCommon>({ | ||
| model: ProposalModel, | ||
| queryParams: { | ||
| ns: PROPOSAL_NAMESPACE, | ||
| labelSelector: { | ||
| matchLabels: { | ||
| [PROPOSAL_LABEL_FINGERPRINT]: alertFingerprint, | ||
| [PROPOSAL_LABEL_SOURCE]: PROPOSAL_SOURCE_ALERTMANAGER, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| export const useProposalCheck = (alert: Alert) => { | ||
| const alertFingerprint = useMemo(() => getAlertFingerprintPrefix(alert.labels), [alert.labels]); | ||
| const [shouldFetch, setShouldFetch] = useState(false); | ||
|
|
||
| const { data, isFetching } = useQuery({ | ||
| queryKey: ['proposal-check', alertFingerprint], | ||
| queryFn: buildQueryFn(alertFingerprint), | ||
| enabled: shouldFetch, | ||
| staleTime: PROPOSAL_STALE_TIME, | ||
| retry: false, | ||
| }); | ||
|
|
||
| const prefetch = useCallback(() => setShouldFetch(true), []); | ||
|
|
||
| const proposals = useMemo( | ||
| () => (data ?? []).filter((p) => matchesProposal(alert, p)), | ||
| [data, alert], | ||
| ); | ||
|
|
||
| return { | ||
| proposals, | ||
| hasProposal: proposals.length > 0, | ||
| isFetching, | ||
| prefetch, | ||
| alertFingerprint, | ||
| }; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.