Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
@@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { getAlertDetailsContextHandler } from '.';
import { getLogRateAnalysisForAlert } from '../get_log_rate_analysis_for_alert';
import { getLogCategories } from '../get_log_categories';
import { getServiceNameFromSignals } from './get_service_name_from_signals';
import { getContainerIdFromSignals } from './get_container_id_from_signals';
import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client';
import { getApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client';
import { getMlClient } from '../../../lib/helpers/get_ml_client';
import { getRandomSampler } from '../../../lib/helpers/get_random_sampler';
import { getExitSpanChangePoints, getServiceChangePoints } from '../get_changepoints';
import { getAnomalies } from '../get_apm_service_summary/get_anomalies';

jest.mock('../get_log_rate_analysis_for_alert');
jest.mock('../get_log_categories');
jest.mock('./get_service_name_from_signals');
jest.mock('./get_container_id_from_signals');
jest.mock('../../../lib/helpers/get_apm_event_client');
jest.mock('../../../lib/helpers/get_apm_alerts_client');
jest.mock('../../../lib/helpers/get_ml_client');
jest.mock('../../../lib/helpers/get_random_sampler');
jest.mock('../get_apm_service_summary');
jest.mock('../get_apm_downstream_dependencies');
jest.mock('../get_changepoints');
jest.mock('../get_apm_service_summary/get_anomalies');
jest.mock('./get_apm_errors');

const mockLogRateAnalysis = jest.mocked(getLogRateAnalysisForAlert);
const mockLogCategories = jest.mocked(getLogCategories);
const mockGetServiceName = jest.mocked(getServiceNameFromSignals);
const mockGetContainerId = jest.mocked(getContainerIdFromSignals);

function buildMocks() {
(getApmEventClient as jest.Mock).mockResolvedValue({});
(getApmAlertsClient as jest.Mock).mockResolvedValue({});
(getMlClient as jest.Mock).mockResolvedValue({});
(getRandomSampler as jest.Mock).mockResolvedValue({});
(getExitSpanChangePoints as jest.Mock).mockResolvedValue([]);
(getServiceChangePoints as jest.Mock).mockResolvedValue([]);
(getAnomalies as jest.Mock).mockResolvedValue([]);

mockLogRateAnalysis.mockResolvedValue({ logRateAnalysisType: 'spike', significantItems: [] });
mockLogCategories.mockResolvedValue({ logCategories: [], entities: [] });
}

const mockApmCore = {
start: jest.fn().mockResolvedValue({}),
} as any;

const mockResourcePlugins = {
observability: {
setup: {
getScopedAnnotationsClient: jest.fn().mockResolvedValue(undefined),
},
},
alerting: {
start: jest.fn().mockResolvedValue({ getRulesClientWithRequest: jest.fn() }),
},
ruleRegistry: {
start: jest.fn().mockResolvedValue({ getRacClientWithRequest: jest.fn() }),
},
logsDataAccess: {
start: jest.fn().mockResolvedValue({
services: {
logSourcesServiceFactory: {
getScopedLogSourcesService: jest.fn().mockResolvedValue({}),
},
},
}),
},
apmDataAccess: {
setup: { getApmIndices: jest.fn().mockResolvedValue({}) },
},
} as any;

const mockRequestContext = {
core: Promise.resolve({
elasticsearch: { client: { asCurrentUser: {} } },
savedObjects: { client: {} },
}),
licensing: {},
request: {},
} as any;

const baseQuery = {
alert_started_at: '2024-01-01T00:00:00.000Z',
'service.environment': undefined,
'host.name': undefined,
'kubernetes.pod.name': undefined,
alert_rule_parameter_time_size: undefined,
alert_rule_parameter_time_unit: undefined,
'transaction.type': undefined,
'transaction.name': undefined,
} as any;

const mockLogger = { error: jest.fn() } as any;

describe('getAlertDetailsContextHandler', () => {
beforeEach(() => {
jest.clearAllMocks();
buildMocks();
mockGetServiceName.mockResolvedValue(undefined);
mockGetContainerId.mockResolvedValue(undefined);
});

describe('when no entity context is present', () => {
it('does not invoke getLogRateAnalysisForAlert or getLogCategories', async () => {
const handler = getAlertDetailsContextHandler(mockApmCore, mockResourcePlugins, mockLogger);

await handler(mockRequestContext, {
...baseQuery,
'host.name': undefined,
'kubernetes.pod.name': undefined,
});

expect(mockLogRateAnalysis).not.toHaveBeenCalled();
expect(mockLogCategories).not.toHaveBeenCalled();
});
});

describe('when at least one entity is present', () => {
it('invokes both getLogRateAnalysisForAlert and getLogCategories', async () => {
const handler = getAlertDetailsContextHandler(mockApmCore, mockResourcePlugins, mockLogger);

await handler(mockRequestContext, {
...baseQuery,
'host.name': 'my-host',
});

expect(mockLogRateAnalysis).toHaveBeenCalledTimes(1);
expect(mockLogCategories).toHaveBeenCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export const getAlertDetailsContextHandler = (

const dataFetchers: Array<() => Promise<AlertDetailsContextualInsight>> = [];

const hasEntityFilters = !!(serviceName || hostName || containerId || kubernetesPodName);

// service summary
if (serviceName) {
dataFetchers.push(async () => {
Expand Down Expand Up @@ -168,69 +170,71 @@ export const getAlertDetailsContextHandler = (
});
}

// log rate analysis
dataFetchers.push(async () => {
const { logRateAnalysisType, significantItems } = await getLogRateAnalysisForAlert({
esClient,
logSourcesService,
arguments: {
alertStartedAt: moment(alertStartedAt).toISOString(),
alertRuleParameterTimeSize: query.alert_rule_parameter_time_size
? parseInt(query.alert_rule_parameter_time_size, 10)
: undefined,
alertRuleParameterTimeUnit: query.alert_rule_parameter_time_unit,
entities: {
'service.name': serviceName,
'host.name': hostName,
'container.id': containerId,
'kubernetes.pod.name': kubernetesPodName,
if (hasEntityFilters) {
// log rate analysis
dataFetchers.push(async () => {
const { logRateAnalysisType, significantItems } = await getLogRateAnalysisForAlert({
esClient,
logSourcesService,
arguments: {
alertStartedAt: moment(alertStartedAt).toISOString(),
alertRuleParameterTimeSize: query.alert_rule_parameter_time_size
? parseInt(query.alert_rule_parameter_time_size, 10)
: undefined,
alertRuleParameterTimeUnit: query.alert_rule_parameter_time_unit,
entities: {
'service.name': serviceName,
'host.name': hostName,
'container.id': containerId,
'kubernetes.pod.name': kubernetesPodName,
},
},
},
});
});

if (logRateAnalysisType !== 'spike' || significantItems.length === 0) {
return {
key: 'logRateAnalysis',
description:
'Log rate analysis did not identify any significant metadata or log patterns.',
data: [],
};
}

if (logRateAnalysisType !== 'spike' || significantItems.length === 0) {
return {
key: 'logRateAnalysis',
description:
'Log rate analysis did not identify any significant metadata or log patterns.',
data: [],
description: `Statistically significant log metadata and log message patterns occurring in the lookback period before the alert was triggered.`,
data: significantItems,
};
}

return {
key: 'logRateAnalysis',
description: `Statistically significant log metadata and log message patterns occurring in the lookback period before the alert was triggered.`,
data: significantItems,
};
});
});

// log categories
dataFetchers.push(async () => {
const downstreamDependencies = await downstreamDependenciesPromise;
const { logCategories, entities } = await getLogCategories({
apmEventClient,
esClient,
logSourcesService,
arguments: {
start: moment(alertStartedAt).subtract(15, 'minute').toISOString(),
end: alertStartedAt,
entities: {
'service.name': serviceName,
'host.name': hostName,
'container.id': containerId,
'kubernetes.pod.name': kubernetesPodName,
// log categories
dataFetchers.push(async () => {
const downstreamDependencies = await downstreamDependenciesPromise;
const { logCategories, entities } = await getLogCategories({
apmEventClient,
esClient,
logSourcesService,
arguments: {
start: moment(alertStartedAt).subtract(15, 'minute').toISOString(),
end: alertStartedAt,
entities: {
'service.name': serviceName,
'host.name': hostName,
'container.id': containerId,
'kubernetes.pod.name': kubernetesPodName,
},
},
},
});
});

const entitiesAsString = entities.map(({ key, value }) => `${key}:${value}`).join(', ');
const entitiesAsString = entities.map(({ key, value }) => `${key}:${value}`).join(', ');

return {
key: 'logCategories',
description: `Log events occurring up to 15 minutes before the alert was triggered. Filtered by the entities: ${entitiesAsString}`,
data: logCategoriesWithDownstreamServiceName(logCategories, downstreamDependencies),
};
});
return {
key: 'logCategories',
description: `Log events occurring up to 15 minutes before the alert was triggered. Filtered by the entities: ${entitiesAsString}`,
data: logCategoriesWithDownstreamServiceName(logCategories, downstreamDependencies),
};
});
}

// apm errors
if (serviceName) {
Expand Down
Loading