Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-discover-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
DOC_HIDE_TIME_COLUMN_SETTING,
FIELDS_LIMIT_SETTING,
HIDE_ANNOUNCEMENTS,
IS_ESQL_DEFAULT_FEATURE_FLAG_KEY,
MAX_DOC_FIELDS_DISPLAYED,
MODIFY_COLUMNS_ON_SWITCH,
ROW_HEIGHT_OPTION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn';
export const FIELDS_LIMIT_SETTING = 'fields:popularLimit';
export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements';
export const IS_ESQL_DEFAULT_FEATURE_FLAG_KEY = 'discover.isEsqlDefault';
export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed';
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
export const ROW_HEIGHT_OPTION = 'discover:rowHeightOption';
Expand Down
2 changes: 1 addition & 1 deletion src/platform/plugins/shared/discover/public/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export const ADHOC_DATA_VIEW_RENDER_EVENT = 'ad_hoc_data_view';
export const SEARCH_SESSION_ID_QUERY_PARAM = 'searchSessionId';

export const CASCADE_LAYOUT_ENABLED_FEATURE_FLAG_KEY = 'discover.cascadeLayoutEnabled';
export const IS_ESQL_DEFAULT_FEATURE_FLAG_KEY = 'discover.isEsqlDefault';
export { IS_ESQL_DEFAULT_FEATURE_FLAG_KEY } from '@kbn/discover-utils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 type { ObservabilityRootProfileProvider } from '../types';

export const getDefaultEsqlQuery: ObservabilityRootProfileProvider['profile']['getDefaultEsqlQuery'] =

(prev, { context }) =>
() => {
if (!context.allLogsIndexPattern) {
return prev();
}

return { query: `FROM ${context.allLogsIndexPattern}` };
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@

export { createGetAppMenu } from './get_app_menu';
export { getDefaultAdHocDataViews } from './get_default_ad_hoc_data_views';
export { getDefaultEsqlQuery } from './get_default_esql_query';
export { getDocViewer } from './get_doc_viewer';
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ describe('observabilityRootProfileProvider', () => {
});
});

describe('getDefaultEsqlQuery', () => {
it('should return an ES|QL query using the allLogsIndexPattern from the resolved context', async () => {
const result = await observabilityRootProfileProvider.resolve({
solutionNavId: SolutionType.Observability,
});
if (!result.isMatch) {
throw new Error('Expected result to match');
}
expect(result.context.allLogsIndexPattern).toEqual('logs-*');
const defaultEsqlQuery = observabilityRootProfileProvider.profile.getDefaultEsqlQuery?.(
() => ({ query: 'FROM prev-pattern' }),
{ context: result.context }
)();
expect(defaultEsqlQuery).toEqual({ query: 'FROM logs-*' });
});

it('should fall back to the previous profile return value when allLogsIndexPattern is undefined', async () => {
jest
.spyOn(mockServices.logsContextService, 'getAllLogsIndexPattern')
.mockReturnValueOnce(undefined);
const result = await observabilityRootProfileProvider.resolve({
solutionNavId: SolutionType.Observability,
});
if (!result.isMatch) {
throw new Error('Expected result to match');
}
expect(result.context.allLogsIndexPattern).toEqual(undefined);
const prevValue = { query: 'FROM prev-pattern' };
const prev = jest.fn().mockReturnValue(prevValue);
const defaultEsqlQuery = observabilityRootProfileProvider.profile.getDefaultEsqlQuery?.(
prev,
{
context: result.context,
}
)();
expect(prev).toHaveBeenCalled();
expect(defaultEsqlQuery).toEqual(prevValue);
});
});

describe('getDocViewer', () => {
it('does NOT add attributes doc viewer tab to the registry when the record has no attributes fields', () => {
const getDocViewer = observabilityRootProfileProvider.profile.getDocViewer!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
import { SolutionType } from '../../../profiles';
import type { ProfileProviderServices } from '../../profile_provider_services';
import { OBSERVABILITY_ROOT_PROFILE_ID } from '../consts';
import { createGetAppMenu, getDefaultAdHocDataViews, getDocViewer } from './accessors';
import {
createGetAppMenu,
getDefaultAdHocDataViews,
getDefaultEsqlQuery,
getDocViewer,
} from './accessors';
import type { ObservabilityRootProfileProvider } from './types';

export const createObservabilityRootProfileProvider = (
Expand All @@ -20,6 +25,7 @@ export const createObservabilityRootProfileProvider = (
profile: {
getAppMenu: createGetAppMenu(services),
getDefaultAdHocDataViews,
getDefaultEsqlQuery,
getDocViewer,
},
resolve: (params) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ALL_LOGS_DATA_VIEW_ID } from '@kbn/discover-utils/src';
import { LogsLocatorDefinition } from './logs_locator';

const CUSTOM_LOG_PATTERN = 'custom-logs-*,remote:custom-logs-*';

const mockGetLocation = jest.fn().mockResolvedValue({
app: 'discover',
path: '/mock-path',
state: {},
});

const mockLocators = {
get: jest.fn().mockReturnValue({ getLocation: mockGetLocation }),
};

const mockGetFlattenedLogSources = jest.fn().mockResolvedValue(CUSTOM_LOG_PATTERN);

const mockGetLogSourcesService = jest.fn().mockResolvedValue({
getFlattenedLogSources: mockGetFlattenedLogSources,
});

const createLocator = (isEsqlDefault: boolean) =>
new LogsLocatorDefinition({
locators: mockLocators as any,
getLogSourcesService: mockGetLogSourcesService,
getIsEsqlDefault: jest.fn().mockResolvedValue(isEsqlDefault),
});

describe('LogsLocatorDefinition', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('when discover.isEsqlDefault is true', () => {
it('delegates to DISCOVER_APP_LOCATOR with an ES|QL query when no query param is provided', async () => {
const locator = createLocator(true);

await locator.getLocation({});

expect(mockLocators.get).toHaveBeenCalledWith('DISCOVER_APP_LOCATOR');
expect(mockGetLocation).toHaveBeenCalledWith({
query: { esql: `FROM ${CUSTOM_LOG_PATTERN}` },
});
});

it('preserves the caller-provided query when one is given', async () => {
const locator = createLocator(true);
const callerQuery = { language: 'kuery', query: 'host.name: "my-host"' };

await locator.getLocation({ query: callerQuery });

expect(mockGetLocation).toHaveBeenCalledWith({
dataViewId: ALL_LOGS_DATA_VIEW_ID,
query: callerQuery,
});
expect(mockGetFlattenedLogSources).not.toHaveBeenCalled();
});

it('spreads consumer-provided params into the delegated call', async () => {
const locator = createLocator(true);
const extraParams = {
timeRange: { from: 'now-15m', to: 'now' },
filters: [{ meta: { alias: 'test' } }],
};

await locator.getLocation(extraParams as any);

expect(mockGetLocation).toHaveBeenCalledWith({
...extraParams,
query: { esql: `FROM ${CUSTOM_LOG_PATTERN}` },
});
});
});

describe('when discover.isEsqlDefault is false', () => {
it('delegates to DISCOVER_APP_LOCATOR with dataViewId', async () => {
const locator = createLocator(false);

await locator.getLocation({});

expect(mockLocators.get).toHaveBeenCalledWith('DISCOVER_APP_LOCATOR');
expect(mockGetLocation).toHaveBeenCalledWith({
dataViewId: ALL_LOGS_DATA_VIEW_ID,
});
});

it('spreads consumer-provided params into the delegated call', async () => {
const locator = createLocator(false);
const extraParams = {
timeRange: { from: 'now-1h', to: 'now' },
columns: ['message', '@timestamp'],
};

await locator.getLocation(extraParams as any);

expect(mockGetLocation).toHaveBeenCalledWith({
dataViewId: ALL_LOGS_DATA_VIEW_ID,
...extraParams,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,26 @@ export class LogsLocatorDefinition implements LocatorDefinition<LogsLocatorParam
private readonly deps: {
locators: LocatorClient;
getLogSourcesService(): Promise<LogsDataAccessPluginStart['services']['logSourcesService']>;
getIsEsqlDefault(): Promise<boolean>;
}
) {}

public readonly getLocation = async (params: LogsLocatorParams) => {
const discoverAppLocator =
this.deps.locators.get<DiscoverAppLocatorParams>('DISCOVER_APP_LOCATOR')!;

const isEsqlDefault = await this.deps.getIsEsqlDefault();

if (isEsqlDefault && !params.query) {
const logSourcesService = await this.deps.getLogSourcesService();
const flattenedLogSources = await logSourcesService.getFlattenedLogSources();

return discoverAppLocator.getLocation({
...params,
query: { esql: `FROM ${flattenedLogSources}` },
});
}

return discoverAppLocator.getLocation({
dataViewId: ALL_LOGS_DATA_VIEW_ID,
...params,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import type { CoreStart } from '@kbn/core/public';
import { IS_ESQL_DEFAULT_FEATURE_FLAG_KEY } from '@kbn/discover-utils';
import { LogsLocatorDefinition } from '../common/locators';
import { createLogAIAssistant, createLogsAIAssistantRenderer } from './components/log_ai_assistant';
import { createLogsOverview } from './components/logs_overview';
Expand Down Expand Up @@ -35,6 +36,10 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
const [_, pluginsStart] = await coreSetup.getStartServices();
return pluginsStart.logsDataAccess.services.logSourcesService;
},
getIsEsqlDefault: async () => {
const [coreStart] = await coreSetup.getStartServices();
return coreStart.featureFlags.getBooleanValue(IS_ESQL_DEFAULT_FEATURE_FLAG_KEY, false);
},
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilTabIsLoaded();
});

afterEach(async () => {
await PageObjects.discover.resetQueryMode();
});

after(async () => {
await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { createPlaywrightConfig } from '@kbn/scout-oblt';

export default createPlaywrightConfig({
testDir: './tests',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

/**
* NOTE: This suite runs sequentially (not alongside `parallel_tests/`) because it
* toggles the `discover.isEsqlDefault` feature flag, which is server-wide. Placing
* it in `parallel_tests/` would cause the flag change to spill into the other parallel
* workers running `landing.spec.ts` and break their assertions that assume the flag is
* off by default.
*
* TODO: Once `discover.isEsqlDefault` is enabled by default and the feature flag is
* removed, merge these tests into the existing parallel suite at:
* parallel_tests/landing.spec.ts
* and update the classic-mode redirect assertion there to expect an ES|QL Discover URL.
*/

import { tags } from '@kbn/scout-oblt';
import { expect } from '@kbn/scout-oblt/ui';
import { test } from '../fixtures';
import { generateLogsData, TEST_START_DATE, TEST_END_DATE } from '../fixtures/generators';
import { BIGGER_TIMEOUT } from '../fixtures/constants';

test.describe(
'Observability Landing Page (discover.isEsqlDefault enabled)',
{ tag: [...tags.stateful.classic] },
() => {
test.beforeAll(async ({ apiServices }) => {
await apiServices.core.settings({
'feature_flags.overrides': {
'discover.isEsqlDefault': 'true',
},
});
});

test.beforeEach(async ({ browserAuth, logsSynthtraceEsClient }) => {
await browserAuth.loginAsAdmin();
await logsSynthtraceEsClient.clean();
});

test.afterAll(async ({ apiServices, logsSynthtraceEsClient }) => {
await logsSynthtraceEsClient.clean();
await apiServices.core.settings({
'feature_flags.overrides': {
'discover.isEsqlDefault': 'false',
},
});
});

test('redirects to Discover with an ES|QL query when logs data exists', async ({
page,
pageObjects,
logsSynthtraceEsClient,
}) => {
await generateLogsData({
from: new Date(TEST_START_DATE).getTime(),
to: new Date(TEST_END_DATE).getTime(),
client: logsSynthtraceEsClient,
});

await pageObjects.observabilityNavigation.gotoLanding();

await expect(page).toHaveURL(/\/app\/discover/, { timeout: BIGGER_TIMEOUT });
// Confirm the Discover URL encodes an ES|QL data source (rison: `dataSource:(type:esql)`)
// rather than a classic data-view source, which would encode `dataSource:(type:dataView,...)`
await expect(page).toHaveURL(/type:esql/);
});

test('redirects to onboarding when no logs data exists', async ({ page, pageObjects }) => {
await pageObjects.observabilityNavigation.gotoLanding();

await expect(page).toHaveURL(/\/app\/observabilityOnboarding/, { timeout: BIGGER_TIMEOUT });
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export default function ({ getService, getPageObjects }: ObservabilityTelemetryF
withTimeoutMs: 500,
});

expect(events.length).to.be(3);
expect(events.length).to.be(2);

// should trigger a new event after opening the doc viewer
await dataGrid.clickRowToggle();
Expand All @@ -235,7 +235,7 @@ export default function ({ getService, getPageObjects }: ObservabilityTelemetryF
withTimeoutMs: 500,
});

expect(events.length).to.be(4);
expect(events.length).to.be(3);

expect(events[events.length - 1].properties).to.eql({
contextLevel: 'documentLevel',
Expand Down
Loading