From 34c86a843094ce4a2b7a32c0a51acc93e0ae71b8 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Fri, 22 May 2026 17:11:56 +0200 Subject: [PATCH 1/9] [Streams] Add classic-stream field mapping performance journey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the existing wired schema TTFMP journey against a classic stream with 1,000 field_overrides set via the public Streams API, so the kibana-streams-performance pipeline measures schema editor load on the classic mapping path. Follow-up to #252288 (review by @flash1293). Dashboard panel and alerting rule deferred until 4 green main-branch runs produce a calibration baseline, matching the Phase 2 §9.2 process. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../streams_classic_field_mapping.ts | 75 +++++++++++++++++++ .../synthtrace_data/streams_data.ts | 66 ++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts diff --git a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts new file mode 100644 index 0000000000000..f9b857fc81396 --- /dev/null +++ b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.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 { Journey } from '@kbn/journeys'; +import { subj } from '@kbn/test-subj-selector'; +import { setupClassicFieldMappingAtScale } from '../synthtrace_data/streams_data'; + +const CLASSIC_MAPPING_STREAM = 'logs-perf-classic-mapping'; + +const getNewFieldName = (): string => { + const phase = (process.env.TEST_PERFORMANCE_PHASE ?? 'local') + .toLowerCase() + .replace(/[^a-z0-9_]+/g, '_'); + + return `attributes.perf_classic_new_field_${phase}`; +}; + +export const journey = new Journey({ + ftrConfigPath: 'x-pack/performance/configs/streams_heavy_config.ts', + beforeSteps: async ({ kibanaServer, log }) => { + await setupClassicFieldMappingAtScale(kibanaServer, log); + }, +}) + .step('Go to classic stream schema page', async ({ page, kbnUrl }) => { + await page.goto(kbnUrl.get(`/app/streams/${CLASSIC_MAPPING_STREAM}/management/schema`)); + await page.waitForSelector(subj('streamsAppContentAddFieldButton'), { timeout: 60000 }); + }) + .step('Open add field flyout', async ({ page }) => { + await page.click(subj('streamsAppContentAddFieldButton')); + await page.waitForSelector(subj('streamsAppSchemaEditorAddFieldFlyoutFieldName'), { + timeout: 30000, + }); + }) + .step('Configure new field mapping', async ({ page, inputDelays }) => { + const comboBox = page.locator(subj('streamsAppSchemaEditorAddFieldFlyoutFieldName')); + const comboInput = comboBox.locator('input[role="combobox"]'); + const fieldName = getNewFieldName(); + await comboInput.click(); + await comboInput.pressSequentially(fieldName, { + delay: inputDelays.TYPING, + timeout: 60000, + }); + await page.keyboard.press('Enter'); + + await page.click(subj('streamsAppFieldFormTypeSelect')); + await page.click(subj('option-type-keyword')); + }) + .step('Add field mapping', async ({ page }) => { + await page.waitForSelector(`${subj('streamsAppSchemaEditorAddFieldButton')}:not([disabled])`, { + timeout: 30000, + }); + await page.click(subj('streamsAppSchemaEditorAddFieldButton')); + await page.waitForSelector(subj('streamsAppSchemaEditorAddFieldFlyoutCloseButton'), { + state: 'detached', + timeout: 30000, + }); + }) + .step('Review and submit field mapping', async ({ page }) => { + await page.waitForSelector(subj('streamsAppSchemaEditorReviewStagedChangesButton'), { + timeout: 60000, + }); + await page.click(subj('streamsAppSchemaEditorReviewStagedChangesButton')); + await page.waitForSelector(subj('streamsAppSchemaChangesReviewModalSubmitButton'), { + timeout: 60000, + }); + await page.click(subj('streamsAppSchemaChangesReviewModalSubmitButton')); + await page.waitForSelector(subj('streamsAppSchemaChangesReviewModalSubmitButton'), { + state: 'detached', + timeout: 30000, + }); + }); diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index 98b2568bf9525..cc33296cf48cf 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -819,6 +819,7 @@ export async function setupLargeWiredHierarchy( } const CHILD_STREAM = `${WIRED_ROOT_STREAM}.child1`; +const CLASSIC_MAPPING_STREAM = 'logs-perf-classic-mapping'; /** Skip heavy setup during performance TEST phase. */ function shouldRunSetup(log: ToolingLog): boolean { @@ -839,6 +840,14 @@ interface IngestConfig { failure_store: Record; } +interface ClassicIngestConfig { + processing: { steps: unknown[] }; + settings: Record; + classic: { field_overrides: Record }; + lifecycle: Record; + failure_store: Record; +} + /** GET ingest, mutate, PUT back (strip read-only processing.updated_at). */ async function getAndUpdateIngestConfig( kibanaServer: KibanaServer, @@ -867,6 +876,34 @@ async function getAndUpdateIngestConfig( }); } +/** Classic-stream equivalent: GET ingest, mutate field_overrides, PUT back. */ +async function getAndUpdateClassicIngestConfig( + kibanaServer: KibanaServer, + streamName: string, + mutate: (config: ClassicIngestConfig) => void +) { + const response = await kibanaServer.request<{ + ingest: ClassicIngestConfig & { processing: { updated_at?: string } }; + }>({ + path: `/api/streams/${streamName}/_ingest`, + method: 'GET', + headers: PUBLIC_API_HEADERS, + }); + + const config = response.data.ingest; + const { updated_at: _updatedAt, ...processingWithoutTimestamp } = config.processing; + config.processing = processingWithoutTimestamp as ClassicIngestConfig['processing']; + + mutate(config); + + await kibanaServer.request({ + path: `/api/streams/${streamName}/_ingest`, + method: 'PUT', + headers: PUBLIC_API_HEADERS, + body: { ingest: config }, + }); +} + /** Setup for the processing journey at scale. */ export async function setupProcessingAtScale(kibanaServer: KibanaServer, log: ToolingLog) { if (!shouldRunSetup(log)) return; @@ -1004,6 +1041,35 @@ export async function setupFieldMappingAtScale(kibanaServer: KibanaServer, log: log.info(`${FIELD_COUNT} fields mapped on ${CHILD_STREAM}`); } +/** + * Setup for the classic-stream field mapping journey at scale. + * Creates one classic stream and sets 1,000 field_overrides via the public + * Streams API (the same path the schema editor uses). + */ +export async function setupClassicFieldMappingAtScale(kibanaServer: KibanaServer, log: ToolingLog) { + if (!shouldRunSetup(log)) return; + + await enableStreams(kibanaServer, log); + await createSingleClassicStream(kibanaServer, CLASSIC_MAPPING_STREAM); + + const FIELD_COUNT = 1000; + const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; + + log.info(`Mapping ${FIELD_COUNT} field_overrides on ${CLASSIC_MAPPING_STREAM}...`); + + const fields: Record = {}; + for (let i = 1; i <= FIELD_COUNT; i++) { + const type = FIELD_TYPES[(i - 1) % FIELD_TYPES.length]; + fields[`attributes.perf_classic_schema_${String(i).padStart(4, '0')}`] = { type }; + } + + await getAndUpdateClassicIngestConfig(kibanaServer, CLASSIC_MAPPING_STREAM, (config) => { + config.classic.field_overrides = { ...config.classic.field_overrides, ...fields }; + }); + + log.info(`${FIELD_COUNT} field_overrides set on ${CLASSIC_MAPPING_STREAM}`); +} + /** Setup for the retention journey at scale. */ export async function setupRetentionAtScale(kibanaServer: KibanaServer, log: ToolingLog) { if (!shouldRunSetup(log)) return; From 1f878768c533ab24cf4e439bbaf9dd2b6f902841 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Fri, 22 May 2026 17:17:05 +0200 Subject: [PATCH 2/9] Bump classic field mapping count from 1,000 to 10,000 Matches the upper bound called out in #252288 review. Buildkite streams-performance pipeline will be triggered manually against this branch before merge, so we will see at 10k whether the schema editor loads within journey timeouts. Co-Authored-By: Claude Opus 4.7 (1M context) --- x-pack/performance/synthtrace_data/streams_data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index cc33296cf48cf..51cbedc0bf66d 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -1043,7 +1043,7 @@ export async function setupFieldMappingAtScale(kibanaServer: KibanaServer, log: /** * Setup for the classic-stream field mapping journey at scale. - * Creates one classic stream and sets 1,000 field_overrides via the public + * Creates one classic stream and sets 10,000 field_overrides via the public * Streams API (the same path the schema editor uses). */ export async function setupClassicFieldMappingAtScale(kibanaServer: KibanaServer, log: ToolingLog) { @@ -1052,7 +1052,7 @@ export async function setupClassicFieldMappingAtScale(kibanaServer: KibanaServer await enableStreams(kibanaServer, log); await createSingleClassicStream(kibanaServer, CLASSIC_MAPPING_STREAM); - const FIELD_COUNT = 1000; + const FIELD_COUNT = 10000; const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; log.info(`Mapping ${FIELD_COUNT} field_overrides on ${CLASSIC_MAPPING_STREAM}...`); @@ -1060,7 +1060,7 @@ export async function setupClassicFieldMappingAtScale(kibanaServer: KibanaServer const fields: Record = {}; for (let i = 1; i <= FIELD_COUNT; i++) { const type = FIELD_TYPES[(i - 1) % FIELD_TYPES.length]; - fields[`attributes.perf_classic_schema_${String(i).padStart(4, '0')}`] = { type }; + fields[`attributes.perf_classic_schema_${String(i).padStart(5, '0')}`] = { type }; } await getAndUpdateClassicIngestConfig(kibanaServer, CLASSIC_MAPPING_STREAM, (config) => { From 86dea481bcb4f70807302d02aa5fee9dcd4c3758 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Sun, 24 May 2026 11:56:32 +0200 Subject: [PATCH 3/9] Register classic-field-mapping journey in pipeline group + FTR manifest Build #11 of kibana-streams-performance ran 6 of the 6 existing journeys but silently skipped the new streams_classic_field_mapping because the JOURNEYS_GROUP=streams filter in run_performance_cli.ts is an explicit allow-list. Adding the new journey there plus in the FTR manifest so both pipelines discover it. Co-Authored-By: Claude Opus 4.7 (1M context) --- .buildkite/ftr-manifests/ftr_platform_stateful_configs.yml | 1 + src/dev/performance/run_performance_cli.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.buildkite/ftr-manifests/ftr_platform_stateful_configs.yml b/.buildkite/ftr-manifests/ftr_platform_stateful_configs.yml index 63a71b92ee398..353806ffa4c68 100644 --- a/.buildkite/ftr-manifests/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr-manifests/ftr_platform_stateful_configs.yml @@ -48,6 +48,7 @@ disabled: - x-pack/performance/configs/http2_config.ts # Streams performance journeys — run exclusively in the scheduled performance pipeline + - x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts - x-pack/performance/journeys_e2e/streams_data_quality.ts - x-pack/performance/journeys_e2e/streams_field_mapping.ts - x-pack/performance/journeys_e2e/streams_listing_page.ts diff --git a/src/dev/performance/run_performance_cli.ts b/src/dev/performance/run_performance_cli.ts index 0e3c5ec3d45fe..4584096b22298 100644 --- a/src/dev/performance/run_performance_cli.ts +++ b/src/dev/performance/run_performance_cli.ts @@ -87,6 +87,7 @@ const journeyTargetGroups: JourneyTargetGroups = { 'streams_processing_step', 'streams_retention', 'streams_field_mapping', + 'streams_classic_field_mapping', 'streams_wired_hierarchy', ], metricsExperience: ['metrics_experience_grid'], From 2b42bb790f492211da6930bca249522e69227ad7 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Mon, 25 May 2026 11:51:35 +0200 Subject: [PATCH 4/9] Raise total_fields.limit before classic field mapping PUT ES default index.mapping.total_fields.limit is 1000. Our journey deliberately maps 10000 fields to stress the schema editor, so the backing data stream's limit must be raised before the _ingest PUT. Without this, setup fails with HTTP 400 "Limit of total fields [1000] has been exceeded" (seen in kibana-streams-performance build #12). --- .../streams_classic_field_mapping.ts | 4 ++-- .../synthtrace_data/streams_data.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts index f9b857fc81396..797ec02d3d277 100644 --- a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts +++ b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts @@ -21,8 +21,8 @@ const getNewFieldName = (): string => { export const journey = new Journey({ ftrConfigPath: 'x-pack/performance/configs/streams_heavy_config.ts', - beforeSteps: async ({ kibanaServer, log }) => { - await setupClassicFieldMappingAtScale(kibanaServer, log); + beforeSteps: async ({ kibanaServer, es, log }) => { + await setupClassicFieldMappingAtScale(kibanaServer, es, log); }, }) .step('Go to classic stream schema page', async ({ page, kbnUrl }) => { diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index 51cbedc0bf66d..6ecf20d6f7651 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -1046,7 +1046,11 @@ export async function setupFieldMappingAtScale(kibanaServer: KibanaServer, log: * Creates one classic stream and sets 10,000 field_overrides via the public * Streams API (the same path the schema editor uses). */ -export async function setupClassicFieldMappingAtScale(kibanaServer: KibanaServer, log: ToolingLog) { +export async function setupClassicFieldMappingAtScale( + kibanaServer: KibanaServer, + es: Client, + log: ToolingLog +) { if (!shouldRunSetup(log)) return; await enableStreams(kibanaServer, log); @@ -1055,6 +1059,18 @@ export async function setupClassicFieldMappingAtScale(kibanaServer: KibanaServer const FIELD_COUNT = 10000; const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; + // ES default `index.mapping.total_fields.limit` is 1000, well below our + // intentional stress target. Raise it on the classic stream's backing index + // before pushing field_overrides so the Streams API mapping update succeeds. + const TOTAL_FIELDS_LIMIT = FIELD_COUNT * 2; + log.info( + `Raising index.mapping.total_fields.limit to ${TOTAL_FIELDS_LIMIT} on ${CLASSIC_MAPPING_STREAM}...` + ); + await es.indices.putSettings({ + index: CLASSIC_MAPPING_STREAM, + settings: { 'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT }, + }); + log.info(`Mapping ${FIELD_COUNT} field_overrides on ${CLASSIC_MAPPING_STREAM}...`); const fields: Record = {}; From 9688b93a7cee10d604f50f45c4ade5b0b7eb7899 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Mon, 25 May 2026 15:31:43 +0200 Subject: [PATCH 5/9] Use putDataStreamSettings to raise total_fields.limit Build #15 still failed with `Limit of total fields [1000] has been exceeded` even though the previous fix called `indices.putSettings` against the data stream name. The Streams classic ingest update validates via `PUT /_data_stream/{name}/_mappings?dry_run=true`, which resolves the limit through the data stream's settings (and the rollover template), not via the live backing index that `putSettings` happened to touch. Switch to `indices.putDataStreamSettings`, which is the API the Streams server itself uses for allowlisted classic-stream settings. It applies to current backing indices and to the next rollover, so the mapping dry-run sees the raised limit. Co-Authored-By: Claude Opus 4.7 (1M context) --- x-pack/performance/synthtrace_data/streams_data.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index 6ecf20d6f7651..32236546a3710 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -1060,14 +1060,15 @@ export async function setupClassicFieldMappingAtScale( const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; // ES default `index.mapping.total_fields.limit` is 1000, well below our - // intentional stress target. Raise it on the classic stream's backing index - // before pushing field_overrides so the Streams API mapping update succeeds. + // intentional stress target. Use the data stream settings API so the new + // limit applies to both current backing indices and the next rollover, + // which is what the Streams mapping dry-run validation reads from. const TOTAL_FIELDS_LIMIT = FIELD_COUNT * 2; log.info( `Raising index.mapping.total_fields.limit to ${TOTAL_FIELDS_LIMIT} on ${CLASSIC_MAPPING_STREAM}...` ); - await es.indices.putSettings({ - index: CLASSIC_MAPPING_STREAM, + await es.indices.putDataStreamSettings({ + name: CLASSIC_MAPPING_STREAM, settings: { 'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT }, }); From 6edae01102b1806cd9dae35722674dde7ff361b2 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Mon, 25 May 2026 17:44:33 +0200 Subject: [PATCH 6/9] Raise total_fields.limit on the data stream's backing indices Build #16 still failed with `Limit of total fields [1000] has been exceeded` after switching to `putDataStreamSettings`. The Streams `_ingest` PUT validates field_overrides via `PUT /_data_stream/{name}/_mappings?dry_run=true`, which reads the limit from the current write index's live settings. `putDataStreamSettings` applied the override at the data-stream level but not down to the existing `.ds-*` backing index, so the validation kept seeing the default 1000. Resolve the backing indices via `getDataStream` and set `index.mapping.total_fields.limit` directly on them with `putSettings`. That guarantees the dry-run sees the raised limit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../synthtrace_data/streams_data.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index 32236546a3710..4dca2254b77fe 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -1060,15 +1060,21 @@ export async function setupClassicFieldMappingAtScale( const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; // ES default `index.mapping.total_fields.limit` is 1000, well below our - // intentional stress target. Use the data stream settings API so the new - // limit applies to both current backing indices and the next rollover, - // which is what the Streams mapping dry-run validation reads from. + // intentional stress target. The Streams `_ingest` PUT validates the field + // overrides via `PUT /_data_stream/{name}/_mappings?dry_run=true`, which + // reads the limit from the current write index's live settings - not from + // the data stream level or matching templates. Raise the limit on each + // backing index directly so the dry-run validation passes. const TOTAL_FIELDS_LIMIT = FIELD_COUNT * 2; + const dataStream = await es.indices.getDataStream({ name: CLASSIC_MAPPING_STREAM }); + const backingIndices = dataStream.data_streams[0].indices.map((idx) => idx.index_name); log.info( - `Raising index.mapping.total_fields.limit to ${TOTAL_FIELDS_LIMIT} on ${CLASSIC_MAPPING_STREAM}...` + `Raising index.mapping.total_fields.limit to ${TOTAL_FIELDS_LIMIT} on backing indices [${backingIndices.join( + ', ' + )}]...` ); - await es.indices.putDataStreamSettings({ - name: CLASSIC_MAPPING_STREAM, + await es.indices.putSettings({ + index: backingIndices, settings: { 'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT }, }); From f50b9267e3360ce0d3dce756b08b4df09a2cf67b Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Mon, 25 May 2026 19:24:21 +0200 Subject: [PATCH 7/9] Raise total_fields.limit via index template before classic stream creation Builds #15-#17 kept failing the classic_field_mapping setup with `Limit of total fields [1000] has been exceeded`, even after switching from `putSettings` to `putDataStreamSettings` to per-backing-index `putSettings`. The Streams `_ingest` PUT applies `field_overrides` via `PUT /_data_stream/{name}/_mappings` followed by a lazy rollover, so the dry-run validation resolves `total_fields.limit` from the matching index template (the next-rollover index), not from any live backing index settings - which is why none of those `putSettings` calls moved the validated number. Install a narrow, high-priority data-stream index template that matches only `logs-perf-classic-mapping` (priority 500, beats the built-in `logs` template) and sets `total_fields.limit=20000` before `_create_classic`. The data stream is then born with the raised limit baked into its template, and the dry-run accepts 10000 field_overrides. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../synthtrace_data/streams_data.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index 4dca2254b77fe..f479a9959ea30 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -1053,31 +1053,33 @@ export async function setupClassicFieldMappingAtScale( ) { if (!shouldRunSetup(log)) return; - await enableStreams(kibanaServer, log); - await createSingleClassicStream(kibanaServer, CLASSIC_MAPPING_STREAM); - const FIELD_COUNT = 10000; const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; - - // ES default `index.mapping.total_fields.limit` is 1000, well below our - // intentional stress target. The Streams `_ingest` PUT validates the field - // overrides via `PUT /_data_stream/{name}/_mappings?dry_run=true`, which - // reads the limit from the current write index's live settings - not from - // the data stream level or matching templates. Raise the limit on each - // backing index directly so the dry-run validation passes. const TOTAL_FIELDS_LIMIT = FIELD_COUNT * 2; - const dataStream = await es.indices.getDataStream({ name: CLASSIC_MAPPING_STREAM }); - const backingIndices = dataStream.data_streams[0].indices.map((idx) => idx.index_name); + + // The Streams `_ingest` PUT applies `field_overrides` via + // `PUT /_data_stream/{name}/_mappings` followed by a lazy rollover, so its + // dry-run validation resolves `total_fields.limit` from the matching index + // template (the next-rollover index), not from any live backing index + // settings. Install a narrow, high-priority data-stream template that + // raises the limit before the classic stream is created. + const PERF_TEMPLATE_NAME = 'streams-perf-classic-mapping-fields-override'; log.info( - `Raising index.mapping.total_fields.limit to ${TOTAL_FIELDS_LIMIT} on backing indices [${backingIndices.join( - ', ' - )}]...` + `Installing index template ${PERF_TEMPLATE_NAME} with index.mapping.total_fields.limit=${TOTAL_FIELDS_LIMIT}...` ); - await es.indices.putSettings({ - index: backingIndices, - settings: { 'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT }, + await es.indices.putIndexTemplate({ + name: PERF_TEMPLATE_NAME, + index_patterns: [CLASSIC_MAPPING_STREAM], + priority: 500, + data_stream: {}, + template: { + settings: { 'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT }, + }, }); + await enableStreams(kibanaServer, log); + await createSingleClassicStream(kibanaServer, CLASSIC_MAPPING_STREAM); + log.info(`Mapping ${FIELD_COUNT} field_overrides on ${CLASSIC_MAPPING_STREAM}...`); const fields: Record = {}; From 6004705c1679a1e8660b8db1f23d0f188e616956 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Tue, 26 May 2026 09:52:01 +0200 Subject: [PATCH 8/9] Extend submit-review timeout for 10000 field_overrides stream Build #18 confirmed the index-template fix from f50b9267e336 works: setup applied all 10000 field_overrides cleanly (visible in the log as `10000 field_overrides set on logs-perf-classic-mapping`). The journey then advanced through `Go to schema page`, `Open add field flyout`, `Configure new field mapping`, and `Add field mapping`, all green. It now fails on the final `Review and submit field mapping` step. After clicking the modal submit button, the test waits up to 30s for it to detach. The Streams server applies the mapping update across all 10001 overrides (10000 existing plus the one the journey adds), which the setup phase already showed takes around 23s in CI. The submit button stays disabled for the duration of the in-flight request, so the 30s `detached` wait races the server and times out. Raise that final wait to 120s so the step measures the actual submit latency at scale instead of failing on an arbitrarily tight UI timeout. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../journeys_e2e/streams_classic_field_mapping.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts index 797ec02d3d277..209f4f69b7be4 100644 --- a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts +++ b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts @@ -68,8 +68,12 @@ export const journey = new Journey({ timeout: 60000, }); await page.click(subj('streamsAppSchemaChangesReviewModalSubmitButton')); + // Adding one new field to a stream that already has 10000 field_overrides + // requires the server to apply a mapping update over all 10001 entries, which + // observed at ~25s in CI. Give the modal a generous window to close so this + // step measures real submit latency rather than racing a short timeout. await page.waitForSelector(subj('streamsAppSchemaChangesReviewModalSubmitButton'), { state: 'detached', - timeout: 30000, + timeout: 120000, }); }); From 746bee7d86af62e3f145252d345d9470f93e8690 Mon Sep 17 00:00:00 2001 From: Robert Stelmach Date: Tue, 26 May 2026 14:20:36 +0200 Subject: [PATCH 9/9] fix failing journey --- .../streams_classic_field_mapping.ts | 5 +- .../synthtrace_data/streams_data.ts | 78 ++++++++++++++----- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts index 209f4f69b7be4..6fe802c0237f1 100644 --- a/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts +++ b/x-pack/performance/journeys_e2e/streams_classic_field_mapping.ts @@ -30,7 +30,10 @@ export const journey = new Journey({ await page.waitForSelector(subj('streamsAppContentAddFieldButton'), { timeout: 60000 }); }) .step('Open add field flyout', async ({ page }) => { - await page.click(subj('streamsAppContentAddFieldButton')); + await page.waitForSelector(subj('streamsAppSchemaEditorFieldsTableLoaded'), { + timeout: 120000, + }); + await page.click(subj('streamsAppContentAddFieldButton'), { timeout: 120000 }); await page.waitForSelector(subj('streamsAppSchemaEditorAddFieldFlyoutFieldName'), { timeout: 30000, }); diff --git a/x-pack/performance/synthtrace_data/streams_data.ts b/x-pack/performance/synthtrace_data/streams_data.ts index f479a9959ea30..7ee82f4c129a1 100644 --- a/x-pack/performance/synthtrace_data/streams_data.ts +++ b/x-pack/performance/synthtrace_data/streams_data.ts @@ -61,6 +61,15 @@ function isConflictError(error: unknown): boolean { return err?.response?.status === 409; } +function isNotFoundError(error: unknown): boolean { + const err = error as { + response?: { status?: number }; + statusCode?: number; + meta?: { statusCode?: number }; + }; + return err?.response?.status === 404 || err?.statusCode === 404 || err?.meta?.statusCode === 404; +} + function isLockContentionError(error: unknown): boolean { const err = error as { response?: { status?: number } }; return err?.response?.status === 422; @@ -158,6 +167,50 @@ async function createSingleClassicStream(kibanaServer: KibanaServer, name: strin } } +async function updateLogsCustomTotalFieldsLimit( + es: Client, + log: ToolingLog, + totalFieldsLimit: number +): Promise { + let existingComponentTemplate: + | Awaited< + ReturnType + >['component_templates'][number]['component_template'] + | undefined; + + try { + const response = await es.cluster.getComponentTemplate({ + name: LOGS_CUSTOM_COMPONENT_TEMPLATE, + }); + existingComponentTemplate = response.component_templates[0]?.component_template; + } catch (error) { + if (!isNotFoundError(error)) { + throw error; + } + } + + const existingTemplate = existingComponentTemplate?.template ?? {}; + const version = existingComponentTemplate?.version; + + log.info( + `Updating component template ${LOGS_CUSTOM_COMPONENT_TEMPLATE} with ` + + `index.mapping.total_fields.limit=${totalFieldsLimit}...` + ); + + await es.cluster.putComponentTemplate({ + name: LOGS_CUSTOM_COMPONENT_TEMPLATE, + template: { + ...existingTemplate, + settings: { + ...existingTemplate.settings, + 'index.mapping.total_fields.limit': totalFieldsLimit, + }, + }, + ...(existingComponentTemplate?._meta ? { _meta: existingComponentTemplate._meta } : {}), + ...(typeof version === 'number' ? { version } : {}), + }); +} + /** Create classic streams serially to reduce lock contention. */ export async function createClassicStreams( kibanaServer: KibanaServer, @@ -820,6 +873,8 @@ export async function setupLargeWiredHierarchy( const CHILD_STREAM = `${WIRED_ROOT_STREAM}.child1`; const CLASSIC_MAPPING_STREAM = 'logs-perf-classic-mapping'; +const LOGS_CUSTOM_COMPONENT_TEMPLATE = 'logs@custom'; +const LEGACY_CLASSIC_MAPPING_TEMPLATE = 'streams-perf-classic-mapping-fields-override'; /** Skip heavy setup during performance TEST phase. */ function shouldRunSetup(log: ToolingLog): boolean { @@ -1057,25 +1112,12 @@ export async function setupClassicFieldMappingAtScale( const FIELD_TYPES = ['keyword', 'long', 'double', 'boolean', 'ip', 'date']; const TOTAL_FIELDS_LIMIT = FIELD_COUNT * 2; - // The Streams `_ingest` PUT applies `field_overrides` via - // `PUT /_data_stream/{name}/_mappings` followed by a lazy rollover, so its - // dry-run validation resolves `total_fields.limit` from the matching index - // template (the next-rollover index), not from any live backing index - // settings. Install a narrow, high-priority data-stream template that - // raises the limit before the classic stream is created. - const PERF_TEMPLATE_NAME = 'streams-perf-classic-mapping-fields-override'; - log.info( - `Installing index template ${PERF_TEMPLATE_NAME} with index.mapping.total_fields.limit=${TOTAL_FIELDS_LIMIT}...` + await es.indices.deleteIndexTemplate( + { name: LEGACY_CLASSIC_MAPPING_TEMPLATE }, + { ignore: [404] } ); - await es.indices.putIndexTemplate({ - name: PERF_TEMPLATE_NAME, - index_patterns: [CLASSIC_MAPPING_STREAM], - priority: 500, - data_stream: {}, - template: { - settings: { 'index.mapping.total_fields.limit': TOTAL_FIELDS_LIMIT }, - }, - }); + + await updateLogsCustomTotalFieldsLimit(es, log, TOTAL_FIELDS_LIMIT); await enableStreams(kibanaServer, log); await createSingleClassicStream(kibanaServer, CLASSIC_MAPPING_STREAM);