[8.x] [RCA] [Recent events] Create API endpoint to get events (#192947) (#193463)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[RCA] [Recent events] Create API endpoint to get events
(#192947)](https://github.com/elastic/kibana/pull/192947)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Bena
Kansara","email":"69037875+benakansara@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-09-17T09:50:09Z","message":"[RCA]
[Recent events] Create API endpoint to get events (#192947)\n\nCloses
https://github.com/elastic/observability-dev/issues/3924\r\nCloses
https://github.com/elastic/observability-dev/issues/3927\r\n\r\nThis PR
introduces an events API (`/api/observability/events`) that
will\r\nfetch -\r\n- All the \"point in time\" annotations from`
observability-annotations`\r\nindex. This includes both manual and auto
(e.g. service deployment)\r\nannotations\r\n- The annotations will be
filtered with supported source fields\r\n(host.name, service.name,
slo.id, slo.instanceId) when specified as\r\n`filter`\r\n- Alerts that
newly triggered on same source in given time range. The\r\nsource needs
to be specified as `filter`, when no filter is specified\r\nall alerts
triggered in given time range will be returned\r\n\r\n### Testing\r\n-
Create annotations (APM service deployment annotations and
annotations\r\nusing Observability UI)\r\n- Generate some alerts\r\n-
API call should return annotations and alerts, example API
requests\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"annotation.type\":\"deployment\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"slo.id\":\"*\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"host.name\":\"host-0\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z`\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"808212e97e413216655aaa9e755c671656decb46","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","auto-backport","ci:project-deploy-observability","Team:obs-ux-management","v8.16.0","backport:version"],"title":"[RCA]
[Recent events] Create API endpoint to get
events","number":192947,"url":"https://github.com/elastic/kibana/pull/192947","mergeCommit":{"message":"[RCA]
[Recent events] Create API endpoint to get events (#192947)\n\nCloses
https://github.com/elastic/observability-dev/issues/3924\r\nCloses
https://github.com/elastic/observability-dev/issues/3927\r\n\r\nThis PR
introduces an events API (`/api/observability/events`) that
will\r\nfetch -\r\n- All the \"point in time\" annotations from`
observability-annotations`\r\nindex. This includes both manual and auto
(e.g. service deployment)\r\nannotations\r\n- The annotations will be
filtered with supported source fields\r\n(host.name, service.name,
slo.id, slo.instanceId) when specified as\r\n`filter`\r\n- Alerts that
newly triggered on same source in given time range. The\r\nsource needs
to be specified as `filter`, when no filter is specified\r\nall alerts
triggered in given time range will be returned\r\n\r\n### Testing\r\n-
Create annotations (APM service deployment annotations and
annotations\r\nusing Observability UI)\r\n- Generate some alerts\r\n-
API call should return annotations and alerts, example API
requests\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"annotation.type\":\"deployment\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"slo.id\":\"*\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"host.name\":\"host-0\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z`\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"808212e97e413216655aaa9e755c671656decb46"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/192947","number":192947,"mergeCommit":{"message":"[RCA]
[Recent events] Create API endpoint to get events (#192947)\n\nCloses
https://github.com/elastic/observability-dev/issues/3924\r\nCloses
https://github.com/elastic/observability-dev/issues/3927\r\n\r\nThis PR
introduces an events API (`/api/observability/events`) that
will\r\nfetch -\r\n- All the \"point in time\" annotations from`
observability-annotations`\r\nindex. This includes both manual and auto
(e.g. service deployment)\r\nannotations\r\n- The annotations will be
filtered with supported source fields\r\n(host.name, service.name,
slo.id, slo.instanceId) when specified as\r\n`filter`\r\n- Alerts that
newly triggered on same source in given time range. The\r\nsource needs
to be specified as `filter`, when no filter is specified\r\nall alerts
triggered in given time range will be returned\r\n\r\n### Testing\r\n-
Create annotations (APM service deployment annotations and
annotations\r\nusing Observability UI)\r\n- Generate some alerts\r\n-
API call should return annotations and alerts, example API
requests\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"annotation.type\":\"deployment\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"slo.id\":\"*\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z&filter={\"host.name\":\"host-0\"}`\r\n-\r\n`http://localhost:5601/kibana/api/observability/events?rangeFrom=2024-09-01T19:53:20.243Z&rangeTo=2024-09-19T19:53:20.243Z`\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"808212e97e413216655aaa9e755c671656decb46"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Bena Kansara <69037875+benakansara@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-09-20 05:43:54 +10:00 committed by GitHub
parent 0125a030f9
commit 438284e946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 359 additions and 20 deletions

View file

@ -0,0 +1,18 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { z } from '@kbn/zod';
import { eventSchema } from '../schema';
const eventResponseSchema = eventSchema;
type EventResponse = z.output<typeof eventResponseSchema>;
export { eventResponseSchema };
export type { EventResponse };

View file

@ -0,0 +1,31 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { z } from '@kbn/zod';
import { eventResponseSchema } from './event';
const getEventsParamsSchema = z
.object({
query: z
.object({
rangeFrom: z.string(),
rangeTo: z.string(),
filter: z.string(),
})
.partial(),
})
.partial();
const getEventsResponseSchema = z.array(eventResponseSchema);
type GetEventsParams = z.infer<typeof getEventsParamsSchema.shape.query>;
type GetEventsResponse = z.output<typeof getEventsResponseSchema>;
export { getEventsParamsSchema, getEventsResponseSchema };
export type { GetEventsParams, GetEventsResponse };

View file

@ -25,6 +25,8 @@ export type * from './investigation_note';
export type * from './update';
export type * from './update_item';
export type * from './update_note';
export type * from './event';
export type * from './get_events';
export * from './create';
export * from './create_item';
@ -44,3 +46,5 @@ export * from './investigation_note';
export * from './update';
export * from './update_item';
export * from './update_note';
export * from './event';
export * from './get_events';

View file

@ -0,0 +1,51 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { z } from '@kbn/zod';
const eventTypeSchema = z.union([
z.literal('annotation'),
z.literal('alert'),
z.literal('error_rate'),
z.literal('latency'),
z.literal('anomaly'),
]);
const annotationEventSchema = z.object({
eventType: z.literal('annotation'),
annotationType: z.string().optional(),
});
const alertStatusSchema = z.union([
z.literal('active'),
z.literal('flapping'),
z.literal('recovered'),
z.literal('untracked'),
]);
const alertEventSchema = z.object({
eventType: z.literal('alert'),
alertStatus: alertStatusSchema,
});
const sourceSchema = z.record(z.string(), z.any());
const eventSchema = z.intersection(
z.object({
id: z.string(),
title: z.string(),
description: z.string(),
timestamp: z.number(),
eventType: eventTypeSchema,
source: sourceSchema.optional(),
}),
z.discriminatedUnion('eventType', [annotationEventSchema, alertEventSchema])
);
export { eventSchema };

View file

@ -11,5 +11,6 @@ export * from './investigation';
export * from './investigation_item';
export * from './investigation_note';
export * from './origin';
export * from './event';
export type * from './investigation';

View file

@ -19,6 +19,9 @@
"datasetQuality",
"unifiedSearch",
"security",
"observability",
"licensing",
"ruleRegistry"
],
"requiredBundles": [
"esql",

View file

@ -21,7 +21,10 @@ import {
updateInvestigationItemParamsSchema,
updateInvestigationNoteParamsSchema,
updateInvestigationParamsSchema,
getEventsParamsSchema,
GetEventsResponse,
} from '@kbn/investigation-shared';
import { ScopedAnnotationsClient } from '@kbn/observability-plugin/server';
import { createInvestigation } from '../services/create_investigation';
import { createInvestigationItem } from '../services/create_investigation_item';
import { createInvestigationNote } from '../services/create_investigation_note';
@ -35,6 +38,8 @@ import { getInvestigationItems } from '../services/get_investigation_items';
import { getInvestigationNotes } from '../services/get_investigation_notes';
import { investigationRepositoryFactory } from '../services/investigation_repository';
import { updateInvestigation } from '../services/update_investigation';
import { getAlertEvents, getAnnotationEvents } from '../services/get_events';
import { AlertsClient, getAlertsClient } from '../services/get_alerts_client';
import { updateInvestigationItem } from '../services/update_investigation_item';
import { updateInvestigationNote } from '../services/update_investigation_note';
import { createInvestigateAppServerRoute } from './create_investigate_app_server_route';
@ -313,6 +318,32 @@ const deleteInvestigationItemRoute = createInvestigateAppServerRoute({
},
});
const getEventsRoute = createInvestigateAppServerRoute({
endpoint: 'GET /api/observability/events 2023-10-31',
options: {
tags: [],
},
params: getEventsParamsSchema,
handler: async ({ params, context, request, plugins }) => {
const annotationsClient: ScopedAnnotationsClient | undefined =
await plugins.observability.setup.getScopedAnnotationsClient(context, request);
const alertsClient: AlertsClient = await getAlertsClient({ plugins, request });
const events: GetEventsResponse = [];
if (annotationsClient) {
const annotationEvents = await getAnnotationEvents(params?.query ?? {}, annotationsClient);
events.push(...annotationEvents);
}
if (alertsClient) {
const alertEvents = await getAlertEvents(params?.query ?? {}, alertsClient);
events.push(...alertEvents);
}
return events;
},
});
export function getGlobalInvestigateAppServerRouteRepository() {
return {
...createInvestigationRoute,
@ -328,6 +359,7 @@ export function getGlobalInvestigateAppServerRouteRepository() {
...deleteInvestigationItemRoute,
...updateInvestigationItemRoute,
...getInvestigationItemsRoute,
...getEventsRoute,
...getAllInvestigationStatsRoute,
...getAllInvestigationTagsRoute,
};

View file

@ -14,6 +14,7 @@ import type {
SavedObjectsClientContract,
} from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
import type { InvestigateAppSetupDependencies, InvestigateAppStartDependencies } from '../types';
export type InvestigateAppRequestHandlerContext = Omit<
@ -33,6 +34,7 @@ export type InvestigateAppRequestHandlerContext = Omit<
};
coreStart: CoreStart;
}>;
licensing: Promise<LicensingApiRequestHandlerContext>;
};
export interface InvestigateAppRouteHandlerResources {

View file

@ -0,0 +1,49 @@
/*
* 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 { isEmpty } from 'lodash';
import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { InvestigateAppRouteHandlerResources } from '../routes/types';
export type AlertsClient = Awaited<ReturnType<typeof getAlertsClient>>;
export async function getAlertsClient({
plugins,
request,
}: Pick<InvestigateAppRouteHandlerResources, 'plugins' | 'request'>) {
const ruleRegistryPluginStart = await plugins.ruleRegistry.start();
const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request);
const alertsIndices = await alertsClient.getAuthorizedAlertsIndices([
'logs',
'infrastructure',
'apm',
'slo',
'uptime',
'observability',
]);
if (!alertsIndices || isEmpty(alertsIndices)) {
throw Error('No alert indices exist');
}
type RequiredParams = ESSearchRequest & {
size: number;
track_total_hits: boolean | number;
};
return {
search<TParams extends RequiredParams>(
searchParams: TParams
): Promise<InferSearchResponseOf<ParsedTechnicalFields, TParams>> {
return alertsClient.find({
...searchParams,
index: alertsIndices.join(','),
}) as Promise<any>;
},
};
}

View file

@ -0,0 +1,123 @@
/*
* 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 datemath from '@elastic/datemath';
import { estypes } from '@elastic/elasticsearch';
import {
GetEventsParams,
GetEventsResponse,
getEventsResponseSchema,
} from '@kbn/investigation-shared';
import { ScopedAnnotationsClient } from '@kbn/observability-plugin/server';
import {
ALERT_REASON,
ALERT_RULE_CATEGORY,
ALERT_START,
ALERT_STATUS,
ALERT_UUID,
} from '@kbn/rule-data-utils';
import { AlertsClient } from './get_alerts_client';
export function rangeQuery(
start: number,
end: number,
field = '@timestamp'
): estypes.QueryDslQueryContainer[] {
return [
{
range: {
[field]: {
gte: start,
lte: end,
format: 'epoch_millis',
},
},
},
];
}
export async function getAnnotationEvents(
params: GetEventsParams,
annotationsClient: ScopedAnnotationsClient
): Promise<GetEventsResponse> {
const response = await annotationsClient.find({
start: params?.rangeFrom,
end: params?.rangeTo,
filter: params?.filter,
size: 100,
});
// we will return only "point_in_time" annotations
const events = response.items
.filter((item) => !item.event?.end)
.map((item) => {
const hostName = item.host?.name;
const serviceName = item.service?.name;
const serviceVersion = item.service?.version;
const sloId = item.slo?.id;
const sloInstanceId = item.slo?.instanceId;
return {
id: item.id,
title: item.annotation.title,
description: item.message,
timestamp: new Date(item['@timestamp']).getTime(),
eventType: 'annotation',
annotationType: item.annotation.type,
source: {
...(hostName ? { 'host.name': hostName } : undefined),
...(serviceName ? { 'service.name': serviceName } : undefined),
...(serviceVersion ? { 'service.version': serviceVersion } : undefined),
...(sloId ? { 'slo.id': sloId } : undefined),
...(sloInstanceId ? { 'slo.instanceId': sloInstanceId } : undefined),
},
};
});
return getEventsResponseSchema.parse(events);
}
export async function getAlertEvents(
params: GetEventsParams,
alertsClient: AlertsClient
): Promise<GetEventsResponse> {
const startInMs = datemath.parse(params?.rangeFrom ?? 'now-15m')!.valueOf();
const endInMs = datemath.parse(params?.rangeTo ?? 'now')!.valueOf();
const filterJSON = params?.filter ? JSON.parse(params.filter) : {};
const body = {
size: 100,
track_total_hits: false,
query: {
bool: {
filter: [
...rangeQuery(startInMs, endInMs, ALERT_START),
...Object.keys(filterJSON).map((filterKey) => ({
term: { [filterKey]: filterJSON[filterKey] },
})),
],
},
},
};
const response = await alertsClient.search(body);
const events = response.hits.hits.map((hit) => {
const _source = hit._source;
return {
id: _source[ALERT_UUID],
title: `${_source[ALERT_RULE_CATEGORY]} breached`,
description: _source[ALERT_REASON],
timestamp: new Date(_source['@timestamp']).getTime(),
eventType: 'alert',
alertStatus: _source[ALERT_STATUS],
};
});
return getEventsResponseSchema.parse(events);
}

View file

@ -5,13 +5,24 @@
* 2.0.
*/
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
import {
RuleRegistryPluginSetupContract,
RuleRegistryPluginStartContract,
} from '@kbn/rule-registry-plugin/server';
/* eslint-disable @typescript-eslint/no-empty-interface*/
export interface ConfigSchema {}
export interface InvestigateAppSetupDependencies {}
export interface InvestigateAppSetupDependencies {
observability: ObservabilityPluginSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
}
export interface InvestigateAppStartDependencies {}
export interface InvestigateAppStartDependencies {
ruleRegistry: RuleRegistryPluginStartContract;
}
export interface InvestigateAppServerSetup {}

View file

@ -58,5 +58,8 @@
"@kbn/lens-embeddable-utils",
"@kbn/i18n-react",
"@kbn/zod",
"@kbn/observability-plugin",
"@kbn/licensing-plugin",
"@kbn/rule-data-utils",
],
}

View file

@ -96,6 +96,8 @@ export const findAnnotationRt = t.partial({
sloId: t.string,
sloInstanceId: t.string,
serviceName: t.string,
filter: t.string,
size: t.number,
});
export const updateAnnotationRt = t.intersection([

View file

@ -202,7 +202,12 @@ export function createAnnotationsClient(params: {
};
}),
find: ensureGoldLicense(async (findParams: FindAnnotationParams) => {
const { start, end, sloId, sloInstanceId, serviceName } = findParams ?? {};
const { start, end, sloId, sloInstanceId, serviceName, filter, size } = findParams ?? {};
const filterJSON = filter ? JSON.parse(filter) : {};
const termsFilter = Object.keys(filterJSON).map((filterKey) => ({
term: { [filterKey]: filterJSON[filterKey] },
}));
const shouldClauses: QueryDslQueryContainer[] = [];
if (sloId || sloInstanceId) {
@ -246,7 +251,7 @@ export function createAnnotationsClient(params: {
const result = await esClient.search({
index: readIndex,
size: 10000,
size: size ?? 10000,
ignore_unavailable: true,
query: {
bool: {
@ -259,22 +264,26 @@ export function createAnnotationsClient(params: {
},
},
},
{
bool: {
should: [
...(serviceName
? [
{
term: {
'service.name': serviceName,
},
},
]
: []),
...shouldClauses,
],
},
},
...(Object.keys(filterJSON).length !== 0
? termsFilter
: [
{
bool: {
should: [
...(serviceName
? [
{
term: {
'service.name': serviceName,
},
},
]
: []),
...shouldClauses,
],
},
},
]),
],
},
},