diff --git a/src/plugins/share/common/url_service/locators/locator.test.ts b/src/plugins/share/common/url_service/locators/locator.test.ts
index 18bd21c0ed4fd..648695f71f008 100644
--- a/src/plugins/share/common/url_service/locators/locator.test.ts
+++ b/src/plugins/share/common/url_service/locators/locator.test.ts
@@ -13,8 +13,9 @@ import { KibanaLocation } from '../../../public';
import { LocatorGetUrlParams } from '.';
import { decompressFromBase64 } from 'lz-string';
-const setup = () => {
- const baseUrl = 'http://localhost:5601';
+const setup = (
+ { baseUrl = 'http://localhost:5601' }: { baseUrl: string } = { baseUrl: 'http://localhost:5601' }
+) => {
const version = '1.2.3';
const deps: LocatorDependencies = {
baseUrl,
@@ -88,6 +89,48 @@ describe('Locator', () => {
baz: 'b',
});
});
+
+ test('returns URL of the redirect endpoint with custom spaceid', async () => {
+ const { locator } = setup();
+ const url = await locator.getRedirectUrl(
+ { foo: 'a', baz: 'b' },
+ { spaceId: 'custom-space-id' }
+ );
+
+ expect(url).toBe(
+ 'http://localhost:5601/s/custom-space-id/app/r?l=TEST_LOCATOR&v=1.2.3&lz=N4IgZg9hIFwghiANCARvAXrNIC%2BQ'
+ );
+ });
+
+ test('returns URL of the redirect endpoint with replaced spaceid', async () => {
+ const { locator } = setup({ baseUrl: 'http://localhost:5601/s/space-id' });
+ const url = await locator.getRedirectUrl(
+ { foo: 'a', baz: 'b' },
+ { spaceId: 'custom-space-id' }
+ );
+
+ expect(url).toBe(
+ 'http://localhost:5601/s/custom-space-id/app/r?l=TEST_LOCATOR&v=1.2.3&lz=N4IgZg9hIFwghiANCARvAXrNIC%2BQ'
+ );
+ });
+
+ test('returns URL of the redirect endpoint without spaceid', async () => {
+ const { locator } = setup({ baseUrl: 'http://localhost:5601/s/space-id' });
+ const url = await locator.getRedirectUrl({ foo: 'a', baz: 'b' }, { spaceId: 'default' });
+
+ expect(url).toBe(
+ 'http://localhost:5601/app/r?l=TEST_LOCATOR&v=1.2.3&lz=N4IgZg9hIFwghiANCARvAXrNIC%2BQ'
+ );
+ });
+
+ test('returns URL of the redirect endpoint with untouched spaceId', async () => {
+ const { locator } = setup({ baseUrl: 'http://localhost:5601/s/space-id' });
+ const url = await locator.getRedirectUrl({ foo: 'a', baz: 'b' });
+
+ expect(url).toBe(
+ 'http://localhost:5601/s/space-id/app/r?l=TEST_LOCATOR&v=1.2.3&lz=N4IgZg9hIFwghiANCARvAXrNIC%2BQ'
+ );
+ });
});
describe('.navigate()', () => {
diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts
index ba532449463f3..d479eac25c266 100644
--- a/src/plugins/share/common/url_service/locators/locator.ts
+++ b/src/plugins/share/common/url_service/locators/locator.ts
@@ -19,7 +19,13 @@ import type {
LocatorNavigationParams,
LocatorGetUrlParams,
} from './types';
-import { formatSearchParams, FormatSearchParamsOptions, RedirectOptions } from './redirect';
+import {
+ formatSearchParams,
+ FormatSearchParamsOptions,
+ RedirectOptions,
+ GetRedirectUrlOptions,
+ addSpaceIdToPath,
+} from './redirect';
export interface LocatorDependencies {
/**
@@ -92,7 +98,7 @@ export class Locator
implements LocatorPublic
{
return url;
}
- public getRedirectUrl(params: P, options: FormatSearchParamsOptions = {}): string {
+ public getRedirectUrl(params: P, options: GetRedirectUrlOptions = {}): string {
const { baseUrl = '', version = '0.0.0' } = this.deps;
const redirectOptions: RedirectOptions = {
id: this.definition.id,
@@ -100,12 +106,16 @@ export class Locator
implements LocatorPublic
{
params,
};
const formatOptions: FormatSearchParamsOptions = {
- ...options,
lzCompress: options.lzCompress ?? true,
};
const search = formatSearchParams(redirectOptions, formatOptions).toString();
+ const path = '/app/r?' + search;
- return baseUrl + '/app/r?' + search;
+ if (options.spaceId) {
+ return addSpaceIdToPath(baseUrl, options.spaceId, path);
+ } else {
+ return baseUrl + path;
+ }
}
public async navigate(
diff --git a/src/plugins/share/common/url_service/locators/redirect/index.ts b/src/plugins/share/common/url_service/locators/redirect/index.ts
index 9e2a9d8f433e9..7e37c2b8b6d23 100644
--- a/src/plugins/share/common/url_service/locators/redirect/index.ts
+++ b/src/plugins/share/common/url_service/locators/redirect/index.ts
@@ -10,3 +10,4 @@
export * from './types';
export * from './format_search_params';
export * from './parse_search_params';
+export * from './space_url_parser';
diff --git a/src/plugins/share/common/url_service/locators/redirect/space_url_parser.test.ts b/src/plugins/share/common/url_service/locators/redirect/space_url_parser.test.ts
new file mode 100644
index 0000000000000..d6dc64c63b0f4
--- /dev/null
+++ b/src/plugins/share/common/url_service/locators/redirect/space_url_parser.test.ts
@@ -0,0 +1,47 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { addSpaceIdToPath } from './space_url_parser';
+
+describe('addSpaceIdToPath', () => {
+ test('handles no parameters', () => {
+ expect(addSpaceIdToPath()).toEqual(`/`);
+ });
+
+ test('it adds to the basePath correctly', () => {
+ expect(addSpaceIdToPath('/my/base/path', 'url-context')).toEqual('/my/base/path/s/url-context');
+ });
+
+ test('it appends the requested path to the end of the url context', () => {
+ expect(addSpaceIdToPath('/base', 'context', '/final/destination')).toEqual(
+ '/base/s/context/final/destination'
+ );
+ });
+
+ test('it replaces existing space identifiers', () => {
+ expect(addSpaceIdToPath('/my/base/path/s/old-space/', 'new-space')).toEqual(
+ '/my/base/path/s/new-space'
+ );
+
+ expect(addSpaceIdToPath('/my/base/path/s/old-space-no-trailing', 'new-space')).toEqual(
+ '/my/base/path/s/new-space'
+ );
+ });
+
+ test('it removes existing space identifier when spaceId is default', () => {
+ expect(addSpaceIdToPath('/my/base/path/s/old-space', 'default')).toEqual('/my/base/path');
+ expect(addSpaceIdToPath('/my/base/path/s/old-space')).toEqual('/my/base/path');
+ });
+
+ test('it throws an error when the requested path does not start with a slash', () => {
+ expect(() => {
+ addSpaceIdToPath('', '', 'foo');
+ }).toThrowErrorMatchingInlineSnapshot(`"path must start with a /"`);
+ });
+});
diff --git a/src/plugins/share/common/url_service/locators/redirect/space_url_parser.ts b/src/plugins/share/common/url_service/locators/redirect/space_url_parser.ts
new file mode 100644
index 0000000000000..9c9cb519d1c7f
--- /dev/null
+++ b/src/plugins/share/common/url_service/locators/redirect/space_url_parser.ts
@@ -0,0 +1,31 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export function addSpaceIdToPath(
+ basePath: string = '/',
+ spaceId: string = '',
+ requestedPath: string = ''
+): string {
+ if (requestedPath && !requestedPath.startsWith('/')) {
+ throw new Error(`path must start with a /`);
+ }
+
+ if (basePath.includes('/s/')) {
+ // If the base path already contains a space identifier, remove it
+ basePath = basePath.replace(/\/s\/[^/]+/, '');
+ }
+
+ const normalizedBasePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
+
+ if (spaceId && spaceId !== 'default') {
+ return `${normalizedBasePath}/s/${spaceId}${requestedPath}`;
+ }
+
+ return `${normalizedBasePath}${requestedPath}` || '/';
+}
diff --git a/src/plugins/share/common/url_service/locators/redirect/types.ts b/src/plugins/share/common/url_service/locators/redirect/types.ts
index dfd6211c97434..bbc1b14f2e428 100644
--- a/src/plugins/share/common/url_service/locators/redirect/types.ts
+++ b/src/plugins/share/common/url_service/locators/redirect/types.ts
@@ -8,6 +8,7 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
+import type { FormatSearchParamsOptions } from './format_search_params';
/**
* @public
@@ -27,3 +28,13 @@ export interface RedirectOptions
extends Persistable
* @param params URL locator parameters.
* @param options URL serialization options.
*/
- getRedirectUrl(params: P, options?: FormatSearchParamsOptions): string;
+ getRedirectUrl(params: P, options?: GetRedirectUrlOptions): string;
/**
* Navigate using the `core.application.navigateToApp()` method to a Kibana
diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/ecs/fields/custom/system.yml b/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/ecs/fields/custom/system.yml
index 168a1028952f1..656c410a18e08 100644
--- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/ecs/fields/custom/system.yml
+++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/ecs/fields/custom/system.yml
@@ -44,3 +44,7 @@
level: custom
type: long
description: "Number of outgoing bytes"
+ - name: core.system.ticks
+ level: custom
+ type: long
+ description: "The amount of CPU time spent in kernel space"
diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/index.ts b/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/index.ts
index becee1697c0c8..c46b3c3e95bf7 100644
--- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/index.ts
+++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_hosts/index.ts
@@ -121,6 +121,11 @@ export const generateEvent: GeneratorFunction = (config, schedule, index, timest
bytes: generateNetworkData(timestamp.toISOString()),
},
},
+ core: {
+ system: {
+ ticks: randomBetween(1_000_000, 1_500_100),
+ },
+ },
},
metricset: {
period: interval,
@@ -159,6 +164,11 @@ export const generateEvent: GeneratorFunction = (config, schedule, index, timest
bytes: generateNetworkData(timestamp.toISOString()),
},
},
+ core: {
+ system: {
+ ticks: randomBetween(1_000_000, 1_500_100),
+ },
+ },
},
metricset: {
period: interval,
diff --git a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts
index 94ff0139414f0..e275a89e18b3a 100644
--- a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts
+++ b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.test.ts
@@ -59,15 +59,18 @@ describe('getViewInAppUrl', () => {
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
- expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
- dataset: args.dataViewId,
- timeRange: returnedTimeRange,
- filters: [],
- query: {
- query: 'mockedFilter and mockedCountFilter',
- language: 'kuery',
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: args.dataViewId,
+ timeRange: returnedTimeRange,
+ filters: [],
+ query: {
+ query: 'mockedFilter and mockedCountFilter',
+ language: 'kuery',
+ },
},
- });
+ {}
+ );
});
it('should call getRedirectUrl with only count filter', () => {
@@ -85,15 +88,18 @@ describe('getViewInAppUrl', () => {
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
- expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
- dataset: undefined,
- timeRange: returnedTimeRange,
- filters: [],
- query: {
- query: 'mockedCountFilter',
- language: 'kuery',
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: undefined,
+ timeRange: returnedTimeRange,
+ filters: [],
+ query: {
+ query: 'mockedCountFilter',
+ language: 'kuery',
+ },
},
- });
+ {}
+ );
});
it('should call getRedirectUrl with only filter', () => {
@@ -111,15 +117,18 @@ describe('getViewInAppUrl', () => {
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
- expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
- dataset: undefined,
- timeRange: returnedTimeRange,
- filters: [],
- query: {
- query: 'mockedFilter',
- language: 'kuery',
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: undefined,
+ timeRange: returnedTimeRange,
+ filters: [],
+ query: {
+ query: 'mockedFilter',
+ language: 'kuery',
+ },
},
- });
+ {}
+ );
});
it('should call getRedirectUrl with empty query if metrics and filter are not not provided', () => {
@@ -130,15 +139,18 @@ describe('getViewInAppUrl', () => {
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
- expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
- dataset: undefined,
- timeRange: returnedTimeRange,
- filters: [],
- query: {
- query: '',
- language: 'kuery',
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: undefined,
+ timeRange: returnedTimeRange,
+ filters: [],
+ query: {
+ query: '',
+ language: 'kuery',
+ },
},
- });
+ {}
+ );
});
it('should call getRedirectUrl with empty if there are multiple metrics', () => {
@@ -161,15 +173,18 @@ describe('getViewInAppUrl', () => {
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
- expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
- dataset: undefined,
- timeRange: returnedTimeRange,
- filters: [],
- query: {
- query: '',
- language: 'kuery',
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: undefined,
+ timeRange: returnedTimeRange,
+ filters: [],
+ query: {
+ query: '',
+ language: 'kuery',
+ },
},
- });
+ {}
+ );
});
it('should call getRedirectUrl with filters if group and searchConfiguration filter are provided', () => {
@@ -217,33 +232,67 @@ describe('getViewInAppUrl', () => {
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
- expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
- dataset: undefined,
- timeRange: returnedTimeRange,
- filters: [
- {
- meta: {},
- query: {
- term: {
- field: {
- value: 'justTesting',
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: undefined,
+ timeRange: returnedTimeRange,
+ filters: [
+ {
+ meta: {},
+ query: {
+ term: {
+ field: {
+ value: 'justTesting',
+ },
},
},
},
- },
- {
- meta: {},
- query: {
- match_phrase: {
- 'host.name': 'host-1',
+ {
+ meta: {},
+ query: {
+ match_phrase: {
+ 'host.name': 'host-1',
+ },
},
},
+ ],
+ query: {
+ query: 'mockedFilter',
+ language: 'kuery',
+ },
+ },
+ {}
+ );
+ });
+
+ it('should call getRedirectUrl with spaceId', () => {
+ const spaceId = 'mockedSpaceId';
+ const args: GetViewInAppUrlArgs = {
+ metrics: [
+ {
+ name: 'A',
+ aggType: Aggregators.COUNT,
+ filter: 'mockedCountFilter',
},
],
- query: {
- query: 'mockedFilter',
- language: 'kuery',
+ logsExplorerLocator,
+ startedAt,
+ endedAt,
+ spaceId,
+ };
+
+ expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
+ expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith(
+ {
+ dataset: undefined,
+ timeRange: returnedTimeRange,
+ filters: [],
+ query: {
+ query: 'mockedCountFilter',
+ language: 'kuery',
+ },
},
- });
+ { spaceId }
+ );
});
});
diff --git a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.ts b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.ts
index 5411eff43bc3d..0d6095f6b520f 100644
--- a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.ts
+++ b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/get_view_in_app_url.ts
@@ -22,6 +22,7 @@ export interface GetViewInAppUrlArgs {
logsExplorerLocator?: LocatorPublic;
metrics?: CustomThresholdExpressionMetric[];
startedAt?: string;
+ spaceId?: string;
}
export const getViewInAppUrl = ({
@@ -32,6 +33,7 @@ export const getViewInAppUrl = ({
metrics = [],
searchConfiguration,
startedAt = new Date().toISOString(),
+ spaceId,
}: GetViewInAppUrlArgs) => {
if (!logsExplorerLocator) return '';
@@ -56,10 +58,13 @@ export const getViewInAppUrl = ({
query.query = searchConfigurationQuery;
}
- return logsExplorerLocator?.getRedirectUrl({
- dataset,
- timeRange,
- query,
- filters: [...searchConfigurationFilters, ...groupFilters],
- });
+ return logsExplorerLocator?.getRedirectUrl(
+ {
+ dataset,
+ timeRange,
+ query,
+ filters: [...searchConfigurationFilters, ...groupFilters],
+ },
+ { spaceId }
+ );
};
diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
index b8dff520ff119..fb5aef4e3ddcb 100644
--- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
+++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.test.ts
@@ -38,6 +38,7 @@ const initialRuleState: TestRuleState = {
};
const fakeLogger = (msg: string, meta?: Meta) => {};
+const MOCKED_SPACE_ID = 'mockedSpaceId';
const logger = {
trace: fakeLogger,
@@ -90,7 +91,7 @@ const mockOptions = {
},
trackedAlertsRecovered: {},
},
- spaceId: '',
+ spaceId: MOCKED_SPACE_ID,
rule: {
id: '',
name: '',
@@ -1563,7 +1564,7 @@ describe('The custom threshold alert type', () => {
expect(services.alertsClient.setAlertData).toBeCalledTimes(1);
expect(services.alertsClient.setAlertData).toBeCalledWith({
context: {
- alertDetailsUrl: 'http://localhost:5601/app/observability/alerts/uuid-a',
+ alertDetailsUrl: `http://localhost:5601/s/${MOCKED_SPACE_ID}/app/observability/alerts/uuid-a`,
viewInAppUrl: 'mockedViewInApp',
group: [
{
@@ -1584,6 +1585,7 @@ describe('The custom threshold alert type', () => {
});
expect(getViewInAppUrl).lastCalledWith({
dataViewId: 'c34a7c79-a88b-4b4a-ad19-72f6d24104e4',
+ spaceId: MOCKED_SPACE_ID,
groups: [
{
field: 'host.name',
@@ -1800,7 +1802,7 @@ describe('The custom threshold alert type', () => {
await execute(true);
const recentAlert = getLastReportedAlert(instanceID);
expect(recentAlert?.context).toEqual({
- alertDetailsUrl: 'http://localhost:5601/app/observability/alerts/uuid-*',
+ alertDetailsUrl: `http://localhost:5601/s/${MOCKED_SPACE_ID}/app/observability/alerts/uuid-*`,
reason: 'Average test.metric.3 reported no data in the last 1m',
timestamp: STARTED_AT_MOCK_DATE.toISOString(),
value: ['[NO DATA]', null],
@@ -3438,6 +3440,7 @@ describe('The custom threshold alert type', () => {
const execute = (alertOnNoData: boolean, sourceId: string = 'default') =>
executor({
...mockOptions,
+ spaceId: '',
services,
params: {
...mockOptions.params,
diff --git a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
index 591e8062d5ca7..72c9795122dc8 100644
--- a/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
+++ b/x-pack/plugins/observability_solution/observability/server/lib/rules/custom_threshold/custom_threshold_executor.ts
@@ -285,6 +285,7 @@ export const createCustomThresholdExecutor = ({
metrics: alertResults.length === 1 ? alertResults[0][group].metrics : [],
searchConfiguration: params.searchConfiguration,
startedAt: indexedStartedAt,
+ spaceId,
}),
...additionalContext,
},
diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts
index 8ed42269e569b..f8f4a8a7df66b 100644
--- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_pct_fired.ts
@@ -28,6 +28,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const dataViewApi = getService('dataViewApi');
const logger = getService('log');
const config = getService('config');
+ const spacesService = getService('spaces');
const isServerless = config.get('serverless');
const expectedConsumer = isServerless ? 'observability' : 'logs';
let roleAuthc: RoleCredentials;
@@ -39,6 +40,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const DATA_VIEW_TITLE = 'kbn-data-forge-fake_hosts.fake_hosts-*';
const DATA_VIEW_NAME = 'data-view-name';
const DATA_VIEW_ID = 'data-view-id';
+ const SPACE_ID = 'test-space';
let dataForgeConfig: PartialConfig;
let dataForgeIndices: string[];
let actionId: string;
@@ -73,8 +75,15 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
name: DATA_VIEW_NAME,
id: DATA_VIEW_ID,
title: DATA_VIEW_TITLE,
+ spaceId: SPACE_ID,
roleAuthc,
});
+ await spacesService.create({
+ id: SPACE_ID,
+ name: 'Test Space',
+ disabledFeatures: [],
+ color: '#AABBCC',
+ });
});
after(async () => {
@@ -98,11 +107,13 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
});
await dataViewApi.delete({
id: DATA_VIEW_ID,
+ spaceId: SPACE_ID,
roleAuthc,
});
await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]);
await cleanup({ client: esClient, config: dataForgeConfig, logger });
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
+ await spacesService.delete(SPACE_ID);
});
describe('Rule creation', () => {
@@ -111,10 +122,12 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
roleAuthc,
name: 'Index Connector: Threshold API test',
indexName: ALERT_ACTION_INDEX,
+ spaceId: SPACE_ID,
});
const createdRule = await alertingApi.createRule({
roleAuthc,
+ spaceId: SPACE_ID,
tags: ['observability'],
consumer: expectedConsumer,
name: 'Threshold rule',
@@ -174,12 +187,13 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
roleAuthc,
ruleId,
expectedStatus: 'active',
+ spaceId: SPACE_ID,
});
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
- const match = await alertingApi.findInRules(roleAuthc, ruleId);
+ const match = await alertingApi.findInRules(roleAuthc, ruleId, SPACE_ID);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be(expectedConsumer);
});
@@ -204,7 +218,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
'observability.rules.custom_threshold'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.uuid', ruleId);
- expect(resp.hits.hits[0]._source).property('kibana.space_ids').contain('default');
+ expect(resp.hits.hits[0]._source).property('kibana.space_ids').contain(SPACE_ID);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.tags')
.contain('observability');
@@ -245,7 +259,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort();
expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold');
expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql(
- `${protocol}://${hostname}${port ? `:${port}` : ''}/app/observability/alerts/${alertId}`
+ `${protocol}://${hostname}${
+ port ? `:${port}` : ''
+ }/s/${SPACE_ID}/app/observability/alerts/${alertId}`
);
expect(resp.hits.hits[0]._source?.reason).eql(
`Average system.cpu.user.pct is 250%, above the threshold of 50%. (duration: 5 mins, data view: ${DATA_VIEW_NAME})`
@@ -255,6 +271,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const parsedViewInAppUrl = parseSearchParams(
new URL(resp.hits.hits[0]._source?.viewInAppUrl || '').search
);
+ const viewInAppUrlPathName = new URL(resp.hits.hits[0]._source?.viewInAppUrl || '')
+ .pathname;
+
+ expect(viewInAppUrlPathName).contain(`/s/${SPACE_ID}/app/r`);
expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('LOGS_EXPLORER_LOCATOR');
expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({
diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_us_fired.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts
similarity index 71%
rename from x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_us_fired.ts
rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts
index 05b6ded5191d1..852363b0d8593 100644
--- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_us_fired.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/avg_ticks_fired.ts
@@ -5,45 +5,44 @@
* 2.0.
*/
-import moment from 'moment';
-import { format } from 'url';
+import { omit } from 'lodash';
import expect from '@kbn/expect';
+import { cleanup, generate, Dataset, PartialConfig } from '@kbn/data-forge';
import { COMPARATORS } from '@kbn/alerting-comparators';
-import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { Aggregators } from '@kbn/observability-plugin/common/custom_threshold_rule/types';
import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/custom_threshold/constants';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
+import { parseSearchParams } from '@kbn/share-plugin/common/url_service';
import { kbnTestConfig } from '@kbn/test';
import type { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-functional-services';
import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
-import { getSyntraceClient, generateData } from './helpers/syntrace';
-import { ActionDocument } from './types';
+import { ISO_DATE_REGEX } from './constants';
+import { ActionDocument, LogsExplorerLocatorParsedParams } from './types';
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
- const start = moment(Date.now()).subtract(10, 'minutes').valueOf();
- const end = moment(Date.now()).add(15, 'minutes').valueOf();
const esClient = getService('es');
const samlAuth = getService('samlAuth');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esDeleteAllIndices = getService('esDeleteAllIndices');
const alertingApi = getService('alertingApi');
const dataViewApi = getService('dataViewApi');
+ const logger = getService('log');
const config = getService('config');
- const kibanaServerConfig = config.get('servers.kibana');
const isServerless = config.get('serverless');
const expectedConsumer = isServerless ? 'observability' : 'logs';
- const kibanaUrl = format(kibanaServerConfig);
+ const spacesService = getService('spaces');
let roleAuthc: RoleCredentials;
let internalReqHeader: InternalRequestHeader;
- describe('AVG - US - FIRED', () => {
+ describe('AVG - TICKS - FIRED', () => {
const CUSTOM_THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default';
const ALERT_ACTION_INDEX = 'alert-action-threshold';
- const DATA_VIEW = 'traces-apm*,metrics-apm*,logs-apm*';
+ const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*';
const DATA_VIEW_ID = 'data-view-id';
const DATA_VIEW_NAME = 'test-data-view-name';
-
- let synthtraceEsClient: ApmSynthtraceEsClient;
+ const SPACE_ID = 'test-space';
+ let dataForgeConfig: PartialConfig;
+ let dataForgeIndices: string[];
let actionId: string;
let ruleId: string;
let alertId: string;
@@ -51,14 +50,47 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
before(async () => {
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
internalReqHeader = samlAuth.getInternalRequestHeader();
- synthtraceEsClient = await getSyntraceClient({ esClient, kibanaUrl });
- await generateData({ synthtraceEsClient, start, end });
+ dataForgeConfig = {
+ schedule: [
+ {
+ template: 'good',
+ start: 'now-10m',
+ end: 'now+5m',
+ metrics: [
+ {
+ name: 'system.core.system.ticks',
+ method: 'linear',
+ start: 10_000_000,
+ end: 10_000_000,
+ },
+ ],
+ },
+ ],
+ indexing: {
+ dataset: 'fake_hosts' as Dataset,
+ eventsPerCycle: 1,
+ interval: 10000,
+ alignEventsToInterval: true,
+ },
+ };
+ dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger });
+ await alertingApi.waitForDocumentInIndex({
+ indexName: dataForgeIndices.join(','),
+ docCountTarget: 270,
+ });
await dataViewApi.create({
name: DATA_VIEW_NAME,
id: DATA_VIEW_ID,
title: DATA_VIEW,
+ spaceId: SPACE_ID,
roleAuthc,
});
+ await spacesService.create({
+ id: SPACE_ID,
+ name: 'Test Space',
+ disabledFeatures: [],
+ color: '#AABBCC',
+ });
});
after(async () => {
@@ -70,7 +102,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
.delete(`/api/actions/connector/${actionId}`)
.set(roleAuthc.apiKeyHeader)
.set(internalReqHeader);
- await esDeleteAllIndices([ALERT_ACTION_INDEX]);
+ await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]);
await esClient.deleteByQuery({
index: CUSTOM_THRESHOLD_RULE_ALERT_INDEX,
query: { term: { 'kibana.alert.rule.uuid': ruleId } },
@@ -79,11 +111,13 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
index: '.kibana-event-log-*',
query: { term: { 'kibana.alert.rule.consumer': expectedConsumer } },
});
- await synthtraceEsClient.clean();
await dataViewApi.delete({
id: DATA_VIEW_ID,
+ spaceId: SPACE_ID,
roleAuthc,
});
+ await cleanup({ client: esClient, config: dataForgeConfig, logger });
+ await spacesService.delete(SPACE_ID);
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
});
@@ -93,10 +127,12 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
roleAuthc,
name: 'Index Connector: Threshold API test',
indexName: ALERT_ACTION_INDEX,
+ spaceId: SPACE_ID,
});
const createdRule = await alertingApi.createRule({
roleAuthc,
+ spaceId: SPACE_ID,
tags: ['observability'],
consumer: expectedConsumer,
name: 'Threshold rule',
@@ -109,7 +145,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
timeSize: 5,
timeUnit: 'm',
metrics: [
- { name: 'A', field: 'span.self_time.sum.us', aggType: Aggregators.AVERAGE },
+ { name: 'A', field: 'system.core.system.ticks', aggType: Aggregators.AVERAGE },
],
},
],
@@ -134,6 +170,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
alertDetailsUrl: '{{context.alertDetailsUrl}}',
reason: '{{context.reason}}',
value: '{{context.value}}',
+ viewInAppUrl: '{{context.viewInAppUrl}}',
},
],
},
@@ -154,6 +191,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
roleAuthc,
ruleId,
expectedStatus: 'active',
+ spaceId: SPACE_ID,
});
expect(executionStatus).to.be('active');
});
@@ -178,7 +216,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
'observability.rules.custom_threshold'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.uuid', ruleId);
- expect(resp.hits.hits[0]._source).property('kibana.space_ids').contain('default');
+ expect(resp.hits.hits[0]._source).property('kibana.space_ids').contain(SPACE_ID);
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.tags')
.contain('observability');
@@ -203,7 +241,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
threshold: [7500000],
timeSize: 5,
timeUnit: 'm',
- metrics: [{ name: 'A', field: 'span.self_time.sum.us', aggType: 'avg' }],
+ metrics: [{ name: 'A', field: 'system.core.system.ticks', aggType: 'avg' }],
},
],
alertOnNoData: true,
@@ -220,12 +258,30 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
const { protocol, hostname, port } = kbnTestConfig.getUrlPartsWithStrippedDefaultPort();
expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.custom_threshold');
expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql(
- `${protocol}://${hostname}${port ? `:${port}` : ''}/app/observability/alerts/${alertId}`
+ `${protocol}://${hostname}${
+ port ? `:${port}` : ''
+ }/s/${SPACE_ID}/app/observability/alerts/${alertId}`
);
expect(resp.hits.hits[0]._source?.reason).eql(
- `Average span.self_time.sum.us is 10,000,000, above the threshold of 7,500,000. (duration: 5 mins, data view: ${DATA_VIEW_NAME})`
+ `Average system.core.system.ticks is 10,000,000, above the threshold of 7,500,000. (duration: 5 mins, data view: ${DATA_VIEW_NAME})`
);
expect(resp.hits.hits[0]._source?.value).eql('10,000,000');
+
+ const parsedViewInAppUrl = parseSearchParams(
+ new URL(resp.hits.hits[0]._source?.viewInAppUrl || '').search
+ );
+ const viewInAppUrlPathName = new URL(resp.hits.hits[0]._source?.viewInAppUrl || '')
+ .pathname;
+
+ expect(viewInAppUrlPathName).contain(`/s/${SPACE_ID}/app/r`);
+ expect(resp.hits.hits[0]._source?.viewInAppUrl).contain('LOGS_EXPLORER_LOCATOR');
+ expect(omit(parsedViewInAppUrl.params, 'timeRange.from')).eql({
+ dataset: DATA_VIEW_ID,
+ timeRange: { to: 'now' },
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ });
+ expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX);
});
});
});
diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts
index 96a3351043ae6..45a8f2d8b1b40 100644
--- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/custom_threshold/index.ts
@@ -11,7 +11,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
describe('Custom Threshold rule', () => {
loadTestFile(require.resolve('./avg_pct_fired'));
loadTestFile(require.resolve('./avg_pct_no_data'));
- loadTestFile(require.resolve('./avg_us_fired'));
+ loadTestFile(require.resolve('./avg_ticks_fired'));
loadTestFile(require.resolve('./custom_eq_avg_bytes_fired'));
loadTestFile(require.resolve('./documents_count_fired'));
loadTestFile(require.resolve('./group_by_fired'));
diff --git a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts
index ee1047d6024ca..dd596a9a109c8 100644
--- a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts
@@ -942,14 +942,16 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
ruleId,
expectedStatus,
roleAuthc,
+ spaceId,
}: {
ruleId: string;
expectedStatus: string;
roleAuthc: RoleCredentials;
+ spaceId?: string;
}) {
return await retry.tryForTime(retryTimeout, async () => {
const response = await supertestWithoutAuth
- .get(`/api/alerting/rule/${ruleId}`)
+ .get(`${spaceId ? '/s/' + spaceId : ''}/api/alerting/rule/${ruleId}`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.timeout(requestTimeout);
@@ -1034,13 +1036,15 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
name,
indexName,
roleAuthc,
+ spaceId,
}: {
name: string;
indexName: string;
roleAuthc: RoleCredentials;
+ spaceId?: string;
}) {
const { body } = await supertestWithoutAuth
- .post(`/api/actions/connector`)
+ .post(`${spaceId ? '/s/' + spaceId : ''}/api/actions/connector`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send({
@@ -1063,6 +1067,7 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
schedule,
consumer,
roleAuthc,
+ spaceId,
}: {
ruleTypeId: string;
name: string;
@@ -1080,9 +1085,10 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
schedule?: { interval: string };
consumer: string;
roleAuthc: RoleCredentials;
+ spaceId?: string;
}) {
const { body } = await supertestWithoutAuth
- .post(`/api/alerting/rule`)
+ .post(`${spaceId ? '/s/' + spaceId : ''}/api/alerting/rule`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.send({
@@ -1118,17 +1124,17 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
});
},
- async findInRules(roleAuthc: RoleCredentials, ruleId: string) {
+ async findInRules(roleAuthc: RoleCredentials, ruleId: string, spaceId?: string) {
const response = await supertestWithoutAuth
- .get('/api/alerting/rules/_find')
+ .get(`${spaceId ? '/s/' + spaceId : ''}/api/alerting/rules/_find`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader());
return response.body.data.find((obj: any) => obj.id === ruleId);
},
- async searchRules(roleAuthc: RoleCredentials, filter: string) {
+ async searchRules(roleAuthc: RoleCredentials, filter: string, spaceId?: string) {
return supertestWithoutAuth
- .get('/api/alerting/rules/_find')
+ .get(`${spaceId ? '/s/' + spaceId : ''}/api/alerting/rules/_find`)
.query({ filter })
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader());
diff --git a/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts
index 33e829d8c9e39..6b03bdf46b273 100644
--- a/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/services/data_view_api.ts
@@ -18,14 +18,16 @@ export function DataViewApiProvider({ getService }: DeploymentAgnosticFtrProvide
id,
name,
title,
+ spaceId,
}: {
roleAuthc: RoleCredentials;
id: string;
name: string;
title: string;
+ spaceId?: string;
}) {
const { body } = await supertestWithoutAuth
- .post(`/api/content_management/rpc/create`)
+ .post(`${spaceId ? '/s/' + spaceId : ''}/api/content_management/rpc/create`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.set(samlAuth.getCommonRequestHeader())
@@ -48,9 +50,17 @@ export function DataViewApiProvider({ getService }: DeploymentAgnosticFtrProvide
return body;
},
- async delete({ roleAuthc, id }: { roleAuthc: RoleCredentials; id: string }) {
+ async delete({
+ roleAuthc,
+ id,
+ spaceId,
+ }: {
+ roleAuthc: RoleCredentials;
+ id: string;
+ spaceId?: string;
+ }) {
const { body } = await supertestWithoutAuth
- .post(`/api/content_management/rpc/delete`)
+ .post(`${spaceId ? '/s/' + spaceId : ''}/api/content_management/rpc/delete`)
.set(roleAuthc.apiKeyHeader)
.set(samlAuth.getInternalRequestHeader())
.set(samlAuth.getCommonRequestHeader())
diff --git a/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts b/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts
index 9623df1bebbd0..08a085e2fcd9b 100644
--- a/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts
+++ b/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts
@@ -26,4 +26,5 @@ export const deploymentAgnosticServices = _.pick(apiIntegrationServices, [
'retry',
'security',
'usageAPI',
+ 'spaces',
]);