diff --git a/packages/kbn-investigation-shared/src/rest_specs/event.ts b/packages/kbn-investigation-shared/src/rest_specs/event.ts new file mode 100644 index 000000000000..df2f3941ad33 --- /dev/null +++ b/packages/kbn-investigation-shared/src/rest_specs/event.ts @@ -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; + +export { eventResponseSchema }; +export type { EventResponse }; diff --git a/packages/kbn-investigation-shared/src/rest_specs/get_events.ts b/packages/kbn-investigation-shared/src/rest_specs/get_events.ts new file mode 100644 index 000000000000..064a75fab156 --- /dev/null +++ b/packages/kbn-investigation-shared/src/rest_specs/get_events.ts @@ -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; +type GetEventsResponse = z.output; + +export { getEventsParamsSchema, getEventsResponseSchema }; +export type { GetEventsParams, GetEventsResponse }; diff --git a/packages/kbn-investigation-shared/src/rest_specs/index.ts b/packages/kbn-investigation-shared/src/rest_specs/index.ts index c00ec5035765..42bec32041af 100644 --- a/packages/kbn-investigation-shared/src/rest_specs/index.ts +++ b/packages/kbn-investigation-shared/src/rest_specs/index.ts @@ -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'; diff --git a/packages/kbn-investigation-shared/src/schema/event.ts b/packages/kbn-investigation-shared/src/schema/event.ts new file mode 100644 index 000000000000..c954a0de13fb --- /dev/null +++ b/packages/kbn-investigation-shared/src/schema/event.ts @@ -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 }; diff --git a/packages/kbn-investigation-shared/src/schema/index.ts b/packages/kbn-investigation-shared/src/schema/index.ts index 7491ecce76cc..f65fe9baf1f6 100644 --- a/packages/kbn-investigation-shared/src/schema/index.ts +++ b/packages/kbn-investigation-shared/src/schema/index.ts @@ -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'; diff --git a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc index ecfe77b5a058..2cc904dafac0 100644 --- a/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc +++ b/x-pack/plugins/observability_solution/investigate_app/kibana.jsonc @@ -19,6 +19,9 @@ "datasetQuality", "unifiedSearch", "security", + "observability", + "licensing", + "ruleRegistry" ], "requiredBundles": [ "esql", diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts index b0ecd8927591..195fbdb23436 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/get_global_investigate_app_server_route_repository.ts @@ -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, }; diff --git a/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts b/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts index 2e882296adff..afb022cdc9b7 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/routes/types.ts @@ -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; }; export interface InvestigateAppRouteHandlerResources { diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_alerts_client.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_alerts_client.ts new file mode 100644 index 000000000000..bf1070307742 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_alerts_client.ts @@ -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>; + +export async function getAlertsClient({ + plugins, + request, +}: Pick) { + 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( + searchParams: TParams + ): Promise> { + return alertsClient.find({ + ...searchParams, + index: alertsIndices.join(','), + }) as Promise; + }, + }; +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts b/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts new file mode 100644 index 000000000000..52eeea7a4cbc --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/server/services/get_events.ts @@ -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 { + 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 { + 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); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/server/types.ts b/x-pack/plugins/observability_solution/investigate_app/server/types.ts index 6fa1196b23b7..fa4db6ccfcb0 100644 --- a/x-pack/plugins/observability_solution/investigate_app/server/types.ts +++ b/x-pack/plugins/observability_solution/investigate_app/server/types.ts @@ -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 {} diff --git a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json index 03651f3530c6..cd687f2dcfe7 100644 --- a/x-pack/plugins/observability_solution/investigate_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/investigate_app/tsconfig.json @@ -58,5 +58,8 @@ "@kbn/lens-embeddable-utils", "@kbn/i18n-react", "@kbn/zod", + "@kbn/observability-plugin", + "@kbn/licensing-plugin", + "@kbn/rule-data-utils", ], } diff --git a/x-pack/plugins/observability_solution/observability/common/annotations.ts b/x-pack/plugins/observability_solution/observability/common/annotations.ts index 16c6e11b81e8..874234acc6ce 100644 --- a/x-pack/plugins/observability_solution/observability/common/annotations.ts +++ b/x-pack/plugins/observability_solution/observability/common/annotations.ts @@ -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([ diff --git a/x-pack/plugins/observability_solution/observability/server/lib/annotations/create_annotations_client.ts b/x-pack/plugins/observability_solution/observability/server/lib/annotations/create_annotations_client.ts index 6cf3b6345982..5bd7395c3ca7 100644 --- a/x-pack/plugins/observability_solution/observability/server/lib/annotations/create_annotations_client.ts +++ b/x-pack/plugins/observability_solution/observability/server/lib/annotations/create_annotations_client.ts @@ -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, + ], + }, + }, + ]), ], }, },