mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[8.14] [Obs AI Assistant] Address feedback for contextual insights on alert details page (#181049) (#181414)
# Backport This will backport the following commits from `main` to `8.14`: - [[Obs AI Assistant] Address feedback for contextual insights on alert details page (#181049)](https://github.com/elastic/kibana/pull/181049) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Søren Louv-Jansen","email":"soren.louv@elastic.co"},"sourceCommit":{"committedDate":"2024-04-23T10:48:08Z","message":"[Obs AI Assistant] Address feedback for contextual insights on alert details page (#181049)\n\n- This PR addresses comments made in\r\nhttps://github.com/elastic/kibana/pull/180766\r\n- Adds API tests for `GET\r\n/internal/apm/assistant/get_obs_alert_details_context`\r\n- Fixes some bugs I found during testing\r\n- Removes the option `forceSyntheticSource` in the APM app since it is\r\nno longer needed and causes problems when applied to the traces data\r\nstream","sha":"2141ba76ce4bd690dd5c96e8e41480383797615f","branchLabelMapping":{"^v8.15.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-infra_services","Team:obs-ux-management","apm:review","v8.14.0","v8.15.0"],"title":"[Obs AI Assistant] Address feedback for contextual insights on alert details page","number":181049,"url":"https://github.com/elastic/kibana/pull/181049","mergeCommit":{"message":"[Obs AI Assistant] Address feedback for contextual insights on alert details page (#181049)\n\n- This PR addresses comments made in\r\nhttps://github.com/elastic/kibana/pull/180766\r\n- Adds API tests for `GET\r\n/internal/apm/assistant/get_obs_alert_details_context`\r\n- Fixes some bugs I found during testing\r\n- Removes the option `forceSyntheticSource` in the APM app since it is\r\nno longer needed and causes problems when applied to the traces data\r\nstream","sha":"2141ba76ce4bd690dd5c96e8e41480383797615f"}},"sourceBranch":"main","suggestedTargetBranches":["8.14"],"targetPullRequestStates":[{"branch":"8.14","label":"v8.14.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.15.0","branchLabelMappingKey":"^v8.15.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/181049","number":181049,"mergeCommit":{"message":"[Obs AI Assistant] Address feedback for contextual insights on alert details page (#181049)\n\n- This PR addresses comments made in\r\nhttps://github.com/elastic/kibana/pull/180766\r\n- Adds API tests for `GET\r\n/internal/apm/assistant/get_obs_alert_details_context`\r\n- Fixes some bugs I found during testing\r\n- Removes the option `forceSyntheticSource` in the APM app since it is\r\nno longer needed and causes problems when applied to the traces data\r\nstream","sha":"2141ba76ce4bd690dd5c96e8e41480383797615f"}}]}] BACKPORT--> Co-authored-by: Søren Louv-Jansen <soren.louv@elastic.co>
This commit is contained in:
parent
ae58cd36b4
commit
2a5bc89e8a
14 changed files with 744 additions and 217 deletions
|
@ -50,7 +50,8 @@ const configSchema = schema.object({
|
|||
enabled: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
}),
|
||||
forceSyntheticSource: schema.boolean({ defaultValue: false }),
|
||||
|
||||
forceSyntheticSource: schema.boolean({ defaultValue: false }), // deprecated
|
||||
latestAgentVersionsUrl: schema.string({
|
||||
defaultValue: 'https://apm-agent-versions.elastic.co/versions.json',
|
||||
}),
|
||||
|
@ -103,6 +104,9 @@ export const config: PluginConfigDescriptor<APMConfig> = {
|
|||
renameFromRoot('xpack.apm.maxServiceSelection', `uiSettings.overrides[${maxSuggestions}]`, {
|
||||
level: 'warning',
|
||||
}),
|
||||
unused('forceSyntheticSource', {
|
||||
level: 'warning',
|
||||
}),
|
||||
],
|
||||
exposeToBrowser: {
|
||||
serviceMapEnabled: true,
|
||||
|
|
|
@ -45,7 +45,6 @@ describe('APMEventClient', () => {
|
|||
indices: {} as any,
|
||||
options: {
|
||||
includeFrozen: false,
|
||||
forceSyntheticSource: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -90,7 +90,6 @@ export interface APMEventClientConfig {
|
|||
indices: APMIndices;
|
||||
options: {
|
||||
includeFrozen: boolean;
|
||||
forceSyntheticSource: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -100,7 +99,6 @@ export class APMEventClient {
|
|||
private readonly request: KibanaRequest;
|
||||
public readonly indices: APMIndices;
|
||||
private readonly includeFrozen: boolean;
|
||||
private readonly forceSyntheticSource: boolean;
|
||||
|
||||
constructor(config: APMEventClientConfig) {
|
||||
this.esClient = config.esClient;
|
||||
|
@ -108,7 +106,6 @@ export class APMEventClient {
|
|||
this.request = config.request;
|
||||
this.indices = config.indices;
|
||||
this.includeFrozen = config.options.includeFrozen;
|
||||
this.forceSyntheticSource = config.options.forceSyntheticSource;
|
||||
}
|
||||
|
||||
private callAsyncWithDebug<T extends { body: any }>({
|
||||
|
@ -156,14 +153,11 @@ export class APMEventClient {
|
|||
operationName: string,
|
||||
params: TParams
|
||||
): Promise<TypedSearchResponse<TParams>> {
|
||||
const { events, index, filters } = getRequestBase({
|
||||
const { index, filters } = getRequestBase({
|
||||
apm: params.apm,
|
||||
indices: this.indices,
|
||||
});
|
||||
|
||||
const forceSyntheticSourceForThisRequest =
|
||||
this.forceSyntheticSource && events.includes(ProcessorEvent.metric);
|
||||
|
||||
const searchParams = {
|
||||
...omit(params, 'apm', 'body'),
|
||||
index,
|
||||
|
@ -180,7 +174,6 @@ export class APMEventClient {
|
|||
ignore_unavailable: true,
|
||||
preference: 'any',
|
||||
expand_wildcards: ['open' as const, 'hidden' as const],
|
||||
...(forceSyntheticSourceForThisRequest ? { force_synthetic_source: true } : {}),
|
||||
};
|
||||
|
||||
return this.callAsyncWithDebug({
|
||||
|
|
|
@ -36,7 +36,6 @@ export async function getApmEventClient({
|
|||
indices,
|
||||
options: {
|
||||
includeFrozen,
|
||||
forceSyntheticSource: config.forceSyntheticSource,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,12 @@ import datemath from '@elastic/datemath';
|
|||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { CoreRequestHandlerContext } from '@kbn/core/server';
|
||||
import { aiAssistantLogsIndexPattern } from '@kbn/observability-ai-assistant-plugin/server';
|
||||
import { SERVICE_NAME, CONTAINER_ID, HOST_NAME } from '../../../../common/es_fields/apm';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
CONTAINER_ID,
|
||||
HOST_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
} from '../../../../common/es_fields/apm';
|
||||
import { getTypedSearch } from '../../../utils/create_typed_es_client';
|
||||
|
||||
export type LogCategories =
|
||||
|
@ -33,6 +38,7 @@ export async function getLogCategories({
|
|||
'service.name'?: string;
|
||||
'host.name'?: string;
|
||||
'container.id'?: string;
|
||||
'kubernetes.pod.name'?: string;
|
||||
};
|
||||
}): Promise<LogCategories> {
|
||||
const start = datemath.parse(args.start)?.valueOf()!;
|
||||
|
@ -42,10 +48,10 @@ export async function getLogCategories({
|
|||
{ field: SERVICE_NAME, value: args[SERVICE_NAME] },
|
||||
{ field: CONTAINER_ID, value: args[CONTAINER_ID] },
|
||||
{ field: HOST_NAME, value: args[HOST_NAME] },
|
||||
{ field: KUBERNETES_POD_NAME, value: args[KUBERNETES_POD_NAME] },
|
||||
]);
|
||||
|
||||
const index =
|
||||
(await coreContext.uiSettings.client.get<string>(aiAssistantLogsIndexPattern)) ?? 'logs-*';
|
||||
const index = await coreContext.uiSettings.client.get<string>(aiAssistantLogsIndexPattern);
|
||||
|
||||
const search = getTypedSearch(esClient);
|
||||
|
||||
|
|
|
@ -8,11 +8,15 @@
|
|||
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
|
||||
import { aiAssistantLogsIndexPattern } from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import { rangeQuery, termQuery, typedSearch } from '@kbn/observability-plugin/server/utils/queries';
|
||||
import { rangeQuery, typedSearch } from '@kbn/observability-plugin/server/utils/queries';
|
||||
import * as t from 'io-ts';
|
||||
import moment from 'moment';
|
||||
import { ESSearchRequest } from '@kbn/es-types';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import {
|
||||
APMEventClient,
|
||||
APMEventESSearchRequest,
|
||||
} from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { observabilityAlertDetailsContextRt } from '.';
|
||||
import { RollupInterval } from '../../../../common/rollup';
|
||||
|
||||
|
@ -31,37 +35,14 @@ export async function getContainerIdFromSignals({
|
|||
return query['container.id'];
|
||||
}
|
||||
|
||||
if (query['service.name']) {
|
||||
const containerId = await getContainerIdFromTrace({
|
||||
query,
|
||||
apmEventClient,
|
||||
});
|
||||
|
||||
if (containerId) {
|
||||
return containerId;
|
||||
}
|
||||
|
||||
return getContainerIdFromLogs({ query, esClient, coreContext });
|
||||
if (!query['service.name']) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function getContainerIdFromLogs({
|
||||
query,
|
||||
esClient,
|
||||
coreContext,
|
||||
}: {
|
||||
query: t.TypeOf<typeof observabilityAlertDetailsContextRt>;
|
||||
esClient: ElasticsearchClient;
|
||||
coreContext: CoreRequestHandlerContext;
|
||||
}) {
|
||||
const index =
|
||||
(await coreContext.uiSettings.client.get<string>(aiAssistantLogsIndexPattern)) ?? 'logs-*';
|
||||
const start = moment(query.alert_started_at).subtract(30, 'minutes').valueOf();
|
||||
const end = moment(query.alert_started_at).valueOf();
|
||||
|
||||
const start = moment(query.alert_started_at).subtract(30, 'minutes').unix();
|
||||
const end = moment(query.alert_started_at).unix();
|
||||
|
||||
const res = await typedSearch<{ container: { id: string } }, any>(esClient, {
|
||||
index,
|
||||
const params: APMEventESSearchRequest['body'] = {
|
||||
_source: ['container.id'],
|
||||
terminate_after: 1,
|
||||
size: 1,
|
||||
|
@ -69,28 +50,51 @@ async function getContainerIdFromLogs({
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'service.name': query['service.name'] } },
|
||||
{ exists: { field: 'container.id' } },
|
||||
...termQuery('service.name', query['service.name']),
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const containerId = await getContainerIdFromTraces({
|
||||
params,
|
||||
apmEventClient,
|
||||
});
|
||||
|
||||
if (containerId) {
|
||||
return containerId;
|
||||
}
|
||||
|
||||
return getContainerIdFromLogs({ params, esClient, coreContext });
|
||||
}
|
||||
|
||||
async function getContainerIdFromLogs({
|
||||
params,
|
||||
esClient,
|
||||
coreContext,
|
||||
}: {
|
||||
params: ESSearchRequest['body'];
|
||||
esClient: ElasticsearchClient;
|
||||
coreContext: CoreRequestHandlerContext;
|
||||
}) {
|
||||
const index = await coreContext.uiSettings.client.get<string>(aiAssistantLogsIndexPattern);
|
||||
const res = await typedSearch<{ container: { id: string } }, any>(esClient, {
|
||||
index,
|
||||
...params,
|
||||
});
|
||||
|
||||
return res.hits.hits[0]?._source?.container?.id;
|
||||
}
|
||||
|
||||
async function getContainerIdFromTrace({
|
||||
query,
|
||||
async function getContainerIdFromTraces({
|
||||
params,
|
||||
apmEventClient,
|
||||
}: {
|
||||
query: t.TypeOf<typeof observabilityAlertDetailsContextRt>;
|
||||
params: APMEventESSearchRequest['body'];
|
||||
apmEventClient: APMEventClient;
|
||||
}) {
|
||||
const start = moment(query.alert_started_at).subtract(30, 'minutes').unix();
|
||||
const end = moment(query.alert_started_at).unix();
|
||||
|
||||
const res = await apmEventClient.search('get_container_id', {
|
||||
const res = await apmEventClient.search('get_container_id_from_traces', {
|
||||
apm: {
|
||||
sources: [
|
||||
{
|
||||
|
@ -99,21 +103,7 @@ async function getContainerIdFromTrace({
|
|||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
_source: ['container.id'],
|
||||
terminate_after: 1,
|
||||
size: 1,
|
||||
track_total_hits: false,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ exists: { field: 'container.id' } },
|
||||
...termQuery('service.name', query['service.name']),
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
|
||||
return res.hits.hits[0]?._source.container?.id;
|
||||
|
|
|
@ -11,8 +11,12 @@ import { aiAssistantLogsIndexPattern } from '@kbn/observability-ai-assistant-plu
|
|||
import { rangeQuery, termQuery, typedSearch } from '@kbn/observability-plugin/server/utils/queries';
|
||||
import * as t from 'io-ts';
|
||||
import moment from 'moment';
|
||||
import { ESSearchRequest } from '@kbn/es-types';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import {
|
||||
APMEventClient,
|
||||
APMEventESSearchRequest,
|
||||
} from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { observabilityAlertDetailsContextRt } from '.';
|
||||
import { RollupInterval } from '../../../../common/rollup';
|
||||
|
||||
|
@ -31,37 +35,14 @@ export async function getServiceNameFromSignals({
|
|||
return query['service.name'];
|
||||
}
|
||||
|
||||
if (query['container.id']) {
|
||||
const serviceName = await getServiceNameFromTraces({
|
||||
query,
|
||||
apmEventClient,
|
||||
});
|
||||
|
||||
if (serviceName) {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
return getServiceNameFromLogs({ query, esClient, coreContext });
|
||||
if (!query['kubernetes.pod.name'] && !query['container.id']) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function getServiceNameFromLogs({
|
||||
query,
|
||||
esClient,
|
||||
coreContext,
|
||||
}: {
|
||||
query: t.TypeOf<typeof observabilityAlertDetailsContextRt>;
|
||||
esClient: ElasticsearchClient;
|
||||
coreContext: CoreRequestHandlerContext;
|
||||
}) {
|
||||
const index =
|
||||
(await coreContext.uiSettings.client.get<string>(aiAssistantLogsIndexPattern)) ?? 'logs-*';
|
||||
const start = moment(query.alert_started_at).subtract(30, 'minutes').valueOf();
|
||||
const end = moment(query.alert_started_at).valueOf();
|
||||
|
||||
const start = moment(query.alert_started_at).subtract(30, 'minutes').unix();
|
||||
const end = moment(query.alert_started_at).unix();
|
||||
|
||||
const res = await typedSearch<{ service: { name: string } }, any>(esClient, {
|
||||
index,
|
||||
const params: APMEventESSearchRequest['body'] = {
|
||||
_source: ['service.name'],
|
||||
terminate_after: 1,
|
||||
size: 1,
|
||||
|
@ -69,28 +50,60 @@ async function getServiceNameFromLogs({
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
...termQuery('container.id', query['container.id']),
|
||||
...termQuery('kubernetes.pod.name', query['kubernetes.pod.name']),
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{ exists: { field: 'service.name' } },
|
||||
...termQuery('container.id', query['container.id']),
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const serviceName = await getServiceNameFromTraces({
|
||||
params,
|
||||
apmEventClient,
|
||||
});
|
||||
|
||||
if (serviceName) {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
return getServiceNameFromLogs({ params, esClient, coreContext });
|
||||
}
|
||||
|
||||
async function getServiceNameFromLogs({
|
||||
params,
|
||||
esClient,
|
||||
coreContext,
|
||||
}: {
|
||||
params: ESSearchRequest['body'];
|
||||
esClient: ElasticsearchClient;
|
||||
coreContext: CoreRequestHandlerContext;
|
||||
}) {
|
||||
const index = await coreContext.uiSettings.client.get<string>(aiAssistantLogsIndexPattern);
|
||||
const res = await typedSearch<{ service: { name: string } }, any>(esClient, {
|
||||
index,
|
||||
...params,
|
||||
});
|
||||
|
||||
return res.hits.hits[0]?._source?.service?.name;
|
||||
}
|
||||
|
||||
async function getServiceNameFromTraces({
|
||||
query,
|
||||
params,
|
||||
apmEventClient,
|
||||
}: {
|
||||
query: t.TypeOf<typeof observabilityAlertDetailsContextRt>;
|
||||
params: APMEventESSearchRequest['body'];
|
||||
apmEventClient: APMEventClient;
|
||||
}) {
|
||||
const start = moment(query.alert_started_at).subtract(30, 'minutes').unix();
|
||||
const end = moment(query.alert_started_at).unix();
|
||||
|
||||
const res = await apmEventClient.search('get_service_name', {
|
||||
const res = await apmEventClient.search('get_service_name_from_traces', {
|
||||
apm: {
|
||||
sources: [
|
||||
{
|
||||
|
@ -99,21 +112,7 @@ async function getServiceNameFromTraces({
|
|||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
_source: ['service.name'],
|
||||
terminate_after: 1,
|
||||
size: 1,
|
||||
track_total_hits: false,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ exists: { field: 'service.name' } },
|
||||
...termQuery('container.id', query['container.id']),
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
|
||||
return res.hits.hits[0]?._source.service.name;
|
||||
|
|
|
@ -36,6 +36,7 @@ export const observabilityAlertDetailsContextRt = t.intersection([
|
|||
// infrastructure fields
|
||||
'host.name': t.string,
|
||||
'container.id': t.string,
|
||||
'kubernetes.pod.name': t.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -61,6 +62,7 @@ export async function getObservabilityAlertDetailsContext({
|
|||
const alertStartedAt = query.alert_started_at;
|
||||
const serviceEnvironment = query['service.environment'];
|
||||
const hostName = query['host.name'];
|
||||
const kubernetesPodName = query['kubernetes.pod.name'];
|
||||
const [serviceName, containerId] = await Promise.all([
|
||||
getServiceNameFromSignals({
|
||||
query,
|
||||
|
@ -76,70 +78,93 @@ export async function getObservabilityAlertDetailsContext({
|
|||
}),
|
||||
]);
|
||||
|
||||
async function handleError<T>(cb: () => Promise<T>): Promise<T | undefined> {
|
||||
try {
|
||||
return await cb();
|
||||
} catch (error) {
|
||||
logger.error('Error while fetching observability alert details context');
|
||||
logger.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const serviceSummaryPromise = serviceName
|
||||
? getApmServiceSummary({
|
||||
apmEventClient,
|
||||
annotationsClient,
|
||||
esClient,
|
||||
apmAlertsClient,
|
||||
mlClient,
|
||||
logger,
|
||||
arguments: {
|
||||
'service.name': serviceName,
|
||||
'service.environment': serviceEnvironment,
|
||||
start: moment(alertStartedAt).subtract(5, 'minute').toISOString(),
|
||||
end: alertStartedAt,
|
||||
},
|
||||
})
|
||||
? handleError(() =>
|
||||
getApmServiceSummary({
|
||||
apmEventClient,
|
||||
annotationsClient,
|
||||
esClient,
|
||||
apmAlertsClient,
|
||||
mlClient,
|
||||
logger,
|
||||
arguments: {
|
||||
'service.name': serviceName,
|
||||
'service.environment': serviceEnvironment,
|
||||
start: moment(alertStartedAt).subtract(5, 'minute').toISOString(),
|
||||
end: alertStartedAt,
|
||||
},
|
||||
})
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const downstreamDependenciesPromise = serviceName
|
||||
? getAssistantDownstreamDependencies({
|
||||
apmEventClient,
|
||||
arguments: {
|
||||
'service.name': serviceName,
|
||||
'service.environment': serviceEnvironment,
|
||||
start: moment(alertStartedAt).subtract(5, 'minute').toISOString(),
|
||||
end: alertStartedAt,
|
||||
},
|
||||
})
|
||||
? handleError(() =>
|
||||
getAssistantDownstreamDependencies({
|
||||
apmEventClient,
|
||||
arguments: {
|
||||
'service.name': serviceName,
|
||||
'service.environment': serviceEnvironment,
|
||||
start: moment(alertStartedAt).subtract(5, 'minute').toISOString(),
|
||||
end: alertStartedAt,
|
||||
},
|
||||
})
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const logCategoriesPromise = getLogCategories({
|
||||
esClient,
|
||||
coreContext,
|
||||
arguments: {
|
||||
start: moment(alertStartedAt).subtract(5, 'minute').toISOString(),
|
||||
end: alertStartedAt,
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
'container.id': containerId,
|
||||
},
|
||||
});
|
||||
const logCategoriesPromise = handleError(() =>
|
||||
getLogCategories({
|
||||
esClient,
|
||||
coreContext,
|
||||
arguments: {
|
||||
start: moment(alertStartedAt).subtract(5, 'minute').toISOString(),
|
||||
end: alertStartedAt,
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
'container.id': containerId,
|
||||
'kubernetes.pod.name': kubernetesPodName,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const serviceChangePointsPromise = getServiceChangePoints({
|
||||
apmEventClient,
|
||||
alertStartedAt,
|
||||
serviceName,
|
||||
serviceEnvironment,
|
||||
transactionType: query['transaction.type'],
|
||||
transactionName: query['transaction.name'],
|
||||
});
|
||||
const serviceChangePointsPromise = handleError(() =>
|
||||
getServiceChangePoints({
|
||||
apmEventClient,
|
||||
alertStartedAt,
|
||||
serviceName,
|
||||
serviceEnvironment,
|
||||
transactionType: query['transaction.type'],
|
||||
transactionName: query['transaction.name'],
|
||||
})
|
||||
);
|
||||
|
||||
const exitSpanChangePointsPromise = getExitSpanChangePoints({
|
||||
apmEventClient,
|
||||
alertStartedAt,
|
||||
serviceName,
|
||||
serviceEnvironment,
|
||||
});
|
||||
const exitSpanChangePointsPromise = handleError(() =>
|
||||
getExitSpanChangePoints({
|
||||
apmEventClient,
|
||||
alertStartedAt,
|
||||
serviceName,
|
||||
serviceEnvironment,
|
||||
})
|
||||
);
|
||||
|
||||
const anomaliesPromise = getAnomalies({
|
||||
start: moment(alertStartedAt).subtract(1, 'hour').valueOf(),
|
||||
end: moment(alertStartedAt).valueOf(),
|
||||
environment: serviceEnvironment,
|
||||
mlClient,
|
||||
logger,
|
||||
});
|
||||
const anomaliesPromise = handleError(() =>
|
||||
getAnomalies({
|
||||
start: moment(alertStartedAt).subtract(1, 'hour').valueOf(),
|
||||
end: moment(alertStartedAt).valueOf(),
|
||||
environment: serviceEnvironment,
|
||||
mlClient,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
|
||||
const [
|
||||
serviceSummary,
|
||||
|
|
|
@ -34,7 +34,7 @@ import { LogCategories } from './get_log_categories';
|
|||
const getObservabilityAlertDetailsContextRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
options: {
|
||||
tags: ['access:apm', 'access:ai_assistant'],
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
|
||||
params: t.type({
|
||||
|
@ -45,16 +45,16 @@ const getObservabilityAlertDetailsContextRoute = createApmServerRoute({
|
|||
): Promise<{
|
||||
serviceSummary?: ServiceSummary;
|
||||
downstreamDependencies?: APMDownstreamDependency[];
|
||||
logCategories: LogCategories;
|
||||
serviceChangePoints: Array<{
|
||||
logCategories?: LogCategories;
|
||||
serviceChangePoints?: Array<{
|
||||
title: string;
|
||||
changes: TimeseriesChangePoint[];
|
||||
}>;
|
||||
exitSpanChangePoints: Array<{
|
||||
exitSpanChangePoints?: Array<{
|
||||
title: string;
|
||||
changes: TimeseriesChangePoint[];
|
||||
}>;
|
||||
anomalies: ApmAnomalies;
|
||||
anomalies?: ApmAnomalies;
|
||||
}> => {
|
||||
const { context, request, plugins, logger, params } = resources;
|
||||
const { query } = params;
|
||||
|
|
|
@ -171,7 +171,8 @@ export async function getServiceMetadataDetails({
|
|||
|
||||
const response = await apmEventClient.search('get_service_metadata_details', params);
|
||||
|
||||
if (response.hits.total.value === 0) {
|
||||
const hit = response.hits.hits[0]?._source as ServiceMetadataDetailsRaw | undefined;
|
||||
if (!hit) {
|
||||
return {
|
||||
service: undefined,
|
||||
container: undefined,
|
||||
|
@ -179,8 +180,7 @@ export async function getServiceMetadataDetails({
|
|||
};
|
||||
}
|
||||
|
||||
const { service, agent, host, kubernetes, container, cloud, labels } = response.hits.hits[0]
|
||||
._source as ServiceMetadataDetailsRaw;
|
||||
const { service, agent, host, kubernetes, container, cloud, labels } = hit;
|
||||
|
||||
const serviceMetadataDetails = {
|
||||
versions: response.aggregations?.serviceVersions.buckets.map((bucket) => bucket.key as string),
|
||||
|
|
|
@ -28,40 +28,42 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu
|
|||
return [];
|
||||
}
|
||||
|
||||
const res = await http.get('/internal/apm/assistant/get_obs_alert_details_context', {
|
||||
query: {
|
||||
alert_started_at: new Date(alert.formatted.start).toISOString(),
|
||||
try {
|
||||
const res = await http.get('/internal/apm/assistant/get_obs_alert_details_context', {
|
||||
query: {
|
||||
alert_started_at: new Date(alert.formatted.start).toISOString(),
|
||||
|
||||
// service fields
|
||||
'service.name': fields['service.name'],
|
||||
'service.environment': fields['service.environment'],
|
||||
'transaction.type': fields['transaction.type'],
|
||||
'transaction.name': fields['transaction.name'],
|
||||
// service fields
|
||||
'service.name': fields['service.name'],
|
||||
'service.environment': fields['service.environment'],
|
||||
'transaction.type': fields['transaction.type'],
|
||||
'transaction.name': fields['transaction.name'],
|
||||
|
||||
// infra fields
|
||||
'host.name': fields['host.name'],
|
||||
'container.id': fields['container.id'],
|
||||
},
|
||||
});
|
||||
// infra fields
|
||||
'host.name': fields['host.name'],
|
||||
'container.id': fields['container.id'],
|
||||
'kubernetes.pod.name': fields['kubernetes.pod.name'],
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
serviceSummary,
|
||||
downstreamDependencies,
|
||||
logCategories,
|
||||
serviceChangePoints,
|
||||
exitSpanChangePoints,
|
||||
anomalies,
|
||||
} = res as any;
|
||||
const {
|
||||
serviceSummary,
|
||||
downstreamDependencies,
|
||||
logCategories,
|
||||
serviceChangePoints,
|
||||
exitSpanChangePoints,
|
||||
anomalies,
|
||||
} = res as any;
|
||||
|
||||
const serviceName = fields['service.name'];
|
||||
const serviceEnvironment = fields['service.environment'];
|
||||
const serviceName = fields['service.name'];
|
||||
const serviceEnvironment = fields['service.environment'];
|
||||
|
||||
const obsAlertContext = `${
|
||||
!isEmpty(serviceSummary)
|
||||
? `Metadata for the service where the alert occurred:
|
||||
const obsAlertContext = `${
|
||||
!isEmpty(serviceSummary)
|
||||
? `Metadata for the service where the alert occurred:
|
||||
${JSON.stringify(serviceSummary, null, 2)}`
|
||||
: ''
|
||||
}
|
||||
: ''
|
||||
}
|
||||
|
||||
${
|
||||
!isEmpty(downstreamDependencies)
|
||||
|
@ -99,21 +101,29 @@ ${JSON.stringify(downstreamDependencies, null, 2)}`
|
|||
}
|
||||
`;
|
||||
|
||||
return observabilityAIAssistant.getContextualInsightMessages({
|
||||
message: `I'm looking at an alert and trying to understand why it was triggered`,
|
||||
instructions: dedent(
|
||||
`I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next.
|
||||
return observabilityAIAssistant.getContextualInsightMessages({
|
||||
message: `I'm looking at an alert and trying to understand why it was triggered`,
|
||||
instructions: dedent(
|
||||
`I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next.
|
||||
|
||||
The following contextual information is available to help me understand the alert:
|
||||
${obsAlertContext}
|
||||
|
||||
Be brief and to the point.
|
||||
Do not list the alert details as bullet points.
|
||||
Do refer to the contextual information provided above when relevant.
|
||||
Refer to the contextual information provided above when relevant.
|
||||
Pay specific attention to why the alert happened and what may have contributed to it.
|
||||
`
|
||||
),
|
||||
});
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
return observabilityAIAssistant.getContextualInsightMessages({
|
||||
message: `I'm looking at an alert and trying to understand why it was triggered`,
|
||||
instructions: dedent(
|
||||
`I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next.`
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [alert, http, observabilityAIAssistant]);
|
||||
|
||||
if (!ObservabilityAIAssistantContextualInsight) {
|
||||
|
|
|
@ -31,6 +31,9 @@ export function createApmApiClient(st: supertest.SuperTest<supertest.Test>) {
|
|||
const pathnameWithSpaceId = options.spaceId ? `/s/${options.spaceId}${pathname}` : pathname;
|
||||
const url = format({ pathname: pathnameWithSpaceId, query: params?.query });
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Calling APM API: ${method.toUpperCase()} ${url}`);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'kbn-xsrf': 'foo',
|
||||
'x-elastic-internal-origin': 'foo',
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
|
||||
import { ApmUsername } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication';
|
||||
import { createApmUsers } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/create_apm_users';
|
||||
import { ApmSynthtraceEsClient, ApmSynthtraceKibanaClient } from '@kbn/apm-synthtrace';
|
||||
import {
|
||||
ApmSynthtraceEsClient,
|
||||
ApmSynthtraceKibanaClient,
|
||||
LogsSynthtraceEsClient,
|
||||
createLogger,
|
||||
LogLevel,
|
||||
} from '@kbn/apm-synthtrace';
|
||||
import { FtrConfigProviderContext, kbnTestConfig } from '@kbn/test';
|
||||
import supertest from 'supertest';
|
||||
import { format, UrlObject } from 'url';
|
||||
|
@ -66,6 +72,9 @@ export interface CreateTest {
|
|||
services: InheritedServices & {
|
||||
apmFtrConfig: () => ApmFtrConfig;
|
||||
registry: ({ getService }: FtrProviderContext) => ReturnType<typeof RegistryProvider>;
|
||||
logSynthtraceEsClient: (
|
||||
context: InheritedFtrProviderContext
|
||||
) => Promise<LogsSynthtraceEsClient>;
|
||||
synthtraceEsClient: (context: InheritedFtrProviderContext) => Promise<ApmSynthtraceEsClient>;
|
||||
synthtraceKibanaClient: (
|
||||
context: InheritedFtrProviderContext
|
||||
|
@ -106,6 +115,12 @@ export function createTestConfig(
|
|||
synthtraceEsClient: (context: InheritedFtrProviderContext) => {
|
||||
return bootstrapApmSynthtrace(context, synthtraceKibanaClient);
|
||||
},
|
||||
logSynthtraceEsClient: (context: InheritedFtrProviderContext) =>
|
||||
new LogsSynthtraceEsClient({
|
||||
client: context.getService('es'),
|
||||
logger: createLogger(LogLevel.info),
|
||||
refreshAfterIndex: true,
|
||||
}),
|
||||
synthtraceKibanaClient: () => synthtraceKibanaClient,
|
||||
apmApiClient: async (context: InheritedFtrProviderContext) => {
|
||||
const { username, password } = servers.kibana;
|
||||
|
|
|
@ -0,0 +1,484 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import { log, apm, generateShortId, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { SupertestReturnType } from '../../common/apm_api_supertest';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const apmSynthtraceClient = getService('synthtraceEsClient');
|
||||
const logSynthtraceClient = getService('logSynthtraceEsClient');
|
||||
|
||||
registry.when(
|
||||
'fetching observability alerts details context for AI assistant contextual insights',
|
||||
{ config: 'trial', archives: [] },
|
||||
() => {
|
||||
const start = moment().subtract(10, 'minutes').valueOf();
|
||||
const end = moment().valueOf();
|
||||
const range = timerange(start, end);
|
||||
|
||||
describe('when no traces or logs are available', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns nothing', () => {
|
||||
expect(response.body).to.eql({
|
||||
serviceChangePoints: [],
|
||||
exitSpanChangePoints: [],
|
||||
anomalies: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when traces and logs are ingested and logs are not annotated with service.name', async () => {
|
||||
before(async () => {
|
||||
await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' });
|
||||
await ingestLogs({
|
||||
'container.id': 'my-container-a',
|
||||
'kubernetes.pod.name': 'pod-a',
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
describe('when no params are specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no service summary', async () => {
|
||||
expect(response.body.serviceSummary).to.be(undefined);
|
||||
});
|
||||
|
||||
it('returns no downstream dependencies', async () => {
|
||||
expect(response.body.downstreamDependencies ?? []).to.eql([]);
|
||||
});
|
||||
|
||||
it('returns 1 log category', async () => {
|
||||
expect(response.body.logCategories?.map(({ errorCategory }) => errorCategory)).to.eql([
|
||||
'Error message from container my-container-a',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when service name is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'service.name': 'Backend',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns service summary', () => {
|
||||
expect(response.body.serviceSummary).to.eql({
|
||||
'service.name': 'Backend',
|
||||
'service.environment': ['production'],
|
||||
'agent.name': 'java',
|
||||
'service.version': ['1.0.0'],
|
||||
'language.name': 'java',
|
||||
instances: 1,
|
||||
anomalies: [],
|
||||
alerts: [],
|
||||
deployments: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns downstream dependencies', async () => {
|
||||
expect(response.body.downstreamDependencies).to.eql([
|
||||
{
|
||||
'span.destination.service.resource': 'elasticsearch',
|
||||
'span.type': 'db',
|
||||
'span.subtype': 'elasticsearch',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns log categories', () => {
|
||||
expect(response.body.logCategories).to.have.length(1);
|
||||
|
||||
const logCategory = response.body.logCategories?.[0];
|
||||
expect(logCategory?.sampleMessage).to.match(
|
||||
/Error message #\d{16} from container my-container-a/
|
||||
);
|
||||
expect(logCategory?.docCount).to.be.greaterThan(0);
|
||||
expect(logCategory?.errorCategory).to.be('Error message from container my-container-a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when container id is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'container.id': 'my-container-a',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns service summary', () => {
|
||||
expect(response.body.serviceSummary).to.eql({
|
||||
'service.name': 'Backend',
|
||||
'service.environment': ['production'],
|
||||
'agent.name': 'java',
|
||||
'service.version': ['1.0.0'],
|
||||
'language.name': 'java',
|
||||
instances: 1,
|
||||
anomalies: [],
|
||||
alerts: [],
|
||||
deployments: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns downstream dependencies', async () => {
|
||||
expect(response.body.downstreamDependencies).to.eql([
|
||||
{
|
||||
'span.destination.service.resource': 'elasticsearch',
|
||||
'span.type': 'db',
|
||||
'span.subtype': 'elasticsearch',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns log categories', () => {
|
||||
expect(response.body.logCategories).to.have.length(1);
|
||||
|
||||
const logCategory = response.body.logCategories?.[0];
|
||||
expect(logCategory?.sampleMessage).to.match(
|
||||
/Error message #\d{16} from container my-container-a/
|
||||
);
|
||||
expect(logCategory?.docCount).to.be.greaterThan(0);
|
||||
expect(logCategory?.errorCategory).to.be('Error message from container my-container-a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when non-existing container id is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'container.id': 'non-existing-container',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns nothing', () => {
|
||||
expect(response.body).to.eql({
|
||||
logCategories: [],
|
||||
serviceChangePoints: [],
|
||||
exitSpanChangePoints: [],
|
||||
anomalies: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when non-existing service.name is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'service.name': 'non-existing-service',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty service summary', () => {
|
||||
expect(response.body.serviceSummary).to.eql({
|
||||
'service.name': 'non-existing-service',
|
||||
'service.environment': [],
|
||||
instances: 1,
|
||||
anomalies: [],
|
||||
alerts: [],
|
||||
deployments: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no downstream dependencies', async () => {
|
||||
expect(response.body.downstreamDependencies).to.eql([]);
|
||||
});
|
||||
|
||||
it('returns log categories', () => {
|
||||
expect(response.body.logCategories).to.have.length(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when traces and logs are ingested and logs are annotated with service.name', async () => {
|
||||
before(async () => {
|
||||
await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' });
|
||||
await ingestLogs({
|
||||
'service.name': 'Backend',
|
||||
'container.id': 'my-container-a',
|
||||
'kubernetes.pod.name': 'pod-a',
|
||||
});
|
||||
|
||||
// also ingest unrelated Frontend traces and logs that should not show up in the response when fetching "Backend"-related things
|
||||
await ingestTraces({ 'service.name': 'Frontend', 'container.id': 'my-container-b' });
|
||||
await ingestLogs({
|
||||
'service.name': 'Frontend',
|
||||
'container.id': 'my-container-b',
|
||||
'kubernetes.pod.name': 'pod-b',
|
||||
});
|
||||
|
||||
// also ingest logs that are not annotated with service.name
|
||||
await ingestLogs({
|
||||
'container.id': 'my-container-c',
|
||||
'kubernetes.pod.name': 'pod-c',
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
describe('when no params are specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no service summary', async () => {
|
||||
expect(response.body.serviceSummary).to.be(undefined);
|
||||
});
|
||||
|
||||
it('returns 1 log category', async () => {
|
||||
expect(response.body.logCategories?.map(({ errorCategory }) => errorCategory)).to.eql([
|
||||
'Error message from service',
|
||||
'Error message from container my-container-c',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when service name is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'service.name': 'Backend',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns log categories', () => {
|
||||
expect(response.body.logCategories).to.have.length(1);
|
||||
|
||||
const logCategory = response.body.logCategories?.[0];
|
||||
expect(logCategory?.sampleMessage).to.match(
|
||||
/Error message #\d{16} from service Backend/
|
||||
);
|
||||
expect(logCategory?.docCount).to.be.greaterThan(0);
|
||||
expect(logCategory?.errorCategory).to.be('Error message from service Backend');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when container id is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'container.id': 'my-container-a',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns log categories', () => {
|
||||
expect(response.body.logCategories).to.have.length(1);
|
||||
|
||||
const logCategory = response.body.logCategories?.[0];
|
||||
expect(logCategory?.sampleMessage).to.match(
|
||||
/Error message #\d{16} from service Backend/
|
||||
);
|
||||
expect(logCategory?.docCount).to.be.greaterThan(0);
|
||||
expect(logCategory?.errorCategory).to.be('Error message from service Backend');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when non-existing service.name is specified', async () => {
|
||||
let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>;
|
||||
before(async () => {
|
||||
response = await apmApiClient.writeUser({
|
||||
endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context',
|
||||
params: {
|
||||
query: {
|
||||
alert_started_at: new Date(end).toISOString(),
|
||||
'service.name': 'non-existing-service',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty service summary', () => {
|
||||
expect(response.body.serviceSummary).to.eql({
|
||||
'service.name': 'non-existing-service',
|
||||
'service.environment': [],
|
||||
instances: 1,
|
||||
anomalies: [],
|
||||
alerts: [],
|
||||
deployments: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return log categories', () => {
|
||||
expect(response.body.logCategories?.map(({ errorCategory }) => errorCategory)).to.eql([
|
||||
'Error message from container my-container-c',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function ingestTraces(eventMetadata: {
|
||||
'service.name': string;
|
||||
'container.id'?: string;
|
||||
'host.name'?: string;
|
||||
'kubernetes.pod.name'?: string;
|
||||
}) {
|
||||
const serviceInstance = apm
|
||||
.service({
|
||||
name: eventMetadata['service.name'],
|
||||
environment: 'production',
|
||||
agentName: 'java',
|
||||
})
|
||||
.instance('my-instance');
|
||||
|
||||
const events = range
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return serviceInstance
|
||||
.transaction({ transactionName: 'tx' })
|
||||
.timestamp(timestamp)
|
||||
.duration(10000)
|
||||
.defaults({ 'service.version': '1.0.0', ...eventMetadata })
|
||||
.outcome('success')
|
||||
.children(
|
||||
serviceInstance
|
||||
.span({
|
||||
spanName: 'GET apm-*/_search',
|
||||
spanType: 'db',
|
||||
spanSubtype: 'elasticsearch',
|
||||
})
|
||||
.duration(1000)
|
||||
.success()
|
||||
.destination('elasticsearch')
|
||||
.timestamp(timestamp)
|
||||
);
|
||||
});
|
||||
|
||||
await apmSynthtraceClient.index(events);
|
||||
}
|
||||
|
||||
function ingestLogs(eventMetadata: {
|
||||
'service.name'?: string;
|
||||
'container.id'?: string;
|
||||
'kubernetes.pod.name'?: string;
|
||||
'host.name'?: string;
|
||||
}) {
|
||||
const getMessage = () => {
|
||||
const msgPrefix = `Error message #${generateShortId()}`;
|
||||
|
||||
if (eventMetadata['service.name']) {
|
||||
return `${msgPrefix} from service ${eventMetadata['service.name']}`;
|
||||
}
|
||||
|
||||
if (eventMetadata['container.id']) {
|
||||
return `${msgPrefix} from container ${eventMetadata['container.id']}`;
|
||||
}
|
||||
|
||||
if (eventMetadata['kubernetes.pod.name']) {
|
||||
return `${msgPrefix} from pod ${eventMetadata['kubernetes.pod.name']}`;
|
||||
}
|
||||
|
||||
if (eventMetadata['host.name']) {
|
||||
return `${msgPrefix} from host ${eventMetadata['host.name']}`;
|
||||
}
|
||||
|
||||
return msgPrefix;
|
||||
};
|
||||
|
||||
const events = range
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return [
|
||||
log
|
||||
.create()
|
||||
.message(getMessage())
|
||||
.logLevel('error')
|
||||
.defaults({
|
||||
'trace.id': generateShortId(),
|
||||
'agent.name': 'synth-agent',
|
||||
...eventMetadata,
|
||||
})
|
||||
.timestamp(timestamp),
|
||||
];
|
||||
});
|
||||
|
||||
return logSynthtraceClient.index(events);
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
await apmSynthtraceClient.clean();
|
||||
await logSynthtraceClient.clean();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue