From 81a2d4edc8e06f0869fa325a08c332b57f27d460 Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Fri, 7 Mar 2025 11:27:31 -0800 Subject: [PATCH 1/6] Database selector in Integration install Signed-off-by: Aaron Alvarez --- .../components/setup_integration.tsx | 172 +++++++++++------- .../components/setup_integration_inputs.tsx | 13 ++ 2 files changed, 116 insertions(+), 69 deletions(-) diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx index 6d0e187ea5..974ed36b7a 100644 --- a/public/components/integrations/components/setup_integration.tsx +++ b/public/components/integrations/components/setup_integration.tsx @@ -34,6 +34,7 @@ export interface IntegrationSetupInputs { connectionDataSource: string; connectionLocation: string; checkpointLocation: string; + databaseName: string; connectionTableName: string; enabledWorkflows: string[]; } @@ -109,7 +110,9 @@ const runQuery = async ( }; const makeTableName = (config: IntegrationSetupInputs): string => { - return `${config.connectionDataSource}.default.${config.connectionTableName}`; + return `${config.connectionDataSource}.${config.databaseName || 'default'}.${ + config.connectionTableName + }`; }; const prepareQuery = (query: string, config: IntegrationSetupInputs): string => { @@ -151,85 +154,110 @@ const addIntegration = async ({ }) => { setLoading(true); let sessionId: string | undefined; - - if (config.connectionType === 'index') { - let enabledWorkflows: string[] | undefined; - if (integration.workflows) { - enabledWorkflows = integration.workflows - .filter((w) => - w.applicable_data_sources ? w.applicable_data_sources.includes('index') : true - ) - .map((w) => w.name); - } - const res = await addIntegrationRequest({ - addSample: false, - templateName: integration.name, - integration, - setToast: setCalloutLikeToast, - dataSourceMDSId, - dataSourceMDSLabel, - name: config.displayName, - indexPattern: config.connectionDataSource, - skipRedirect: setIsInstalling ? true : false, - workflows: enabledWorkflows, - }); - if (setIsInstalling) { - setIsInstalling(false, res); - } - if (!res) { - setLoading(false); - } - } else if (config.connectionType === 's3') { - const http = coreRefs.http!; - - const assets: { data: ParsedIntegrationAsset[] } = await http.get( - `${INTEGRATIONS_BASE}/repository/${integration.name}/assets` - ); - for (const query of assets.data.filter( - (a: ParsedIntegrationAsset): a is ParsedIntegrationAsset & { type: 'query' } => - a.type === 'query' - )) { - // Skip any queries that have conditional workflows but aren't enabled - if (query.workflows && !query.workflows.some((w) => config.enabledWorkflows.includes(w))) { - continue; - } - - const queryStr = prepareQuery(query.query, config); + try { + // First create the database if one is specified + if (config.databaseName) { + const createDbQuery = `CREATE DATABASE IF NOT EXISTS ${config.databaseName}`; const result = await runQuery( - queryStr, + createDbQuery, config.connectionDataSource, sessionId, dataSourceMDSId ); + if (!result.ok) { setLoading(false); - setCalloutLikeToast('Failed to add integration', 'danger', result.error.message); + setCalloutLikeToast('Failed to create database', 'danger', result.error.message); return; } - sessionId = result.value.sessionId ?? sessionId; + sessionId = result.value.sessionId; } - // Once everything is ready, add the integration to the new datasource as usual - const res = await addIntegrationRequest({ - addSample: false, - templateName: integration.name, - integration, - setToast: setCalloutLikeToast, - dataSourceMDSId, - dataSourceMDSLabel, - name: config.displayName, - indexPattern: `flint_${config.connectionDataSource}_default_${config.connectionTableName}__*`, - workflows: config.enabledWorkflows, - skipRedirect: setIsInstalling ? true : false, - dataSourceInfo: { dataSource: config.connectionDataSource, tableName: makeTableName(config) }, - }); - if (setIsInstalling) { - setIsInstalling(false, res); - } - if (!res) { - setLoading(false); + + if (config.connectionType === 'index') { + let enabledWorkflows: string[] | undefined; + if (integration.workflows) { + enabledWorkflows = integration.workflows + .filter((w) => + w.applicable_data_sources ? w.applicable_data_sources.includes('index') : true + ) + .map((w) => w.name); + } + const res = await addIntegrationRequest({ + addSample: false, + templateName: integration.name, + integration, + setToast: setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + name: config.displayName, + indexPattern: config.connectionDataSource, + skipRedirect: setIsInstalling ? true : false, + workflows: enabledWorkflows, + }); + if (setIsInstalling) { + setIsInstalling(false, res); + } + if (!res) { + setLoading(false); + } + } else if (config.connectionType === 's3') { + const http = coreRefs.http!; + + const assets: { data: ParsedIntegrationAsset[] } = await http.get( + `${INTEGRATIONS_BASE}/repository/${integration.name}/assets` + ); + for (const query of assets.data.filter( + (a: ParsedIntegrationAsset): a is ParsedIntegrationAsset & { type: 'query' } => + a.type === 'query' + )) { + // Skip any queries that have conditional workflows but aren't enabled + if (query.workflows && !query.workflows.some((w) => config.enabledWorkflows.includes(w))) { + continue; + } + + const queryStr = prepareQuery(query.query, config); + const result = await runQuery( + queryStr, + config.connectionDataSource, + sessionId, + dataSourceMDSId + ); + if (!result.ok) { + setLoading(false); + setCalloutLikeToast('Failed to add integration', 'danger', result.error.message); + return; + } + sessionId = result.value.sessionId ?? sessionId; + } + // Once everything is ready, add the integration to the new datasource as usual + const res = await addIntegrationRequest({ + addSample: false, + templateName: integration.name, + integration, + setToast: setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + name: config.displayName, + indexPattern: `flint_${config.connectionDataSource}_${config.databaseName}_${config.connectionTableName}__*`, + workflows: config.enabledWorkflows, + skipRedirect: setIsInstalling ? true : false, + dataSourceInfo: { + dataSource: config.connectionDataSource, + tableName: makeTableName(config), + }, + }); + if (setIsInstalling) { + setIsInstalling(false, res); + } + if (!res) { + setLoading(false); + } + } else { + console.error('Invalid data source type'); } - } else { - console.error('Invalid data source type'); + } catch (error) { + setCalloutLikeToast('Failed to add integration', 'danger'); + setLoading(false); } }; @@ -237,6 +265,12 @@ const isConfigValid = (config: IntegrationSetupInputs, integration: IntegrationC if (config.displayName.length < 1 || config.connectionDataSource.length < 1) { return false; } + + // Add database name validation + if (config.databaseName && !/^[a-zA-Z0-9_]+$/.test(config.databaseName)) { + return false; + } + if (config.connectionType === 's3') { if (integration.workflows && config.enabledWorkflows.length < 1) { return false; diff --git a/public/components/integrations/components/setup_integration_inputs.tsx b/public/components/integrations/components/setup_integration_inputs.tsx index 0794fc0383..90a173b30a 100644 --- a/public/components/integrations/components/setup_integration_inputs.tsx +++ b/public/components/integrations/components/setup_integration_inputs.tsx @@ -321,6 +321,19 @@ export function IntegrationQueryInputs({ isInvalid={config.connectionTableName.length === 0} /> + {/* Add the new database name field */} + + { + updateConfig({ databaseName: evt.target.value }); + }} + /> + Date: Fri, 7 Mar 2025 11:56:46 -0800 Subject: [PATCH 2/6] Database selector in Integration install Signed-off-by: Aaron Alvarez --- .../components/setup_integration.tsx | 31 +++++++++++++++++++ .../components/setup_integration_inputs.tsx | 17 ++++++++++ 2 files changed, 48 insertions(+) diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx index 974ed36b7a..0fa37c8da0 100644 --- a/public/components/integrations/components/setup_integration.tsx +++ b/public/components/integrations/components/setup_integration.tsx @@ -28,12 +28,16 @@ import { coreRefs } from '../../../framework/core_refs'; import { addIntegrationRequest } from './create_integration_helpers'; import { SetupIntegrationFormInputs } from './setup_integration_inputs'; +/** + * Configuration inputs for integration setup + */ export interface IntegrationSetupInputs { displayName: string; connectionType: string; connectionDataSource: string; connectionLocation: string; checkpointLocation: string; + /** Name of the database to connect to */ databaseName: string; connectionTableName: string; enabledWorkflows: string[]; @@ -109,6 +113,15 @@ const runQuery = async ( } }; +/** + * Constructs a fully qualified table name from the integration configuration. + * + * @param config - The integration setup configuration object + * @param config.connectionDataSource - The data source connection name + * @param config.databaseName - The database name (defaults to 'default' if not provided) + * @param config.connectionTableName - The table name + * @returns A string representing the fully qualified table name in the format: dataSource.database.table + */ const makeTableName = (config: IntegrationSetupInputs): string => { return `${config.connectionDataSource}.${config.databaseName || 'default'}.${ config.connectionTableName @@ -135,6 +148,24 @@ const prepareQuery = (query: string, config: IntegrationSetupInputs): string => return queryStr; }; +/** + * Adds a new integration by setting up necessary database configurations and running required queries. + * + * @param {Object} params - The parameters object + * @param {IntegrationSetupInputs} params.config - Configuration settings for the integration setup + * @param {IntegrationConfig} params.integration - Integration configuration details + * @param {Function} params.setLoading - Callback function to update loading state + * @param {Function} params.setCalloutLikeToast - Callback function to display toast notifications + * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source + * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source + * @param {Function} [params.setIsInstalling] - Optional callback to update installation status + * + * @throws Will throw an error if database creation or query execution fails + * + * The function handles different connection types: + * - For 'index' type: Sets up index-based integration + * - For 's3' type: Creates database, runs asset queries, and sets up S3 integration + */ const addIntegration = async ({ config, integration, diff --git a/public/components/integrations/components/setup_integration_inputs.tsx b/public/components/integrations/components/setup_integration_inputs.tsx index 90a173b30a..f7e546cc0e 100644 --- a/public/components/integrations/components/setup_integration_inputs.tsx +++ b/public/components/integrations/components/setup_integration_inputs.tsx @@ -292,6 +292,23 @@ export function IntegrationConnectionInputs({ ); } +/** + * A component that renders input fields for integration setup configuration. + * + * @component + * @param {Object} props - The component props + * @param {IntegrationSetupInputs} props.config - The current configuration object for the integration setup + * @param {Function} props.updateConfig - Callback function to update the configuration + * @param {IntegrationConfig} props.integration - The integration configuration object + * @returns {JSX.Element} A React fragment containing form input fields + * + * @example + * + */ export function IntegrationQueryInputs({ config, updateConfig, From 76edb96b961553fada82ab4f0b35fe5c72e8c9df Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Fri, 7 Mar 2025 12:13:26 -0800 Subject: [PATCH 3/6] Database selector in Integration install Signed-off-by: Aaron Alvarez --- .../setup_integration_inputs.test.tsx.snap | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/public/components/integrations/components/__tests__/__snapshots__/setup_integration_inputs.test.tsx.snap b/public/components/integrations/components/__tests__/__snapshots__/setup_integration_inputs.test.tsx.snap index 3cf290fc93..8d79800cdf 100644 --- a/public/components/integrations/components/__tests__/__snapshots__/setup_integration_inputs.test.tsx.snap +++ b/public/components/integrations/components/__tests__/__snapshots__/setup_integration_inputs.test.tsx.snap @@ -2120,6 +2120,103 @@ exports[`Integration Setup Inputs Renders the query inputs 1`] = ` + +
+
+ + + +
+
+ + + +
+
+ + + + +
+
+
+
+
+ +
+ Enter the name of the database to store your data. The 'default' database will be used if no input is provided. +
+
+
+
+
Date: Fri, 7 Mar 2025 16:34:06 -0800 Subject: [PATCH 4/6] These changes are in response to PR comments Signed-off-by: Aaron Alvarez --- .../components/setup_integration.tsx | 315 ++++++++++++------ 1 file changed, 205 insertions(+), 110 deletions(-) diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx index 0fa37c8da0..f1a81eaa3f 100644 --- a/public/components/integrations/components/setup_integration.tsx +++ b/public/components/integrations/components/setup_integration.tsx @@ -123,9 +123,7 @@ const runQuery = async ( * @returns A string representing the fully qualified table name in the format: dataSource.database.table */ const makeTableName = (config: IntegrationSetupInputs): string => { - return `${config.connectionDataSource}.${config.databaseName || 'default'}.${ - config.connectionTableName - }`; + return `${config.connectionDataSource}.${config.databaseName}.${config.connectionTableName}`; }; const prepareQuery = (query: string, config: IntegrationSetupInputs): string => { @@ -149,22 +147,19 @@ const prepareQuery = (query: string, config: IntegrationSetupInputs): string => }; /** - * Adds a new integration by setting up necessary database configurations and running required queries. + * Handles the integration setup process based on the connection type. * * @param {Object} params - The parameters object * @param {IntegrationSetupInputs} params.config - Configuration settings for the integration setup * @param {IntegrationConfig} params.integration - Integration configuration details - * @param {Function} params.setLoading - Callback function to update loading state + * @param {Function} params.setLoading - Callback function to set loading state * @param {Function} params.setCalloutLikeToast - Callback function to display toast notifications * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source - * @param {Function} [params.setIsInstalling] - Optional callback to update installation status + * @param {Function} [params.setIsInstalling] - Optional callback to set installation status * - * @throws Will throw an error if database creation or query execution fails - * - * The function handles different connection types: - * - For 'index' type: Sets up index-based integration - * - For 's3' type: Creates database, runs asset queries, and sets up S3 integration + * @throws {Error} Throws an error if the connection type is invalid + * @returns {Promise} A promise that resolves when the integration is added */ const addIntegration = async ({ config, @@ -182,112 +177,211 @@ const addIntegration = async ({ dataSourceMDSId?: string; dataSourceMDSLabel?: string; setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; -}) => { +}): Promise => { setLoading(true); + + if (config.connectionType === 'index') { + await addNativeIntegration({ + config, + integration, + setLoading, + setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + setIsInstalling, + }); + } else if (config.connectionType === 's3') { + await addFlintIntegration({ + config, + integration, + setLoading, + setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + setIsInstalling, + }); + } else { + console.error('Invalid data source type'); + setLoading(false); + } +}; + +/** + * Handles the installation of an integration index by processing the configuration and making the integration request. + * + * @param {Object} params - The parameters object + * @param {IntegrationSetupInputs} params.config - Configuration inputs for the integration setup + * @param {IntegrationConfig} params.integration - Integration configuration object + * @param {Function} params.setLoading - Function to set the loading state + * @param {Function} params.setCalloutLikeToast - Function to display toast notifications + * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source + * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source + * @param {Function} [params.setIsInstalling] - Optional function to set installation status + * + * @returns {Promise} A promise that resolves when the installation is complete + * + * @example + * await addNativeIntegration({ + * config: setupInputs, + * integration: integrationConfig, + * setLoading: (loading) => setLoadingState(loading), + * setCalloutLikeToast: (title, color, text) => showToast(title, color, text) + * }); + */ +const addNativeIntegration = async ({ + config, + integration, + setLoading, + setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + setIsInstalling, +}: { + config: IntegrationSetupInputs; + integration: IntegrationConfig; + setLoading: (loading: boolean) => void; + setCalloutLikeToast: (title: string, color?: Color, text?: string) => void; + dataSourceMDSId?: string; + dataSourceMDSLabel?: string; + setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; +}): Promise => { + let enabledWorkflows: string[] | undefined; + if (integration.workflows) { + enabledWorkflows = integration.workflows + .filter((w) => + w.applicable_data_sources ? w.applicable_data_sources.includes('index') : true + ) + .map((w) => w.name); + } + + const res = await addIntegrationRequest({ + addSample: false, + templateName: integration.name, + integration, + setToast: setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + name: config.displayName, + indexPattern: config.connectionDataSource, + skipRedirect: setIsInstalling ? true : false, + workflows: enabledWorkflows, + }); + + if (setIsInstalling) { + setIsInstalling(false, res); + } + if (!res) { + setLoading(false); + } +}; + +/** + * Handles the installation process for S3 integration by creating a database (if specified), + * processing integration assets, and executing necessary queries. + * + * @param {Object} params - The parameters object + * @param {IntegrationSetupInputs} params.config - Configuration settings for the integration setup + * @param {IntegrationConfig} params.integration - Integration configuration details + * @param {Function} params.setLoading - Callback function to set loading state + * @param {Function} params.setCalloutLikeToast - Callback function to display toast notifications + * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source + * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source + * @param {Function} [params.setIsInstalling] - Optional callback to set installation status + * + * @returns {Promise} A promise that resolves when the installation is complete + * + * @throws Will set error toast if database creation fails or integration addition fails + */ +const addFlintIntegration = async ({ + config, + integration, + setLoading, + setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + setIsInstalling, +}: { + config: IntegrationSetupInputs; + integration: IntegrationConfig; + setLoading: (loading: boolean) => void; + setCalloutLikeToast: (title: string, color?: Color, text?: string) => void; + dataSourceMDSId?: string; + dataSourceMDSLabel?: string; + setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; +}): Promise => { let sessionId: string | undefined; - try { - // First create the database if one is specified - if (config.databaseName) { - const createDbQuery = `CREATE DATABASE IF NOT EXISTS ${config.databaseName}`; - const result = await runQuery( - createDbQuery, - config.connectionDataSource, - sessionId, - dataSourceMDSId - ); - if (!result.ok) { - setLoading(false); - setCalloutLikeToast('Failed to create database', 'danger', result.error.message); - return; - } - sessionId = result.value.sessionId; + // Create database if specified + if (config.databaseName) { + const createDbQuery = `CREATE DATABASE IF NOT EXISTS ${config.databaseName}`; + const result = await runQuery( + createDbQuery, + config.connectionDataSource, + sessionId, + dataSourceMDSId + ); + + if (!result.ok) { + setLoading(false); + setCalloutLikeToast('Failed to create database', 'danger', result.error.message); + return; } + sessionId = result.value.sessionId; + } - if (config.connectionType === 'index') { - let enabledWorkflows: string[] | undefined; - if (integration.workflows) { - enabledWorkflows = integration.workflows - .filter((w) => - w.applicable_data_sources ? w.applicable_data_sources.includes('index') : true - ) - .map((w) => w.name); - } - const res = await addIntegrationRequest({ - addSample: false, - templateName: integration.name, - integration, - setToast: setCalloutLikeToast, - dataSourceMDSId, - dataSourceMDSLabel, - name: config.displayName, - indexPattern: config.connectionDataSource, - skipRedirect: setIsInstalling ? true : false, - workflows: enabledWorkflows, - }); - if (setIsInstalling) { - setIsInstalling(false, res); - } - if (!res) { - setLoading(false); - } - } else if (config.connectionType === 's3') { - const http = coreRefs.http!; + // Process integration assets + const http = coreRefs.http!; + const assets: { data: ParsedIntegrationAsset[] } = await http.get( + `${INTEGRATIONS_BASE}/repository/${integration.name}/assets` + ); - const assets: { data: ParsedIntegrationAsset[] } = await http.get( - `${INTEGRATIONS_BASE}/repository/${integration.name}/assets` - ); - for (const query of assets.data.filter( - (a: ParsedIntegrationAsset): a is ParsedIntegrationAsset & { type: 'query' } => - a.type === 'query' - )) { - // Skip any queries that have conditional workflows but aren't enabled - if (query.workflows && !query.workflows.some((w) => config.enabledWorkflows.includes(w))) { - continue; - } - - const queryStr = prepareQuery(query.query, config); - const result = await runQuery( - queryStr, - config.connectionDataSource, - sessionId, - dataSourceMDSId - ); - if (!result.ok) { - setLoading(false); - setCalloutLikeToast('Failed to add integration', 'danger', result.error.message); - return; - } - sessionId = result.value.sessionId ?? sessionId; - } - // Once everything is ready, add the integration to the new datasource as usual - const res = await addIntegrationRequest({ - addSample: false, - templateName: integration.name, - integration, - setToast: setCalloutLikeToast, - dataSourceMDSId, - dataSourceMDSLabel, - name: config.displayName, - indexPattern: `flint_${config.connectionDataSource}_${config.databaseName}_${config.connectionTableName}__*`, - workflows: config.enabledWorkflows, - skipRedirect: setIsInstalling ? true : false, - dataSourceInfo: { - dataSource: config.connectionDataSource, - tableName: makeTableName(config), - }, - }); - if (setIsInstalling) { - setIsInstalling(false, res); - } - if (!res) { - setLoading(false); - } - } else { - console.error('Invalid data source type'); + // Execute queries + for (const query of assets.data.filter( + (a: ParsedIntegrationAsset): a is ParsedIntegrationAsset & { type: 'query' } => + a.type === 'query' + )) { + if (query.workflows && !query.workflows.some((w) => config.enabledWorkflows.includes(w))) { + continue; + } + + const queryStr = prepareQuery(query.query, config); + const result = await runQuery( + queryStr, + config.connectionDataSource, + sessionId, + dataSourceMDSId + ); + + if (!result.ok) { + setLoading(false); + setCalloutLikeToast('Failed to add integration', 'danger', result.error.message); + return; } - } catch (error) { - setCalloutLikeToast('Failed to add integration', 'danger'); + sessionId = result.value.sessionId ?? sessionId; + } + + // Add integration to the new datasource + const res = await addIntegrationRequest({ + addSample: false, + templateName: integration.name, + integration, + setToast: setCalloutLikeToast, + dataSourceMDSId, + dataSourceMDSLabel, + name: config.displayName, + indexPattern: `flint_${config.connectionDataSource}_${config.databaseName}_${config.connectionTableName}__*`, + workflows: config.enabledWorkflows, + skipRedirect: setIsInstalling ? true : false, + dataSourceInfo: { + dataSource: config.connectionDataSource, + tableName: makeTableName(config), + }, + }); + + if (setIsInstalling) { + setIsInstalling(false, res); + } + if (!res) { setLoading(false); } }; @@ -452,6 +546,7 @@ export function SetupIntegrationForm({ connectionLocation: '', checkpointLocation: '', connectionTableName: integration, + databaseName: 'default', enabledWorkflows: [], }); From d1a5262209f695bc8131ce9af16b7f3985b0229b Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Fri, 7 Mar 2025 15:55:51 -0800 Subject: [PATCH 5/6] Support custom logs correlation (#2375) * support custom logs correlation Signed-off-by: Shenoy Pratik * add support for custom field mappings in logs Signed-off-by: Shenoy Pratik * update explorer fields Signed-off-by: Shenoy Pratik * add support for custom timestamp field Signed-off-by: Shenoy Pratik --------- Signed-off-by: Shenoy Pratik Signed-off-by: Aaron Alvarez --- common/constants/trace_analytics.ts | 12 ++ common/types/trace_analytics.ts | 6 + .../event_analytics/explorer/explorer.tsx | 13 +- .../__tests__/custom_index_flyout.test.tsx | 126 ++++++++++++++++++ .../components/common/custom_index_flyout.tsx | 92 +++++++++++-- .../components/common/helper_functions.tsx | 53 ++++++-- .../components/services/service_view.tsx | 11 +- .../components/traces/span_detail_flyout.tsx | 11 +- public/components/trace_analytics/home.tsx | 14 +- server/plugin_helper/register_settings.ts | 43 +++++- 10 files changed, 340 insertions(+), 41 deletions(-) create mode 100644 public/components/trace_analytics/components/common/__tests__/custom_index_flyout.test.tsx diff --git a/common/constants/trace_analytics.ts b/common/constants/trace_analytics.ts index 85dafdf851..771127208c 100644 --- a/common/constants/trace_analytics.ts +++ b/common/constants/trace_analytics.ts @@ -25,6 +25,18 @@ export const TRACE_ANALYTICS_DSL_ROUTE = '/api/observability/trace_analytics/que export const TRACE_CUSTOM_SPAN_INDEX_SETTING = 'observability:traceAnalyticsSpanIndices'; export const TRACE_CUSTOM_SERVICE_INDEX_SETTING = 'observability:traceAnalyticsServiceIndices'; export const TRACE_CUSTOM_MODE_DEFAULT_SETTING = 'observability:traceAnalyticsCustomModeDefault'; +export const TRACE_CORRELATED_LOGS_INDEX_SETTING = + 'observability:traceAnalyticsCorrelatedLogsIndices'; +export const TRACE_LOGS_FIELD_MAPPNIGS_SETTING = + 'observability:traceAnalyticsCorrelatedLogsFieldMappings'; + +export const DEFAULT_SS4O_LOGS_INDEX = 'ss4o_logs-*'; +export const DEFAULT_CORRELATED_LOGS_FIELD_MAPPINGS = ` +{ + "serviceName": "serviceName", + "spanId": "spanId", + "timestamp": "time" +}`; export enum TRACE_TABLE_TITLES { all_spans = 'All Spans', diff --git a/common/types/trace_analytics.ts b/common/types/trace_analytics.ts index 028794be71..1a6a8aa725 100644 --- a/common/types/trace_analytics.ts +++ b/common/types/trace_analytics.ts @@ -57,3 +57,9 @@ export interface GraphVisEdge { export type TraceAnalyticsMode = 'jaeger' | 'data_prepper' | 'custom_data_prepper'; export type TraceQueryMode = keyof typeof TRACE_TABLE_TITLES; + +export interface CorrelatedLogsFieldMappings { + serviceName: string; + spanId: string; + timestamp: string; +} diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index 29cee4d7ee..bc6b96062c 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -111,8 +111,8 @@ import { import { getVizContainerProps } from '../../visualizations/charts/helpers'; import { TabContext, useFetchEvents, useFetchPatterns, useFetchVisualizations } from '../hooks'; import { - render as updateCountDistribution, selectCountDistribution, + render as updateCountDistribution, } from '../redux/slices/count_distribution_slice'; import { selectFields, updateFields } from '../redux/slices/field_slice'; import { selectQueryResult } from '../redux/slices/query_result_slice'; @@ -122,8 +122,8 @@ import { selectExplorerVisualization } from '../redux/slices/visualization_slice import { change as changeVisualizationConfig, change as changeVizConfig, - change as updateVizConfig, selectVisualizationConfig, + change as updateVizConfig, } from '../redux/slices/viualization_config_slice'; import { getDefaultVisConfig } from '../utils'; import { formatError, getContentTabTitle } from '../utils/utils'; @@ -274,6 +274,7 @@ export const Explorer = ({ datasourceName, datasourceType, queryToRun, + timestampField, startTimeRange, endTimeRange, }: any = historyFromRedirection.location.state; @@ -295,6 +296,14 @@ export const Explorer = ({ }) ); } + if (timestampField) { + dispatch( + changeQuery({ + tabId, + query: { [SELECTED_TIMESTAMP]: timestampField }, + }) + ); + } if (queryToRun) { dispatch( changeQuery({ diff --git a/public/components/trace_analytics/components/common/__tests__/custom_index_flyout.test.tsx b/public/components/trace_analytics/components/common/__tests__/custom_index_flyout.test.tsx new file mode 100644 index 0000000000..d1c140ff1a --- /dev/null +++ b/public/components/trace_analytics/components/common/__tests__/custom_index_flyout.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import { useToast } from '../../../../common/toast'; +import { CustomIndexFlyout } from '../custom_index_flyout'; +import { TraceSettings } from '../helper_functions'; + +// Mock TraceSettings functions +jest.mock('../helper_functions', () => ({ + TraceSettings: { + getCustomSpanIndex: jest.fn(), + getCustomServiceIndex: jest.fn(), + getCorrelatedLogsIndex: jest.fn(), + getCorrelatedLogsFieldMappings: jest.fn(), + getCustomModeSetting: jest.fn(), + setCustomSpanIndex: jest.fn(), + setCustomServiceIndex: jest.fn(), + setCorrelatedLogsIndex: jest.fn(), + setCorrelatedLogsFieldMappings: jest.fn(), + setCustomModeSetting: jest.fn(), + }, +})); + +// Mock the toast hook +jest.mock('../../../../common/toast', () => ({ + useToast: jest.fn(), +})); + +describe('CustomIndexFlyout test', () => { + let setIsFlyoutVisibleMock: jest.Mock; + let setToastMock: jest.Mock; + + beforeEach(() => { + setIsFlyoutVisibleMock = jest.fn(); + setToastMock = jest.fn(); + + (useToast as jest.Mock).mockReturnValue({ setToast: setToastMock }); + + (TraceSettings.getCustomSpanIndex as jest.Mock).mockReturnValue('span-index-1'); + (TraceSettings.getCustomServiceIndex as jest.Mock).mockReturnValue('service-index-1'); + (TraceSettings.getCorrelatedLogsIndex as jest.Mock).mockReturnValue('logs-index-1'); + (TraceSettings.getCorrelatedLogsFieldMappings as jest.Mock).mockReturnValue({ + serviceName: 'service_field', + spanId: 'span_field', + timestamp: 'timestamp_field', + }); + (TraceSettings.getCustomModeSetting as jest.Mock).mockReturnValue(false); + }); + + it('renders flyout when isFlyoutVisible is true', () => { + render( + + ); + + expect(screen.getByText('Manage custom source')).toBeInTheDocument(); + expect(screen.getByLabelText('spanIndices')).toHaveValue('span-index-1'); + expect(screen.getByLabelText('serviceIndices')).toHaveValue('service-index-1'); + expect(screen.getByLabelText('logsIndices')).toHaveValue('logs-index-1'); + expect(screen.getByLabelText('Enable custom source as default mode')).not.toBeChecked(); + }); + + it('updates span indices when input changes', () => { + render( + + ); + + const input = screen.getByLabelText('Custom span indices'); + fireEvent.change(input, { target: { value: 'new-span-index' } }); + + expect(input).toHaveValue('new-span-index'); + }); + + it('calls TraceSettings set functions when save button is clicked', async () => { + render( + + ); + + const saveButton = screen.getByText('Save'); + fireEvent.click(saveButton); + + await waitFor(() => { + expect(TraceSettings.setCustomSpanIndex).toHaveBeenCalledWith('span-index-1'); + expect(TraceSettings.setCustomServiceIndex).toHaveBeenCalledWith('service-index-1'); + expect(TraceSettings.setCorrelatedLogsIndex).toHaveBeenCalledWith('logs-index-1'); + expect(TraceSettings.setCorrelatedLogsFieldMappings).toHaveBeenCalled(); + expect(TraceSettings.setCustomModeSetting).toHaveBeenCalledWith(false); + expect(setToastMock).toHaveBeenCalledWith( + 'Updated trace analytics settings successfully', + 'success' + ); + }); + }); + + it('shows error toast if save settings fail', async () => { + (TraceSettings.setCustomSpanIndex as jest.Mock).mockRejectedValue(new Error('Save error')); + + render( + + ); + + const saveButton = screen.getByText('Save'); + fireEvent.click(saveButton); + + await waitFor(() => { + expect(setToastMock).toHaveBeenCalledWith( + 'Failed to update trace analytics settings', + 'danger' + ); + }); + }); + + it('closes the flyout when Close button is clicked', () => { + render( + + ); + + const closeButton = screen.getByText('Close'); + fireEvent.click(closeButton); + + expect(setIsFlyoutVisibleMock).toHaveBeenCalledWith(false); + }); +}); diff --git a/public/components/trace_analytics/components/common/custom_index_flyout.tsx b/public/components/trace_analytics/components/common/custom_index_flyout.tsx index 926b0b225a..3bfed12415 100644 --- a/public/components/trace_analytics/components/common/custom_index_flyout.tsx +++ b/public/components/trace_analytics/components/common/custom_index_flyout.tsx @@ -21,13 +21,11 @@ import { EuiTitle, } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; -import { - TRACE_CUSTOM_SERVICE_INDEX_SETTING, - TRACE_CUSTOM_SPAN_INDEX_SETTING, - TRACE_CUSTOM_MODE_DEFAULT_SETTING, -} from '../../../../../common/constants/trace_analytics'; +import { DEFAULT_CORRELATED_LOGS_FIELD_MAPPINGS } from '../../../../../common/constants/trace_analytics'; +import { CorrelatedLogsFieldMappings } from '../../../../../common/types/trace_analytics'; import { uiSettingsService } from '../../../../../common/utils'; import { useToast } from '../../../common/toast'; +import { TraceSettings } from './helper_functions'; interface CustomIndexFlyoutProps { isFlyoutVisible: boolean; @@ -42,6 +40,10 @@ export const CustomIndexFlyout = ({ const [spanIndices, setSpanIndices] = useState(''); const [serviceIndices, setServiceIndices] = useState(''); const [customModeDefault, setCustomModeDefault] = useState(false); + const [correlatedLogsIndices, setCorrelatedLogsIndices] = useState(''); + const [correlatedLogsFieldMappings, setCorrelatedLogsFieldMappings] = useState( + {} as CorrelatedLogsFieldMappings + ); const [isLoading, setIsLoading] = useState(false); const onChangeSpanIndices = (e: { target: { value: React.SetStateAction } }) => { @@ -52,22 +54,41 @@ export const CustomIndexFlyout = ({ setServiceIndices(e.target.value); }; + const onChangeCorrelatedLogsIndices = (e: { + target: { value: React.SetStateAction }; + }) => { + setCorrelatedLogsIndices(e.target.value); + }; + + const onChangeLogsFieldMappings = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + setCorrelatedLogsFieldMappings((prevMappings) => ({ + ...prevMappings, + [name]: value, + })); + }; + const onToggleCustomModeDefault = (e: { target: { checked: boolean } }) => { setCustomModeDefault(e.target.checked); }; useEffect(() => { - setSpanIndices(uiSettingsService.get(TRACE_CUSTOM_SPAN_INDEX_SETTING)); - setServiceIndices(uiSettingsService.get(TRACE_CUSTOM_SERVICE_INDEX_SETTING)); - setCustomModeDefault(uiSettingsService.get(TRACE_CUSTOM_MODE_DEFAULT_SETTING) || false); + setSpanIndices(TraceSettings.getCustomSpanIndex()); + setServiceIndices(TraceSettings.getCustomServiceIndex()); + setCorrelatedLogsIndices(TraceSettings.getCorrelatedLogsIndex()); + setCorrelatedLogsFieldMappings(TraceSettings.getCorrelatedLogsFieldMappings()); + setCustomModeDefault(TraceSettings.getCustomModeSetting()); }, [uiSettingsService]); const onSaveSettings = async () => { try { setIsLoading(true); - await uiSettingsService.set(TRACE_CUSTOM_SPAN_INDEX_SETTING, spanIndices); - await uiSettingsService.set(TRACE_CUSTOM_SERVICE_INDEX_SETTING, serviceIndices); - await uiSettingsService.set(TRACE_CUSTOM_MODE_DEFAULT_SETTING, customModeDefault); + await TraceSettings.setCustomSpanIndex(spanIndices); + await TraceSettings.setCustomServiceIndex(serviceIndices); + await TraceSettings.setCorrelatedLogsIndex(correlatedLogsIndices); + await TraceSettings.setCorrelatedLogsFieldMappings(correlatedLogsFieldMappings); + await TraceSettings.setCustomModeSetting(customModeDefault); setIsLoading(false); setToast('Updated trace analytics settings successfully', 'success'); } catch (error) { @@ -77,6 +98,25 @@ export const CustomIndexFlyout = ({ setIsLoading(false); }; + const correlatedFieldsForm = () => { + const correlatedFieldsFromSettings: CorrelatedLogsFieldMappings = TraceSettings.getCorrelatedLogsFieldMappings(); + return Object.keys(correlatedFieldsFromSettings).map((key) => { + return ( + <> + + + + + ); + }); + }; + const callout = ( + Correlated logs indices} + description={ + + Configure custom logs indices to be used by the trace analytics plugin to correlate + spans and services + + } + > + + + + + Correlated logs fields} + description={ + + Configure correlated logs fields, to be used by the trace analytics plugin for + correlate spans and services to logs + + } + > + {correlatedFieldsForm()} + Set default mode} description={ diff --git a/public/components/trace_analytics/components/common/helper_functions.tsx b/public/components/trace_analytics/components/common/helper_functions.tsx index 4a3f2852cb..1ffac8f090 100644 --- a/public/components/trace_analytics/components/common/helper_functions.tsx +++ b/public/components/trace_analytics/components/common/helper_functions.tsx @@ -12,13 +12,18 @@ import React from 'react'; import { DATA_PREPPER_INDEX_NAME, DATA_PREPPER_SERVICE_INDEX_NAME, + DEFAULT_CORRELATED_LOGS_FIELD_MAPPINGS, JAEGER_INDEX_NAME, JAEGER_SERVICE_INDEX_NAME, TRACE_ANALYTICS_DOCUMENTATION_LINK, + TRACE_CORRELATED_LOGS_INDEX_SETTING, + TRACE_CUSTOM_MODE_DEFAULT_SETTING, TRACE_CUSTOM_SERVICE_INDEX_SETTING, TRACE_CUSTOM_SPAN_INDEX_SETTING, + TRACE_LOGS_FIELD_MAPPNIGS_SETTING, } from '../../../../../common/constants/trace_analytics'; import { + CorrelatedLogsFieldMappings, GraphVisEdge, GraphVisNode, TraceAnalyticsMode, @@ -26,9 +31,9 @@ import { import { uiSettingsService } from '../../../../../common/utils'; import { FieldCapResponse } from '../../../common/types'; import { serviceMapColorPalette } from './color_palette'; +import { NANOS_TO_MS, ParsedHit } from './constants'; import { FilterType } from './filters/filters'; import { ServiceObject } from './plots/service_map'; -import { NANOS_TO_MS, ParsedHit } from './constants'; const missingJaegerTracesConfigurationMessage = `The indices required for trace analytics (${JAEGER_INDEX_NAME} and ${JAEGER_SERVICE_INDEX_NAME}) do not exist or you do not have permission to access them.`; @@ -559,26 +564,46 @@ export const getAttributeFieldNames = (response: FieldCapResponse): string[] => ); }; -export const getTraceCustomSpanIndex = () => { - return uiSettingsService.get(TRACE_CUSTOM_SPAN_INDEX_SETTING); -}; +export const TraceSettings = { + getCustomSpanIndex: () => uiSettingsService.get(TRACE_CUSTOM_SPAN_INDEX_SETTING), -export const getTraceCustomServiceIndex = () => { - return uiSettingsService.get(TRACE_CUSTOM_SERVICE_INDEX_SETTING); -}; + getCustomServiceIndex: () => uiSettingsService.get(TRACE_CUSTOM_SERVICE_INDEX_SETTING), -export const setTraceCustomSpanIndex = (value: string) => { - return uiSettingsService.set(TRACE_CUSTOM_SPAN_INDEX_SETTING, value); -}; + getCorrelatedLogsIndex: () => uiSettingsService.get(TRACE_CORRELATED_LOGS_INDEX_SETTING), + + getCorrelatedLogsFieldMappings: (): CorrelatedLogsFieldMappings => { + const defaultMappings = JSON.parse(DEFAULT_CORRELATED_LOGS_FIELD_MAPPINGS); + try { + const storedValue = uiSettingsService.get(TRACE_LOGS_FIELD_MAPPNIGS_SETTING); + return storedValue ? JSON.parse(storedValue) : defaultMappings; + } catch (error) { + console.error('Error parsing TRACE_LOGS_FIELD_MAPPNIGS_SETTING:', error); + return defaultMappings; + } + }, + + getCustomModeSetting: () => uiSettingsService.get(TRACE_CUSTOM_MODE_DEFAULT_SETTING) || false, + + setCustomSpanIndex: (value: string) => + uiSettingsService.set(TRACE_CUSTOM_SPAN_INDEX_SETTING, value), + + setCustomServiceIndex: (value: string) => + uiSettingsService.set(TRACE_CUSTOM_SERVICE_INDEX_SETTING, value), + + setCorrelatedLogsIndex: (value: string) => + uiSettingsService.set(TRACE_CORRELATED_LOGS_INDEX_SETTING, value), + + setCorrelatedLogsFieldMappings: (value: CorrelatedLogsFieldMappings) => + uiSettingsService.set(TRACE_LOGS_FIELD_MAPPNIGS_SETTING, JSON.stringify(value)), -export const setTraceCustomServiceIndex = (value: string) => { - return uiSettingsService.set(TRACE_CUSTOM_SERVICE_INDEX_SETTING, value); + setCustomModeSetting: (value: boolean) => + uiSettingsService.set(TRACE_CUSTOM_MODE_DEFAULT_SETTING, value), }; export const getSpanIndices = (mode: TraceAnalyticsMode) => { switch (mode) { case 'custom_data_prepper': - return getTraceCustomSpanIndex(); + return TraceSettings.getCustomSpanIndex(); case 'data_prepper': return DATA_PREPPER_INDEX_NAME; case 'jaeger': @@ -590,7 +615,7 @@ export const getSpanIndices = (mode: TraceAnalyticsMode) => { export const getServiceIndices = (mode: TraceAnalyticsMode) => { switch (mode) { case 'custom_data_prepper': - return getTraceCustomServiceIndex(); + return TraceSettings.getCustomServiceIndex(); case 'data_prepper': return DATA_PREPPER_SERVICE_INDEX_NAME; case 'jaeger': diff --git a/public/components/trace_analytics/components/services/service_view.tsx b/public/components/trace_analytics/components/services/service_view.tsx index c4fae341c1..c482b55962 100644 --- a/public/components/trace_analytics/components/services/service_view.tsx +++ b/public/components/trace_analytics/components/services/service_view.tsx @@ -49,6 +49,7 @@ import { TraceFilter } from '../common/constants'; import { FilterType } from '../common/filters/filters'; import { PanelTitle, + TraceSettings, filtersToDsl, generateServiceUrl, processTimeStamp, @@ -201,6 +202,11 @@ export function ServiceView(props: ServiceViewProps) { name: 'View logs', 'data-test-subj': 'viewLogsButton', onClick: () => { + const correlatedLogsIndex = TraceSettings.getCorrelatedLogsIndex(); + const correlatedServiceNameField = TraceSettings.getCorrelatedLogsFieldMappings() + .serviceName; + const correlatedTimestampField = TraceSettings.getCorrelatedLogsFieldMappings() + .timestamp; // NOTE: Discover has issue with PPL Time filter, hence adding +3/-3 days to actual timestamp const startTime = dateMath @@ -218,7 +224,7 @@ export function ServiceView(props: ServiceViewProps) { props.dataSourceMDSId[0].id ?? '' }',title:'${props.dataSourceMDSId[0].label}',type:DATA_SOURCE),id:'${ props.dataSourceMDSId[0].id ?? '' - }::ss4o_logs-*',timeFieldName:'time',title:'ss4o_logs-*',type:INDEXES),language:PPL,query:'source%20%3D%20ss4o_logs-%2A%20%7C%20where%20serviceName%20%3D%20%22${ + }::${correlatedLogsIndex}',timeFieldName:'${correlatedTimestampField}',title:'${correlatedLogsIndex}',type:INDEXES),language:PPL,query:'source%20%3D%20${correlatedLogsIndex}%20%7C%20where%20${correlatedServiceNameField}%20%3D%20%22${ props.serviceName }%22'))`, }); @@ -228,7 +234,8 @@ export function ServiceView(props: ServiceViewProps) { state: { DEFAULT_DATA_SOURCE_NAME, DEFAULT_DATA_SOURCE_TYPE, - queryToRun: `source = ss4o_logs-* | where serviceName='${props.serviceName}'`, + queryToRun: `source = ${correlatedLogsIndex} | where ${correlatedServiceNameField}='${props.serviceName}'`, + timestampField: correlatedTimestampField, startTimeRange: props.startTime, endTimeRange: props.endTime, }, diff --git a/public/components/trace_analytics/components/traces/span_detail_flyout.tsx b/public/components/trace_analytics/components/traces/span_detail_flyout.tsx index 38d1d8437c..8edfa1c09f 100644 --- a/public/components/trace_analytics/components/traces/span_detail_flyout.tsx +++ b/public/components/trace_analytics/components/traces/span_detail_flyout.tsx @@ -33,7 +33,7 @@ import { TRACE_ANALYTICS_DATE_FORMAT } from '../../../../../common/constants/tra import { SpanField, TraceAnalyticsMode } from '../../../../../common/types/trace_analytics'; import { coreRefs } from '../../../../framework/core_refs'; import { handleSpansFlyoutRequest } from '../../requests/traces_request_handler'; -import { microToMilliSec, nanoToMilliSec } from '../common/helper_functions'; +import { microToMilliSec, nanoToMilliSec, TraceSettings } from '../common/helper_functions'; import { FlyoutListItem } from './flyout_list_item'; const MODE_TO_FIELDS: Record> = { @@ -324,13 +324,15 @@ export function SpanDetailFlyout(props: { }; const redirectToExplorer = () => { + const correlatedLogsIndex = TraceSettings.getCorrelatedLogsIndex(); + const correlatedSpanField = TraceSettings.getCorrelatedLogsFieldMappings().spanId; + const correlatedTimestampField = TraceSettings.getCorrelatedLogsFieldMappings().timestamp; // NOTE: Discover has issue with PPL Time filter, hence adding +3/-3 days to actual timestamp const startTime = moment(span.startTime).subtract(3, 'days').format(TRACE_ANALYTICS_DATE_FORMAT) ?? 'now-3y'; const endTime = moment(span.endTime).add(3, 'days').format(TRACE_ANALYTICS_DATE_FORMAT) ?? 'now'; const spanId = getSpanValue(span, mode, 'SPAN_ID'); - const spanField = getSpanFieldKey(mode, 'SPAN_ID'); if (coreRefs?.dataSource?.dataSourceEnabled) { coreRefs?.application!.navigateToApp('data-explorer', { @@ -338,7 +340,7 @@ export function SpanDetailFlyout(props: { props.dataSourceMDSId ?? '' }',title:${props.dataSourceMDSLabel},type:DATA_SOURCE),id:'${ props.dataSourceMDSId ?? '' - }::ss4o_logs-*',timeFieldName:'time',title:'ss4o_logs-*',type:INDEXES),language:PPL,query:'source%20%3D%20ss4o_logs-*%20%7C%20where%20${spanField}%20%3D%20!'${spanId}!''))`, + }::${correlatedLogsIndex}',timeFieldName:'${correlatedTimestampField}',title:'${correlatedLogsIndex}',type:INDEXES),language:PPL,query:'source%20%3D%20${correlatedLogsIndex}%20%7C%20where%20${correlatedSpanField}%20%3D%20!'${spanId}!''))`, }); } else { coreRefs?.application!.navigateToApp(observabilityLogsID, { @@ -346,7 +348,8 @@ export function SpanDetailFlyout(props: { state: { DEFAULT_DATA_SOURCE_NAME, DEFAULT_DATA_SOURCE_TYPE, - queryToRun: `source = ss4o_logs-* | where ${spanField}='${spanId}'`, + queryToRun: `source = ${correlatedLogsIndex} | where ${correlatedSpanField}='${spanId}'`, + timestampField: correlatedTimestampField, startTimeRange: startTime, endTimeRange: endTime, }, diff --git a/public/components/trace_analytics/home.tsx b/public/components/trace_analytics/home.tsx index 3ddf1268bb..0841082053 100644 --- a/public/components/trace_analytics/home.tsx +++ b/public/components/trace_analytics/home.tsx @@ -23,14 +23,15 @@ import { } from '../../../../../src/plugins/data_source_management/public'; import { DataSourceAttributes } from '../../../../../src/plugins/data_source_management/public/types'; import { observabilityTracesNewNavID } from '../../../common/constants/shared'; -import { - TRACE_CUSTOM_MODE_DEFAULT_SETTING, - TRACE_TABLE_TYPE_KEY, -} from '../../../common/constants/trace_analytics'; +import { TRACE_TABLE_TYPE_KEY } from '../../../common/constants/trace_analytics'; import { TraceAnalyticsMode, TraceQueryMode } from '../../../common/types/trace_analytics'; import { coreRefs } from '../../framework/core_refs'; import { FilterType } from './components/common/filters/filters'; -import { getAttributeFieldNames, getSpanIndices } from './components/common/helper_functions'; +import { + TraceSettings, + getAttributeFieldNames, + getSpanIndices, +} from './components/common/helper_functions'; import { SearchBarProps } from './components/common/search_bar'; import { ServiceView, Services } from './components/services'; import { ServiceFlyout } from './components/services/service_flyout'; @@ -41,7 +42,6 @@ import { handleJaegerIndicesExistRequest, } from './requests/request_handler'; import { TraceSideBar } from './trace_side_nav'; -import { uiSettingsService } from '../../../common/utils'; const newNavigation = coreRefs.chrome?.navGroup.getNavGroupEnabled(); @@ -278,7 +278,7 @@ export const Home = (props: HomeProps) => { urlParts.length > 1 ? new URLSearchParams(urlParts[1].split('#')[0]) : new URLSearchParams(); const urlMode = queryParams.get('mode'); - const isCustomModeEnabled = uiSettingsService.get(TRACE_CUSTOM_MODE_DEFAULT_SETTING) || false; + const isCustomModeEnabled = TraceSettings.getCustomModeSetting(); if (!urlMode && isCustomModeEnabled) { const newUrl = updateUrlWithMode('custom_data_prepper'); diff --git a/server/plugin_helper/register_settings.ts b/server/plugin_helper/register_settings.ts index d07a6f300e..09c5e52967 100644 --- a/server/plugin_helper/register_settings.ts +++ b/server/plugin_helper/register_settings.ts @@ -3,13 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { i18n } from '@osd/i18n'; import { schema } from '@osd/config-schema'; +import { i18n } from '@osd/i18n'; import { UiSettingsServiceSetup } from '../../../../src/core/server/ui_settings'; import { + DEFAULT_CORRELATED_LOGS_FIELD_MAPPINGS, + DEFAULT_SS4O_LOGS_INDEX, + TRACE_CORRELATED_LOGS_INDEX_SETTING, TRACE_CUSTOM_MODE_DEFAULT_SETTING, TRACE_CUSTOM_SERVICE_INDEX_SETTING, TRACE_CUSTOM_SPAN_INDEX_SETTING, + TRACE_LOGS_FIELD_MAPPNIGS_SETTING, } from '../../common/constants/trace_analytics'; export const registerObservabilityUISettings = (uiSettings: UiSettingsServiceSetup) => { @@ -57,4 +61,41 @@ export const registerObservabilityUISettings = (uiSettings: UiSettingsServiceSet schema: schema.boolean(), }, }); + + uiSettings.register({ + [TRACE_CORRELATED_LOGS_INDEX_SETTING]: { + name: i18n.translate('observability.traceAnalyticsCorrelatedLogsIndices.name', { + defaultMessage: 'Trace analytics correlated logs indices', + }), + value: DEFAULT_SS4O_LOGS_INDEX, + category: ['Observability'], + description: i18n.translate('observability.traceAnalyticsCorrelatedLogsIndices.description', { + defaultMessage: + 'Experimental feature: Configure correlated logs indices, to be used by the trace analytics plugin to correlate spans and services to logs', + }), + schema: schema.string(), + }, + }); + + uiSettings.register({ + [TRACE_LOGS_FIELD_MAPPNIGS_SETTING]: { + name: i18n.translate('observability.traceAnalyticsCorrelatedLogsFieldMappings.name', { + defaultMessage: 'Trace analytics correlated logs fields', + }), + value: DEFAULT_CORRELATED_LOGS_FIELD_MAPPINGS, + category: ['Observability'], + description: i18n.translate( + 'observability.traceAnalyticsCorrelatedLogsFieldMappings.description', + { + defaultMessage: + 'Experimental feature: Configure correlated logs fields, to be used by the trace analytics plugin for correlate spans and services to logs', + } + ), + schema: schema.object({ + serviceName: schema.string(), + spanId: schema.string(), + timestamp: schema.string(), + }), + }, + }); }; From c131f9222328618a4bd686e75dad46cebf26c40e Mon Sep 17 00:00:00 2001 From: Aaron Alvarez Date: Mon, 10 Mar 2025 13:02:10 -0700 Subject: [PATCH 6/6] These changes are in response to latest PR comments and suggestions made by Simeon Signed-off-by: Aaron Alvarez --- .../setup_integration.test.tsx.snap | 4 + .../components/setup_integration.tsx | 161 ++++++++++-------- 2 files changed, 90 insertions(+), 75 deletions(-) diff --git a/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap index 6f9ec2e2d8..6182051390 100644 --- a/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap +++ b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap @@ -38,6 +38,7 @@ exports[`Integration Setup Page Renders integration setup page as expected 1`] = "connectionLocation": "", "connectionTableName": "sample", "connectionType": "index", + "databaseName": "default", "displayName": "sample Integration", "enabledWorkflows": Array [], } @@ -114,6 +115,7 @@ exports[`Integration Setup Page Renders integration setup page as expected 1`] = "connectionLocation": "", "connectionTableName": "sample", "connectionType": "index", + "databaseName": "default", "displayName": "sample Integration", "enabledWorkflows": Array [], } @@ -262,6 +264,7 @@ exports[`Integration Setup Page Renders integration setup page as expected 1`] = "connectionLocation": "", "connectionTableName": "sample", "connectionType": "index", + "databaseName": "default", "displayName": "sample Integration", "enabledWorkflows": Array [], } @@ -943,6 +946,7 @@ exports[`Integration Setup Page Renders integration setup page as expected 1`] = "connectionLocation": "", "connectionTableName": "sample", "connectionType": "index", + "databaseName": "default", "displayName": "sample Integration", "enabledWorkflows": Array [], } diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx index f1a81eaa3f..6c0ad3b4e8 100644 --- a/public/components/integrations/components/setup_integration.tsx +++ b/public/components/integrations/components/setup_integration.tsx @@ -56,6 +56,32 @@ export interface IntegrationConfigProps { handleSelectedDataSourceChange: (dataSourceMDSId?: string, dataSourceMDSLabel?: string) => void; } +/** + * Interface for the parameters used in the addIntegration function + */ +interface AddIntegrationParams { + /** Configuration settings for the integration setup */ + config: IntegrationSetupInputs; + + /** Integration configuration details */ + integration: IntegrationConfig; + + /** Callback function to set loading state */ + setLoading: (loading: boolean) => void; + + /** Callback function to display toast notifications */ + setCalloutLikeToast: (title: string, color?: Color, text?: string) => void; + + /** Optional MDS ID for the data source */ + dataSourceMDSId?: string; + + /** Optional MDS label for the data source */ + dataSourceMDSLabel?: string; + + /** Optional callback to set installation status */ + setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; +} + type SetupCallout = { show: true; title: string; color?: Color; text?: string } | { show: false }; const sqlService = new SQLService(coreRefs.http!); @@ -149,15 +175,6 @@ const prepareQuery = (query: string, config: IntegrationSetupInputs): string => /** * Handles the integration setup process based on the connection type. * - * @param {Object} params - The parameters object - * @param {IntegrationSetupInputs} params.config - Configuration settings for the integration setup - * @param {IntegrationConfig} params.integration - Integration configuration details - * @param {Function} params.setLoading - Callback function to set loading state - * @param {Function} params.setCalloutLikeToast - Callback function to display toast notifications - * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source - * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source - * @param {Function} [params.setIsInstalling] - Optional callback to set installation status - * * @throws {Error} Throws an error if the connection type is invalid * @returns {Promise} A promise that resolves when the integration is added */ @@ -169,15 +186,7 @@ const addIntegration = async ({ dataSourceMDSId, dataSourceMDSLabel, setIsInstalling, -}: { - config: IntegrationSetupInputs; - integration: IntegrationConfig; - setLoading: (loading: boolean) => void; - setCalloutLikeToast: (title: string, color?: Color, text?: string) => void; - dataSourceMDSId?: string; - dataSourceMDSLabel?: string; - setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; -}): Promise => { +}: AddIntegrationParams): Promise => { setLoading(true); if (config.connectionType === 'index') { @@ -209,24 +218,8 @@ const addIntegration = async ({ /** * Handles the installation of an integration index by processing the configuration and making the integration request. * - * @param {Object} params - The parameters object - * @param {IntegrationSetupInputs} params.config - Configuration inputs for the integration setup - * @param {IntegrationConfig} params.integration - Integration configuration object - * @param {Function} params.setLoading - Function to set the loading state - * @param {Function} params.setCalloutLikeToast - Function to display toast notifications - * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source - * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source - * @param {Function} [params.setIsInstalling] - Optional function to set installation status - * * @returns {Promise} A promise that resolves when the installation is complete * - * @example - * await addNativeIntegration({ - * config: setupInputs, - * integration: integrationConfig, - * setLoading: (loading) => setLoadingState(loading), - * setCalloutLikeToast: (title, color, text) => showToast(title, color, text) - * }); */ const addNativeIntegration = async ({ config, @@ -236,15 +229,7 @@ const addNativeIntegration = async ({ dataSourceMDSId, dataSourceMDSLabel, setIsInstalling, -}: { - config: IntegrationSetupInputs; - integration: IntegrationConfig; - setLoading: (loading: boolean) => void; - setCalloutLikeToast: (title: string, color?: Color, text?: string) => void; - dataSourceMDSId?: string; - dataSourceMDSLabel?: string; - setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; -}): Promise => { +}: AddIntegrationParams): Promise => { let enabledWorkflows: string[] | undefined; if (integration.workflows) { enabledWorkflows = integration.workflows @@ -279,15 +264,6 @@ const addNativeIntegration = async ({ * Handles the installation process for S3 integration by creating a database (if specified), * processing integration assets, and executing necessary queries. * - * @param {Object} params - The parameters object - * @param {IntegrationSetupInputs} params.config - Configuration settings for the integration setup - * @param {IntegrationConfig} params.integration - Integration configuration details - * @param {Function} params.setLoading - Callback function to set loading state - * @param {Function} params.setCalloutLikeToast - Callback function to display toast notifications - * @param {string} [params.dataSourceMDSId] - Optional MDS ID for the data source - * @param {string} [params.dataSourceMDSLabel] - Optional MDS label for the data source - * @param {Function} [params.setIsInstalling] - Optional callback to set installation status - * * @returns {Promise} A promise that resolves when the installation is complete * * @throws Will set error toast if database creation fails or integration addition fails @@ -300,34 +276,22 @@ const addFlintIntegration = async ({ dataSourceMDSId, dataSourceMDSLabel, setIsInstalling, -}: { - config: IntegrationSetupInputs; - integration: IntegrationConfig; - setLoading: (loading: boolean) => void; - setCalloutLikeToast: (title: string, color?: Color, text?: string) => void; - dataSourceMDSId?: string; - dataSourceMDSLabel?: string; - setIsInstalling?: (isInstalling: boolean, success?: boolean) => void; -}): Promise => { +}: AddIntegrationParams): Promise => { let sessionId: string | undefined; // Create database if specified - if (config.databaseName) { - const createDbQuery = `CREATE DATABASE IF NOT EXISTS ${config.databaseName}`; - const result = await runQuery( - createDbQuery, - config.connectionDataSource, - sessionId, - dataSourceMDSId - ); + const dbResult = await createDatabase( + config, + sessionId, + dataSourceMDSId, + setLoading, + setCalloutLikeToast + ); - if (!result.ok) { - setLoading(false); - setCalloutLikeToast('Failed to create database', 'danger', result.error.message); - return; - } - sessionId = result.value.sessionId; + if (!dbResult.success) { + return; } + sessionId = dbResult.sessionId; // Process integration assets const http = coreRefs.http!; @@ -386,6 +350,53 @@ const addFlintIntegration = async ({ } }; +/** + * Creates a database if it doesn't already exist using the provided configuration. + * + * @param config - Configuration object containing database details + * @param config.databaseName - Name of the database to create + * @param config.connectionDataSource - Data source connection string + * @param sessionId - Current session identifier + * @param dataSourceMDSId - Data source MDS identifier + * @param setLoading - Callback function to update loading state + * @param setCalloutLikeToast - Callback function to display toast notifications + * @param setCalloutLikeToast.message - Message to display in the toast + * @param setCalloutLikeToast.type - Type of toast notification (e.g., 'danger') + * @param setCalloutLikeToast.details - Optional details for the toast message + * + * @returns Promise resolving to an object containing: + * - success: boolean indicating if the operation was successful + * - sessionId: the current or updated session identifier + * + */ +const createDatabase = async ( + config: { databaseName: string; connectionDataSource: string }, + sessionId: string, + dataSourceMDSId: string, + setLoading: (loading: boolean) => void, + setCalloutLikeToast: (message: string, type: string, details?: string) => void +): Promise<{ success: boolean; sessionId: string }> => { + if (!config.databaseName) { + return { success: true, sessionId }; + } + + const createDbQuery = `CREATE DATABASE IF NOT EXISTS ${config.databaseName}`; + const result = await runQuery( + createDbQuery, + config.connectionDataSource, + sessionId, + dataSourceMDSId + ); + + if (!result.ok) { + setLoading(false); + setCalloutLikeToast('Failed to create database', 'danger', result.error.message); + return { success: false, sessionId }; + } + + return { success: true, sessionId: result.value.sessionId }; +}; + const isConfigValid = (config: IntegrationSetupInputs, integration: IntegrationConfig): boolean => { if (config.displayName.length < 1 || config.connectionDataSource.length < 1) { return false;