diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts index 0b8e7748509ef..9a9a29719cfce 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-server/allow_lists.ts @@ -29,6 +29,7 @@ export const AGENT_BUILDER_BUILTIN_TOOLS: string[] = [ `${internalNamespaces.observability}.get_log_change_points`, `${internalNamespaces.observability}.get_metric_change_points`, `${internalNamespaces.observability}.get_index_info`, + `${internalNamespaces.observability}.get_trace_change_points`, // Dashboards 'platform.dashboard.create_dashboard', diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/README.md b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/README.md new file mode 100644 index 0000000000000..085f1fdcc55e8 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/README.md @@ -0,0 +1,42 @@ +# get_trace_change_points + +Detects statistically significant changes (e.g., "spike", "dip", "trend_change", "step_change", "distribution_change", "non_stationary", "stationary", or "indeterminable") in trace metrics (latency, throughput, and failure rate). Returns the top 25 most significant change points ordered by p-value. + +## Examples + +### Basic time range + +``` +POST kbn://api/agent_builder/tools/_execute +{ + "tool_id": "observability.get_trace_change_points", + "tool_params": { + "start": "now-1h", + "end": "now" + } +} +``` + +``` +POST kbn://api/agent_builder/tools/_execute +{ + "tool_id": "observability.get_trace_change_points", + "tool_params": { + "start": "now-1h", + "end": "now", + "latencyType": "p95" + } +} +``` + +``` +POST kbn://api/agent_builder/tools/_execute +{ + "tool_id": "observability.get_trace_change_points", + "tool_params": { + "start": "now-1h", + "end": "now", + "groupBy": "transaction.name" + } +} +``` diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/handler.ts b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/handler.ts new file mode 100644 index 0000000000000..dab3f6f6dd78e --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/handler.ts @@ -0,0 +1,222 @@ +/* + * 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 type { CoreSetup, KibanaRequest, Logger } from '@kbn/core/server'; +import { ApmDocumentType } from '@kbn/apm-data-access-plugin/common'; +import type { ChangePointType } from '@kbn/es-types/src'; +import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types'; +import { intervalToSeconds } from '@kbn/apm-data-access-plugin/common/utils/get_preferred_bucket_size_and_data_source'; +import { + getOutcomeAggregation, + getDurationFieldForTransactions, +} from '@kbn/apm-data-access-plugin/server/utils'; +import type { + ObservabilityAgentBuilderPluginSetupDependencies, + ObservabilityAgentBuilderPluginStart, + ObservabilityAgentBuilderPluginStartDependencies, +} from '../../types'; +import { timeRangeFilter, kqlFilter as buildKqlFilter } from '../../utils/dsl_filters'; +import { parseDatemath } from '../../utils/time'; +import { buildApmResources } from '../../utils/build_apm_resources'; +import { getPreferredDocumentSource } from '../../utils/get_preferred_document_source'; +import type { ChangePointDetails } from '../../utils/get_change_points'; + +interface Bucket { + key: string | number; + key_as_string?: string; + doc_count: number; +} + +interface ChangePointResult { + type: Record; + bucket?: Bucket; +} + +interface BucketChangePoints extends Bucket { + changes_latency: ChangePointResult; + changes_throughput: ChangePointResult; + changes_failure_rate: ChangePointResult; + time_series: { + buckets: Array< + Bucket & { + latency: { + value: number | null; + }; + throughput: { + value: number | null; + }; + failure_rate: { + value: number | null; + }; + } + >; + }; +} + +type LatencyAggregationType = 'avg' | 'p99' | 'p95'; + +type DocumentType = + | ApmDocumentType.ServiceTransactionMetric + | ApmDocumentType.TransactionMetric + | ApmDocumentType.TransactionEvent; + +function getChangePointsAggs(bucketsPath: string) { + const changePointAggs = { + change_point: { + buckets_path: bucketsPath, + }, + // elasticsearch@9.0.0 change_point aggregation is missing in the types: https://github.com/elastic/elasticsearch-specification/issues/3671 + } as AggregationsAggregationContainer; + return changePointAggs; +} + +function getLatencyAggregation(latencyAggregationType: LatencyAggregationType, field: string) { + return { + latency: { + ...(latencyAggregationType === 'avg' + ? { avg: { field } } + : { + percentiles: { + field, + percents: [latencyAggregationType === 'p95' ? 95 : 99], + }, + }), + }, + }; +} + +export async function getToolHandler({ + core, + plugins, + request, + logger, + start, + end, + kqlFilter, + groupBy, + latencyType = 'avg', +}: { + core: CoreSetup< + ObservabilityAgentBuilderPluginStartDependencies, + ObservabilityAgentBuilderPluginStart + >; + plugins: ObservabilityAgentBuilderPluginSetupDependencies; + request: KibanaRequest; + logger: Logger; + start: string; + end: string; + kqlFilter?: string; + groupBy: string; + latencyType: LatencyAggregationType | undefined; +}): Promise { + const { apmEventClient, apmDataAccessServices } = await buildApmResources({ + core, + plugins, + request, + logger, + }); + + const startMs = parseDatemath(start); + const endMs = parseDatemath(end); + const source = await getPreferredDocumentSource({ + apmDataAccessServices, + start: startMs, + end: endMs, + groupBy, + kqlFilter, + }); + + const { rollupInterval, hasDurationSummaryField } = source; + const documentType = source.documentType as DocumentType; + // cant calculate percentile aggregation on transaction.duration.summary field + const useDurationSummaryField = + hasDurationSummaryField && latencyType !== 'p95' && latencyType !== 'p99'; + const durationField = getDurationFieldForTransactions(documentType, useDurationSummaryField); + const bucketSizeInSeconds = intervalToSeconds(rollupInterval); + + const calculateFailedTransactionRate = + 'params.successful_or_failed != null && params.successful_or_failed > 0 ? (params.successful_or_failed - params.success) / params.successful_or_failed : 0'; + + const response = await apmEventClient.search('get_trace_change_points', { + apm: { + sources: [{ documentType, rollupInterval }], + }, + size: 0, + track_total_hits: false, + query: { + bool: { + filter: [ + ...timeRangeFilter('@timestamp', { + start: startMs, + end: endMs, + }), + ...buildKqlFilter(kqlFilter), + ], + }, + }, + aggs: { + groups: { + terms: { + field: groupBy, + }, + aggs: { + time_series: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${bucketSizeInSeconds}s`, + }, + aggs: { + ...getOutcomeAggregation(documentType), + ...getLatencyAggregation(latencyType, durationField), + failure_rate: + documentType === ApmDocumentType.ServiceTransactionMetric + ? { + bucket_script: { + buckets_path: { + successful_or_failed: 'successful_or_failed', + success: 'successful', + }, + script: { + source: calculateFailedTransactionRate, + }, + }, + } + : { + bucket_script: { + buckets_path: { + successful_or_failed: 'successful_or_failed>_count', + success: 'successful>_count', + }, + script: { + source: calculateFailedTransactionRate, + }, + }, + }, + throughput: { + bucket_script: { + buckets_path: { + count: '_count', + }, + script: { + source: 'params.count != null ? params.count / (params.bucketSize / 60.0) : 0', + params: { + bucketSize: bucketSizeInSeconds, + }, + }, + }, + }, + }, + }, + changes_latency: getChangePointsAggs('time_series>latency'), + changes_throughput: getChangePointsAggs('time_series>throughput'), + changes_failure_rate: getChangePointsAggs('time_series>failure_rate'), + }, + }, + }, + }); + + return (response.aggregations?.groups?.buckets as BucketChangePoints[]) ?? []; +} diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/tool.ts b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/tool.ts new file mode 100644 index 0000000000000..941e5a23e853b --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_change_points/tool.ts @@ -0,0 +1,121 @@ +/* + * 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 { z } from '@kbn/zod'; +import { ToolType } from '@kbn/agent-builder-common'; +import { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; +import type { BuiltinToolDefinition } from '@kbn/agent-builder-server'; +import type { CoreSetup, Logger } from '@kbn/core/server'; +import type { + ObservabilityAgentBuilderPluginSetupDependencies, + ObservabilityAgentBuilderPluginStart, + ObservabilityAgentBuilderPluginStartDependencies, +} from '../../types'; +import { timeRangeSchemaRequired } from '../../utils/tool_schemas'; +import { getToolHandler } from './handler'; + +export const OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID = + 'observability.get_trace_change_points'; + +const getTraceChangePointsSchema = z.object({ + ...timeRangeSchemaRequired, + kqlFilter: z + .string() + .describe( + 'Optional KQL query to filter the trace documents. Examples: trace.id:"abc123", service.name:"my-service"' + ) + .optional(), + groupBy: z + .string() + .describe( + `Field to group results by. Use only low-cardinality fields. Using many fields or high-cardinality fields can cause a large number of groups and severely impact performance. Common fields to group by include: +- Service level: 'service.name', 'service.environment', 'service.version' +- Transaction level: 'transaction.name', 'transaction.type' +- Infrastructure level: 'host.name', 'container.id', 'kubernetes.pod.name' +` + ) + .optional(), + latencyType: z + .enum(['avg', 'p95', 'p99']) + .describe('Aggregation type for latency change points analysis. default is avg.') + .optional(), +}); + +export function createGetTraceChangePointsTool({ + core, + plugins, + logger, +}: { + core: CoreSetup< + ObservabilityAgentBuilderPluginStartDependencies, + ObservabilityAgentBuilderPluginStart + >; + plugins: ObservabilityAgentBuilderPluginSetupDependencies; + logger: Logger; +}) { + const toolDefinition: BuiltinToolDefinition = { + id: OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID, + type: ToolType.builtin, + description: `Analyzes traces to detect statistically significant change points in latency, throughput, and failure rate across group (e.g., service, transaction, host). +Trace metrics: +- Latency: avg/p95/p99 response time. +- Throughput: requests per minute. +- Failure rate: percentage of failed transactions. + +Supports optional KQL filtering + +When to use: +- Detecting significant changes in trace behavior (spike, dip, step change, trend change, distribution change, stationary/non‑stationary, indeterminable) and identifying when they occur. +`, + schema: getTraceChangePointsSchema, + tags: ['observability', 'traces'], + handler: async ( + { start, end, kqlFilter, groupBy = 'service.name', latencyType = 'avg' }, + { request } + ) => { + try { + const changePoints = await getToolHandler({ + core, + plugins, + request, + logger, + start, + end, + kqlFilter, + groupBy, + latencyType, + }); + + return { + results: [ + { + type: ToolResultType.other, + data: { + changePoints, + }, + }, + ], + }; + } catch (error) { + logger.error(`Error getting trace change points: ${error.message}`); + logger.debug(error); + return { + results: [ + { + type: ToolResultType.error, + data: { + message: `Error getting trace change points: ${error.message}`, + stack: error.stack, + }, + }, + ], + }; + } + }, + }; + + return toolDefinition; +} diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_metrics/handler.ts b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_metrics/handler.ts index c3680613d243a..ec02ddd0c4656 100644 --- a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_metrics/handler.ts +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/get_trace_metrics/handler.ts @@ -7,10 +7,6 @@ import type { CoreSetup, KibanaRequest, Logger } from '@kbn/core/server'; import type { ApmDocumentType } from '@kbn/apm-data-access-plugin/common'; -import { - getPreferredBucketSizeAndDataSource, - getBucketSize, -} from '@kbn/apm-data-access-plugin/common'; import { calculateFailedTransactionRate, getOutcomeAggregation, @@ -25,6 +21,7 @@ import type { import { parseDatemath } from '../../utils/time'; import { buildApmResources } from '../../utils/build_apm_resources'; import { timeRangeFilter, kqlFilter as buildKqlFilter } from '../../utils/dsl_filters'; +import { getPreferredDocumentSource } from '../../utils/get_preferred_document_source'; export interface TraceMetricsItem { group: string; @@ -68,30 +65,12 @@ export async function getToolHandler({ const startMs = parseDatemath(start); const endMs = parseDatemath(end); - - // Get preferred document source based on groupBy, filter, and data availability - const kueryParts: string[] = []; - if (kqlFilter) { - kueryParts.push(kqlFilter); - } - kueryParts.push(`${groupBy}: *`); - const kuery = kueryParts.join(' AND '); - - const documentSources = await apmDataAccessServices.getDocumentSources({ + const source = await getPreferredDocumentSource({ + apmDataAccessServices, start: startMs, end: endMs, - kuery, - }); - - const { bucketSize } = getBucketSize({ - start: startMs, - end: endMs, - numBuckets: 100, - }); - - const { source } = getPreferredBucketSizeAndDataSource({ - sources: documentSources, - bucketSizeInSeconds: bucketSize, + groupBy, + kqlFilter, }); const { rollupInterval, hasDurationSummaryField } = source; diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/register_tools.ts b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/register_tools.ts index 416f4a2a114a7..ec24016b999e3 100644 --- a/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/register_tools.ts +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/tools/register_tools.ts @@ -49,6 +49,10 @@ import { OBSERVABILITY_GET_METRIC_CHANGE_POINTS_TOOL_ID, createGetMetricChangePointsTool, } from './get_metric_change_points/tool'; +import { + OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID, + createGetTraceChangePointsTool, +} from './get_trace_change_points/tool'; import { OBSERVABILITY_GET_INDEX_INFO_TOOL_ID, createGetIndexInfoTool } from './get_index_info'; const PLATFORM_TOOL_IDS = [ @@ -71,6 +75,7 @@ const OBSERVABILITY_TOOL_IDS = [ OBSERVABILITY_GET_TRACE_METRICS_TOOL_ID, OBSERVABILITY_GET_LOG_CHANGE_POINTS_TOOL_ID, OBSERVABILITY_GET_METRIC_CHANGE_POINTS_TOOL_ID, + OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID, OBSERVABILITY_GET_INDEX_INFO_TOOL_ID, ]; @@ -102,6 +107,7 @@ export async function registerTools({ createGetTraceMetricsTool({ core, plugins, logger }), createGetLogChangePointsTool({ core, plugins, logger }), createGetMetricChangePointsTool({ core, plugins, logger }), + createGetTraceChangePointsTool({ core, plugins, logger }), createGetIndexInfoTool({ core, plugins, logger }), ]; diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_change_points.ts b/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_change_points.ts index 39c9a866c915a..22b1450b2bd6e 100644 --- a/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_change_points.ts +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_change_points.ts @@ -7,7 +7,7 @@ import type { ChangePointType } from '@kbn/es-types/src'; -interface ChangePointDetails { +export interface ChangePointDetails { change_point?: number; r_value?: number; trend?: string; diff --git a/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_preferred_document_source.ts b/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_preferred_document_source.ts new file mode 100644 index 0000000000000..5a934d2f82241 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_agent_builder/server/utils/get_preferred_document_source.ts @@ -0,0 +1,61 @@ +/* + * 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 type { ApmDataAccessServices } from '@kbn/apm-data-access-plugin/server'; +import { + getPreferredBucketSizeAndDataSource, + getBucketSize, +} from '@kbn/apm-data-access-plugin/common'; + +/** + * Gets the preferred document source based on groupBy, filter, and data availability. + * + * Uses getDocumentSources to determine which document types have data for the given + * filter and groupBy field. This automatically handles: + * - ServiceTransactionMetric: Most efficient, but only has service.name, service.environment, transaction.type + * - TransactionMetric: Has more dimensions (transaction.*, host.*, container.*, kubernetes.*, cloud.*, faas.*, etc.) + * - TransactionEvent: Raw transaction docs, fallback when metrics don't have required fields + */ +export async function getPreferredDocumentSource({ + apmDataAccessServices, + start, + end, + groupBy, + kqlFilter, +}: { + apmDataAccessServices: ApmDataAccessServices; + start: number; + end: number; + groupBy: string; + kqlFilter?: string; +}) { + const kueryParts: string[] = []; + if (kqlFilter) { + kueryParts.push(kqlFilter); + } + kueryParts.push(`${groupBy}: *`); + const kuery = kueryParts.join(' AND '); + + const documentSources = await apmDataAccessServices.getDocumentSources({ + start, + end, + kuery, + }); + + const { bucketSize } = getBucketSize({ + start, + end, + numBuckets: 100, + }); + + const { source } = getPreferredBucketSizeAndDataSource({ + sources: documentSources, + bucketSizeInSeconds: bucketSize, + }); + + return source; +} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/index.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/index.ts index 9cefde8dc0614..40307b7699ed1 100644 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/index.ts +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/index.ts @@ -21,7 +21,9 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('./tools/get_trace_metrics.spec.ts')); loadTestFile(require.resolve('./tools/get_log_change_points.spec.ts')); loadTestFile(require.resolve('./tools/get_metric_change_points.spec.ts')); + loadTestFile(require.resolve('./tools/get_trace_change_points.spec.ts')); loadTestFile(require.resolve('./tools/get_index_info.spec.ts')); + // ai insights loadTestFile(require.resolve('./ai_insights/error.spec.ts')); loadTestFile(require.resolve('./ai_insights/alert.spec.ts')); diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/tools/get_trace_change_points.spec.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/tools/get_trace_change_points.spec.ts new file mode 100644 index 0000000000000..f73f90d452ba2 --- /dev/null +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/tools/get_trace_change_points.spec.ts @@ -0,0 +1,81 @@ +/* + * 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 expect from '@kbn/expect'; +import type { ApmSynthtraceEsClient } from '@kbn/synthtrace'; +import type { ToolResultType } from '@kbn/agent-builder-common/tools/tool_result'; +import { OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID } from '@kbn/observability-agent-builder-plugin/server/tools/get_trace_change_points/tool'; +import type { ChangePoint } from '@kbn/observability-agent-builder-plugin/server/utils/get_change_points'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { createAgentBuilderApiClient } from '../utils/agent_builder_client'; +import { + SERVICE_NAME, + TRACE_CHANGE_POINTS_ANALYSIS_WINDOW, + createTraceChangePointsData, +} from '../utils/synthtrace_scenarios/create_trace_change_points_data'; + +interface ToolResult { + type: ToolResultType.other; + data: { + changePoints: ChangePoint[]; + }; +} + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); + const synthtrace = getService('synthtrace'); + + describe(`tool: ${OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID}`, function () { + let agentBuilderApiClient: ReturnType; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + const supertest = await roleScopedSupertest.getSupertestWithRoleScope('admin'); + agentBuilderApiClient = createAgentBuilderApiClient(supertest); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await createTraceChangePointsData({ apmSynthtraceEsClient }); + }); + + after(async () => { + await apmSynthtraceEsClient.clean(); + }); + + describe('when retrieving trace change points', () => { + let traceChangePoints: ChangePoint[]; + + before(async () => { + const toolResults: ToolResult[] = await agentBuilderApiClient.executeTool({ + id: OBSERVABILITY_GET_TRACE_CHANGE_POINTS_TOOL_ID, + params: { + start: TRACE_CHANGE_POINTS_ANALYSIS_WINDOW.start, + end: TRACE_CHANGE_POINTS_ANALYSIS_WINDOW.end, + }, + }); + traceChangePoints = toolResults[0]?.data?.changePoints ?? []; + }); + + it('should return results grouped by service.name', () => { + const serviceNames = traceChangePoints.find((cp: ChangePoint) => cp.key === SERVICE_NAME); + expect(serviceNames).to.not.be(undefined); + }); + + it('should include changes_latency, changes_throughput, and changes_failure_rate change points results', () => { + traceChangePoints.forEach((cp: ChangePoint) => { + expect(cp).to.have.property('changes_latency'); + expect(cp).to.have.property('changes_throughput'); + expect(cp).to.have.property('changes_failure_rate'); + }); + }); + + it('should include time series data for visualization', () => { + traceChangePoints.forEach((cp: ChangePoint) => { + expect(cp).to.have.property('time_series'); + }); + }); + }); + }); +} diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/utils/synthtrace_scenarios/create_trace_change_points_data.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/utils/synthtrace_scenarios/create_trace_change_points_data.ts new file mode 100644 index 0000000000000..0a939237e4e52 --- /dev/null +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/observability_agent_builder/utils/synthtrace_scenarios/create_trace_change_points_data.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import datemath from '@elastic/datemath'; +import type { ApmSynthtraceEsClient } from '@kbn/synthtrace'; +import { apm, timerange } from '@kbn/synthtrace-client'; + +export const SERVICE_NAME = 'test-service'; +export const TRACE_CHANGE_POINTS_INDEX = `traces-apm.app.${SERVICE_NAME}-default`; +export const TRACE_CHANGE_POINTS_ANALYSIS_WINDOW = { + start: 'now-60m', + end: 'now', +}; +export const TRACE_CHANGE_POINTS_ANALYSIS_SPIKE_WINDOW = { + start: 'now-30m', + end: 'now-28m', +}; + +/** + * Creates trace data with SPIKE pattern. + */ +export async function createTraceChangePointsData({ + apmSynthtraceEsClient, +}: { + apmSynthtraceEsClient: ApmSynthtraceEsClient; +}) { + const range = timerange( + TRACE_CHANGE_POINTS_ANALYSIS_WINDOW.start, + TRACE_CHANGE_POINTS_ANALYSIS_WINDOW.end + ); + + const spikeStart = datemath.parse(TRACE_CHANGE_POINTS_ANALYSIS_SPIKE_WINDOW.start)!.valueOf(); + const spikeEnd = datemath.parse(TRACE_CHANGE_POINTS_ANALYSIS_SPIKE_WINDOW.end)!.valueOf(); + + const instance = apm + .service({ name: SERVICE_NAME, environment: 'test', agentName: 'test-agent' }) + .instance('instance-test'); + + const transactionName = 'GET /api/orders'; + + const traceStream = range + .interval('1m') + .rate(1) + .generator((timestamp) => { + const isSpike = timestamp >= spikeStart && timestamp < spikeEnd; + + const txDurationUs = isSpike ? 5_000 : 500; + const spanDurationUs = isSpike ? 10_000 : 1_000; + + const traces = []; + for (let i = 0; i < 25; i++) { + traces.push( + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(txDurationUs) + .children( + instance + .span({ spanName: 'db.query', spanType: 'db' }) + .timestamp(timestamp) + .duration(spanDurationUs) + .success() + ) + .success() + ); + } + return traces; + }); + + await apmSynthtraceEsClient.index([traceStream]); +}