diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts index 63b62d183da4c4..3c885eef658d5b 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_alerts_client.ts @@ -12,7 +12,10 @@ import type { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/re export type ApmAlertsClient = Awaited>; -export async function getApmAlertsClient({ plugins, request }: MinimalAPMRouteHandlerResources) { +export async function getApmAlertsClient({ + plugins, + request, +}: Pick) { const ruleRegistryPluginStart = await plugins.ruleRegistry.start(); const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request); const apmAlertsIndices = await alertsClient.getAuthorizedAlertsIndices(['apm']); diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts index 3b80c5ede61d3a..b756876eb3212b 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_apm_event_client.ts @@ -13,12 +13,11 @@ import { MinimalAPMRouteHandlerResources } from '../../routes/apm_routes/registe export async function getApmEventClient({ context, params, - config, getApmIndices, request, }: Pick< MinimalAPMRouteHandlerResources, - 'context' | 'params' | 'config' | 'getApmIndices' | 'request' + 'context' | 'params' | 'getApmIndices' | 'request' >): Promise { return withApmSpan('get_apm_event_client', async () => { const coreContext = await context.core; diff --git a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_ml_client.ts b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_ml_client.ts index 16b19b7ebed4f6..b94a1abd67e2a8 100644 --- a/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_ml_client.ts +++ b/x-pack/plugins/observability_solution/apm/server/lib/helpers/get_ml_client.ts @@ -15,7 +15,11 @@ export interface MlClient { modules: MlModules; } -export async function getMlClient({ plugins, context, request }: MinimalAPMRouteHandlerResources) { +export async function getMlClient({ + plugins, + context, + request, +}: Pick) { const [coreContext, licensingContext] = await Promise.all([context.core, context.licensing]); const mlplugin = plugins.ml; diff --git a/x-pack/plugins/observability_solution/apm/server/plugin.ts b/x-pack/plugins/observability_solution/apm/server/plugin.ts index 415fd90af7daa8..2c2392b8454158 100644 --- a/x-pack/plugins/observability_solution/apm/server/plugin.ts +++ b/x-pack/plugins/observability_solution/apm/server/plugin.ts @@ -40,6 +40,7 @@ import { createApmSourceMapIndexTemplate } from './routes/source_maps/create_apm import { addApiKeysToEveryPackagePolicyIfMissing } from './routes/fleet/api_keys/add_api_keys_to_policies_if_missing'; import { apmTutorialCustomIntegration } from '../common/tutorial/tutorials'; import { registerAssistantFunctions } from './assistant_functions'; +import { getAlertDetailsContextHandler } from './routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler'; export class APMPlugin implements Plugin @@ -52,7 +53,7 @@ export class APMPlugin } public setup(core: CoreSetup, plugins: APMPluginSetupDependencies) { - this.logger = this.initContext.logger.get(); + const logger = (this.logger = this.initContext.logger.get()); const config$ = this.initContext.config.create(); core.savedObjects.registerType(apmTelemetry); @@ -221,6 +222,10 @@ export class APMPlugin }) ); + plugins.observability.alertDetailsContextualInsightsService.registerHandler( + getAlertDetailsContextHandler(resourcePlugins, logger) + ); + return { config$ }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts new file mode 100644 index 00000000000000..2ffbdc30a1c525 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_changepoints/index.ts @@ -0,0 +1,146 @@ +/* + * 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 { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { ApmTimeseriesType, getApmTimeseries, TimeseriesChangePoint } from '../get_apm_timeseries'; + +export interface ChangePointGrouping { + title: string; + grouping: string; + changes: TimeseriesChangePoint[]; +} + +export async function getServiceChangePoints({ + apmEventClient, + alertStartedAt, + serviceName, + serviceEnvironment, + transactionType, + transactionName, +}: { + apmEventClient: APMEventClient; + alertStartedAt: string; + serviceName: string | undefined; + serviceEnvironment: string | undefined; + transactionType: string | undefined; + transactionName: string | undefined; +}): Promise { + if (!serviceName) { + return []; + } + + const res = await getApmTimeseries({ + apmEventClient, + arguments: { + start: moment(alertStartedAt).subtract(12, 'hours').toISOString(), + end: alertStartedAt, + stats: [ + { + title: 'Latency', + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + timeseries: { + name: ApmTimeseriesType.transactionLatency, + function: LatencyAggregationType.p95, + 'transaction.type': transactionType, + 'transaction.name': transactionName, + }, + }, + { + title: 'Throughput', + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + timeseries: { + name: ApmTimeseriesType.transactionThroughput, + 'transaction.type': transactionType, + 'transaction.name': transactionName, + }, + }, + { + title: 'Failure rate', + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + timeseries: { + name: ApmTimeseriesType.transactionFailureRate, + 'transaction.type': transactionType, + 'transaction.name': transactionName, + }, + }, + { + title: 'Error events', + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + timeseries: { + name: ApmTimeseriesType.errorEventRate, + }, + }, + ], + }, + }); + + return res + .filter((timeseries) => timeseries.changes.length > 0) + .map((timeseries) => ({ + title: timeseries.stat.title, + grouping: timeseries.id, + changes: timeseries.changes, + })); +} + +export async function getExitSpanChangePoints({ + apmEventClient, + alertStartedAt, + serviceName, + serviceEnvironment, +}: { + apmEventClient: APMEventClient; + alertStartedAt: string; + serviceName: string | undefined; + serviceEnvironment: string | undefined; +}): Promise { + if (!serviceName) { + return []; + } + + const res = await getApmTimeseries({ + apmEventClient, + arguments: { + start: moment(alertStartedAt).subtract(30, 'minute').toISOString(), + end: alertStartedAt, + stats: [ + { + title: 'Exit span latency', + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + timeseries: { + name: ApmTimeseriesType.exitSpanLatency, + }, + }, + { + title: 'Exit span failure rate', + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + timeseries: { + name: ApmTimeseriesType.exitSpanFailureRate, + }, + }, + ], + }, + }); + + return res + .filter((timeseries) => timeseries.changes.length > 0) + .map((timeseries) => { + return { + title: timeseries.stat.title, + grouping: timeseries.id, + changes: timeseries.changes, + }; + }); +} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts index c842512507becd..990b63f412f764 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_log_categories/index.ts @@ -31,7 +31,7 @@ export async function getLogCategories({ arguments: args, }: { esClient: ElasticsearchClient; - coreContext: CoreRequestHandlerContext; + coreContext: Pick; arguments: { start: string; end: string; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts new file mode 100644 index 00000000000000..cd1a56d56f45e4 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts @@ -0,0 +1,85 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import { + AlertDetailsContextualInsightsHandlerQuery, + AlertDetailsContextualInsightsRequestContext, +} from '@kbn/observability-plugin/server/services'; +import { getApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; +import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client'; +import { getMlClient } from '../../../lib/helpers/get_ml_client'; +import { getRandomSampler } from '../../../lib/helpers/get_random_sampler'; +import { getObservabilityAlertDetailsContext } from '.'; +import { APMRouteHandlerResources } from '../../apm_routes/register_apm_server_routes'; + +export const getAlertDetailsContextHandler = ( + resourcePlugins: APMRouteHandlerResources['plugins'], + logger: Logger +) => { + return async ( + requestContext: AlertDetailsContextualInsightsRequestContext, + query: AlertDetailsContextualInsightsHandlerQuery + ) => { + const resources = { + getApmIndices: async () => { + const coreContext = await requestContext.core; + return resourcePlugins.apmDataAccess.setup.getApmIndices(coreContext.savedObjects.client); + }, + request: requestContext.request, + params: { query: { _inspect: false } }, + plugins: resourcePlugins, + context: { + core: requestContext.core, + licensing: requestContext.licensing, + alerting: resourcePlugins.alerting!.start().then((startContract) => { + return { + getRulesClient() { + return startContract.getRulesClientWithRequest(requestContext.request); + }, + }; + }), + rac: resourcePlugins.ruleRegistry.start().then((startContract) => { + return { + getAlertsClient() { + return startContract.getRacClientWithRequest(requestContext.request); + }, + }; + }), + }, + }; + + const [apmEventClient, annotationsClient, apmAlertsClient, coreContext, mlClient] = + await Promise.all([ + getApmEventClient(resources), + resourcePlugins.observability.setup.getScopedAnnotationsClient( + resources.context, + requestContext.request + ), + getApmAlertsClient(resources), + requestContext.core, + getMlClient(resources), + getRandomSampler({ + security: resourcePlugins.security, + probability: 1, + request: requestContext.request, + }), + ]); + const esClient = coreContext.elasticsearch.client.asCurrentUser; + + return getObservabilityAlertDetailsContext({ + coreContext, + apmEventClient, + annotationsClient, + apmAlertsClient, + mlClient, + esClient, + query, + logger, + }); + }; +}; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_apm_alert_details_context_prompt.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_apm_alert_details_context_prompt.ts new file mode 100644 index 00000000000000..4a28a0460ebbdd --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_apm_alert_details_context_prompt.ts @@ -0,0 +1,85 @@ +/* + * 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 { AlertDetailsContextualInsight } from '@kbn/observability-plugin/server/services'; +import { APMDownstreamDependency } from '../get_apm_downstream_dependencies'; +import { ServiceSummary } from '../get_apm_service_summary'; +import { LogCategories } from '../get_log_categories'; +import { ApmAnomalies } from '../get_apm_service_summary/get_anomalies'; +import { ChangePointGrouping } from '../get_changepoints'; + +export function getApmAlertDetailsContextPrompt({ + serviceName, + serviceEnvironment, + serviceSummary, + downstreamDependencies, + logCategories, + serviceChangePoints, + exitSpanChangePoints, + anomalies, +}: { + serviceName?: string; + serviceEnvironment?: string; + serviceSummary?: ServiceSummary; + downstreamDependencies?: APMDownstreamDependency[]; + logCategories: LogCategories; + serviceChangePoints?: ChangePointGrouping[]; + exitSpanChangePoints?: ChangePointGrouping[]; + anomalies?: ApmAnomalies; +}): AlertDetailsContextualInsight[] { + const prompt: AlertDetailsContextualInsight[] = []; + if (!isEmpty(serviceSummary)) { + prompt.push({ + key: 'serviceSummary', + description: 'Metadata for the service where the alert occurred', + data: serviceSummary, + }); + } + + if (!isEmpty(downstreamDependencies)) { + prompt.push({ + key: 'downstreamDependencies', + description: `Downstream dependencies from the service "${serviceName}". Problems in these services can negatively affect the performance of "${serviceName}"`, + data: downstreamDependencies, + }); + } + + if (!isEmpty(serviceChangePoints)) { + prompt.push({ + key: 'serviceChangePoints', + description: `Significant change points for "${serviceName}". Use this to spot dips and spikes in throughput, latency and failure rate`, + data: serviceChangePoints, + }); + } + + if (!isEmpty(exitSpanChangePoints)) { + prompt.push({ + key: 'exitSpanChangePoints', + description: `Significant change points for the dependencies of "${serviceName}". Use this to spot dips or spikes in throughput, latency and failure rate for downstream dependencies`, + data: exitSpanChangePoints, + }); + } + + if (!isEmpty(logCategories)) { + prompt.push({ + key: 'logCategories', + description: `Log events occurring around the time of the alert`, + data: logCategories, + }); + } + + if (!isEmpty(anomalies)) { + prompt.push({ + key: 'anomalies', + description: `Anomalies for services running in the environment "${serviceEnvironment}"`, + data: anomalies, + }); + } + + return prompt; +} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts index 953b49890a5216..22679dd55ded02 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_container_id_from_signals.ts @@ -12,12 +12,12 @@ import { rangeQuery, typedSearch } from '@kbn/observability-plugin/server/utils/ import * as t from 'io-ts'; import moment from 'moment'; import { ESSearchRequest } from '@kbn/es-types'; +import { observabilityAlertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, APMEventESSearchRequest, } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { observabilityAlertDetailsContextRt } from '.'; import { RollupInterval } from '../../../../common/rollup'; export async function getContainerIdFromSignals({ @@ -28,7 +28,7 @@ export async function getContainerIdFromSignals({ }: { query: t.TypeOf; esClient: ElasticsearchClient; - coreContext: CoreRequestHandlerContext; + coreContext: Pick; apmEventClient: APMEventClient; }) { if (query['container.id']) { @@ -76,7 +76,7 @@ async function getContainerIdFromLogs({ }: { params: ESSearchRequest['body']; esClient: ElasticsearchClient; - coreContext: CoreRequestHandlerContext; + coreContext: Pick; }) { const index = await coreContext.uiSettings.client.get(aiAssistantLogsIndexPattern); const res = await typedSearch<{ container: { id: string } }, any>(esClient, { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts index 3f79178b76d195..bd62b998bee994 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_service_name_from_signals.ts @@ -12,12 +12,12 @@ import { rangeQuery, termQuery, typedSearch } from '@kbn/observability-plugin/se import * as t from 'io-ts'; import moment from 'moment'; import { ESSearchRequest } from '@kbn/es-types'; +import { observabilityAlertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, APMEventESSearchRequest, } from '../../../lib/helpers/create_es_client/create_apm_event_client'; -import { observabilityAlertDetailsContextRt } from '.'; import { RollupInterval } from '../../../../common/rollup'; export async function getServiceNameFromSignals({ @@ -28,7 +28,7 @@ export async function getServiceNameFromSignals({ }: { query: t.TypeOf; esClient: ElasticsearchClient; - coreContext: CoreRequestHandlerContext; + coreContext: Pick; apmEventClient: APMEventClient; }) { if (query['service.name']) { @@ -85,7 +85,7 @@ async function getServiceNameFromLogs({ }: { params: ESSearchRequest['body']; esClient: ElasticsearchClient; - coreContext: CoreRequestHandlerContext; + coreContext: Pick; }) { const index = await coreContext.uiSettings.client.get(aiAssistantLogsIndexPattern); const res = await typedSearch<{ service: { name: string } }, any>(esClient, { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts index 0b76089d1e1c58..d6022876c9f3b9 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts @@ -8,37 +8,22 @@ import type { ScopedAnnotationsClient } from '@kbn/observability-plugin/server'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { CoreRequestHandlerContext, Logger } from '@kbn/core/server'; +import { + AlertDetailsContextualInsight, + AlertDetailsContextualInsightsHandlerQuery, +} from '@kbn/observability-plugin/server/services'; import moment from 'moment'; -import * as t from 'io-ts'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import type { MlClient } from '../../../lib/helpers/get_ml_client'; import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import type { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; import { getApmServiceSummary } from '../get_apm_service_summary'; import { getAssistantDownstreamDependencies } from '../get_apm_downstream_dependencies'; import { getLogCategories } from '../get_log_categories'; -import { ApmTimeseriesType, getApmTimeseries } from '../get_apm_timeseries'; import { getAnomalies } from '../get_apm_service_summary/get_anomalies'; import { getServiceNameFromSignals } from './get_service_name_from_signals'; import { getContainerIdFromSignals } from './get_container_id_from_signals'; - -export const observabilityAlertDetailsContextRt = t.intersection([ - t.type({ - alert_started_at: t.string, - }), - t.partial({ - // apm fields - 'service.name': t.string, - 'service.environment': t.string, - 'transaction.type': t.string, - 'transaction.name': t.string, - - // infrastructure fields - 'host.name': t.string, - 'container.id': t.string, - 'kubernetes.pod.name': t.string, - }), -]); +import { getApmAlertDetailsContextPrompt } from './get_apm_alert_details_context_prompt'; +import { getExitSpanChangePoints, getServiceChangePoints } from '../get_changepoints'; export async function getObservabilityAlertDetailsContext({ coreContext, @@ -50,15 +35,15 @@ export async function getObservabilityAlertDetailsContext({ mlClient, query, }: { - coreContext: CoreRequestHandlerContext; + coreContext: Pick; annotationsClient?: ScopedAnnotationsClient; apmAlertsClient: ApmAlertsClient; apmEventClient: APMEventClient; esClient: ElasticsearchClient; logger: Logger; mlClient?: MlClient; - query: t.TypeOf; -}) { + query: AlertDetailsContextualInsightsHandlerQuery; +}): Promise { const alertStartedAt = query.alert_started_at; const serviceEnvironment = query['service.environment']; const hostName = query['host.name']; @@ -182,141 +167,14 @@ export async function getObservabilityAlertDetailsContext({ anomaliesPromise, ]); - return { + return getApmAlertDetailsContextPrompt({ + serviceName, + serviceEnvironment, serviceSummary, downstreamDependencies, logCategories, serviceChangePoints, exitSpanChangePoints, anomalies, - }; -} - -async function getServiceChangePoints({ - apmEventClient, - alertStartedAt, - serviceName, - serviceEnvironment, - transactionType, - transactionName, -}: { - apmEventClient: APMEventClient; - alertStartedAt: string; - serviceName: string | undefined; - serviceEnvironment: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; -}) { - if (!serviceName) { - return []; - } - - const res = await getApmTimeseries({ - apmEventClient, - arguments: { - start: moment(alertStartedAt).subtract(12, 'hours').toISOString(), - end: alertStartedAt, - stats: [ - { - title: 'Latency', - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - timeseries: { - name: ApmTimeseriesType.transactionLatency, - function: LatencyAggregationType.p95, - 'transaction.type': transactionType, - 'transaction.name': transactionName, - }, - }, - { - title: 'Throughput', - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - timeseries: { - name: ApmTimeseriesType.transactionThroughput, - 'transaction.type': transactionType, - 'transaction.name': transactionName, - }, - }, - { - title: 'Failure rate', - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - timeseries: { - name: ApmTimeseriesType.transactionFailureRate, - 'transaction.type': transactionType, - 'transaction.name': transactionName, - }, - }, - { - title: 'Error events', - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - timeseries: { - name: ApmTimeseriesType.errorEventRate, - }, - }, - ], - }, }); - - return res - .filter((timeseries) => timeseries.changes.length > 0) - .map((timeseries) => ({ - title: timeseries.stat.title, - grouping: timeseries.id, - changes: timeseries.changes, - })); -} - -async function getExitSpanChangePoints({ - apmEventClient, - alertStartedAt, - serviceName, - serviceEnvironment, -}: { - apmEventClient: APMEventClient; - alertStartedAt: string; - serviceName: string | undefined; - serviceEnvironment: string | undefined; -}) { - if (!serviceName) { - return []; - } - - const res = await getApmTimeseries({ - apmEventClient, - arguments: { - start: moment(alertStartedAt).subtract(30, 'minute').toISOString(), - end: alertStartedAt, - stats: [ - { - title: 'Exit span latency', - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - timeseries: { - name: ApmTimeseriesType.exitSpanLatency, - }, - }, - { - title: 'Exit span failure rate', - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - timeseries: { - name: ApmTimeseriesType.exitSpanFailureRate, - }, - }, - ], - }, - }); - - return res - .filter((timeseries) => timeseries.changes.length > 0) - .map((timeseries) => { - return { - title: timeseries.stat.title, - grouping: timeseries.id, - changes: timeseries.changes, - }; - }); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts index a94b13b79577cf..af3dfac613bd57 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/route.ts @@ -6,33 +6,26 @@ */ import * as t from 'io-ts'; import { omit } from 'lodash'; +import { + AlertDetailsContextualInsight, + observabilityAlertDetailsContextRt, +} from '@kbn/observability-plugin/server/services'; import { getApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client'; import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; import { getMlClient } from '../../lib/helpers/get_ml_client'; import { getRandomSampler } from '../../lib/helpers/get_random_sampler'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; -import { - observabilityAlertDetailsContextRt, - getObservabilityAlertDetailsContext, -} from './get_observability_alert_details_context'; +import { getObservabilityAlertDetailsContext } from './get_observability_alert_details_context'; import { downstreamDependenciesRouteRt, getAssistantDownstreamDependencies, type APMDownstreamDependency, } from './get_apm_downstream_dependencies'; -import { type ServiceSummary } from './get_apm_service_summary'; -import { ApmAnomalies } from './get_apm_service_summary/get_anomalies'; -import { - getApmTimeseries, - getApmTimeseriesRt, - TimeseriesChangePoint, - type ApmTimeseries, -} from './get_apm_timeseries'; -import { LogCategories } from './get_log_categories'; +import { getApmTimeseries, getApmTimeseriesRt, type ApmTimeseries } from './get_apm_timeseries'; const getObservabilityAlertDetailsContextRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', options: { tags: ['access:apm'], }, @@ -40,22 +33,7 @@ const getObservabilityAlertDetailsContextRoute = createApmServerRoute({ params: t.type({ query: observabilityAlertDetailsContextRt, }), - handler: async ( - resources - ): Promise<{ - serviceSummary?: ServiceSummary; - downstreamDependencies?: APMDownstreamDependency[]; - logCategories?: LogCategories; - serviceChangePoints?: Array<{ - title: string; - changes: TimeseriesChangePoint[]; - }>; - exitSpanChangePoints?: Array<{ - title: string; - changes: TimeseriesChangePoint[]; - }>; - anomalies?: ApmAnomalies; - }> => { + handler: async (resources): Promise<{ context: AlertDetailsContextualInsight[] }> => { const { context, request, plugins, logger, params } = resources; const { query } = params; @@ -74,7 +52,7 @@ const getObservabilityAlertDetailsContextRoute = createApmServerRoute({ ]); const esClient = coreContext.elasticsearch.client.asCurrentUser; - return getObservabilityAlertDetailsContext({ + const obsAlertContext = await getObservabilityAlertDetailsContext({ coreContext, annotationsClient, apmAlertsClient, @@ -84,6 +62,8 @@ const getObservabilityAlertDetailsContextRoute = createApmServerRoute({ mlClient, query, }); + + return { context: obsAlertContext }; }, }); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx index 5b043e3ac89282..1de4b4a136919e 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details_contextual_insights.tsx @@ -10,7 +10,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import dedent from 'dedent'; -import { isEmpty } from 'lodash'; import { useKibana } from '../../utils/kibana_react'; import { AlertData } from '../../hooks/use_fetch_alert_detail'; @@ -29,7 +28,9 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu } try { - const res = await http.get('/internal/apm/assistant/get_obs_alert_details_context', { + const { context } = await http.get<{ + context: Array<{ description: string; data: unknown }>; + }>('/internal/apm/assistant/alert_details_contextual_insights', { query: { alert_started_at: new Date(alert.formatted.start).toISOString(), @@ -46,60 +47,9 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu }, }); - const { - serviceSummary, - downstreamDependencies, - logCategories, - serviceChangePoints, - exitSpanChangePoints, - anomalies, - } = res as any; - - const serviceName = fields['service.name']; - const serviceEnvironment = fields['service.environment']; - - const obsAlertContext = `${ - !isEmpty(serviceSummary) - ? `Metadata for the service where the alert occurred: -${JSON.stringify(serviceSummary, null, 2)}` - : '' - } - - ${ - !isEmpty(downstreamDependencies) - ? `Downstream dependencies from the service "${serviceName}". Problems in these services can negatively affect the performance of "${serviceName}": -${JSON.stringify(downstreamDependencies, null, 2)}` - : '' - } - - ${ - !isEmpty(serviceChangePoints) - ? `Significant change points for "${serviceName}". Use this to spot dips and spikes in throughput, latency and failure rate: - ${JSON.stringify(serviceChangePoints, null, 2)}` - : '' - } - - ${ - !isEmpty(exitSpanChangePoints) - ? `Significant change points for the dependencies of "${serviceName}". Use this to spot dips or spikes in throughput, latency and failure rate for downstream dependencies: - ${JSON.stringify(exitSpanChangePoints, null, 2)}` - : '' - } - - ${ - !isEmpty(logCategories) - ? `Log events occurring around the time of the alert: - ${JSON.stringify(logCategories, null, 2)}` - : '' - } - - ${ - !isEmpty(anomalies) - ? `Anomalies for services running in the environment "${serviceEnvironment}": - ${anomalies}` - : '' - } - `; + const obsAlertContext = context + .map(({ description, data }) => `${description}:\n${JSON.stringify(data, null, 2)}`) + .join('\n\n'); return observabilityAIAssistant.getContextualInsightMessages({ message: `I'm looking at an alert and trying to understand why it was triggered`, diff --git a/x-pack/plugins/observability_solution/observability/server/plugin.ts b/x-pack/plugins/observability_solution/observability/server/plugin.ts index a9ae78ee146aea..fad3db3f5fd109 100644 --- a/x-pack/plugins/observability_solution/observability/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/server/plugin.ts @@ -52,6 +52,7 @@ import { registerRuleTypes } from './lib/rules/register_rule_types'; import { getObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; import { registerRoutes } from './routes/register_routes'; import { threshold } from './saved_objects/threshold'; +import { AlertDetailsContextualInsightsService } from './services'; import { uiSettings } from './ui_settings'; export type ObservabilityPluginSetup = ReturnType; @@ -99,6 +100,8 @@ export class ObservabilityPlugin implements Plugin { const logsExplorerLocator = plugins.share.url.locators.get(LOGS_EXPLORER_LOCATOR_ID); + const alertDetailsContextualInsightsService = new AlertDetailsContextualInsightsService(); + plugins.features.registerKibanaFeature({ id: casesFeatureId, name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { @@ -293,6 +296,9 @@ export class ObservabilityPlugin implements Plugin { }, spaces: pluginStart.spaces, ruleDataService, + assistant: { + alertDetailsContextualInsightsService, + }, getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest, }, logger: this.logger, @@ -312,6 +318,7 @@ export class ObservabilityPlugin implements Plugin { const api = await annotationsApiPromise; return api?.getScopedAnnotationsClient(...args); }, + alertDetailsContextualInsightsService, alertsLocator, }; } diff --git a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts index 92980f20c4646b..373e91d89a1c3a 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts @@ -19,6 +19,7 @@ import axios from 'axios'; import * as t from 'io-ts'; import { ObservabilityConfig } from '..'; import { getHTTPResponseCode, ObservabilityError } from '../errors'; +import { AlertDetailsContextualInsightsService } from '../services'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; @@ -36,6 +37,9 @@ export interface RegisterRoutesDependencies { }; spaces?: SpacesPluginStart; ruleDataService: RuleDataPluginService; + assistant: { + alertDetailsContextualInsightsService: AlertDetailsContextualInsightsService; + }; getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi; } diff --git a/x-pack/plugins/observability_solution/observability/server/services/index.test.ts b/x-pack/plugins/observability_solution/observability/server/services/index.test.ts new file mode 100644 index 00000000000000..d0dcb08e9f31df --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/services/index.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { + AlertDetailsContextualInsightsHandlerQuery, + AlertDetailsContextualInsightsRequestContext, + AlertDetailsContextualInsightsService, +} from '.'; + +describe('AlertDetailsContextualInsightsService', () => { + it('concatenates context from registered handlers', async () => { + const service = new AlertDetailsContextualInsightsService(); + service.registerHandler(async () => [{ key: 'foo', description: 'foo', data: 'hello' }]); + service.registerHandler(async () => [{ key: 'bar', description: 'bar', data: 'hello' }]); + const context = await service.getAlertDetailsContext( + {} as AlertDetailsContextualInsightsRequestContext, + {} as AlertDetailsContextualInsightsHandlerQuery + ); + + expect(context).toEqual([ + { key: 'foo', description: 'foo', data: 'hello' }, + { key: 'bar', description: 'bar', data: 'hello' }, + ]); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability/server/services/index.ts b/x-pack/plugins/observability_solution/observability/server/services/index.ts new file mode 100644 index 00000000000000..7c20d191440d67 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/services/index.ts @@ -0,0 +1,87 @@ +/* + * 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 * as t from 'io-ts'; +import { + IScopedClusterClient, + IUiSettingsClient, + KibanaRequest, + SavedObjectsClientContract, +} from '@kbn/core/server'; +import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; +import { concat } from 'lodash'; + +export const observabilityAlertDetailsContextRt = t.intersection([ + t.type({ + alert_started_at: t.string, + }), + t.partial({ + // apm fields + 'service.name': t.string, + 'service.environment': t.string, + 'transaction.type': t.string, + 'transaction.name': t.string, + + // infrastructure fields + 'host.name': t.string, + 'container.id': t.string, + 'kubernetes.pod.name': t.string, + }), +]); + +export type AlertDetailsContextualInsightsHandlerQuery = t.TypeOf< + typeof observabilityAlertDetailsContextRt +>; + +export interface AlertDetailsContextualInsight { + key: string; + description: string; + data: unknown; +} + +export interface AlertDetailsContextualInsightsRequestContext { + request: KibanaRequest; + core: Promise<{ + elasticsearch: { + client: IScopedClusterClient; + }; + uiSettings: { + client: IUiSettingsClient; + globalClient: IUiSettingsClient; + }; + savedObjects: { + client: SavedObjectsClientContract; + }; + }>; + licensing: Promise; +} +type AlertDetailsContextualInsightsHandler = ( + context: AlertDetailsContextualInsightsRequestContext, + query: AlertDetailsContextualInsightsHandlerQuery +) => Promise; + +export class AlertDetailsContextualInsightsService { + private handlers: AlertDetailsContextualInsightsHandler[] = []; + + constructor() {} + + registerHandler(handler: AlertDetailsContextualInsightsHandler) { + this.handlers.push(handler); + } + + async getAlertDetailsContext( + context: AlertDetailsContextualInsightsRequestContext, + query: AlertDetailsContextualInsightsHandlerQuery + ): Promise { + if (this.handlers.length === 0) return []; + + return Promise.all(this.handlers.map((handler) => handler(context, query))).then((results) => { + const [head, ...rest] = results; + return concat(head, ...rest); + }); + } +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts index c63a4a902b3379..c0b37b7142a83a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts @@ -40,6 +40,7 @@ export type ObservabilityAIAssistantRequestHandlerContext = Omit< }; uiSettings: { client: IUiSettingsClient; + globalClient: IUiSettingsClient; }; savedObjects: { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc index b64d31e3f13b90..17a9812631e398 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc @@ -11,6 +11,7 @@ "aiAssistantManagementSelection", "observabilityAIAssistant", "observabilityShared", + "observability", "actions", "data", "dataViews", diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts index 903c3c0c268264..63e06818a2b70b 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/plugin.ts @@ -96,6 +96,7 @@ export class ObservabilityAIAssistantAppPlugin }, uiSettings: { client: coreStart.uiSettings.asScopedToClient(savedObjectsClient), + globalClient: coreStart.uiSettings.globalAsScopedToClient(savedObjectsClient), }, savedObjects: { client: savedObjectsClient, @@ -113,7 +114,12 @@ export class ObservabilityAIAssistantAppPlugin }; }; - plugins.actions.registerType(getObsAIAssistantConnectorType(initResources)); + plugins.actions.registerType( + getObsAIAssistantConnectorType( + initResources, + plugins.observability.alertDetailsContextualInsightsService + ) + ); plugins.alerting.registerConnectorAdapter(getObsAIAssistantConnectorAdapter()); return {}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts index 190ce8c9ef95c1..479ffeaa40f4f5 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.test.ts @@ -16,6 +16,7 @@ import { } from '.'; import { Observable } from 'rxjs'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public'; +import { AlertDetailsContextualInsightsService } from '@kbn/observability-plugin/server/services'; describe('observabilityAIAssistant rule_connector', () => { describe('getObsAIAssistantConnectorAdapter', () => { @@ -56,7 +57,10 @@ describe('observabilityAIAssistant rule_connector', () => { const initResources = jest .fn() .mockResolvedValue({} as ObservabilityAIAssistantRouteHandlerResources); - const connectorType = getObsAIAssistantConnectorType(initResources); + const connectorType = getObsAIAssistantConnectorType( + initResources, + new AlertDetailsContextualInsightsService() + ); expect(connectorType.id).toEqual(OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID); expect(connectorType.isSystemActionType).toEqual(true); expect(connectorType.minimumLicenseRequired).toEqual('enterprise'); @@ -66,7 +70,10 @@ describe('observabilityAIAssistant rule_connector', () => { const initResources = jest .fn() .mockResolvedValue({} as ObservabilityAIAssistantRouteHandlerResources); - const connectorType = getObsAIAssistantConnectorType(initResources); + const connectorType = getObsAIAssistantConnectorType( + initResources, + new AlertDetailsContextualInsightsService() + ); const result = await connectorType.executor({ actionId: 'observability-ai-assistant', request: getFakeKibanaRequest({ id: 'foo', api_key: 'bar' }), @@ -106,7 +113,10 @@ describe('observabilityAIAssistant rule_connector', () => { }, } as unknown as ObservabilityAIAssistantRouteHandlerResources); - const connectorType = getObsAIAssistantConnectorType(initResources); + const connectorType = getObsAIAssistantConnectorType( + initResources, + new AlertDetailsContextualInsightsService() + ); const result = await connectorType.executor({ actionId: 'observability-ai-assistant', request: getFakeKibanaRequest({ id: 'foo', api_key: 'bar' }), @@ -142,6 +152,26 @@ describe('observabilityAIAssistant rule_connector', () => { content: 'hello', }, }, + { + '@timestamp': expect.any(String), + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'get_alerts_context', + arguments: JSON.stringify({}), + trigger: MessageRole.Assistant as const, + }, + }, + }, + { + '@timestamp': expect.any(String), + message: { + role: MessageRole.User, + name: 'get_alerts_context', + content: expect.any(String), + }, + }, { '@timestamp': expect.any(String), message: { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts index b46fec93d1dd1b..c6b8b56b45a874 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/rule_connector/index.ts @@ -6,6 +6,8 @@ */ import { filter } from 'rxjs'; +import { get } from 'lodash'; +import dedent from 'dedent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { KibanaRequest, Logger } from '@kbn/core/server'; @@ -32,6 +34,7 @@ import { } from '@kbn/observability-ai-assistant-plugin/common'; import { concatenateChatCompletionChunks } from '@kbn/observability-ai-assistant-plugin/common/utils/concatenate_chat_completion_chunks'; import { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/common/functions/types'; +import { AlertDetailsContextualInsightsService } from '@kbn/observability-plugin/server/services'; import { getSystemMessageFromInstructions } from '@kbn/observability-ai-assistant-plugin/server/service/util/get_system_message_from_instructions'; import { convertSchemaToOpenApi } from './convert_schema_to_open_api'; import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector'; @@ -87,7 +90,8 @@ export type ObsAIAssistantConnectorTypeExecutorOptions = ConnectorTypeExecutorOp >; export function getObsAIAssistantConnectorType( - initResources: (request: KibanaRequest) => Promise + initResources: (request: KibanaRequest) => Promise, + alertDetailsContextService: AlertDetailsContextualInsightsService ): ObsAIAssistantConnectorType { return { id: OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID, @@ -112,7 +116,7 @@ export function getObsAIAssistantConnectorType( }, renderParameterTemplates, executor(options) { - return executor(options, initResources); + return executor(options, initResources, alertDetailsContextService); }, }; } @@ -132,7 +136,8 @@ function renderParameterTemplates( async function executor( execOptions: ObsAIAssistantConnectorTypeExecutorOptions, - initResources: (request: KibanaRequest) => Promise + initResources: (request: KibanaRequest) => Promise, + alertDetailsContextService: AlertDetailsContextualInsightsService ): Promise> { const request = execOptions.request; const alerts = execOptions.params.alerts; @@ -172,9 +177,34 @@ async function executor( }); }); - const backgroundInstruction = getBackgroundProcessInstruction( + const backgroundInstruction = dedent( + `You are called as a background process because alerts have changed state. + As a background process you are not interacting with a user. Because of that DO NOT ask for user + input if tasked to execute actions. You can generate multiple responses in a row. + If available, include the link of the conversation at the end of your answer.` + ); + + const alertsContext = await getAlertsContext( execOptions.params.rule, - execOptions.params.alerts + execOptions.params.alerts, + async (alert: Record) => { + const prompt = await alertDetailsContextService.getAlertDetailsContext( + { + core: resources.context.core, + licensing: resources.context.licensing, + request: resources.request, + }, + { + alert_started_at: get(alert, 'kibana.alert.start'), + 'service.name': get(alert, 'service.name'), + 'service.environment': get(alert, 'service.environment'), + 'host.name': get(alert, 'host.name'), + } + ); + return prompt + .map(({ description, data }) => `${description}:\n${JSON.stringify(data, null, 2)}`) + .join('\n\n'); + } ); client @@ -206,6 +236,26 @@ async function executor( content: execOptions.params.message, }, }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'get_alerts_context', + arguments: JSON.stringify({}), + trigger: MessageRole.Assistant as const, + }, + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + name: 'get_alerts_context', + content: JSON.stringify({ context: alertsContext }), + }, + }, { '@timestamp': new Date().toISOString(), message: { @@ -268,23 +318,38 @@ export const getObsAIAssistantConnectorAdapter = (): ConnectorAdapter< }; }; -function getBackgroundProcessInstruction(rule: RuleType, alerts: AlertSummary) { - let instruction = `You are called as a background process because the following alerts have changed state for the rule ${JSON.stringify( - rule +async function getAlertsContext( + rule: RuleType, + alerts: AlertSummary, + getAlertContext: (alert: Record) => Promise +): Promise { + const getAlertGroupDetails = async (alertGroup: Array>) => { + const formattedDetails = await Promise.all( + alertGroup.map(async (alert) => { + return `- ${JSON.stringify( + alert + )}. The following contextual information is available:\n${await getAlertContext(alert)}`; + }) + ).then((messages) => messages.join('\n')); + + return formattedDetails; + }; + + let details = `The following alerts have changed state for the rule ${JSON.stringify( + rule, + null, + 2 )}:\n`; if (alerts.new.length > 0) { - instruction += `- ${alerts.new.length} alerts have fired: ${JSON.stringify(alerts.new)}\n`; + details += `- ${alerts.new.length} alerts have fired:\n${await getAlertGroupDetails( + alerts.new + )}\n`; } if (alerts.recovered.length > 0) { - instruction += `- ${alerts.recovered.length} alerts have recovered: ${JSON.stringify( + details += `- ${alerts.recovered.length} alerts have recovered\n: ${await getAlertGroupDetails( alerts.recovered )}\n`; } - instruction += - ' As a background process you are not interacting with a user. Because of that DO NOT ask for user'; - instruction += - ' input if tasked to execute actions. You can generate multiple responses in a row.'; - instruction += ' If available, include the link of the conversation at the end of your answer.'; - return instruction; + return details; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts index 902774bacd8004..680e5b2409b7c1 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts @@ -37,6 +37,7 @@ import type { } from '@kbn/task-manager-plugin/server'; import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilityAIAssistantAppServerStart {} @@ -67,6 +68,7 @@ export interface ObservabilityAIAssistantAppPluginSetupDependencies { features: FeaturesPluginSetup; taskManager: TaskManagerSetupContract; dataViews: DataViewsServerPluginSetup; + observability: ObservabilityPluginSetup; cloud?: CloudSetup; serverless?: ServerlessPluginSetup; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json index 5b32c8ce5aa7cd..90c4f4d4151428 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json @@ -68,6 +68,7 @@ "@kbn/serverless", "@kbn/task-manager-plugin", "@kbn/cloud-plugin", + "@kbn/observability-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts b/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts index fe9967683ec8d3..5a98ec708bcf37 100644 --- a/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts +++ b/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts @@ -8,6 +8,7 @@ import moment from 'moment'; import { log, apm, generateShortId, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; +import { LogCategories } from '@kbn/apm-plugin/server/routes/assistant_functions/get_log_categories'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { SupertestReturnType } from '../../common/apm_api_supertest'; @@ -26,10 +27,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { 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'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -39,11 +40,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns nothing', () => { - expect(response.body).to.eql({ - serviceChangePoints: [], - exitSpanChangePoints: [], - anomalies: [], - }); + expect(response.body.context).to.eql([]); }); }); @@ -61,10 +58,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when no params are specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -73,26 +70,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - 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', - ]); + it('returns only 1 log category', async () => { + expect(response.body.context).to.have.length(1); + expect( + (response.body.context[0]?.data as LogCategories)?.map( + ({ errorCategory }: { errorCategory: string }) => 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'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -103,7 +95,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns service summary', () => { - expect(response.body.serviceSummary).to.eql({ + const serviceSummary = response.body.context.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ 'service.name': 'Backend', 'service.environment': ['production'], 'agent.name': 'java', @@ -117,7 +112,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns downstream dependencies', async () => { - expect(response.body.downstreamDependencies).to.eql([ + const downstreamDependencies = response.body.context.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies?.data).to.eql([ { 'span.destination.service.resource': 'elasticsearch', 'span.type': 'db', @@ -127,9 +125,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns log categories', () => { - expect(response.body.logCategories).to.have.length(1); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect(logCategories?.data).to.have.length(1); - const logCategory = response.body.logCategories?.[0]; + const logCategory = (logCategories?.data as LogCategories)?.[0]; expect(logCategory?.sampleMessage).to.match( /Error message #\d{16} from container my-container-a/ ); @@ -139,10 +138,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -153,7 +152,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns service summary', () => { - expect(response.body.serviceSummary).to.eql({ + const serviceSummary = response.body.context.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ 'service.name': 'Backend', 'service.environment': ['production'], 'agent.name': 'java', @@ -167,7 +169,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns downstream dependencies', async () => { - expect(response.body.downstreamDependencies).to.eql([ + const downstreamDependencies = response.body.context.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies?.data).to.eql([ { 'span.destination.service.resource': 'elasticsearch', 'span.type': 'db', @@ -177,9 +182,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns log categories', () => { - expect(response.body.logCategories).to.have.length(1); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect(logCategories?.data).to.have.length(1); - const logCategory = response.body.logCategories?.[0]; + const logCategory = (logCategories?.data as LogCategories)?.[0]; expect(logCategory?.sampleMessage).to.match( /Error message #\d{16} from container my-container-a/ ); @@ -189,10 +195,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when non-existing container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -203,20 +209,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns nothing', () => { - expect(response.body).to.eql({ - logCategories: [], - serviceChangePoints: [], - exitSpanChangePoints: [], - anomalies: [], - }); + expect(response.body.context).to.eql([]); }); }); describe('when non-existing service.name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -227,7 +228,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns empty service summary', () => { - expect(response.body.serviceSummary).to.eql({ + const serviceSummary = response.body.context.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ 'service.name': 'non-existing-service', 'service.environment': [], instances: 1, @@ -238,11 +242,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns no downstream dependencies', async () => { - expect(response.body.downstreamDependencies).to.eql([]); + const downstreamDependencies = response.body.context.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies).to.eql(undefined); }); it('returns log categories', () => { - expect(response.body.logCategories).to.have.length(1); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect(logCategories?.data).to.have.length(1); }); }); }); @@ -276,10 +284,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when no params are specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -289,22 +297,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns no service summary', async () => { - expect(response.body.serviceSummary).to.be(undefined); + const serviceSummary = response.body.context.find( + ({ key }) => key === 'serviceSummary' + ); + expect(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', - ]); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect( + (logCategories?.data as LogCategories)?.map( + ({ errorCategory }: { errorCategory: string }) => 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'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -315,9 +328,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns log categories', () => { - expect(response.body.logCategories).to.have.length(1); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect(logCategories?.data).to.have.length(1); - const logCategory = response.body.logCategories?.[0]; + const logCategory = (logCategories?.data as LogCategories)?.[0]; expect(logCategory?.sampleMessage).to.match( /Error message #\d{16} from service Backend/ ); @@ -327,10 +341,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -341,9 +355,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns log categories', () => { - expect(response.body.logCategories).to.have.length(1); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect(logCategories?.data).to.have.length(1); - const logCategory = response.body.logCategories?.[0]; + const logCategory = (logCategories?.data as LogCategories)?.[0]; expect(logCategory?.sampleMessage).to.match( /Error message #\d{16} from service Backend/ ); @@ -353,10 +368,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when non-existing service.name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/get_obs_alert_details_context'>; + let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; before(async () => { response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/get_obs_alert_details_context', + endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', params: { query: { alert_started_at: new Date(end).toISOString(), @@ -367,7 +382,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns empty service summary', () => { - expect(response.body.serviceSummary).to.eql({ + const serviceSummary = response.body.context.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ 'service.name': 'non-existing-service', 'service.environment': [], instances: 1, @@ -378,9 +396,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('does not return log categories', () => { - expect(response.body.logCategories?.map(({ errorCategory }) => errorCategory)).to.eql([ - 'Error message from container my-container-c', - ]); + const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); + expect(logCategories?.data).to.have.length(1); + + expect( + (logCategories?.data as LogCategories)?.map( + ({ errorCategory }: { errorCategory: string }) => errorCategory + ) + ).to.eql(['Error message from container my-container-c']); }); }); });