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
6 changes: 5 additions & 1 deletion cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ module.exports = defineConfig({
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./.cypress/plugins/index.js')(on, config)
config.env.NODE_OPTIONS = '—max-old-space-size=8192';
return require('./.cypress/plugins/index.js')(on, config);
},
specPattern: '.cypress/integration/*.spec.js',
supportFile: '.cypress/support/index.js',
// Performance optimizations
numTestsKeptInMemory: 0,
experimentalMemoryManagement: true,
},
})
211 changes: 211 additions & 0 deletions public/components/AlertInsight/AlertInsight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import moment from 'moment';
import { escape } from 'lodash';
import { OPERATORS_PPL_QUERY_MAP } from '../../pages/CreateMonitor/containers/CreateMonitor/utils/whereFilters';
import {
BUCKET_UNIT_PPL_UNIT_MAP,
DEFAULT_ACTIVE_ALERTS_AI_TOP_N,
DEFAULT_DSL_QUERY_DATE_FORMAT,
DEFAULT_LOG_PATTERN_SAMPLE_SIZE,
DEFAULT_LOG_PATTERN_TOP_N,
DEFAULT_PPL_QUERY_DATE_FORMAT,
PERIOD_END_PLACEHOLDER,
PPL_SEARCH_PATH,
} from '../../pages/Dashboard/utils/constants';
import { MONITOR_TYPE, SEARCH_TYPE } from '../../utils/constants';
import { getTime } from '../../pages/MonitorDetails/components/MonitorOverview/utils/getOverviewStats';
import {
filterActiveAlerts,
findLongestStringField,
searchQuery,
} from '../../pages/Dashboard/utils/helpers';
import { getApplication, getAssistantDashboards, getClient } from '../../services';
import { dataSourceEnabled } from '../../pages/utils/helpers';

export interface AlertInsightProps {
alert: any;
alertId: string;
isAgentConfigured: boolean;
children: React.ReactElement;
datasourceId?: string;
}

export const AlertInsight: React.FC<AlertInsightProps> = (props: AlertInsightProps) => {
const { alert, children, isAgentConfigured, alertId, datasourceId } = props;
const httpClient = getClient();
const dataSourceQuery = dataSourceEnabled()
? { query: { dataSourceId: datasourceId || '' } }
: undefined;

const contextProvider = async () => {
// 1. get monitor definition
const monitorResp = await httpClient.get(
`../api/alerting/monitors/${alert.monitor_id}`,
dataSourceQuery
);
const monitorDefinition = monitorResp.resp;
// 2. If the monitor is created via visual editor, translate ui_metadata dsl filter to ppl filter
let formikToPPLFilters = [];
let pplBucketValue = 1;
let pplBucketUnitOfTime = 'HOUR';
let pplTimeField = '';
const isVisualEditorMonitor =
monitorDefinition?.ui_metadata?.search?.searchType === SEARCH_TYPE.GRAPH;
if (isVisualEditorMonitor) {
const uiFilters = monitorDefinition?.ui_metadata?.search?.filters || [];
formikToPPLFilters = uiFilters.map((filter) =>
OPERATORS_PPL_QUERY_MAP[filter.operator].query(filter)
);
pplBucketValue = monitorDefinition?.ui_metadata?.search?.bucketValue || 1;
pplBucketUnitOfTime =
BUCKET_UNIT_PPL_UNIT_MAP[monitorDefinition?.ui_metadata?.search?.bucketUnitOfTime] ||
'HOUR';
pplTimeField = monitorDefinition?.ui_metadata?.search?.timeField;
}
delete monitorDefinition.ui_metadata;
delete monitorDefinition.data_sources;

// 3. get data triggers the alert and fetch log patterns
let monitorDefinitionStr = JSON.stringify(monitorDefinition);
let alertTriggeredByValue = '';
let dsl = '';
let index = '';
let topNLogPatternData = '';
if (
monitorResp.resp.monitor_type === MONITOR_TYPE.QUERY_LEVEL ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own understanding, what's the process for other monitor types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other monitor types like per cluster metrics don't have a query dsl associated with it, so we don't have the process to query data.

we may have a follow up to review Per document and Composite monitor monitor types

monitorResp.resp.monitor_type === MONITOR_TYPE.BUCKET_LEVEL
) {
// 3.1 preprocess index, only support first index use case
const search = monitorResp.resp.inputs[0].search;
index = String(search.indices).split(',')[0]?.trim() || '';
// 3.2 preprocess dsl query with right time range
let query = JSON.stringify(search.query);
// Only keep the query part
dsl = JSON.stringify({ query: search.query.query });
let latestAlertTriggerTime = '';
if (query.indexOf(PERIOD_END_PLACEHOLDER) !== -1) {
query = query.replaceAll(PERIOD_END_PLACEHOLDER, alert.last_notification_time);
latestAlertTriggerTime = moment
.utc(alert.last_notification_time)
.format(DEFAULT_DSL_QUERY_DATE_FORMAT);
dsl = dsl.replaceAll(PERIOD_END_PLACEHOLDER, latestAlertTriggerTime);
// as we changed the format, remove it
dsl = dsl.replaceAll('"format":"epoch_millis",', '');
monitorDefinitionStr = monitorDefinitionStr.replaceAll(
PERIOD_END_PLACEHOLDER,
getTime(alert.last_notification_time) // human-readable time format for summary
);
// as we changed the format, remove it
monitorDefinitionStr = monitorDefinitionStr.replaceAll('"format":"epoch_millis",', '');
}
// 3.3 preprocess ppl query base with concatenated filters
const pplAlertTriggerTime = moment
.utc(alert.last_notification_time)
.format(DEFAULT_PPL_QUERY_DATE_FORMAT);
const basePPL =
`source=${index} | ` +
`where ${pplTimeField} >= TIMESTAMPADD(${pplBucketUnitOfTime}, -${pplBucketValue}, '${pplAlertTriggerTime}') and ` +
`${pplTimeField} <= TIMESTAMP('${pplAlertTriggerTime}')`;
const basePPLWithFilters = formikToPPLFilters.reduce((acc, filter) => {
return `${acc} | where ${filter}`;
}, basePPL);
const firstSamplePPL = `${basePPLWithFilters} | head 1`;

if (index) {
// 3.4 dsl query result with aggregation results
const alertData = await searchQuery(
httpClient,
`${index}/_search`,
'GET',
dataSourceQuery,
query
);
alertTriggeredByValue = JSON.stringify(
alertData.body.aggregations?.metric?.value || alertData.body.hits.total.value
);

if (isVisualEditorMonitor) {
// 3.5 find the log pattern field by longest length in the first sample data
const firstSampleData = await searchQuery(
httpClient,
PPL_SEARCH_PATH,
'POST',
dataSourceQuery,
JSON.stringify({ query: firstSamplePPL })
);
const patternField = findLongestStringField(firstSampleData);

// 3.6 log pattern query to get top N log patterns
if (patternField) {
const topNLogPatternPPL =
`${basePPLWithFilters} | patterns ${patternField} | ` +
`stats count() as count, take(${patternField}, ${DEFAULT_LOG_PATTERN_SAMPLE_SIZE}) by patterns_field | ` +
`sort - count | head ${DEFAULT_LOG_PATTERN_TOP_N}`;
const logPatternData = await searchQuery(
httpClient,
PPL_SEARCH_PATH,
'POST',
dataSourceQuery,
JSON.stringify({ query: topNLogPatternPPL })
);
topNLogPatternData = escape(JSON.stringify(logPatternData?.body?.datarows || ''));
}
}
}
}

// 3.6 only keep top N active alerts and replace time with human-readable timezone format
const activeAlerts = filterActiveAlerts(alert.alerts || [alert])
.slice(0, DEFAULT_ACTIVE_ALERTS_AI_TOP_N)
.map((activeAlert) => ({
...activeAlert,
start_time: getTime(activeAlert.start_time),
last_notification_time: getTime(activeAlert.last_notification_time),
}));
// Reduce llm input token size by taking topN active alerts
const filteredAlert = {
...alert,
alerts: activeAlerts,
start_time: getTime(alert.start_time),
last_notification_time: getTime(alert.last_notification_time),
};

// 4. build the context
return {
context: `
Here is the detail information about alert ${alert.trigger_name}
### Monitor definition\n ${monitorDefinitionStr}\n
### Active Alert\n ${JSON.stringify(filteredAlert)}\n
### Value triggers this alert\n ${alertTriggeredByValue}\n
### Alert query DSL ${dsl} \n`,
additionalInfo: {
monitorType: monitorResp.resp.monitor_type,
dsl,
index,
topNLogPatternData,
isVisualEditorMonitor,
},
dataSourceId: dataSourceQuery?.query?.dataSourceId,
};
};

const assistantEnabled = getApplication().capabilities?.assistant?.enabled === true;
const assistantFeatureStatus = getAssistantDashboards().getFeatureStatus();
if (assistantFeatureStatus.alertInsight && assistantEnabled && isAgentConfigured) {
getAssistantDashboards().registerIncontextInsight([
{
key: alertId,
type: 'generate',
suggestions: [`Please summarize this alert`],
contextProvider,
},
]);

return getAssistantDashboards().renderIncontextInsight({ children });
}
return children;
};
8 changes: 8 additions & 0 deletions public/components/AlertInsight/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { AlertInsight } from './AlertInsight';

export { AlertInsight };
46 changes: 32 additions & 14 deletions public/components/DataSourceAlertsCard/DataSourceAlertsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { dataSourceFilterFn, getSeverityColor, getSeverityBadgeText, getTruncate
import { renderTime } from "../../pages/Dashboard/utils/tableUtils";
import { ALERTS_NAV_ID, MONITORS_NAV_ID } from "../../../utils/constants";
import { APP_PATH, DEFAULT_EMPTY_DATA } from "../../utils/constants";
import { dataSourceEnabled, getURL } from "../../pages/utils/helpers.js";
import { dataSourceEnabled, getIsAgentConfigured, getURL } from "../../pages/utils/helpers.js";
import { AlertInsight } from '../AlertInsight';

export interface DataSourceAlertsCardProps {
getDataSourceMenu?: DataSourceManagementPluginSetup['ui']['getDataSourceMenu'];
Expand All @@ -28,6 +29,7 @@ export const DataSourceAlertsCard: React.FC<DataSourceAlertsCardProps> = ({ get
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<DataSourceOption>();
const [alerts, setAlerts] = useState<any[]>([]);
const [agentAvailable, setAgentAvailable] = useState<boolean>(false);

useEffect(() => {
setLoading(true);
Expand All @@ -48,6 +50,15 @@ export const DataSourceAlertsCard: React.FC<DataSourceAlertsCardProps> = ({ get
})
}, [dataSource]);

useEffect(() => {
const checkAgentConfig = async () => {
const isConfigured = await getIsAgentConfigured(dataSource?.id);
setAgentAvailable(isConfigured);
};

checkAgentConfig();
}, [dataSource?.id]);

const onDataSourceSelected = useCallback((options: any[]) => {
if (dataSource?.id === undefined || dataSource?.id !== options[0]?.id) {
setDataSource(options[0]);
Expand All @@ -61,26 +72,33 @@ export const DataSourceAlertsCard: React.FC<DataSourceAlertsCardProps> = ({ get
alert.alert_source === 'workflow' ? alert.workflow_id : alert.monitor_id
}?&type=${alert.alert_source}`;
const url = getURL(monitorUrl, dataSource?.id);

const alertId = `alerts_${alert.id}`;
return (
<EuiFlexGroup gutterSize="s" justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<div>
<EuiBadge color={severityColor?.background} style={{ padding: '1px 4px', color: severityColor?.text }}>{getSeverityBadgeText(alert.severity)}</EuiBadge>
&nbsp;&nbsp;
<EuiLink href={url}>
<span style={{ color: '#006BB4' }} className="eui-textTruncate">
{getTruncatedText(triggerName)}
</span>
</EuiLink>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AlertInsight
alert={alert}
isAgentConfigured={agentAvailable}
alertId={alertId}
datasourceId={dataSource?.id}
>
<div key={alertId}>
<EuiBadge color={severityColor?.background} style={{ padding: '1px 4px', color: severityColor?.text }}>{getSeverityBadgeText(alert.severity)}</EuiBadge>
&nbsp;&nbsp;
<EuiLink href={url}>
<span style={{ color: '#006BB4' }} className="eui-textTruncate">
{getTruncatedText(triggerName)}
</span>
</EuiLink>
</div>
</AlertInsight>
</EuiFlexItem>
<EuiFlexItem grow={false} >
<EuiText color="subdued" size="s">{renderTime(alert.start_time, { showFromNow: true })}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)
}, []);
}, [agentAvailable]);

const createAlertDetailsDescription = useCallback((alert) => {
const monitorName = alert.monitor_name ?? DEFAULT_EMPTY_DATA;
Expand Down
3 changes: 2 additions & 1 deletion public/pages/Dashboard/containers/Dashboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { mount } from 'enzyme';
import Dashboard from './Dashboard';
import { historyMock, httpClientMock } from '../../../../test/mocks';
import { setupCoreStart } from '../../../../test/utils/helpers';
import { setAssistantDashboards, setAssistantClient } from '../../../services';
import { setAssistantDashboards, setAssistantClient, setClient } from '../../../services';

const location = {
hash: '',
Expand Down Expand Up @@ -64,6 +64,7 @@ beforeAll(() => {

describe('Dashboard', () => {
setAssistantDashboards({ getFeatureStatus: () => ({ chat: false, alertInsight: false }) });
setClient(httpClientMock);
setAssistantClient({agentConfigExists: (agentConfigName, options) => {return Promise.resolve({ exists: false });}})
beforeEach(() => {
jest.clearAllMocks();
Expand Down
Loading
Loading