diff --git a/packages/kbn-apm-synthtrace/index.ts b/packages/kbn-apm-synthtrace/index.ts index 8ed45126e7cf9..1ebdc647258c6 100644 --- a/packages/kbn-apm-synthtrace/index.ts +++ b/packages/kbn-apm-synthtrace/index.ts @@ -9,7 +9,8 @@ export { createLogger, LogLevel } from './src/lib/utils/create_logger'; export { ApmSynthtraceEsClient } from './src/lib/apm/client/apm_synthtrace_es_client'; -export { ApmSynthtraceKibanaClient } from './src/lib/apm/client/apm_synthtrace_kibana_client'; +export { SynthtraceKibanaClient } from './src/lib/apm/client/apm_synthtrace_kibana_client'; + export { InfraSynthtraceEsClient } from './src/lib/infra/infra_synthtrace_es_client'; export { InfraSynthtraceKibanaClient } from './src/lib/infra/infra_synthtrace_kibana_client'; export { MonitoringSynthtraceEsClient } from './src/lib/monitoring/monitoring_synthtrace_es_client'; diff --git a/packages/kbn-apm-synthtrace/src/cli/utils/get_kibana_client.ts b/packages/kbn-apm-synthtrace/src/cli/utils/get_kibana_client.ts index 7396164b058e8..14a5a20530bd8 100644 --- a/packages/kbn-apm-synthtrace/src/cli/utils/get_kibana_client.ts +++ b/packages/kbn-apm-synthtrace/src/cli/utils/get_kibana_client.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { ApmSynthtraceKibanaClient } from '../../lib/apm/client/apm_synthtrace_kibana_client'; +import { SynthtraceKibanaClient } from '../../lib/apm/client/apm_synthtrace_kibana_client'; import { Logger } from '../../lib/utils/create_logger'; export function getKibanaClient({ target, logger }: { target: string; logger: Logger }) { - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ logger, target, }); diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts b/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts index caf6f47be45ce..1a6c2e43f7444 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/client/apm_synthtrace_kibana_client.ts @@ -11,7 +11,7 @@ import pRetry from 'p-retry'; import { Logger } from '../../utils/create_logger'; import { kibanaHeaders } from '../../shared/client_headers'; -export class ApmSynthtraceKibanaClient { +export class SynthtraceKibanaClient { private readonly logger: Logger; private target: string; diff --git a/packages/kbn-journeys/services/synthtrace.ts b/packages/kbn-journeys/services/synthtrace.ts index ffb4f06f20007..059d2173e429c 100644 --- a/packages/kbn-journeys/services/synthtrace.ts +++ b/packages/kbn-journeys/services/synthtrace.ts @@ -8,7 +8,7 @@ import { ApmSynthtraceEsClient, - ApmSynthtraceKibanaClient, + SynthtraceKibanaClient, InfraSynthtraceEsClient, InfraSynthtraceKibanaClient, } from '@kbn/apm-synthtrace'; @@ -103,7 +103,7 @@ async function initApmSynthtraceClient(options: SynthtraceClientOptions) { auth: `${auth.getUsername()}:${auth.getPassword()}`, }); - const synthKbnClient = new ApmSynthtraceKibanaClient({ + const synthKbnClient = new SynthtraceKibanaClient({ logger, target: kibanaUrlWithAuth, }); diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress_test_runner.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress_test_runner.ts index 73e42cff27db8..bc2454cd6bf94 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress_test_runner.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress_test_runner.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ApmSynthtraceKibanaClient, createLogger, LogLevel } from '@kbn/apm-synthtrace'; +import { SynthtraceKibanaClient, createLogger, LogLevel } from '@kbn/apm-synthtrace'; import cypress from 'cypress'; import path from 'path'; import Url from 'url'; @@ -39,7 +39,7 @@ export async function cypressTestRunner({ getService }: FtrProviderContext) { }); const esRequestTimeout = config.get('timeouts.esRequestTimeout'); - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ logger: createLogger(LogLevel.info), target: kibanaUrl, }); diff --git a/x-pack/plugins/observability_solution/apm/server/plugin.ts b/x-pack/plugins/observability_solution/apm/server/plugin.ts index 2c2392b845415..7e93a5f3c3324 100644 --- a/x-pack/plugins/observability_solution/apm/server/plugin.ts +++ b/x-pack/plugins/observability_solution/apm/server/plugin.ts @@ -40,7 +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'; +import { getAlertDetailsContextHandler } from './routes/assistant_functions/get_observability_alert_details_context'; export class APMPlugin implements Plugin 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 index 2ffbdc30a1c52..99ea7ec691dab 100644 --- 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 @@ -5,7 +5,6 @@ * 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'; @@ -18,14 +17,16 @@ export interface ChangePointGrouping { export async function getServiceChangePoints({ apmEventClient, - alertStartedAt, + start, + end, serviceName, serviceEnvironment, transactionType, transactionName, }: { apmEventClient: APMEventClient; - alertStartedAt: string; + start: string; + end: string; serviceName: string | undefined; serviceEnvironment: string | undefined; transactionType: string | undefined; @@ -38,8 +39,8 @@ export async function getServiceChangePoints({ const res = await getApmTimeseries({ apmEventClient, arguments: { - start: moment(alertStartedAt).subtract(12, 'hours').toISOString(), - end: alertStartedAt, + start, + end, stats: [ { title: 'Latency', @@ -95,12 +96,14 @@ export async function getServiceChangePoints({ export async function getExitSpanChangePoints({ apmEventClient, - alertStartedAt, + start, + end, serviceName, serviceEnvironment, }: { apmEventClient: APMEventClient; - alertStartedAt: string; + start: string; + end: string; serviceName: string | undefined; serviceEnvironment: string | undefined; }): Promise { @@ -111,8 +114,8 @@ export async function getExitSpanChangePoints({ const res = await getApmTimeseries({ apmEventClient, arguments: { - start: moment(alertStartedAt).subtract(30, 'minute').toISOString(), - end: alertStartedAt, + start, + end, stats: [ { title: 'Exit span latency', 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 990b63f412f76..e3e570c05e13e 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 @@ -17,13 +17,11 @@ import { } from '../../../../common/es_fields/apm'; import { getTypedSearch } from '../../../utils/create_typed_es_client'; -export type LogCategories = - | Array<{ - errorCategory: string; - docCount: number; - sampleMessage: string; - }> - | undefined; +export interface LogCategory { + errorCategory: string; + docCount: number; + sampleMessage: string; +} export async function getLogCategories({ esClient, @@ -40,7 +38,7 @@ export async function getLogCategories({ 'container.id'?: string; 'kubernetes.pod.name'?: string; }; -}): Promise { +}): Promise { const start = datemath.parse(args.start)?.valueOf()!; const end = datemath.parse(args.end)?.valueOf()!; 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 deleted file mode 100644 index cd1a56d56f45e..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/get_alert_details_context_handler.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 index 4a28a0460ebbd..4ce7c2c6c0bd8 100644 --- 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 @@ -9,7 +9,7 @@ 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 { LogCategory } from '../get_log_categories'; import { ApmAnomalies } from '../get_apm_service_summary/get_anomalies'; import { ChangePointGrouping } from '../get_changepoints'; @@ -27,7 +27,7 @@ export function getApmAlertDetailsContextPrompt({ serviceEnvironment?: string; serviceSummary?: ServiceSummary; downstreamDependencies?: APMDownstreamDependency[]; - logCategories: LogCategories; + logCategories?: LogCategory[]; serviceChangePoints?: ChangePointGrouping[]; exitSpanChangePoints?: ChangePointGrouping[]; anomalies?: ApmAnomalies; 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 22679dd55ded0..638903e813545 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,7 +12,7 @@ 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 { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, @@ -26,7 +26,7 @@ export async function getContainerIdFromSignals({ coreContext, apmEventClient, }: { - query: t.TypeOf; + query: t.TypeOf; esClient: ElasticsearchClient; coreContext: Pick; apmEventClient: APMEventClient; 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 bd62b998bee99..284c286766c76 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,7 +12,7 @@ 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 { alertDetailsContextRt } from '@kbn/observability-plugin/server/services'; import { ApmDocumentType } from '../../../../common/document_type'; import { APMEventClient, @@ -26,7 +26,7 @@ export async function getServiceNameFromSignals({ coreContext, apmEventClient, }: { - query: t.TypeOf; + query: t.TypeOf; esClient: ElasticsearchClient; coreContext: Pick; apmEventClient: APMEventClient; 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 d6022876c9f3b..0a21c60625c03 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 @@ -5,17 +5,16 @@ * 2.0. */ -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 { Logger } from '@kbn/core/server'; import { - AlertDetailsContextualInsight, AlertDetailsContextualInsightsHandlerQuery, + AlertDetailsContextualInsightsRequestContext, } from '@kbn/observability-plugin/server/services'; import moment from 'moment'; -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 { 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 { getApmServiceSummary } from '../get_apm_service_summary'; import { getAssistantDownstreamDependencies } from '../get_apm_downstream_dependencies'; import { getLogCategories } from '../get_log_categories'; @@ -24,157 +23,196 @@ import { getServiceNameFromSignals } from './get_service_name_from_signals'; import { getContainerIdFromSignals } from './get_container_id_from_signals'; import { getApmAlertDetailsContextPrompt } from './get_apm_alert_details_context_prompt'; import { getExitSpanChangePoints, getServiceChangePoints } from '../get_changepoints'; +import { APMRouteHandlerResources } from '../../apm_routes/register_apm_server_routes'; -export async function getObservabilityAlertDetailsContext({ - coreContext, - annotationsClient, - apmAlertsClient, - apmEventClient, - esClient, - logger, - mlClient, - query, -}: { - coreContext: Pick; - annotationsClient?: ScopedAnnotationsClient; - apmAlertsClient: ApmAlertsClient; - apmEventClient: APMEventClient; - esClient: ElasticsearchClient; - logger: Logger; - mlClient?: MlClient; - query: AlertDetailsContextualInsightsHandlerQuery; -}): Promise { - const alertStartedAt = query.alert_started_at; - const serviceEnvironment = query['service.environment']; - const hostName = query['host.name']; - const kubernetesPodName = query['kubernetes.pod.name']; - const [serviceName, containerId] = await Promise.all([ - getServiceNameFromSignals({ - query, - esClient, - coreContext, - apmEventClient, - }), - getContainerIdFromSignals({ - query, - esClient, - coreContext, - apmEventClient, - }), - ]); +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; + + const alertStartedAt = query.alert_started_at; + const serviceEnvironment = query['service.environment']; + const hostName = query['host.name']; + const kubernetesPodName = query['kubernetes.pod.name']; + const [serviceName, containerId] = await Promise.all([ + getServiceNameFromSignals({ + query, + esClient, + coreContext, + apmEventClient, + }), + getContainerIdFromSignals({ + query, + esClient, + coreContext, + apmEventClient, + }), + ]); - async function handleError(cb: () => Promise): Promise { - try { - return await cb(); - } catch (error) { - logger.error('Error while fetching observability alert details context'); - logger.error(error); - return; + async function handleError(cb: () => Promise): Promise { + try { + return await cb(); + } catch (error) { + logger.error('Error while fetching observability alert details context'); + logger.error(error); + return; + } } - } - const serviceSummaryPromise = serviceName - ? handleError(() => - getApmServiceSummary({ - apmEventClient, - annotationsClient, - esClient, - apmAlertsClient, - mlClient, - logger, - arguments: { - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), - end: alertStartedAt, - }, - }) - ) - : undefined; + const serviceSummaryPromise = serviceName + ? handleError(() => + getApmServiceSummary({ + apmEventClient, + annotationsClient, + esClient, + apmAlertsClient, + mlClient, + logger, + arguments: { + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), + end: alertStartedAt, + }, + }) + ) + : undefined; - const downstreamDependenciesPromise = serviceName - ? handleError(() => - getAssistantDownstreamDependencies({ - apmEventClient, - arguments: { - 'service.name': serviceName, - 'service.environment': serviceEnvironment, - start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), - end: alertStartedAt, - }, - }) - ) - : undefined; + const downstreamDependenciesPromise = serviceName + ? handleError(() => + getAssistantDownstreamDependencies({ + apmEventClient, + arguments: { + 'service.name': serviceName, + 'service.environment': serviceEnvironment, + start: moment(alertStartedAt).subtract(15, 'minute').toISOString(), + end: alertStartedAt, + }, + }) + ) + : undefined; - const logCategoriesPromise = handleError(() => - getLogCategories({ - esClient, - coreContext, - arguments: { - start: moment(alertStartedAt).subtract(5, 'minute').toISOString(), - end: alertStartedAt, - 'service.name': serviceName, - 'host.name': hostName, - 'container.id': containerId, - 'kubernetes.pod.name': kubernetesPodName, - }, - }) - ); + const logCategoriesPromise = handleError(() => + getLogCategories({ + esClient, + coreContext, + arguments: { + start: moment(alertStartedAt).subtract(15, 'minute').toISOString(), + end: alertStartedAt, + 'service.name': serviceName, + 'host.name': hostName, + 'container.id': containerId, + 'kubernetes.pod.name': kubernetesPodName, + }, + }) + ); - const serviceChangePointsPromise = handleError(() => - getServiceChangePoints({ - apmEventClient, - alertStartedAt, - serviceName, - serviceEnvironment, - transactionType: query['transaction.type'], - transactionName: query['transaction.name'], - }) - ); + const serviceChangePointsPromise = handleError(() => + getServiceChangePoints({ + apmEventClient, + start: moment(alertStartedAt).subtract(6, 'hours').toISOString(), + end: alertStartedAt, + serviceName, + serviceEnvironment, + transactionType: query['transaction.type'], + transactionName: query['transaction.name'], + }) + ); - const exitSpanChangePointsPromise = handleError(() => - getExitSpanChangePoints({ - apmEventClient, - alertStartedAt, - serviceName, - serviceEnvironment, - }) - ); + const exitSpanChangePointsPromise = handleError(() => + getExitSpanChangePoints({ + apmEventClient, + start: moment(alertStartedAt).subtract(6, 'hours').toISOString(), + end: alertStartedAt, + serviceName, + serviceEnvironment, + }) + ); - const anomaliesPromise = handleError(() => - getAnomalies({ - start: moment(alertStartedAt).subtract(1, 'hour').valueOf(), - end: moment(alertStartedAt).valueOf(), - environment: serviceEnvironment, - mlClient, - logger, - }) - ); + const anomaliesPromise = handleError(() => + getAnomalies({ + start: moment(alertStartedAt).subtract(1, 'hour').valueOf(), + end: moment(alertStartedAt).valueOf(), + environment: serviceEnvironment, + mlClient, + logger, + }) + ); - const [ - serviceSummary, - downstreamDependencies, - logCategories, - serviceChangePoints, - exitSpanChangePoints, - anomalies, - ] = await Promise.all([ - serviceSummaryPromise, - downstreamDependenciesPromise, - logCategoriesPromise, - serviceChangePointsPromise, - exitSpanChangePointsPromise, - anomaliesPromise, - ]); + const [ + serviceSummary, + downstreamDependencies, + logCategories, + serviceChangePoints, + exitSpanChangePoints, + anomalies, + ] = await Promise.all([ + serviceSummaryPromise, + downstreamDependenciesPromise, + logCategoriesPromise, + serviceChangePointsPromise, + exitSpanChangePointsPromise, + anomaliesPromise, + ]); - return getApmAlertDetailsContextPrompt({ - serviceName, - serviceEnvironment, - serviceSummary, - downstreamDependencies, - logCategories, - serviceChangePoints, - exitSpanChangePoints, - anomalies, - }); -} + return getApmAlertDetailsContextPrompt({ + serviceName, + serviceEnvironment, + serviceSummary, + downstreamDependencies, + logCategories, + serviceChangePoints, + exitSpanChangePoints, + anomalies, + }); + }; +}; 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 af3dfac613bd5..68b7362f85d85 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,16 +6,8 @@ */ 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 { getObservabilityAlertDetailsContext } from './get_observability_alert_details_context'; import { downstreamDependenciesRouteRt, @@ -24,49 +16,6 @@ import { } from './get_apm_downstream_dependencies'; import { getApmTimeseries, getApmTimeseriesRt, type ApmTimeseries } from './get_apm_timeseries'; -const getObservabilityAlertDetailsContextRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - options: { - tags: ['access:apm'], - }, - - params: t.type({ - query: observabilityAlertDetailsContextRt, - }), - handler: async (resources): Promise<{ context: AlertDetailsContextualInsight[] }> => { - const { context, request, plugins, logger, params } = resources; - const { query } = params; - - const [apmEventClient, annotationsClient, coreContext, apmAlertsClient, mlClient] = - await Promise.all([ - getApmEventClient(resources), - plugins.observability.setup.getScopedAnnotationsClient(context, request), - context.core, - getApmAlertsClient(resources), - getMlClient(resources), - getRandomSampler({ - security: resources.plugins.security, - probability: 1, - request: resources.request, - }), - ]); - const esClient = coreContext.elasticsearch.client.asCurrentUser; - - const obsAlertContext = await getObservabilityAlertDetailsContext({ - coreContext, - annotationsClient, - apmAlertsClient, - apmEventClient, - esClient, - logger, - mlClient, - query, - }); - - return { context: obsAlertContext }; - }, -}); - const getApmTimeSeriesRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/assistant/get_apm_timeseries', options: { @@ -120,6 +69,5 @@ const getDownstreamDependenciesRoute = createApmServerRoute({ export const assistantRouteRepository = { ...getApmTimeSeriesRoute, - ...getObservabilityAlertDetailsContextRoute, ...getDownstreamDependenciesRoute, }; 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 1de4b4a136919..7754badbf121d 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,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import dedent from 'dedent'; +import { type AlertDetailsContextualInsight } from '../../../server/services'; import { useKibana } from '../../utils/kibana_react'; import { AlertData } from '../../hooks/use_fetch_alert_detail'; @@ -21,16 +22,16 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu const ObservabilityAIAssistantContextualInsight = observabilityAIAssistant?.ObservabilityAIAssistantContextualInsight; - const getPromptMessages = useCallback(async () => { + const getAlertContextMessages = useCallback(async () => { const fields = alert?.formatted.fields as Record | undefined; if (!observabilityAIAssistant || !fields || !alert) { return []; } try { - const { context } = await http.get<{ - context: Array<{ description: string; data: unknown }>; - }>('/internal/apm/assistant/alert_details_contextual_insights', { + const { alertContext } = await http.get<{ + alertContext: AlertDetailsContextualInsight[]; + }>('/internal/observability/assistant/alert_details_contextual_insights', { query: { alert_started_at: new Date(alert.formatted.start).toISOString(), @@ -47,26 +48,28 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu }, }); - const obsAlertContext = context + const obsAlertContext = alertContext .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`, instructions: dedent( - `I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next. + `I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next. - The following contextual information is available to help me understand the alert: + The following contextual information is available to help you understand the alert: ${obsAlertContext} Be brief and to the point. Do not list the alert details as bullet points. Refer to the contextual information provided above when relevant. - Pay specific attention to why the alert happened and what may have contributed to it. + Pay special attention to regressions in downstream dependencies like big increases or decreases in throughput, latency or failure rate + Suggest reasons why the alert happened and what may have contributed to it. ` ), }); } catch (e) { + console.error('An error occurred while fetching alert context', e); return observabilityAIAssistant.getContextualInsightMessages({ message: `I'm looking at an alert and trying to understand why it was triggered`, instructions: dedent( @@ -88,7 +91,7 @@ export function AlertDetailContextualInsights({ alert }: { alert: AlertData | nu 'xpack.observability.alertDetailContextualInsights.InsightButtonLabel', { defaultMessage: 'Help me understand this alert' } )} - messages={getPromptMessages} + messages={getAlertContextMessages} /> diff --git a/x-pack/plugins/observability_solution/observability/server/routes/assistant/route.ts b/x-pack/plugins/observability_solution/observability/server/routes/assistant/route.ts new file mode 100644 index 0000000000000..e6e04704971d2 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/routes/assistant/route.ts @@ -0,0 +1,37 @@ +/* + * 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 { alertDetailsContextRt } from '../../services'; +import { createObservabilityServerRoute } from '../create_observability_server_route'; + +const getObservabilityAlertDetailsContextRoute = createObservabilityServerRoute({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + options: { + tags: [], + }, + params: t.type({ + query: alertDetailsContextRt, + }), + handler: async ({ dependencies, params, context, request }) => { + const alertContext = + await dependencies.assistant.alertDetailsContextualInsightsService.getAlertDetailsContext( + { + core: context.core, + licensing: context.licensing, + request, + }, + params.query + ); + + return { alertContext }; + }, +}); + +export const aiAssistantRouteRepository = { + ...getObservabilityAlertDetailsContextRoute, +}; diff --git a/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts b/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts index 78c4a2614b528..1516c42f86fd1 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/get_global_observability_server_route_repository.ts @@ -5,11 +5,14 @@ * 2.0. */ +import { EndpointOf } from '@kbn/server-route-repository'; import { ObservabilityConfig } from '..'; +import { aiAssistantRouteRepository } from './assistant/route'; import { rulesRouteRepository } from './rules/route'; export function getObservabilityServerRouteRepository(config: ObservabilityConfig) { const repository = { + ...aiAssistantRouteRepository, ...rulesRouteRepository, }; return repository; @@ -18,3 +21,5 @@ export function getObservabilityServerRouteRepository(config: ObservabilityConfi export type ObservabilityServerRouteRepository = ReturnType< typeof getObservabilityServerRouteRepository >; + +export type APIEndpoint = EndpointOf; diff --git a/x-pack/plugins/observability_solution/observability/server/routes/types.ts b/x-pack/plugins/observability_solution/observability/server/routes/types.ts index a0ef6ee6c0c74..3940253137640 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/types.ts @@ -7,12 +7,15 @@ import type { EndpointOf, ReturnOf, ServerRouteRepository } from '@kbn/server-route-repository'; import { KibanaRequest, Logger } from '@kbn/core/server'; -import { ObservabilityServerRouteRepository } from './get_global_observability_server_route_repository'; +import { + ObservabilityServerRouteRepository, + APIEndpoint, +} from './get_global_observability_server_route_repository'; import { ObservabilityRequestHandlerContext } from '../types'; import { RegisterRoutesDependencies } from './register_routes'; import { ObservabilityConfig } from '..'; -export type { ObservabilityServerRouteRepository }; +export type { ObservabilityServerRouteRepository, APIEndpoint }; export interface ObservabilityRouteHandlerResources { context: ObservabilityRequestHandlerContext; diff --git a/x-pack/plugins/observability_solution/observability/server/services/index.ts b/x-pack/plugins/observability_solution/observability/server/services/index.ts index 7c20d191440d6..840bac95ee48b 100644 --- a/x-pack/plugins/observability_solution/observability/server/services/index.ts +++ b/x-pack/plugins/observability_solution/observability/server/services/index.ts @@ -13,9 +13,9 @@ import { SavedObjectsClientContract, } from '@kbn/core/server'; import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server'; -import { concat } from 'lodash'; +import { flatten } from 'lodash'; -export const observabilityAlertDetailsContextRt = t.intersection([ +export const alertDetailsContextRt = t.intersection([ t.type({ alert_started_at: t.string, }), @@ -33,9 +33,7 @@ export const observabilityAlertDetailsContextRt = t.intersection([ }), ]); -export type AlertDetailsContextualInsightsHandlerQuery = t.TypeOf< - typeof observabilityAlertDetailsContextRt ->; +export type AlertDetailsContextualInsightsHandlerQuery = t.TypeOf; export interface AlertDetailsContextualInsight { key: string; @@ -77,11 +75,21 @@ export class AlertDetailsContextualInsightsService { context: AlertDetailsContextualInsightsRequestContext, query: AlertDetailsContextualInsightsHandlerQuery ): Promise { - if (this.handlers.length === 0) return []; + 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); - }); + const results = await Promise.all( + this.handlers.map(async (handler) => { + try { + return await handler(context, query); + } catch (error) { + console.error(`Error: Could not get alert context from handler`, error); + return []; + } + }) + ); + + return flatten(results); } } diff --git a/x-pack/plugins/observability_solution/observability/server/types.ts b/x-pack/plugins/observability_solution/observability/server/types.ts index 76a209f318078..8c298d239a53d 100644 --- a/x-pack/plugins/observability_solution/observability/server/types.ts +++ b/x-pack/plugins/observability_solution/observability/server/types.ts @@ -19,6 +19,7 @@ export type { ObservabilityRouteHandlerResources, AbstractObservabilityServerRouteRepository, ObservabilityServerRouteRepository, + APIEndpoint, ObservabilityAPIReturnType, } from './routes/types'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/setup_synthtrace.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/setup_synthtrace.ts index f6b3180541ee2..cf9fd7bb498a9 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/setup_synthtrace.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/setup_synthtrace.ts @@ -9,7 +9,7 @@ import { ApmSynthtraceEsClient, InfraSynthtraceEsClient, LogsSynthtraceEsClient, - ApmSynthtraceKibanaClient, + SynthtraceKibanaClient, } from '@kbn/apm-synthtrace'; import { ToolingLog } from '@kbn/tooling-log'; import { isPromise } from 'util/types'; @@ -54,7 +54,7 @@ export async function setupSynthtrace({ return result; }, }; - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ target, logger, }); diff --git a/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts b/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts index 259924e80d64d..3aa221e175024 100644 --- a/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts +++ b/x-pack/test/alerting_api_integration/observability/helpers/syntrace.ts @@ -8,7 +8,7 @@ import { Client } from '@elastic/elasticsearch'; import { ApmSynthtraceEsClient, - ApmSynthtraceKibanaClient, + SynthtraceKibanaClient, createLogger, LogLevel, } from '@kbn/apm-synthtrace'; @@ -21,7 +21,7 @@ export const getSyntraceClient = async ({ kibanaUrl: string; esClient: Client; }) => { - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ logger: createLogger(LogLevel.info), target: kibanaUrl, }); diff --git a/x-pack/test/api_integration/apis/asset_manager/config_when_enabled.ts b/x-pack/test/api_integration/apis/asset_manager/config_when_enabled.ts index a8ad78d02ab6f..581fc735330f1 100644 --- a/x-pack/test/api_integration/apis/asset_manager/config_when_enabled.ts +++ b/x-pack/test/api_integration/apis/asset_manager/config_when_enabled.ts @@ -7,7 +7,7 @@ import { ApmSynthtraceEsClient, - ApmSynthtraceKibanaClient, + SynthtraceKibanaClient, createLogger, InfraSynthtraceEsClient, LogLevel, @@ -56,7 +56,7 @@ export default async function createTestConfig({ }) .slice(0, -1); - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ target: kibanaServerUrlWithAuth, logger: createLogger(LogLevel.debug), }); diff --git a/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts b/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts index 6bb8da1f8ee58..c6e4f7e40acbe 100644 --- a/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts +++ b/x-pack/test/apm_api_integration/common/bootstrap_apm_synthtrace.ts @@ -6,7 +6,7 @@ */ import { ApmSynthtraceEsClient, - ApmSynthtraceKibanaClient, + SynthtraceKibanaClient, createLogger, LogLevel, } from '@kbn/apm-synthtrace'; @@ -16,7 +16,7 @@ import { InheritedFtrProviderContext } from './ftr_provider_context'; export async function bootstrapApmSynthtrace( context: InheritedFtrProviderContext, - kibanaClient: ApmSynthtraceKibanaClient + kibanaClient: SynthtraceKibanaClient ) { const es = context.getService('es'); @@ -41,7 +41,7 @@ export function getApmSynthtraceKibanaClient(kibanaServerUrl: string) { }) .slice(0, -1); - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ target: kibanaServerUrlWithAuth, logger: createLogger(LogLevel.debug), }); diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index a4f02524b98cf..78b225416bcfa 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -9,7 +9,7 @@ import { ApmUsername } from '@kbn/apm-plugin/server/test_helpers/create_apm_user import { createApmUsers } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/create_apm_users'; import { ApmSynthtraceEsClient, - ApmSynthtraceKibanaClient, + SynthtraceKibanaClient, LogsSynthtraceEsClient, createLogger, LogLevel, @@ -78,7 +78,7 @@ export interface CreateTest { synthtraceEsClient: (context: InheritedFtrProviderContext) => Promise; synthtraceKibanaClient: ( context: InheritedFtrProviderContext - ) => Promise; + ) => Promise; apmApiClient: (context: InheritedFtrProviderContext) => ApmApiClient; ml: ({ getService }: FtrProviderContext) => ReturnType; }; 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 deleted file mode 100644 index 5a98ec708bcf3..0000000000000 --- a/x-pack/test/apm_api_integration/tests/assistant/obs_alert_details_context.spec.ts +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { log, apm, generateShortId, timerange } from '@kbn/apm-synthtrace-client'; -import expect from '@kbn/expect'; -import { 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'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceClient = getService('synthtraceEsClient'); - const logSynthtraceClient = getService('logSynthtraceEsClient'); - - registry.when( - 'fetching observability alerts details context for AI assistant contextual insights', - { config: 'trial', archives: [] }, - () => { - const start = moment().subtract(10, 'minutes').valueOf(); - const end = moment().valueOf(); - const range = timerange(start, end); - - describe('when no traces or logs are available', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - }, - }, - }); - }); - - it('returns nothing', () => { - expect(response.body.context).to.eql([]); - }); - }); - - describe('when traces and logs are ingested and logs are not annotated with service.name', async () => { - before(async () => { - await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); - await ingestLogs({ - 'container.id': 'my-container-a', - 'kubernetes.pod.name': 'pod-a', - }); - }); - - after(async () => { - await cleanup(); - }); - - describe('when no params are specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - }, - }, - }); - }); - - 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/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'Backend', - }, - }, - }); - }); - - it('returns service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'Backend', - 'service.environment': ['production'], - 'agent.name': 'java', - 'service.version': ['1.0.0'], - 'language.name': 'java', - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('returns downstream dependencies', async () => { - const downstreamDependencies = response.body.context.find( - ({ key }) => key === 'downstreamDependencies' - ); - expect(downstreamDependencies?.data).to.eql([ - { - 'span.destination.service.resource': 'elasticsearch', - 'span.type': 'db', - 'span.subtype': 'elasticsearch', - }, - ]); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from container my-container-a/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); - }); - }); - - describe('when container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'container.id': 'my-container-a', - }, - }, - }); - }); - - it('returns service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'Backend', - 'service.environment': ['production'], - 'agent.name': 'java', - 'service.version': ['1.0.0'], - 'language.name': 'java', - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('returns downstream dependencies', async () => { - const downstreamDependencies = response.body.context.find( - ({ key }) => key === 'downstreamDependencies' - ); - expect(downstreamDependencies?.data).to.eql([ - { - 'span.destination.service.resource': 'elasticsearch', - 'span.type': 'db', - 'span.subtype': 'elasticsearch', - }, - ]); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from container my-container-a/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); - }); - }); - - describe('when non-existing container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'container.id': 'non-existing-container', - }, - }, - }); - }); - - it('returns nothing', () => { - expect(response.body.context).to.eql([]); - }); - }); - - describe('when non-existing service.name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'non-existing-service', - }, - }, - }); - }); - - it('returns empty service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'non-existing-service', - 'service.environment': [], - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('returns no downstream dependencies', async () => { - const downstreamDependencies = response.body.context.find( - ({ key }) => key === 'downstreamDependencies' - ); - expect(downstreamDependencies).to.eql(undefined); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - }); - }); - }); - - describe('when traces and logs are ingested and logs are annotated with service.name', async () => { - before(async () => { - await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); - await ingestLogs({ - 'service.name': 'Backend', - 'container.id': 'my-container-a', - 'kubernetes.pod.name': 'pod-a', - }); - - // also ingest unrelated Frontend traces and logs that should not show up in the response when fetching "Backend"-related things - await ingestTraces({ 'service.name': 'Frontend', 'container.id': 'my-container-b' }); - await ingestLogs({ - 'service.name': 'Frontend', - 'container.id': 'my-container-b', - 'kubernetes.pod.name': 'pod-b', - }); - - // also ingest logs that are not annotated with service.name - await ingestLogs({ - 'container.id': 'my-container-c', - 'kubernetes.pod.name': 'pod-c', - }); - }); - - after(async () => { - await cleanup(); - }); - - describe('when no params are specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - }, - }, - }); - }); - - it('returns no service summary', async () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary).to.be(undefined); - }); - - it('returns 1 log category', async () => { - 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/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'Backend', - }, - }, - }); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from service Backend/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from service Backend'); - }); - }); - - describe('when container id is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'container.id': 'my-container-a', - }, - }, - }); - }); - - it('returns log categories', () => { - const logCategories = response.body.context.find(({ key }) => key === 'logCategories'); - expect(logCategories?.data).to.have.length(1); - - const logCategory = (logCategories?.data as LogCategories)?.[0]; - expect(logCategory?.sampleMessage).to.match( - /Error message #\d{16} from service Backend/ - ); - expect(logCategory?.docCount).to.be.greaterThan(0); - expect(logCategory?.errorCategory).to.be('Error message from service Backend'); - }); - }); - - describe('when non-existing service.name is specified', async () => { - let response: SupertestReturnType<'GET /internal/apm/assistant/alert_details_contextual_insights'>; - before(async () => { - response = await apmApiClient.writeUser({ - endpoint: 'GET /internal/apm/assistant/alert_details_contextual_insights', - params: { - query: { - alert_started_at: new Date(end).toISOString(), - 'service.name': 'non-existing-service', - }, - }, - }); - }); - - it('returns empty service summary', () => { - const serviceSummary = response.body.context.find( - ({ key }) => key === 'serviceSummary' - ); - expect(serviceSummary?.data).to.eql({ - 'service.name': 'non-existing-service', - 'service.environment': [], - instances: 1, - anomalies: [], - alerts: [], - deployments: [], - }); - }); - - it('does not return log categories', () => { - 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']); - }); - }); - }); - - async function ingestTraces(eventMetadata: { - 'service.name': string; - 'container.id'?: string; - 'host.name'?: string; - 'kubernetes.pod.name'?: string; - }) { - const serviceInstance = apm - .service({ - name: eventMetadata['service.name'], - environment: 'production', - agentName: 'java', - }) - .instance('my-instance'); - - const events = range - .interval('1m') - .rate(1) - .generator((timestamp) => { - return serviceInstance - .transaction({ transactionName: 'tx' }) - .timestamp(timestamp) - .duration(10000) - .defaults({ 'service.version': '1.0.0', ...eventMetadata }) - .outcome('success') - .children( - serviceInstance - .span({ - spanName: 'GET apm-*/_search', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .duration(1000) - .success() - .destination('elasticsearch') - .timestamp(timestamp) - ); - }); - - await apmSynthtraceClient.index(events); - } - - function ingestLogs(eventMetadata: { - 'service.name'?: string; - 'container.id'?: string; - 'kubernetes.pod.name'?: string; - 'host.name'?: string; - }) { - const getMessage = () => { - const msgPrefix = `Error message #${generateShortId()}`; - - if (eventMetadata['service.name']) { - return `${msgPrefix} from service ${eventMetadata['service.name']}`; - } - - if (eventMetadata['container.id']) { - return `${msgPrefix} from container ${eventMetadata['container.id']}`; - } - - if (eventMetadata['kubernetes.pod.name']) { - return `${msgPrefix} from pod ${eventMetadata['kubernetes.pod.name']}`; - } - - if (eventMetadata['host.name']) { - return `${msgPrefix} from host ${eventMetadata['host.name']}`; - } - - return msgPrefix; - }; - - const events = range - .interval('1m') - .rate(1) - .generator((timestamp) => { - return [ - log - .create() - .message(getMessage()) - .logLevel('error') - .defaults({ - 'trace.id': generateShortId(), - 'agent.name': 'synth-agent', - ...eventMetadata, - }) - .timestamp(timestamp), - ]; - }); - - return logSynthtraceClient.index(events); - } - - async function cleanup() { - await apmSynthtraceClient.clean(); - await logSynthtraceClient.clean(); - } - } - ); -} diff --git a/x-pack/test/common/services/apm_synthtrace_kibana_client.ts b/x-pack/test/common/services/apm_synthtrace_kibana_client.ts index 63bbd917f93ea..5f7c6bd41a223 100644 --- a/x-pack/test/common/services/apm_synthtrace_kibana_client.ts +++ b/x-pack/test/common/services/apm_synthtrace_kibana_client.ts @@ -7,7 +7,7 @@ import url from 'url'; import { kbnTestConfig } from '@kbn/test'; -import { ApmSynthtraceKibanaClient, createLogger, LogLevel } from '@kbn/apm-synthtrace'; +import { SynthtraceKibanaClient, createLogger, LogLevel } from '@kbn/apm-synthtrace'; const getKibanaServerUrlWithAuth = () => { const kibanaServerUrl = url.format(kbnTestConfig.getUrlParts() as url.UrlObject); @@ -21,7 +21,7 @@ const getKibanaServerUrlWithAuth = () => { }; export function ApmSynthtraceKibanaClientProvider() { const kibanaServerUrlWithAuth = getKibanaServerUrlWithAuth(); - const kibanaClient = new ApmSynthtraceKibanaClient({ + const kibanaClient = new SynthtraceKibanaClient({ target: kibanaServerUrlWithAuth, logger: createLogger(LogLevel.debug), }); diff --git a/x-pack/test/observability_api_integration/common/bootstrap_synthtrace.ts b/x-pack/test/observability_api_integration/common/bootstrap_synthtrace.ts new file mode 100644 index 0000000000000..ada2508d53c46 --- /dev/null +++ b/x-pack/test/observability_api_integration/common/bootstrap_synthtrace.ts @@ -0,0 +1,50 @@ +/* + * 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 { + ApmSynthtraceEsClient, + SynthtraceKibanaClient, + createLogger, + LogLevel, +} from '@kbn/apm-synthtrace'; +import url from 'url'; +import { kbnTestConfig } from '@kbn/test'; +import { FtrProviderContext } from './ftr_provider_context'; + +export async function bootstrapApmSynthtraceEsClient( + context: FtrProviderContext, + kibanaClient: SynthtraceKibanaClient +) { + const es = context.getService('es'); + + const kibanaVersion = await kibanaClient.fetchLatestApmPackageVersion(); + await kibanaClient.installApmPackage(kibanaVersion); + + const esClient = new ApmSynthtraceEsClient({ + client: es, + logger: createLogger(LogLevel.info), + version: kibanaVersion, + refreshAfterIndex: true, + }); + + return esClient; +} + +export function getSynthtraceKibanaClient(kibanaServerUrl: string) { + const kibanaServerUrlWithAuth = url + .format({ + ...url.parse(kibanaServerUrl), + auth: `elastic:${kbnTestConfig.getUrlParts().password}`, + }) + .slice(0, -1); + + const kibanaClient = new SynthtraceKibanaClient({ + target: kibanaServerUrlWithAuth, + logger: createLogger(LogLevel.debug), + }); + + return kibanaClient; +} diff --git a/x-pack/test/observability_api_integration/common/config.ts b/x-pack/test/observability_api_integration/common/config.ts index 83249182084f3..8baf4f5d116f0 100644 --- a/x-pack/test/observability_api_integration/common/config.ts +++ b/x-pack/test/observability_api_integration/common/config.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test'; +import { Config, FtrConfigProviderContext, kbnTestConfig } from '@kbn/test'; +import { format, UrlObject } from 'url'; +import { LogsSynthtraceEsClient, createLogger, LogLevel } from '@kbn/apm-synthtrace'; +import supertest from 'supertest'; +import { bootstrapApmSynthtraceEsClient, getSynthtraceKibanaClient } from './bootstrap_synthtrace'; +import { FtrProviderContext } from './ftr_provider_context'; +import { createObsApiClient } from './obs_api_supertest'; interface Settings { license: 'basic' | 'trial'; @@ -13,6 +19,41 @@ interface Settings { name: string; } +export type CustomApiTestServices = ReturnType; +function getCustomApiTestServices(xPackAPITestsConfig: Config) { + const servers = xPackAPITestsConfig.get('servers'); + const kibanaServer = servers.kibana as UrlObject; + const kibanaServerUrl = format(kibanaServer); + const synthtraceKibanaClient = getSynthtraceKibanaClient(kibanaServerUrl); + + return { + apmSynthtraceEsClient: (context: FtrProviderContext) => { + return bootstrapApmSynthtraceEsClient(context, synthtraceKibanaClient); + }, + logSynthtraceEsClient: (context: FtrProviderContext) => + new LogsSynthtraceEsClient({ + client: context.getService('es'), + logger: createLogger(LogLevel.info), + refreshAfterIndex: true, + }), + synthtraceKibanaClient: () => synthtraceKibanaClient, + obsApiClient: async (context: FtrProviderContext) => { + const getApiClientForUsername = (username: string) => { + const url = format({ + ...kibanaServer, + auth: `${username}:${kbnTestConfig.getUrlParts().password}`, + }); + + return createObsApiClient(supertest(url)); + }; + + return { + adminUser: getApiClientForUsername('elastic'), + }; + }, + }; +} + export function createTestConfig(settings: Settings) { const { testFiles, license, name } = settings; @@ -21,10 +62,15 @@ export function createTestConfig(settings: Settings) { require.resolve('../../api_integration/config.ts') ); + const customTestServices = getCustomApiTestServices(xPackAPITestsConfig); + return { testFiles, servers: xPackAPITestsConfig.get('servers'), - services: xPackAPITestsConfig.get('services'), + services: { + ...xPackAPITestsConfig.get('services'), + ...customTestServices, + }, junit: { reportName: name, }, diff --git a/x-pack/test/observability_api_integration/common/ftr_provider_context.ts b/x-pack/test/observability_api_integration/common/ftr_provider_context.ts index 2ea45b854eb28..b1d69d89e287a 100644 --- a/x-pack/test/observability_api_integration/common/ftr_provider_context.ts +++ b/x-pack/test/observability_api_integration/common/ftr_provider_context.ts @@ -5,4 +5,12 @@ * 2.0. */ +import { GenericFtrProviderContext } from '@kbn/test'; +import { services } from '../../api_integration/services'; +import { CustomApiTestServices } from './config'; + export type { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +export type ObsFtrProviderContext = GenericFtrProviderContext< + typeof services & CustomApiTestServices, + {} +>; diff --git a/x-pack/test/observability_api_integration/common/obs_api_supertest.ts b/x-pack/test/observability_api_integration/common/obs_api_supertest.ts new file mode 100644 index 0000000000000..e0788dcd6785d --- /dev/null +++ b/x-pack/test/observability_api_integration/common/obs_api_supertest.ts @@ -0,0 +1,111 @@ +/* + * 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 { format } from 'url'; +import supertest from 'supertest'; +import request from 'superagent'; +import { formatRequest, ClientRequestParamsOf, ReturnOf } from '@kbn/server-route-repository'; +import type { + ObservabilityServerRouteRepository, + APIEndpoint, +} from '@kbn/observability-plugin/server'; + +export type APIReturnType = ReturnOf< + ObservabilityServerRouteRepository, + TEndpoint +>; + +export type APIClientRequestParamsOf = ClientRequestParamsOf< + ObservabilityServerRouteRepository, + TEndpoint +>; + +export function createObsApiClient(st: supertest.SuperTest) { + return async ( + options: { + type?: 'form-data'; + endpoint: TEndpoint; + spaceId?: string; + } & APIClientRequestParamsOf & { params?: { query?: { _inspect?: boolean } } } + ): Promise> => { + const { endpoint, type } = options; + + const params = 'params' in options ? (options.params as Record) : {}; + + const { method, pathname, version } = formatRequest(endpoint, params.path); + const pathnameWithSpaceId = options.spaceId ? `/s/${options.spaceId}${pathname}` : pathname; + const url = format({ pathname: pathnameWithSpaceId, query: params?.query }); + + // eslint-disable-next-line no-console + console.debug(`Calling Observability API: ${method.toUpperCase()} ${url}`); + + const headers: Record = { + 'kbn-xsrf': 'foo', + 'x-elastic-internal-origin': 'foo', + }; + + if (version) { + headers['Elastic-Api-Version'] = version; + } + + let res: request.Response; + if (type === 'form-data') { + const fields: Array<[string, any]> = Object.entries(params.body); + const formDataRequest = st[method](url) + .set(headers) + .set('Content-type', 'multipart/form-data'); + + for (const field of fields) { + formDataRequest.field(field[0], field[1]); + } + + res = await formDataRequest; + } else if (params.body) { + res = await st[method](url).send(params.body).set(headers); + } else { + res = await st[method](url).set(headers); + } + + // supertest doesn't throw on http errors + if (res?.status !== 200) { + throw new ObservabilityApiError(res, endpoint); + } + + return res; + }; +} + +type ApiErrorResponse = Omit & { + body: { + statusCode: number; + error: string; + message: string; + attributes: object; + }; +}; + +export type ObservabilityApiSupertest = ReturnType; + +export class ObservabilityApiError extends Error { + res: ApiErrorResponse; + + constructor(res: request.Response, endpoint: string) { + super( + `Unhandled ObservabilityApiError. +Status: "${res.status}" +Endpoint: "${endpoint}" +Body: ${JSON.stringify(res.body)}` + ); + + this.res = res; + } +} + +export interface SupertestReturnType { + status: number; + body: APIReturnType; +} diff --git a/x-pack/test/observability_api_integration/trial/tests/index.ts b/x-pack/test/observability_api_integration/trial/tests/index.ts index e426efd90188c..3d7f31517121d 100644 --- a/x-pack/test/observability_api_integration/trial/tests/index.ts +++ b/x-pack/test/observability_api_integration/trial/tests/index.ts @@ -11,5 +11,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Observability specs (trial)', function () { loadTestFile(require.resolve('./annotations')); + loadTestFile(require.resolve('./obs_alert_details_context')); }); } diff --git a/x-pack/test/observability_api_integration/trial/tests/obs_alert_details_context.ts b/x-pack/test/observability_api_integration/trial/tests/obs_alert_details_context.ts new file mode 100644 index 0000000000000..02809dcfe8c0c --- /dev/null +++ b/x-pack/test/observability_api_integration/trial/tests/obs_alert_details_context.ts @@ -0,0 +1,513 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { log, apm, generateShortId, timerange } from '@kbn/apm-synthtrace-client'; +import expect from '@kbn/expect'; +import { LogCategory } from '@kbn/apm-plugin/server/routes/assistant_functions/get_log_categories'; +import { SupertestReturnType } from '../../common/obs_api_supertest'; +import { ObsFtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ApiTest({ getService }: ObsFtrProviderContext) { + const obsApiClient = getService('obsApiClient'); + const apmSynthtraceClient = getService('apmSynthtraceEsClient'); + const logSynthtraceClient = getService('logSynthtraceEsClient'); + + describe('fetching observability alerts details context for AI assistant contextual insights', () => { + const start = moment().subtract(10, 'minutes').valueOf(); + const end = moment().valueOf(); + const range = timerange(start, end); + + describe('when no traces or logs are available', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + }, + }, + }); + }); + + it('returns nothing', () => { + expect(response.body.alertContext).to.eql([]); + }); + }); + + describe('when traces and logs are ingested and logs are not annotated with service.name', async () => { + before(async () => { + await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); + await ingestLogs({ + 'container.id': 'my-container-a', + 'kubernetes.pod.name': 'pod-a', + }); + }); + + after(async () => { + await cleanup(); + }); + + describe('when no params are specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + }, + }, + }); + }); + + it('returns only 1 log category', async () => { + expect(response.body.alertContext).to.have.length(1); + + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + + expect( + 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/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'Backend', + }, + }, + }); + }); + + it('returns service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'Backend', + 'service.environment': ['production'], + 'agent.name': 'java', + 'service.version': ['1.0.0'], + 'language.name': 'java', + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('returns downstream dependencies', async () => { + const downstreamDependencies = response.body.alertContext.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies?.data).to.eql([ + { + 'span.destination.service.resource': 'elasticsearch', + 'span.type': 'db', + 'span.subtype': 'elasticsearch', + }, + ]); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match( + /Error message #\d{16} from container my-container-a/ + ); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); + }); + }); + + describe('when container id is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'container.id': 'my-container-a', + }, + }, + }); + }); + + it('returns service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'Backend', + 'service.environment': ['production'], + 'agent.name': 'java', + 'service.version': ['1.0.0'], + 'language.name': 'java', + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('returns downstream dependencies', async () => { + const downstreamDependencies = response.body.alertContext.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies?.data).to.eql([ + { + 'span.destination.service.resource': 'elasticsearch', + 'span.type': 'db', + 'span.subtype': 'elasticsearch', + }, + ]); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match( + /Error message #\d{16} from container my-container-a/ + ); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from container my-container-a'); + }); + }); + + describe('when non-existing container id is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'container.id': 'non-existing-container', + }, + }, + }); + }); + + it('returns nothing', () => { + expect(response.body.alertContext).to.eql([]); + }); + }); + + describe('when non-existing service.name is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'non-existing-service', + }, + }, + }); + }); + + it('returns empty service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'non-existing-service', + 'service.environment': [], + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('returns no downstream dependencies', async () => { + const downstreamDependencies = response.body.alertContext.find( + ({ key }) => key === 'downstreamDependencies' + ); + expect(downstreamDependencies).to.eql(undefined); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + }); + }); + }); + + describe('when traces and logs are ingested and logs are annotated with service.name', async () => { + before(async () => { + await ingestTraces({ 'service.name': 'Backend', 'container.id': 'my-container-a' }); + await ingestLogs({ + 'service.name': 'Backend', + 'container.id': 'my-container-a', + 'kubernetes.pod.name': 'pod-a', + }); + + // also ingest unrelated Frontend traces and logs that should not show up in the response when fetching "Backend"-related things + await ingestTraces({ 'service.name': 'Frontend', 'container.id': 'my-container-b' }); + await ingestLogs({ + 'service.name': 'Frontend', + 'container.id': 'my-container-b', + 'kubernetes.pod.name': 'pod-b', + }); + + // also ingest logs that are not annotated with service.name + await ingestLogs({ + 'container.id': 'my-container-c', + 'kubernetes.pod.name': 'pod-c', + }); + }); + + after(async () => { + await cleanup(); + }); + + describe('when no params are specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + }, + }, + }); + }); + + it('returns no service summary', async () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary).to.be(undefined); + }); + + it('returns 1 log category', async () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect( + 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/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'Backend', + }, + }, + }); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match(/Error message #\d{16} from service Backend/); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from service Backend'); + }); + }); + + describe('when container id is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'container.id': 'my-container-a', + }, + }, + }); + }); + + it('returns log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + const logCategory = logCategories[0]; + expect(logCategory?.sampleMessage).to.match(/Error message #\d{16} from service Backend/); + expect(logCategory?.docCount).to.be.greaterThan(0); + expect(logCategory?.errorCategory).to.be('Error message from service Backend'); + }); + }); + + describe('when non-existing service.name is specified', async () => { + let response: SupertestReturnType<'GET /internal/observability/assistant/alert_details_contextual_insights'>; + before(async () => { + response = await obsApiClient.adminUser({ + endpoint: 'GET /internal/observability/assistant/alert_details_contextual_insights', + params: { + query: { + alert_started_at: new Date(end).toISOString(), + 'service.name': 'non-existing-service', + }, + }, + }); + }); + + it('returns empty service summary', () => { + const serviceSummary = response.body.alertContext.find( + ({ key }) => key === 'serviceSummary' + ); + expect(serviceSummary?.data).to.eql({ + 'service.name': 'non-existing-service', + 'service.environment': [], + instances: 1, + anomalies: [], + alerts: [], + deployments: [], + }); + }); + + it('does not return log categories', () => { + const logCategories = response.body.alertContext.find( + ({ key }) => key === 'logCategories' + )?.data as LogCategory[]; + expect(logCategories).to.have.length(1); + + expect( + logCategories.map(({ errorCategory }: { errorCategory: string }) => errorCategory) + ).to.eql(['Error message from container my-container-c']); + }); + }); + }); + + async function ingestTraces(eventMetadata: { + 'service.name': string; + 'container.id'?: string; + 'host.name'?: string; + 'kubernetes.pod.name'?: string; + }) { + const serviceInstance = apm + .service({ + name: eventMetadata['service.name'], + environment: 'production', + agentName: 'java', + }) + .instance('my-instance'); + + const events = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return serviceInstance + .transaction({ transactionName: 'tx' }) + .timestamp(timestamp) + .duration(10000) + .defaults({ 'service.version': '1.0.0', ...eventMetadata }) + .outcome('success') + .children( + serviceInstance + .span({ + spanName: 'GET apm-*/_search', + spanType: 'db', + spanSubtype: 'elasticsearch', + }) + .duration(1000) + .success() + .destination('elasticsearch') + .timestamp(timestamp) + ); + }); + + await apmSynthtraceClient.index(events); + } + + function ingestLogs(eventMetadata: { + 'service.name'?: string; + 'container.id'?: string; + 'kubernetes.pod.name'?: string; + 'host.name'?: string; + }) { + const getMessage = () => { + const msgPrefix = `Error message #${generateShortId()}`; + + if (eventMetadata['service.name']) { + return `${msgPrefix} from service ${eventMetadata['service.name']}`; + } + + if (eventMetadata['container.id']) { + return `${msgPrefix} from container ${eventMetadata['container.id']}`; + } + + if (eventMetadata['kubernetes.pod.name']) { + return `${msgPrefix} from pod ${eventMetadata['kubernetes.pod.name']}`; + } + + if (eventMetadata['host.name']) { + return `${msgPrefix} from host ${eventMetadata['host.name']}`; + } + + return msgPrefix; + }; + + const events = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + return [ + log + .create() + .message(getMessage()) + .logLevel('error') + .defaults({ + 'trace.id': generateShortId(), + 'agent.name': 'synth-agent', + ...eventMetadata, + }) + .timestamp(timestamp), + ]; + }); + + return logSynthtraceClient.index(events); + } + + async function cleanup() { + await apmSynthtraceClient.clean(); + await logSynthtraceClient.clean(); + } + }); +}