diff --git a/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx b/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx index ece9ebad4b423..21fd8fa95691c 100644 --- a/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/platform/plugins/shared/discover/public/components/discover_grid/discover_grid.tsx @@ -33,7 +33,7 @@ export const DiscoverGrid: React.FC = ({ rowAdditionalLeadingControls: customRowAdditionalLeadingControls, ...props }) => { - const { dataView, setExpandedDoc } = props; + const { dataView, setExpandedDoc, renderDocumentView } = props; const getRowIndicatorProvider = useProfileAccessor('getRowIndicatorProvider'); const getRowIndicator = useMemo(() => { return getRowIndicatorProvider(() => undefined)({ dataView: props.dataView }); @@ -48,6 +48,7 @@ export const DiscoverGrid: React.FC = ({ query, updateESQLQuery: onUpdateESQLQuery, setExpandedDoc, + isDocViewerEnabled: !!renderDocumentView, }); }, [ customRowAdditionalLeadingControls, @@ -56,6 +57,7 @@ export const DiscoverGrid: React.FC = ({ onUpdateESQLQuery, query, setExpandedDoc, + renderDocumentView, ]); const getPaginationConfigAccessor = useProfileAccessor('getPaginationConfig'); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts index fdc398ae1fa4d..8a69ba8004330 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/accessors/get_row_additional_leading_controls.ts @@ -21,7 +21,7 @@ export const getRowAdditionalLeadingControls: LogsDataSourceProfileProvider['pro (prev, { context }) => (params) => { const additionalControls = prev(params) || []; - const { updateESQLQuery, query, setExpandedDoc } = params; + const { updateESQLQuery, query, setExpandedDoc, isDocViewerEnabled } = params; const isDegradedDocsControlEnabled = isOfAggregateQueryType(query) ? queryContainsMetadataIgnored(query) @@ -52,15 +52,17 @@ export const getRowAdditionalLeadingControls: LogsDataSourceProfileProvider['pro setExpandedDoc(props.record, { initialTabId: 'doc_view_logs_overview' }); }; - return [ - ...additionalControls, - createDegradedDocsControl({ - enabled: isDegradedDocsControlEnabled, - addIgnoredMetadataToQuery, - onClick: leadingControlClick('quality_issues'), - }), - createStacktraceControl({ onClick: leadingControlClick('stacktrace') }), - ]; + return isDocViewerEnabled + ? [ + ...additionalControls, + createDegradedDocsControl({ + enabled: isDegradedDocsControlEnabled, + addIgnoredMetadataToQuery, + onClick: leadingControlClick('quality_issues'), + }), + createStacktraceControl({ onClick: leadingControlClick('stacktrace') }), + ] + : additionalControls; }; const queryContainsMetadataIgnored = (query: AggregateQuery) => diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts index 171817b54988d..b22f950c20d91 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/logs_data_source_profile/profile.test.ts @@ -254,12 +254,29 @@ describe('logsDataSourceProfileProvider', () => { }); const rowAdditionalLeadingControls = getRowAdditionalLeadingControls?.({ dataView: dataViewWithLogLevel, + isDocViewerEnabled: true, }); expect(rowAdditionalLeadingControls).toHaveLength(2); expect(rowAdditionalLeadingControls?.[0].id).toBe('connectedDegradedDocs'); expect(rowAdditionalLeadingControls?.[1].id).toBe('connectedStacktraceDocs'); }); + + it('should not return the passed additional controls if the flag is turned off', () => { + const getRowAdditionalLeadingControls = + logsDataSourceProfileProvider.profile.getRowAdditionalLeadingControls?.(() => undefined, { + context: { + category: DataSourceCategory.Logs, + logOverviewContext$: new BehaviorSubject(undefined), + }, + }); + const rowAdditionalLeadingControls = getRowAdditionalLeadingControls?.({ + dataView: dataViewWithLogLevel, + isDocViewerEnabled: false, + }); + + expect(rowAdditionalLeadingControls).toHaveLength(0); + }); }); }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/types.ts b/src/platform/plugins/shared/discover/public/context_awareness/types.ts index fe1f2bc409e2b..1d48bb5297c80 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/types.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/types.ts @@ -188,6 +188,10 @@ export interface RowControlsExtensionParams { * @param options.initialTabId - The tabId to display in the flyout */ setExpandedDoc?: (record?: DataTableRecord, options?: { initialTabId?: string }) => void; + /** + * Flag to indicate if Flyout opening controls must be rendered or not + */ + isDocViewerEnabled: boolean; } /** diff --git a/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx b/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx index 0467079f3cef1..8993bea98797f 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/components/search_embeddable_grid_component.tsx @@ -98,7 +98,6 @@ export function SearchEmbeddableGridComponent({ // `api.query$` and `api.filters$` are the initial values from the saved search SO (as of now) // `fetchContext.query` and `fetchContext.filters` are Dashboard's query and filters - const savedSearchQuery = apiQuery; const savedSearchFilters = apiFilters; diff --git a/src/platform/plugins/shared/discover/public/embeddable/constants.ts b/src/platform/plugins/shared/discover/public/embeddable/constants.ts index 64028b9cf1b28..f32fd32b3f51a 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/constants.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/constants.ts @@ -23,6 +23,8 @@ export const SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER: Trigger = { 'This trigger is used to replace the cell actions for Discover session embeddable grid.', } as const; +export const LEGACY_LOG_STREAM_EMBEDDABLE = 'LOG_STREAM_EMBEDDABLE'; + export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH'; export const DEFAULT_HEADER_ROW_HEIGHT_LINES = 3; diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_legacy_log_stream_embeddable_factory.ts b/src/platform/plugins/shared/discover/public/embeddable/get_legacy_log_stream_embeddable_factory.ts new file mode 100644 index 0000000000000..5d87e09d8e155 --- /dev/null +++ b/src/platform/plugins/shared/discover/public/embeddable/get_legacy_log_stream_embeddable_factory.ts @@ -0,0 +1,65 @@ +/* + * 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 { SavedSearch } from '@kbn/saved-search-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { getAllLogsDataViewSpec } from '@kbn/discover-utils/src'; +import { toSavedSearchAttributes } from '@kbn/saved-search-plugin/common'; +import { getSearchEmbeddableFactory } from './get_search_embeddable_factory'; +import { LEGACY_LOG_STREAM_EMBEDDABLE } from './constants'; + +export const getLegacyLogStreamEmbeddableFactory = ( + ...[{ startServices, discoverServices }]: Parameters +) => { + const searchEmbeddableFactory = getSearchEmbeddableFactory({ startServices, discoverServices }); + const logStreamEmbeddableFactory: ReturnType = { + type: LEGACY_LOG_STREAM_EMBEDDABLE, + buildEmbeddable: async ({ initialState, ...restParams }) => { + const searchSource = await discoverServices.data.search.searchSource.create(); + let fallbackPattern = 'logs-*-*'; + // Given that the logDataAccess service is an optional dependency with discover, we need to check if it exists + if (discoverServices.logsDataAccess) { + fallbackPattern = + await discoverServices.logsDataAccess.services.logSourcesService.getFlattenedLogSources(); + } + + const spec = getAllLogsDataViewSpec({ allLogsIndexPattern: fallbackPattern }); + const dataView: DataView = await discoverServices.data.dataViews.create(spec); + + // Finally assign the data view to the search source + searchSource.setField('index', dataView); + + const savedSearch: SavedSearch = { + title: initialState.rawState.title, + description: initialState.rawState.description, + timeRange: initialState.rawState.timeRange, + sort: initialState.rawState.sort ?? [], + columns: initialState.rawState.columns ?? [], + searchSource, + managed: false, + }; + const { searchSourceJSON, references } = searchSource.serialize(); + + initialState = { + ...initialState, + rawState: { + ...initialState.rawState, + attributes: { + ...toSavedSearchAttributes(savedSearch, searchSourceJSON), + references, + }, + }, + }; + + return searchEmbeddableFactory.buildEmbeddable({ initialState, ...restParams }); + }, + }; + + return logStreamEmbeddableFactory; +}; diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts index 148a37977776c..1b49db30b7f45 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.test.ts @@ -34,85 +34,112 @@ describe('initialize edit api', () => { const waitOneTick = () => new Promise((resolve) => setTimeout(resolve, 0)); describe('get app target', () => { - const runEditLinkTest = async (dataView?: DataView, byValue?: boolean) => { - jest - .spyOn(discoverServiceMock.locator, 'getUrl') - .mockClear() - .mockResolvedValueOnce('/base/mock-url'); - jest - .spyOn(discoverServiceMock.core.http.basePath, 'remove') - .mockClear() - .mockReturnValueOnce('/mock-url'); - - if (dataView) { - mockedApi.dataViews$.next([dataView]); + const runEditLinkTest = async (dataViewInput?: DataView, byValue?: boolean) => { + const currentDataView = dataViewInput || dataViewMock; + // Determine if the current scenario will use a redirect + const currentSavedObjectId = byValue ? undefined : 'test-id'; + const isDataViewPersisted = currentDataView.isPersisted + ? currentDataView.isPersisted() + : true; // Assume persisted if method undefined + const useRedirect = !currentSavedObjectId && !isDataViewPersisted; + + if (useRedirect) { + // This is the "by value with ad hoc data view" (redirect) case. + jest + .spyOn(discoverServiceMock.locator, 'getUrl') + .mockClear() + .mockResolvedValueOnce('/base/state-url-for-redirect'); // For urlWithoutLocationState + jest + .spyOn(discoverServiceMock.core.http.basePath, 'remove') + .mockClear() + .mockReturnValueOnce('/mock-url'); // For editPath (applied to getRedirectUrl result) } else { - mockedApi.dataViews$.next([dataViewMock]); - } - if (byValue) { - mockedApi.savedObjectId$.next(undefined); - } else { - mockedApi.savedObjectId$.next('test-id'); + // This is a "by reference" or "by value with persisted data view" (non-redirect) case. + jest + .spyOn(discoverServiceMock.locator, 'getUrl') + .mockClear() + .mockResolvedValueOnce('/base/discover-home') // For getUrl({}) -> urlWithoutLocationState + .mockResolvedValueOnce('/base/mock-url'); // For getUrl(locatorParams) -> raw editUrl + jest + .spyOn(discoverServiceMock.core.http.basePath, 'remove') + .mockClear() + .mockReturnValueOnce('/mock-url'); // For remove('/base/mock-url') -> editPath } + + mockedApi.dataViews$.next([currentDataView]); + mockedApi.savedObjectId$.next(currentSavedObjectId); + await waitOneTick(); const { path: editPath, app: editApp, editUrl, + urlWithoutLocationState, } = await getAppTarget(mockedApi, discoverServiceMock); - return { editPath, editApp, editUrl }; + return { editPath, editApp, editUrl, urlWithoutLocationState }; }; - const testByReference = ({ + const testByReferenceOrNonRedirectValue = ({ editPath, editApp, editUrl, + urlWithoutLocationState, }: { editPath: string; editApp: string; editUrl: string; + urlWithoutLocationState: string; }) => { const locatorParams = getDiscoverLocatorParams(mockedApi); - expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledTimes(1); - expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith(locatorParams); + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledTimes(2); + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith({}); // For urlWithoutLocationState + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith(locatorParams); // For raw editUrl + expect(discoverServiceMock.core.http.basePath.remove).toHaveBeenCalledTimes(1); expect(discoverServiceMock.core.http.basePath.remove).toHaveBeenCalledWith('/base/mock-url'); expect(editApp).toBe('discover'); - expect(editPath).toBe('/mock-url'); - expect(editUrl).toBe('/base/mock-url'); + expect(editPath).toBe('/mock-url'); // Result of basePath.remove + expect(editUrl).toBe('/base/mock-url'); // Raw editUrl before basePath.remove + expect(urlWithoutLocationState).toBe('/base/discover-home'); }; it('should correctly output edit link params for by reference saved search', async () => { - const { editPath, editApp, editUrl } = await runEditLinkTest(); - testByReference({ editPath, editApp, editUrl }); + const result = await runEditLinkTest(dataViewMock, false); + testByReferenceOrNonRedirectValue(result); }); it('should correctly output edit link params for by reference saved search with ad hoc data view', async () => { - const { editPath, editApp, editUrl } = await runEditLinkTest(dataViewAdHoc); - testByReference({ editPath, editApp, editUrl }); + // Still "by reference" (savedObjectId exists), so no redirect even with ad-hoc data view. + const result = await runEditLinkTest(dataViewAdHoc, false); + testByReferenceOrNonRedirectValue(result); }); - it('should correctly output edit link params for by value saved search', async () => { - const { editPath, editApp, editUrl } = await runEditLinkTest(undefined, true); - testByReference({ editPath, editApp, editUrl }); + it('should correctly output edit link params for by value saved search (with persisted data view)', async () => { + // "by value" but with a persisted data view (dataViewMock), so no redirect. + const result = await runEditLinkTest(dataViewMock, true); + testByReferenceOrNonRedirectValue(result); }); it('should correctly output edit link params for by value saved search with ad hoc data view', async () => { + // This specific test case mocks getRedirectUrl because it's unique to the redirect flow jest .spyOn(discoverServiceMock.locator, 'getRedirectUrl') .mockClear() - .mockReturnValueOnce('/base/mock-url'); - jest - .spyOn(discoverServiceMock.core.http.basePath, 'remove') - .mockClear() - .mockReturnValueOnce('/mock-url'); + .mockReturnValueOnce('/base/mock-url'); // This will be the raw editUrl - const { editPath, editApp, editUrl } = await runEditLinkTest(dataViewAdHoc, true); + const result = await runEditLinkTest(dataViewAdHoc, true); + const { editPath, editApp, editUrl, urlWithoutLocationState } = result; const locatorParams = getDiscoverLocatorParams(mockedApi); + + // Assertions for urlWithoutLocationState part (getUrl({})) + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledTimes(1); + expect(discoverServiceMock.locator.getUrl).toHaveBeenCalledWith({}); + + // Assertions for redirect part (getRedirectUrl and basePath.remove) expect(discoverServiceMock.locator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(discoverServiceMock.locator.getRedirectUrl).toHaveBeenCalledWith(locatorParams); expect(discoverServiceMock.core.http.basePath.remove).toHaveBeenCalledTimes(1); @@ -121,6 +148,7 @@ describe('initialize edit api', () => { expect(editApp).toBe('r'); expect(editPath).toBe('/mock-url'); expect(editUrl).toBe('/base/mock-url'); + expect(urlWithoutLocationState).toBe('/base/state-url-for-redirect'); }); }); @@ -130,13 +158,26 @@ describe('initialize edit api', () => { navigateToEditor: mockedNavigate, })); mockedApi.dataViews$.next([dataViewMock]); + mockedApi.savedObjectId$.next('test-id'); // Assuming a by-reference scenario for onEdit await waitOneTick(); + // Mocking for getAppTarget call within onEdit + // Assuming a non-redirect case for simplicity + jest + .spyOn(discoverServiceMock.locator, 'getUrl') + .mockClear() + .mockResolvedValueOnce('/base/discover-home-for-onedit') // For getUrl({}) + .mockResolvedValueOnce('/base/mock-url-for-onedit'); // For getUrl(locatorParams) + jest + .spyOn(discoverServiceMock.core.http.basePath, 'remove') + .mockClear() + .mockReturnValueOnce('/mock-url-for-onedit'); + const { onEdit } = initializeEditApi({ uuid: 'test', parentApi: { - getAppContext: jest.fn().mockResolvedValue({ - getCurrentPath: jest.fn(), + getAppContext: jest.fn().mockReturnValue({ + getCurrentPath: jest.fn().mockReturnValue('/current-parent-path'), currentAppId: 'dashboard', }), }, @@ -148,8 +189,12 @@ describe('initialize edit api', () => { await onEdit(); expect(mockedNavigate).toBeCalledTimes(1); expect(mockedNavigate).toBeCalledWith('discover', { - path: '/mock-url', - state: expect.any(Object), + path: '/mock-url-for-onedit', + state: expect.objectContaining({ + embeddableId: 'test', + originatingApp: 'dashboard', + originatingPath: '/current-parent-path', + }), }); }); }); diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts index 10059f60c3d32..0f5db07fe6aa2 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_edit_api.ts @@ -36,13 +36,17 @@ export async function getAppTarget( // We need to use a redirect URL if this is a by value saved search using // an ad hoc data view to ensure the data view spec gets encoded in the URL const useRedirect = !savedObjectId && !dataViews?.[0]?.isPersisted(); + + const urlWithoutLocationState = await discoverServices.locator.getUrl({}); + const editUrl = useRedirect ? discoverServices.locator.getRedirectUrl(locatorParams) : await discoverServices.locator.getUrl(locatorParams); + const editPath = discoverServices.core.http.basePath.remove(editUrl); const editApp = useRedirect ? 'r' : 'discover'; - return { path: editPath, app: editApp, editUrl }; + return { path: editPath, app: editApp, editUrl, urlWithoutLocationState }; } export function initializeEditApi< diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts b/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts index 99069b0e694e8..c86e295ae5d05 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_fetch.ts @@ -64,7 +64,7 @@ const getExecutionContext = async ( api: SavedSearchPartialFetchApi, discoverServices: DiscoverServices ) => { - const { editUrl } = await getAppTarget(api, discoverServices); + const { editUrl, urlWithoutLocationState } = await getAppTarget(api, discoverServices); const childContext: KibanaExecutionContext = { type: SEARCH_EMBEDDABLE_TYPE, name: 'discover', @@ -72,14 +72,41 @@ const getExecutionContext = async ( description: api.title$?.getValue() || api.defaultTitle$?.getValue() || '', url: editUrl, }; - const executionContext = - apiHasParentApi(api) && apiHasExecutionContext(api.parentApi) + const generateExecutionContext = createExecutionContext(api); + const executionContext = generateExecutionContext(childContext); + + if (isExecutionContextWithinLimits(executionContext)) { + return executionContext; + } + + const newChildContext: KibanaExecutionContext = { + ...childContext, + url: urlWithoutLocationState, + }; + return generateExecutionContext(newChildContext); +}; + +const createExecutionContext = + (api: SavedSearchPartialFetchApi) => + (childContext: KibanaExecutionContext): KibanaExecutionContext => { + return apiHasParentApi(api) && apiHasExecutionContext(api.parentApi) ? { ...api.parentApi?.executionContext, child: childContext, } : childContext; - return executionContext; + }; + +const isExecutionContextWithinLimits = (executionContext: KibanaExecutionContext) => { + const value = JSON.stringify(executionContext); + const encoded = encodeURIComponent(value); + + // The max value is set to this arbitrary number because of the following reasons: + // 1. Maximum allowed length of the `baggage` header via which the execution context is passed is 4096 / 4 = 1024 characters. + // 2. The Execution Context Service adds labels (name, page and id) to the context additionally, which can increase the length + // Hence as a safe limit, we set the maximum length of the execution context to 900 characters. + const MAX_VALUE_ALLOWED = 900; + return encoded.length < MAX_VALUE_ALLOWED; }; export function initializeFetch({ diff --git a/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx b/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx index 8472000c315b7..6f7fa273fb1dd 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/initialize_search_embeddable_api.tsx @@ -46,6 +46,7 @@ const initializeSearchSource = async ( ]); searchSource.setParent(parentSearchSource); const dataView = searchSource.getField('index'); + return { searchSource, dataView }; }; diff --git a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts index 9a3d9c2307ff3..01a9b9c83700d 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts +++ b/src/platform/plugins/shared/discover/public/embeddable/utils/serialization_utils.ts @@ -63,13 +63,12 @@ export const deserializeState = async ({ } else { // by value const { byValueToSavedSearch } = discoverServices.savedSearch; - const savedSearch = await byValueToSavedSearch( - inject( - serializedState.rawState as unknown as EmbeddableStateWithType, - serializedState.references ?? [] - ) as SavedSearchUnwrapResult, - true - ); + const savedSearchUnwrappedResult = inject( + serializedState.rawState as unknown as EmbeddableStateWithType, + serializedState.references ?? [] + ) as SavedSearchUnwrapResult; + + const savedSearch = await byValueToSavedSearch(savedSearchUnwrappedResult, true); return { ...savedSearch, ...panelState, diff --git a/src/platform/plugins/shared/discover/public/plugin.tsx b/src/platform/plugins/shared/discover/public/plugin.tsx index ea8f8f9628e6a..237c2958c6dde 100644 --- a/src/platform/plugins/shared/discover/public/plugin.tsx +++ b/src/platform/plugins/shared/discover/public/plugin.tsx @@ -44,6 +44,7 @@ import { defaultCustomizationContext } from './customizations/defaults'; import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER, ACTION_VIEW_SAVED_SEARCH, + LEGACY_LOG_STREAM_EMBEDDABLE, } from './embeddable/constants'; import type { DiscoverCustomizationContext } from './customizations'; import { @@ -462,6 +463,21 @@ export class DiscoverPlugin discoverServices, }); }); + + // We register a specialized saved search embeddable factory for the log stream embeddable to support old log stream panels. + plugins.embeddable.registerReactEmbeddableFactory(LEGACY_LOG_STREAM_EMBEDDABLE, async () => { + const [startServices, discoverServices, { getLegacyLogStreamEmbeddableFactory }] = + await Promise.all([ + getStartServices(), + getDiscoverServicesForEmbeddable(), + getEmbeddableServices(), + ]); + + return getLegacyLogStreamEmbeddableFactory({ + startServices, + discoverServices, + }); + }); } } diff --git a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts index 7d9cff7eb303c..6379bfd0d17f4 100644 --- a/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts +++ b/src/platform/plugins/shared/discover/public/plugin_imports/embeddable_services.ts @@ -9,3 +9,4 @@ export { ViewSavedSearchAction } from '../embeddable/actions/view_saved_search_action'; export { getSearchEmbeddableFactory } from '../embeddable/get_search_embeddable_factory'; +export { getLegacyLogStreamEmbeddableFactory } from '../embeddable/get_legacy_log_stream_embeddable_factory'; diff --git a/src/platform/test/functional/apps/discover/embeddable/_log_stream_embeddable.ts b/src/platform/test/functional/apps/discover/embeddable/_log_stream_embeddable.ts new file mode 100644 index 0000000000000..391584a580266 --- /dev/null +++ b/src/platform/test/functional/apps/discover/embeddable/_log_stream_embeddable.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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 moment from 'moment'; +import { log, timerange } from '@kbn/apm-synthtrace-client'; +import path from 'path'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const synthtrace = getService('logSynthtraceEsClient'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const { dashboard, header, savedObjects } = getPageObjects([ + 'dashboard', + 'header', + 'savedObjects', + ]); + + const start = moment().subtract(30, 'minutes').valueOf(); + const end = moment().add(30, 'minutes').valueOf(); + + const spaceId = 'default'; + const importFileName = 'log_stream_dashboard_saved_object.ndjson'; + const importFilePath = path.join(__dirname, 'exports', importFileName); + + describe('dashboards with log stream embeddable', () => { + before(async () => { + await synthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(5) + .generator((timestamp: number, index: number) => + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset('synth.discover') + .namespace('default') + .logLevel('info') + .defaults({ + 'service.name': 'synth-discover', + }) + ), + ]); + + await savedObjects.importIntoSpace(importFilePath, spaceId); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await synthtrace.clean(); + }); + + it('should load the old log stream but with saved search embeddable', async () => { + await dashboard.navigateToApp(); + await dashboard.gotoDashboardLandingPage(); + + // Load saved dashboard with log stream embeddable + await dashboard.loadSavedDashboard('Logs stream dashboard test with Saves Search Embeddable'); + + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + + // Expect things from saved search embeddable to load + await testSubjects.existOrFail('unifiedDataTableToolbar'); + await testSubjects.existOrFail('dataGridHeader'); + }); + }); +} diff --git a/src/platform/test/functional/apps/discover/embeddable/exports/log_stream_dashboard_saved_object.ndjson b/src/platform/test/functional/apps/discover/embeddable/exports/log_stream_dashboard_saved_object.ndjson new file mode 100644 index 0000000000000..01fa518aaa2f7 --- /dev/null +++ b/src/platform/test/functional/apps/discover/embeddable/exports/log_stream_dashboard_saved_object.ndjson @@ -0,0 +1,2 @@ +{"attributes":{"controlGroupInput":{"chainingSystem":"HIERARCHICAL","controlStyle":"oneLine","ignoreParentSettingsJSON":"{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}","panelsJSON":"{}","showApplySelections":false},"description":"Dashboard to test Old Logs Stream Component","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"type\":\"LOG_STREAM_EMBEDDABLE\",\"title\":\"Log stream\",\"embeddableConfig\":{\"timeRange\":{\"from\":\"now-1d\",\"to\":\"now\"},\"enhancements\":{}},\"panelIndex\":\"007ae236-4c6a-4509-b3e6-0ea82a70a070\",\"gridData\":{\"i\":\"007ae236-4c6a-4509-b3e6-0ea82a70a070\",\"y\":0,\"x\":0,\"w\":29,\"h\":27}}]","timeRestore":false,"title":"Logs stream dashboard test with Saves Search Embeddable","version":3},"coreMigrationVersion":"8.8.0","created_at":"2025-06-16T11:03:19.212Z","created_by":"u_aL2aSMHLXN4K3J3Rnm1qbDWzXwMxid2fsn7Icg_S9eU_0","id":"c3448139-0e6a-4650-926c-7977a752930e","managed":false,"references":[],"type":"dashboard","typeMigrationVersion":"10.2.0","updated_at":"2025-06-16T14:23:15.285Z","updated_by":"u_aL2aSMHLXN4K3J3Rnm1qbDWzXwMxid2fsn7Icg_S9eU_0","version":"WzMxNzI5NywyM10="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} diff --git a/src/platform/test/functional/apps/discover/embeddable/index.ts b/src/platform/test/functional/apps/discover/embeddable/index.ts index 37f8d4eca7d4d..3e46cb4e9bc28 100644 --- a/src/platform/test/functional/apps/discover/embeddable/index.ts +++ b/src/platform/test/functional/apps/discover/embeddable/index.ts @@ -26,5 +26,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_saved_search_embeddable')); loadTestFile(require.resolve('./multiple_data_views')); + loadTestFile(require.resolve('./_log_stream_embeddable.ts')); }); } diff --git a/src/platform/test/functional/services/data_grid.ts b/src/platform/test/functional/services/data_grid.ts index d878f1c2f1cac..1a02a2b6c178a 100644 --- a/src/platform/test/functional/services/data_grid.ts +++ b/src/platform/test/functional/services/data_grid.ts @@ -880,6 +880,12 @@ export class DataGridService extends FtrService { }); } + public async waitForDataTableToLoad() { + await this.retry.try(async () => { + return await this.testSubjects.exists('docTable'); + }); + } + public async getComparisonDisplay() { const display = await this.testSubjects.find('unifiedDataTableComparisonDisplay'); return await display.getVisibleText(); diff --git a/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx b/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx index 45e8080e04b13..6e9bbef978dbf 100644 --- a/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx +++ b/x-pack/platform/packages/shared/logs-overview/src/components/log_category_details/log_category_document_examples_table.tsx @@ -42,7 +42,7 @@ export const LogCategoryDocumentExamplesTable: React.FC diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index a8bcc64096dcd..8f5775ef1f0cf 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -11709,7 +11709,6 @@ "xpack.apm.profiling.topnFunctions.link": "Accéder aux fonctions d'Universal Profiling", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "Aucune donnée disponible", "xpack.apm.propertiesTable.agentFeature.noResultFound": "Pas de résultats pour \"{value}\".", - "xpack.apm.propertiesTable.tabs.logs.serviceName": "Nom de service", "xpack.apm.propertiesTable.tabs.logsLabel": "Logs", "xpack.apm.propertiesTable.tabs.metadataLabel": "Métadonnées", "xpack.apm.propertiesTable.tabs.spanLinks": "Liens d'intervalle", @@ -24876,9 +24875,6 @@ "xpack.infra.hostsViewPage.tabs.alerts.countError": "Le nombre d'alertes actives n'a pas été récupéré correctement. Essayez de recharger la page.", "xpack.infra.hostsViewPage.tabs.alerts.title": "Alertes", "xpack.infra.hostsViewPage.tabs.logs.assetLogsWidgetName": "Logs de {type} \"{name}\"", - "xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel": "Chargement des entrées", - "xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName": "Logs par hôte", - "xpack.infra.hostsViewPage.tabs.logs.textFieldPlaceholder": "Rechercher les entrées de logs...", "xpack.infra.hostsViewPage.tabs.logs.title": "Logs", "xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "Ouvrir dans Lens", "xpack.infra.hostsViewPage.tabs.metricsCharts.title": "Indicateurs", @@ -25151,9 +25147,6 @@ "xpack.infra.logsSettingsPage.loadingButtonLabel": "Chargement", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "La maintenance des panneaux de flux de logs n'est plus assurée. Essayez d'utiliser {savedSearchDocsLink} pour une visualisation similaire.", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.discoverSessionsLinkLabel": "Sessions Discover", - "xpack.infra.logStreamEmbeddable.description": "Ajoutez un tableau de logs de diffusion en direct. Pour une expérience plus efficace, nous vous recommandons d'utiliser la page Découvrir pour créer une session Discovery au lieu d'utiliser Logs Stream.", - "xpack.infra.logStreamEmbeddable.displayName": "Logs Stream (déclassé)", - "xpack.infra.logStreamEmbeddable.title": "Flux de log", "xpack.infra.logStreamPageTemplate.backtoLogsStream": "Retour au flux de logs", "xpack.infra.logStreamPageTemplate.widgetBadge": "Widget", "xpack.infra.logStreamPageTemplate.widgetDescription": "Vous visionnez un widget intégré. Les modifications seront synchronisées avec l'URL, mais elles ne seront pas conservées dans la vue par défaut du flux de logs.", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 0d067ef9f91b5..e8f9a03820e5d 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -11692,7 +11692,6 @@ "xpack.apm.profiling.topnFunctions.link": "ユニバーサルプロファイリング機能に移動", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "利用可能なデータがありません", "xpack.apm.propertiesTable.agentFeature.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", - "xpack.apm.propertiesTable.tabs.logs.serviceName": "サービス名", "xpack.apm.propertiesTable.tabs.logsLabel": "ログ", "xpack.apm.propertiesTable.tabs.metadataLabel": "メタデータ", "xpack.apm.propertiesTable.tabs.spanLinks": "スパンリンク", @@ -24855,9 +24854,6 @@ "xpack.infra.hostsViewPage.tabs.alerts.countError": "アクティブアラート件数が正しく取得されませんでした。ページを再読み込みしてください。", "xpack.infra.hostsViewPage.tabs.alerts.title": "アラート", "xpack.infra.hostsViewPage.tabs.logs.assetLogsWidgetName": "{type} \"{name}\"からのログ", - "xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel": "エントリーを読み込み中", - "xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName": "ホスト別ログ", - "xpack.infra.hostsViewPage.tabs.logs.textFieldPlaceholder": "ログエントリーを検索...", "xpack.infra.hostsViewPage.tabs.logs.title": "ログ", "xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "Lensで開く", "xpack.infra.hostsViewPage.tabs.metricsCharts.title": "メトリック", @@ -25128,9 +25124,6 @@ "xpack.infra.logsSettingsPage.loadingButtonLabel": "読み込み中", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "ログストリームパネルは管理されていません。{savedSearchDocsLink}を同様の視覚化に活用してください。", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.discoverSessionsLinkLabel": "Discoverセッション", - "xpack.infra.logStreamEmbeddable.description": "ライブストリーミングログのテーブルを追加します。体験を効率化するために、ログストリームを使用するのではなく、検出ページを使用して、保存されたDiscoverセッションを作成することをお勧めします。", - "xpack.infra.logStreamEmbeddable.displayName": "ログストリーム(廃止予定)", - "xpack.infra.logStreamEmbeddable.title": "ログストリーム", "xpack.infra.logStreamPageTemplate.backtoLogsStream": "ログストリームに戻る", "xpack.infra.logStreamPageTemplate.widgetBadge": "ウィジェット", "xpack.infra.logStreamPageTemplate.widgetDescription": "埋め込まれたウィジェットを表示しています。変更はURLと同期されますが、デフォルトログストリームビューには永続しません。", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 29436b3cd3d38..1ac3c376bc9de 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -11718,7 +11718,6 @@ "xpack.apm.profiling.topnFunctions.link": "前往 Universal Profiling 函数", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "没有可用数据", "xpack.apm.propertiesTable.agentFeature.noResultFound": "没有“{value}”的结果。", - "xpack.apm.propertiesTable.tabs.logs.serviceName": "服务名称", "xpack.apm.propertiesTable.tabs.logsLabel": "日志", "xpack.apm.propertiesTable.tabs.metadataLabel": "元数据", "xpack.apm.propertiesTable.tabs.spanLinks": "跨度链接", @@ -24906,9 +24905,6 @@ "xpack.infra.hostsViewPage.tabs.alerts.countError": "未正确检索活动告警计数,请尝试重新加载页面。", "xpack.infra.hostsViewPage.tabs.alerts.title": "告警", "xpack.infra.hostsViewPage.tabs.logs.assetLogsWidgetName": "来自 {type}“{name}”的日志", - "xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel": "正在加载条目", - "xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName": "日志(按主机)", - "xpack.infra.hostsViewPage.tabs.logs.textFieldPlaceholder": "搜索日志条目......", "xpack.infra.hostsViewPage.tabs.logs.title": "日志", "xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "在 Lens 中打开", "xpack.infra.hostsViewPage.tabs.metricsCharts.title": "指标", @@ -25181,9 +25177,6 @@ "xpack.infra.logsSettingsPage.loadingButtonLabel": "正在加载", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription": "将不再维护日志流面板。尝试将 {savedSearchDocsLink} 用于类似可视化。", "xpack.infra.logsStreamEmbeddable.deprecationWarningDescription.discoverSessionsLinkLabel": "Discover 会话", - "xpack.infra.logStreamEmbeddable.description": "添加实时流式传输日志的表。为了获得更高效的体验,建议使用 Discover 页面创建保存的 Discover 会话,而不是使用日志流。", - "xpack.infra.logStreamEmbeddable.displayName": "日志流(已过时)", - "xpack.infra.logStreamEmbeddable.title": "日志流", "xpack.infra.logStreamPageTemplate.backtoLogsStream": "返回到日志流", "xpack.infra.logStreamPageTemplate.widgetBadge": "小组件", "xpack.infra.logStreamPageTemplate.widgetDescription": "您正在查看嵌入式小组件。更改将同步到 URL,但不会持续存在于默认日志流视图。", diff --git a/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx b/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx index 58fd6fe89c030..03aa594a5a8eb 100644 --- a/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx +++ b/x-pack/platform/plugins/shared/fleet/.storybook/context/index.tsx @@ -22,6 +22,8 @@ import type { import { CoreScopedHistory } from '@kbn/core/public'; import { coreFeatureFlagsMock } from '@kbn/core/public/mocks'; import { getStorybookContextProvider } from '@kbn/custom-integrations-plugin/storybook'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; @@ -74,6 +76,8 @@ export const StorybookContext: React.FC<{ optIn: () => {}, telemetryCounter$: EMPTY, }, + embeddable: {} as unknown as EmbeddableStart, + logsDataAccess: {} as unknown as LogsDataAccessPluginStart, application: getApplication(), executionContext: getExecutionContext(), featureFlags: coreFeatureFlagsMock.createStart(), diff --git a/x-pack/platform/plugins/shared/fleet/kibana.jsonc b/x-pack/platform/plugins/shared/fleet/kibana.jsonc index 823328da8ada6..4024c94ce0438 100644 --- a/x-pack/platform/plugins/shared/fleet/kibana.jsonc +++ b/x-pack/platform/plugins/shared/fleet/kibana.jsonc @@ -30,6 +30,7 @@ "dashboard", "fieldsMetadata", "logsDataAccess", + "embeddable", "spaces" ], "optionalPlugins": [ @@ -56,4 +57,4 @@ "common" ] } -} \ No newline at end of file +} diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx index 670294db542b6..c766443652e24 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx @@ -21,11 +21,17 @@ jest.mock('@kbn/kibana-utils-plugin/public', () => { }; }); -jest.mock('@kbn/logs-shared-plugin/public', () => { - return { - LogStream: () =>
, - }; -}); +jest.mock('@kbn/saved-search-component', () => ({ + LazySavedSearchComponent: (props: any) =>
, +})); + +jest.mock('@kbn/embeddable-plugin/public', () => ({ + ViewMode: { + VIEW: 'view', + EDIT: 'edit', + }, +})); + jest.mock('@kbn/logs-shared-plugin/common', () => { const originalModule = jest.requireActual('@kbn/logs-shared-plugin/common'); return { @@ -100,21 +106,60 @@ describe('AgentLogsUI', () => { return renderer.render(); }; - const mockStartServices = (isServerlessEnabled?: boolean) => { - mockUseStartServices.mockReturnValue({ - application: {}, - data: { - query: { - timefilter: { - timefilter: { - calculateBounds: jest.fn().mockReturnValue({ - min: '2023-10-04T13:08:53.340Z', - max: '2023-10-05T13:08:53.340Z', - }), - }, - }, + const mockLogSources = { + services: { + logSourcesService: { + getFlattenedLogSources: jest.fn().mockResolvedValue({ + id: 'logs-*', + title: 'Logs', + }), + }, + }, + }; + + const mockData = { + query: { + timefilter: { + timefilter: { + calculateBounds: jest.fn().mockReturnValue({ + min: new Date('2023-04-20T14:00:00.340Z'), + max: new Date('2023-04-20T14:20:00.340Z'), + }), }, }, + }, + search: { + searchSource: { + create: jest.fn(), + }, + }, + dataViews: { + create: jest.fn(), + }, + }; + + const mockEmbeddable = { + EmbeddablePanel: jest.fn().mockImplementation(({ children }) =>
{children}
), + }; + + const mockApplication = { + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: {}, + }, + }; + + const mockStartServices = (isServerlessEnabled?: boolean) => { + mockUseStartServices.mockImplementation(() => ({ + application: mockApplication, + data: mockData, + embeddable: mockEmbeddable, + dataViews: mockData.dataViews, + logsDataAccess: mockLogSources, + searchSource: mockData.search.searchSource, + isServerlessEnabled: isServerlessEnabled || false, share: { url: { locators: { @@ -124,7 +169,7 @@ describe('AgentLogsUI', () => { }, }, }, - }); + })); }; it('should render Open in Logs button if privileges are set', () => { @@ -146,35 +191,4 @@ describe('AgentLogsUI', () => { const result = renderComponent(); expect(result.queryByTestId('viewInLogsBtn')).not.toBeInTheDocument(); }); - - it('should show log level dropdown with correct value', () => { - mockStartServices(); - const result = renderComponent(); - const logLevelDropdown = result.getByTestId('selectAgentLogLevel'); - expect(logLevelDropdown.getElementsByTagName('option').length).toBe(4); - expect(logLevelDropdown).toHaveDisplayValue('debug'); - }); - - it('should always show apply log level changes button', () => { - mockStartServices(); - const result = renderComponent(); - const applyLogLevelBtn = result.getByTestId('applyLogLevelBtn'); - expect(applyLogLevelBtn).toBeInTheDocument(); - expect(applyLogLevelBtn).not.toHaveAttribute('disabled'); - }); - - it('should hide reset log level button for agents version < 8.15.0', () => { - mockStartServices(); - const result = renderComponent(); - const resetLogLevelBtn = result.queryByTestId('resetLogLevelBtn'); - expect(resetLogLevelBtn).not.toBeInTheDocument(); - }); - - it('should show reset log level button for agents version >= 8.15.0', () => { - mockStartServices(); - const result = renderComponent({ agentVersion: '8.15.0' }); - const resetLogLevelBtn = result.getByTestId('resetLogLevelBtn'); - expect(resetLogLevelBtn).toBeInTheDocument(); - expect(resetLogLevelBtn).not.toHaveAttribute('disabled'); - }); }); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 1cb3fc4461118..5af7d9ad61abf 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { memo, useMemo, useState, useCallback, useEffect } from 'react'; +import React, { memo, useMemo, useState, useCallback } from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, @@ -15,7 +15,6 @@ import { EuiCallOut, EuiLink, } from '@elastic/eui'; -import useMeasure from 'react-use/lib/useMeasure'; import { FormattedMessage } from '@kbn/i18n-react'; import { fromKueryExpression } from '@kbn/es-query'; import semverGte from 'semver/functions/gte'; @@ -24,17 +23,16 @@ import semverCoerce from 'semver/functions/coerce'; import { createStateContainerReactHelpers } from '@kbn/kibana-utils-plugin/public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import type { TimeRange } from '@kbn/es-query'; -import { LogStream, type LogStreamProps } from '@kbn/logs-shared-plugin/public'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import useAsync from 'react-use/lib/useAsync'; import type { Agent, AgentPolicy } from '../../../../../types'; import { useLink, useStartServices } from '../../../../../hooks'; -import { DEFAULT_DATE_RANGE } from './constants'; import { DatasetFilter } from './filter_dataset'; import { LogLevelFilter } from './filter_log_level'; import { LogQueryBar } from './query_bar'; import { buildQuery } from './build_query'; -import { SelectLogLevel } from './select_log_level'; import { ViewLogsButton, getFormattedRange } from './view_logs_button'; const WrapperFlexGroup = styled(EuiFlexGroup)` @@ -45,19 +43,6 @@ const DatePickerFlexItem = styled(EuiFlexItem)` max-width: 312px; `; -const LOG_VIEW_SETTINGS: LogStreamProps['logView'] = { - type: 'log-view-reference', - logViewId: 'default', -}; - -const LOG_VIEW_COLUMNS: LogStreamProps['columns'] = [ - { type: 'timestamp' }, - { field: 'event.dataset', type: 'field' }, - { field: 'component.id', type: 'field' }, - { type: 'message' }, - { field: 'error.message', type: 'field' }, -]; - export interface AgentLogsProps { agent: Agent; agentPolicy?: AgentPolicy; @@ -120,13 +105,29 @@ const AgentPolicyLogsNotEnabledCallout: React.FunctionComponent<{ agentPolicy: A export const AgentLogsUI: React.FunctionComponent = memo( ({ agent, agentPolicy, state }) => { - const { data, application } = useStartServices(); + const { + application, + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + data: { + search: { searchSource }, + query: { + timefilter: { timefilter: dataTimefilter }, + }, + dataViews, + }, + } = useStartServices(); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); // Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream) const getDateRangeTimestamps = useCallback( (timeRange: TimeRange) => { - const { min, max } = data.query.timefilter.timefilter.calculateBounds(timeRange); + const { min, max } = dataTimefilter.calculateBounds(timeRange); return min && max ? { start: min.valueOf(), @@ -134,7 +135,7 @@ export const AgentLogsUI: React.FunctionComponent = memo( } : undefined; }, - [data.query.timefilter.timefilter] + [dataTimefilter] ); const tryUpdateDateRange = useCallback( @@ -149,36 +150,6 @@ export const AgentLogsUI: React.FunctionComponent = memo( }, [getDateRangeTimestamps, updateState] ); - - const [dateRangeTimestamps, setDateRangeTimestamps] = useState<{ start: number; end: number }>( - getDateRangeTimestamps({ - from: state.start, - to: state.end, - }) || - getDateRangeTimestamps({ - from: DEFAULT_DATE_RANGE.start, - to: DEFAULT_DATE_RANGE.end, - })! - ); - - // Attempts to parse for timestamps when start/end date expressions change - // If invalid date expressions, set expressions back to default - // Otherwise set the new timestamps - useEffect(() => { - const timestampsFromDateRange = getDateRangeTimestamps({ - from: state.start, - to: state.end, - }); - if (!timestampsFromDateRange) { - tryUpdateDateRange({ - from: DEFAULT_DATE_RANGE.start, - to: DEFAULT_DATE_RANGE.end, - }); - } else { - setDateRangeTimestamps(timestampsFromDateRange); - } - }, [state.start, state.end, getDateRangeTimestamps, tryUpdateDateRange]); - // Query validation helper const isQueryValid = useCallback((testQuery: string) => { try { @@ -209,13 +180,15 @@ export const AgentLogsUI: React.FunctionComponent = memo( // Build final log stream query from agent id, datasets, log levels, and user input const logStreamQuery = useMemo( - () => - buildQuery({ + () => ({ + language: 'kuery', + query: buildQuery({ agentId: agent.id, datasets: state.datasets, logLevels: state.logLevels, userQuery: state.query, }), + }), [agent.id, state.datasets, state.logLevels, state.query] ); @@ -231,14 +204,6 @@ export const AgentLogsUI: React.FunctionComponent = memo( return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); - // Set absolute height on logs component (needed to render correctly in Safari) - // based on available height, or 600px, whichever is greater - const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); - const logPanelHeight = useMemo( - () => Math.max(measuredlogPanelHeight, 600), - [measuredlogPanelHeight] - ); - if (!isLogFeatureAvailable) { return ( = memo( }} > @@ -337,23 +302,32 @@ export const AgentLogsUI: React.FunctionComponent = memo( - - + + {logSources.value ? ( + + ) : null} - - - ); } diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx index 788994a5d9965..685299ca90cc6 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx @@ -58,7 +58,7 @@ export const ViewLogsButton: React.FunctionComponent = ({ }); }, [endTime, logStreamQuery, discoverLogsLocator, startTime]); - return authz.fleet.readAgents && discoverLogsLocator ? ( + return authz.fleet.readAgents && logsUrl ? ( { const cloud = cloudMock.createSetup(); @@ -35,5 +37,7 @@ export const createStartDepsMock = () => { customIntegrations: customIntegrationsMock.createStart(), share: sharePluginMock.createStartContract(), cloud: cloudMock.createStart(), + embeddable: embeddablePluginMock.createStartContract(), + logsDataAccess: logsDataAccessPluginMock.createStartContract(), }; }; diff --git a/x-pack/platform/plugins/shared/fleet/public/plugin.ts b/x-pack/platform/plugins/shared/fleet/public/plugin.ts index ced047f7cc0c4..998e3d27c0952 100644 --- a/x-pack/platform/plugins/shared/fleet/public/plugin.ts +++ b/x-pack/platform/plugins/shared/fleet/public/plugin.ts @@ -54,6 +54,8 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { Subject } from 'rxjs'; +import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; + import type { FleetAuthz } from '../common'; import { appRoutesService, INTEGRATIONS_PLUGIN_ID, PLUGIN_ID, setupRouteService } from '../common'; import { @@ -88,6 +90,7 @@ import type { import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; import { setCustomIntegrations, setCustomIntegrationsStart } from './services/custom_integrations'; import { getFleetDeepLinks } from './deep_links'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; export type { FleetConfigType } from '../common/types'; @@ -136,6 +139,8 @@ export interface FleetStartDeps { cloud?: CloudStart; usageCollection?: UsageCollectionStart; guidedOnboarding?: GuidedOnboardingPluginStart; + embeddable: EmbeddableStart; + logsDataAccess: LogsDataAccessPluginStart; } export interface FleetStartServices extends CoreStart, Exclude { diff --git a/x-pack/platform/plugins/shared/fleet/tsconfig.json b/x-pack/platform/plugins/shared/fleet/tsconfig.json index f10f0b7c67dd8..a2f0dda7e6a39 100644 --- a/x-pack/platform/plugins/shared/fleet/tsconfig.json +++ b/x-pack/platform/plugins/shared/fleet/tsconfig.json @@ -120,5 +120,8 @@ "@kbn/core-security-server", "@kbn/core-http-server-utils", "@kbn/handlebars", + "@kbn/saved-search-component", + "@kbn/logs-data-access-plugin", + "@kbn/embeddable-plugin", ] } diff --git a/x-pack/platform/plugins/shared/logs_data_access/public/mocks.ts b/x-pack/platform/plugins/shared/logs_data_access/public/mocks.ts new file mode 100644 index 0000000000000..66b2eefc7c6b4 --- /dev/null +++ b/x-pack/platform/plugins/shared/logs_data_access/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type LogsDataAccessPluginStart } from './plugin'; + +export type Start = jest.Mocked; + +const createStartContract = (): Start => { + const startContract: Start = { + services: { + logSourcesService: { + getLogSources: jest.fn(), + getFlattenedLogSources: jest.fn(), + setLogSources: jest.fn(), + }, + }, + }; + return startContract; +}; + +export const logsDataAccessPluginMock = { + createStartContract, +}; diff --git a/x-pack/platform/plugins/shared/logs_shared/common/index.ts b/x-pack/platform/plugins/shared/logs_shared/common/index.ts index b2411b4062a0e..9e14204abfb7f 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/index.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/index.ts @@ -102,6 +102,7 @@ export { getLogsLocatorsFromUrlService, getTimeRangeEndFromTime, getTimeRangeStartFromTime, + getNodeQuery, } from './locators'; export type { DiscoverLogsLocatorParams, diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx index b313aa287eaed..26d304c88f9a4 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_logs/index.tsx @@ -93,7 +93,7 @@ export function ClassicServiceLogsStream() { index={logSources.value} timeRange={timeRange} query={query} - height={'60vh'} + height="60vh" displayOptions={{ solutionNavIdOverride: 'oblt', enableDocumentViewer: true, diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx index 97f21eee2d433..276a1235bd6b3 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx @@ -7,8 +7,10 @@ import { EuiSpacer, EuiTab, EuiTabs, EuiSkeletonText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; import React, { useMemo } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import { useKibana } from '../../../../context/kibana_context/use_kibana'; import type { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { TransactionMetadata } from '../../../shared/metadata_table/transaction_metadata'; import { WaterfallContainer } from './waterfall_container'; @@ -161,29 +163,55 @@ function LogsTabContent({ duration: number; traceId: string; }) { + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + }, + } = useKibana(); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const startTimestamp = Math.floor(timestamp / 1000); const endTimestamp = Math.ceil(startTimestamp + duration / 1000); const framePaddingMs = 1000 * 60 * 60 * 24; // 24 hours - return ( - + + const rangeFrom = new Date(startTimestamp - framePaddingMs).toISOString(); + const rangeTo = new Date(endTimestamp + framePaddingMs).toISOString(); + + const timeRange = useMemo(() => { + return { + from: rangeFrom, + to: rangeTo, + }; + }, [rangeFrom, rangeTo]); + + const query = useMemo( + () => ({ + language: 'kuery', + query: `trace.id:"${traceId}" OR (not trace.id:* AND "${traceId}")`, + }), + [traceId] ); + + return logSources.value ? ( + + ) : null; } diff --git a/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index c8000a2f7f5c6..1636558b89713 100644 --- a/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -7,16 +7,19 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; -import type { LogViewReference } from '@kbn/logs-shared-plugin/common'; -import { DEFAULT_LOG_VIEW, getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; +import { + DEFAULT_LOG_VIEW, + getLogsLocatorsFromUrlService, + getNodeQuery, + type LogViewReference, +} from '@kbn/logs-shared-plugin/common'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; import { OpenInLogsExplorerButton } from '@kbn/logs-shared-plugin/public'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import useAsync from 'react-use/lib/useAsync'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { InfraLoadingPanel } from '../../../loading'; import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDataViewsContext } from '../../hooks/use_data_views'; import { useDatePickerContext } from '../../hooks/use_date_picker'; @@ -32,13 +35,27 @@ export const Logs = () => { const { asset } = useAssetDetailsRenderPropsContext(); const { logs } = useDataViewsContext(); - const { loading: logViewLoading, reference: logViewReference } = logs ?? {}; + const { reference: logViewReference } = logs ?? {}; - const { services } = useKibanaContextForPlugin(); - const { nodeLogsLocator } = getLogsLocatorsFromUrlService(services.share.url); + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + share: { url }, + }, + } = useKibanaContextForPlugin(); + const { logsLocator } = getLogsLocatorsFromUrlService(url)!; const [textQuery, setTextQuery] = useState(urlState?.logsSearch ?? ''); const [textQueryDebounced, setTextQueryDebounced] = useState(urlState?.logsSearch ?? ''); + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const currentTimestamp = getDateRangeInTimestamp().to; const state = useIntersectingState(ref, { currentTimestamp, @@ -78,14 +95,28 @@ export const Logs = () => { ); const logsUrl = useMemo(() => { - return nodeLogsLocator.getRedirectUrl({ + const nodeQuery = getNodeQuery({ nodeField: findInventoryFields(asset.type).id, nodeId: asset.id, - time: state.startTimestamp, filter: textQueryDebounced, + }); + return logsLocator.getRedirectUrl({ + filter: nodeQuery.query, + timeRange: { + startTime: state.startTimestamp, + endTime: state.currentTimestamp, + }, logView, }); - }, [nodeLogsLocator, asset.id, asset.type, state.startTimestamp, textQueryDebounced, logView]); + }, [ + logsLocator, + asset.id, + asset.type, + state.startTimestamp, + state.currentTimestamp, + textQueryDebounced, + logView, + ]); return ( @@ -114,34 +145,20 @@ export const Logs = () => { - {logViewLoading || !logViewReference ? ( - - } - /> - ) : ( - - )} + ) : null} ); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx index 66bfb37da357b..f4df4bbc011d4 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/page_content.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks } from '@elast import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Routes, Route } from '@kbn/shared-ux-router'; -import { useKibana, useUiSetting } from '@kbn/kibana-react-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-shared-plugin/public'; import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; import { @@ -18,10 +18,9 @@ import { type ObservabilityOnboardingLocatorParams, } from '@kbn/deeplinks-observability'; import { dynamic } from '@kbn/shared-ux-utility'; -import { safeDecode } from '@kbn/rison'; -import type { LogsLocatorParams } from '@kbn/logs-shared-plugin/common'; import { isDevMode } from '@kbn/xstate-utils'; -import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; +import type { LogsLocatorParams } from '@kbn/logs-shared-plugin/common'; +import { safeDecode } from '@kbn/rison'; import { LazyAlertDropdownWrapper } from '../../alerting/log_threshold'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; @@ -30,16 +29,18 @@ import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params' import { NotFoundPage } from '../404'; import { getLogsAppRoutes } from './routes'; -const StreamPage = dynamic(() => import('./stream').then((mod) => ({ default: mod.StreamPage }))); const LogEntryCategoriesPage = dynamic(() => import('./log_entry_categories').then((mod) => ({ default: mod.LogEntryCategoriesPage })) ); + const LogEntryRatePage = dynamic(() => import('./log_entry_rate').then((mod) => ({ default: mod.LogEntryRatePage })) ); + const LogsSettingsPage = dynamic(() => import('./settings').then((mod) => ({ default: mod.LogsSettingsPage })) ); + const StateMachinePlayground = dynamic(() => import('../../observability_logs/xstate_helpers').then((mod) => ({ default: mod.StateMachinePlayground, @@ -49,8 +50,6 @@ const StateMachinePlayground = dynamic(() => export const LogsPageContent: React.FunctionComponent = () => { const { application, share } = useKibana<{ share: SharePublicStart }>().services; - const isLogsStreamEnabled: boolean = useUiSetting(OBSERVABILITY_ENABLE_LOGS_STREAM, false); - const uiCapabilities = application?.capabilities; const onboardingLocator = share?.url.locators.get( OBSERVABILITY_ONBOARDING_LOCATOR @@ -60,7 +59,7 @@ export const LogsPageContent: React.FunctionComponent = () => { const enableDeveloperRoutes = isDevMode(); useReadOnlyBadge(!uiCapabilities?.logs?.save); - const routes = getLogsAppRoutes({ isLogsStreamEnabled }); + const routes = getLogsAppRoutes(); const settingsLinkProps = useLinkProps({ app: 'logs', @@ -94,34 +93,30 @@ export const LogsPageContent: React.FunctionComponent = () => { )} - {routes.stream ? ( - - ) : ( - { - const searchParams = new URLSearchParams(props.location.search); - const logFilterEncoded = searchParams.get('logFilter'); - let locatorParams: LogsLocatorParams = {}; - - if (logFilterEncoded) { - const logFilter = safeDecode(logFilterEncoded) as LogsLocatorParams; - locatorParams = { - timeRange: logFilter?.timeRange, - query: logFilter?.query, - filters: logFilter?.filters, - refreshInterval: logFilter?.refreshInterval, - }; - } - - share.url.locators - .get(ALL_DATASETS_LOCATOR_ID) - ?.navigate(locatorParams); - return null; - }} - /> - )} + { + const searchParams = new URLSearchParams(props.location.search); + const logFilterEncoded = searchParams.get('logFilter'); + let locatorParams: LogsLocatorParams = {}; + + if (logFilterEncoded) { + const logFilter = safeDecode(logFilterEncoded) as LogsLocatorParams; + locatorParams = { + timeRange: logFilter?.timeRange, + query: logFilter?.query, + filters: logFilter?.filters, + refreshInterval: logFilter?.refreshInterval, + }; + } + + share.url.locators + .get(ALL_DATASETS_LOCATOR_ID) + ?.navigate(locatorParams); + return null; + }} + /> @@ -130,11 +125,7 @@ export const LogsPageContent: React.FunctionComponent = () => { )} - + } /> diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/routes.ts b/x-pack/solutions/observability/plugins/infra/public/pages/logs/routes.ts index a5c38672a8bed..08decc1ce4725 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/routes.ts +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/routes.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - logsAnomaliesTitle, - logCategoriesTitle, - settingsTitle, - streamTitle, -} from '../../translations'; +import { logsAnomaliesTitle, logCategoriesTitle, settingsTitle } from '../../translations'; export interface LogsRoute { id: string; @@ -22,10 +17,9 @@ export interface LogsAppRoutes { logsAnomalies: LogsRoute; logsCategories: LogsRoute; settings: LogsRoute; - stream?: LogsRoute; } -export const getLogsAppRoutes = ({ isLogsStreamEnabled }: { isLogsStreamEnabled: boolean }) => { +export const getLogsAppRoutes = () => { const routes: LogsAppRoutes = { logsAnomalies: { id: 'anomalies', @@ -44,13 +38,5 @@ export const getLogsAppRoutes = ({ isLogsStreamEnabled }: { isLogsStreamEnabled: }, }; - if (isLogsStreamEnabled) { - routes.stream = { - id: 'stream', - title: streamTitle, - path: '/stream', - }; - } - return routes; }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx index 19856622f01aa..d305d03cf5bc6 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -8,9 +8,10 @@ import { EuiFlexGroup, EuiFlexItem, + EuiIcon, + EuiLink, EuiModal, EuiText, - EuiTextColor, EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -18,28 +19,53 @@ import { isEmpty } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import type { LogEntry } from '@kbn/logs-shared-plugin/common'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; +import useAsync from 'react-use/lib/useAsync'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import { i18n } from '@kbn/i18n'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { useDatePickerContext } from '../../../components/asset_details/hooks/use_date_picker'; import { useViewLogInProviderContext } from '../../../containers/logs/view_log_in_context'; import { useViewportDimensions } from '../../../hooks/use_viewport_dimensions'; const MODAL_MARGIN = 25; export const PageViewLogInContext: React.FC = () => { - const [{ contextEntry, startTimestamp, endTimestamp, logViewReference }, { setContextEntry }] = - useViewLogInProviderContext(); + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + share: { url }, + }, + } = useKibanaContextForPlugin(); + + const { dateRange } = useDatePickerContext(); + const { logsLocator } = getLogsLocatorsFromUrlService(url); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + const [{ contextEntry }, { setContextEntry }] = useViewLogInProviderContext(); const closeModal = useCallback(() => setContextEntry(undefined), [setContextEntry]); const { width: vw, height: vh } = useViewportDimensions(); const contextQuery = useMemo(() => { if (contextEntry && !isEmpty(contextEntry.context)) { - return Object.entries(contextEntry.context).reduce((kuery, [key, value]) => { - const currentExpression = `${key} : "${value}"`; - if (kuery.length > 0) { - return `${kuery} AND ${currentExpression}`; - } else { - return currentExpression; - } - }, ''); + return { + language: 'kuery', + query: Object.entries(contextEntry.context).reduce((kuery, [key, value]) => { + const currentExpression = `${key} : "${value}"`; + if (kuery.length > 0) { + return `${kuery} AND ${currentExpression}`; + } else { + return currentExpression; + } + }, ''), + }; } }, [contextEntry]); @@ -47,23 +73,38 @@ export const PageViewLogInContext: React.FC = () => { return null; } + const locatorTimeRange = { + startTime: new Date(dateRange.from).getTime(), + endTime: new Date(dateRange.to).getTime(), + }; + + const discoverLink = logsLocator?.getRedirectUrl({ + timeRange: locatorTimeRange, + filter: contextQuery?.query, + }); + return ( - + - + - + {logSources.value ? ( + + ) : null} @@ -78,7 +119,10 @@ const LogInContextWrapper = euiStyled.div<{ width: number | string; height: numb max-height: 75vh; // Same as EuiModal `; -const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context }) => { +const LogEntryContext: React.FC<{ context: LogEntry['context']; discoverLink?: string }> = ({ + context, + discoverLink, +}) => { let text; if ('container.id' in context) { text = ( @@ -112,10 +156,36 @@ const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context } } return ( - -

- {text} -

-
+ + + +

{text}

+
+
+ {discoverLink && ( + + + + + + + + + {i18n.translate('xpack.infra.logs.viewInContext.openInDiscoverLabel', { + defaultMessage: 'Open in Discover', + })} + + + + + + )} +
); }; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx deleted file mode 100644 index d594f02d36ce5..0000000000000 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 React from 'react'; -import type { LogViewReference } from '@kbn/logs-shared-plugin/common'; -import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; -import { OpenInLogsExplorerButton } from '@kbn/logs-shared-plugin/public'; -import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; - -interface LogsLinkToStreamProps { - startTime: number; - endTime: number; - query: string; - logView: LogViewReference; -} - -export const LogsLinkToStream = ({ startTime, endTime, query, logView }: LogsLinkToStreamProps) => { - const { services } = useKibanaContextForPlugin(); - const { share } = services; - const { logsLocator } = getLogsLocatorsFromUrlService(share.url); - - return ( - - ); -}; diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 577fcf0dfe528..e72986455d0f0 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -6,19 +6,18 @@ */ import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { LogStream } from '@kbn/logs-shared-plugin/public'; import React, { useMemo } from 'react'; -import { InfraLoadingPanel } from '../../../../../../components/loading'; +import { LazySavedSearchComponent } from '@kbn/saved-search-component'; +import useAsync from 'react-use/lib/useAsync'; +import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common'; +import { OpenInLogsExplorerButton } from '@kbn/logs-shared-plugin/public'; import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana'; -import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference'; import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; -import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; -import { LogsLinkToStream } from './logs_link_to_stream'; +import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state'; import { LogsSearchBar } from './logs_search_bar'; export const LogsTabContent = () => { @@ -28,91 +27,114 @@ export const LogsTabContent = () => { }, } = useKibanaContextForPlugin(); const isLogsOverviewEnabled = LogsOverview.useIsEnabled(); - if (isLogsOverviewEnabled) { - return ; - } else { - return ; - } + return isLogsOverviewEnabled ? : ; }; -export const LogsTabLogStreamContent = () => { - const [filterQuery] = useLogsSearchUrlState(); - const { getDateRangeAsTimestamp } = useUnifiedSearchContext(); - const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); +export const LogsSavedSearchComponent = () => { + const { + services: { + logsDataAccess: { + services: { logSourcesService }, + }, + embeddable, + dataViews, + data: { + search: { searchSource }, + }, + share: { url }, + }, + } = useKibanaContextForPlugin(); + + const logSources = useAsync(logSourcesService.getFlattenedLogSources); + + const { logsLocator } = getLogsLocatorsFromUrlService(url); + + const { + getDateRangeAsTimestamp, + parsedDateRange: { from, to }, + } = useUnifiedSearchContext(); + const { hostNodes, loading } = useHostsViewContext(); - const hostsFilterQuery = useMemo( - () => - buildCombinedAssetFilter({ - field: 'host.name', - values: hostNodes.map((p) => p.name), - }), - [hostNodes] - ); + const [filterQuery] = useLogsSearchUrlState(); - const { logViewReference: logView, loading: logViewLoading } = useLogViewReference({ - id: 'hosts-logs-view', - name: i18n.translate('xpack.infra.hostsViewPage.tabs.logs.LogsByHostWidgetName', { - defaultMessage: 'Logs by host', - }), - extraFields: ['host.name'], - }); + const hostsFilterQuery = useMemo(() => { + const hostsQueryPart = hostNodes.length + ? hostNodes.map((node) => `host.name: "${node.name}"`).join(' or ') + : ''; + + const urlQueryPart = filterQuery?.query ? String(filterQuery.query) : ''; + + const parts = [] as string[]; + if (hostsQueryPart) parts.push(hostsQueryPart); + if (urlQueryPart) parts.push(`(${urlQueryPart})`); - const logsLinkToStreamQuery = useMemo(() => { - const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes.map((p) => p.name)); + return { + language: 'kuery', + query: parts.join(' and '), + }; + }, [hostNodes, filterQuery]); - if (filterQuery.query && hostsFilterQueryParam) { - return `${filterQuery.query} and ${hostsFilterQueryParam}`; - } + const memoizedTimeRange = useMemo(() => { + const { from: startTime, to: endTime } = getDateRangeAsTimestamp(); + return { startTime, endTime }; + }, [getDateRangeAsTimestamp]); - return filterQuery.query || hostsFilterQueryParam; - }, [filterQuery.query, hostNodes]); + const memoizedTimeRangeForSavedSearch = useMemo(() => ({ from, to }), [from, to]); + + const discoverLink = logsLocator?.getRedirectUrl({ + timeRange: memoizedTimeRange, + filter: hostsFilterQuery.query, + }); - if (loading || logViewLoading || !logView) { - return ; + if (!hostNodes.length && !loading) { + return ; } - return ( + return logSources.value ? ( - - - ); + ) : null; }; -const createHostsFilterQueryParam = (hostNodes: string[]): string => { - if (!hostNodes.length) { - return ''; - } - - const joinedHosts = hostNodes.join(' or '); - const hostsQueryParam = `host.name:(${joinedHosts})`; - - return hostsQueryParam; -}; +const LogsTabNoResults = () => ( + + + + + + + + + +); const LogsTabLogsOverviewContent = () => { const { @@ -146,20 +168,3 @@ const LogsTabLogsOverviewContent = () => { return ; } }; - -const LogsTabLoadingContent = () => ( - - - - } - /> - - -); diff --git a/x-pack/solutions/observability/plugins/infra/public/plugin.ts b/x-pack/solutions/observability/plugins/infra/public/plugin.ts index 18be51e566f79..423531e355cc6 100644 --- a/x-pack/solutions/observability/plugins/infra/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/infra/public/plugin.ts @@ -27,24 +27,18 @@ import { map, firstValueFrom, } from 'rxjs'; -import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { apiCanAddNewPanel } from '@kbn/presentation-containers'; -import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; -import { ADD_PANEL_OTHER_GROUP } from '@kbn/embeddable-plugin/public'; import { ASSET_DETAILS_LOCATOR_ID, INVENTORY_LOCATOR_ID, type AssetDetailsLocatorParams, type InventoryLocatorParams, } from '@kbn/observability-shared-plugin/common'; -import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import type { NavigationEntry } from '@kbn/observability-shared-plugin/public'; import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability/constants'; import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; import { createMetricThresholdRuleType } from './alerting/metric_threshold'; -import { ADD_LOG_STREAM_ACTION_ID, LOG_STREAM_EMBEDDABLE } from './components/log_stream/constants'; import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers'; import { registerFeatures } from './register_feature'; import { InventoryViewsService } from './services/inventory_views'; @@ -59,7 +53,6 @@ import type { InfraClientStartExports, } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; -import type { LogStreamSerializedState } from './components/log_stream/types'; import { hostsTitle, inventoryTitle, @@ -92,8 +85,6 @@ export class Plugin implements InfraClientPluginClass { } setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) { - const isLogsStreamEnabled = core.uiSettings.get(OBSERVABILITY_ENABLE_LOGS_STREAM, false); - if (pluginsSetup.home) { registerFeatures(pluginsSetup.home); } @@ -139,7 +130,7 @@ export class Plugin implements InfraClientPluginClass { ) ); - const logRoutes = getLogsAppRoutes({ isLogsStreamEnabled }); + const logRoutes = getLogsAppRoutes(); /** !! Need to be kept in sync with the deepLinks in x-pack/solutions/observability/plugins/infra/public/plugin.ts */ pluginsSetup.observabilityShared.navigation.registerSections( @@ -194,18 +185,6 @@ export class Plugin implements InfraClientPluginClass { ) ); - pluginsSetup.embeddable.registerReactEmbeddableFactory(LOG_STREAM_EMBEDDABLE, async () => { - const { getLogStreamEmbeddableFactory } = await import( - './components/log_stream/log_stream_react_embeddable' - ); - const [coreStart, pluginDeps, pluginStart] = await core.getStartServices(); - return getLogStreamEmbeddableFactory({ - coreStart, - pluginDeps, - pluginStart, - }); - }); - pluginsSetup.observability.observabilityRuleTypeRegistry.register( createLogThresholdRuleType(core, pluginsSetup.share.url) ); @@ -334,50 +313,11 @@ export class Plugin implements InfraClientPluginClass { } start(core: InfraClientCoreStart, plugins: InfraClientStartDeps) { - const { http, uiSettings } = core; - const isLogsStreamEnabled = uiSettings.get(OBSERVABILITY_ENABLE_LOGS_STREAM, false); + const { http } = core; const inventoryViews = this.inventoryViews.start({ http }); const metricsExplorerViews = this.metricsExplorerViews?.start({ http }); const telemetry = this.telemetry.start(); - if (isLogsStreamEnabled) { - plugins.uiActions.registerAction({ - id: ADD_LOG_STREAM_ACTION_ID, - grouping: [ADD_PANEL_OTHER_GROUP], - order: 30, - getDisplayName: () => - i18n.translate('xpack.infra.logStreamEmbeddable.displayName', { - defaultMessage: 'Log stream (deprecated)', - }), - getDisplayNameTooltip: () => - i18n.translate('xpack.infra.logStreamEmbeddable.description', { - defaultMessage: - 'Add a table of live streaming logs. For a more efficient experience, we recommend using the Discover Page to create a saved Discover session instead of using Log stream.', - }), - getIconType: () => 'logsApp', - isCompatible: async ({ embeddable }) => { - return apiCanAddNewPanel(embeddable); - }, - execute: async ({ embeddable }) => { - if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError(); - embeddable.addNewPanel( - { - panelType: LOG_STREAM_EMBEDDABLE, - serializedState: { - rawState: { - title: i18n.translate('xpack.infra.logStreamEmbeddable.title', { - defaultMessage: 'Log stream', - }), - }, - }, - }, - true - ); - }, - }); - plugins.uiActions.attachAction(ADD_PANEL_TRIGGER, ADD_LOG_STREAM_ACTION_ID); - } - const startContract: InfraClientStartExports = { inventoryViews, metricsExplorerViews, @@ -412,13 +352,10 @@ const getLogsNavigationEntries = ({ }); } - // Display Stream nav entry when Logs Stream is enabled - if (routes.stream) entries.push(createNavEntryFromRoute(routes.stream)); // Display always Logs Anomalies and Logs Categories entries entries.push(createNavEntryFromRoute(routes.logsAnomalies)); entries.push(createNavEntryFromRoute(routes.logsCategories)); - // Display Logs Settings entry when Logs Stream is not enabled - if (!routes.stream) entries.push(createNavEntryFromRoute(routes.settings)); + entries.push(createNavEntryFromRoute(routes.settings)); return entries; }; diff --git a/x-pack/solutions/observability/plugins/infra/public/types.ts b/x-pack/solutions/observability/plugins/infra/public/types.ts index e8011799f635e..00faf9898a813 100644 --- a/x-pack/solutions/observability/plugins/infra/public/types.ts +++ b/x-pack/solutions/observability/plugins/infra/public/types.ts @@ -90,7 +90,7 @@ export interface InfraClientStartDeps { dataViews: DataViewsPublicPluginStart; discover: DiscoverStart; dashboard: DashboardStart; - embeddable?: EmbeddableStart; + embeddable: EmbeddableStart; lens: LensPublicStart; logsShared: LogsSharedClientStartExports; logsDataAccess: LogsDataAccessPluginStart; diff --git a/x-pack/solutions/observability/plugins/infra/tsconfig.json b/x-pack/solutions/observability/plugins/infra/tsconfig.json index b6f36178df2dc..f9fa656a25a74 100644 --- a/x-pack/solutions/observability/plugins/infra/tsconfig.json +++ b/x-pack/solutions/observability/plugins/infra/tsconfig.json @@ -122,7 +122,8 @@ "@kbn/fields-metadata-plugin", "@kbn/core-chrome-browser", "@kbn/presentation-containers", - "@kbn/object-utils" + "@kbn/object-utils", + "@kbn/saved-search-component", ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 4ff5061c318cb..f63db5f5ce2d8 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -217,6 +217,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'security', 'settings', 'header', + 'discover', ]); // Helpers @@ -635,14 +636,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should load the Logs tab section when clicking on it', async () => { - await testSubjects.existOrFail('hostsView-logs'); + await testSubjects.existOrFail('embeddedSavedSearchDocTable'); }); it('should load the Logs tab with the right columns', async () => { await retry.tryForTime(5000, async () => { - const columnLabels = await pageObjects.infraHostsView.getLogsTableColumnHeaders(); + const columnHeaders = await pageObjects.discover.getDocHeader(); - expect(columnLabels).to.eql(['Timestamp', 'host.name', 'Message']); + expect(columnHeaders).to.have.string('@timestamp'); + expect(columnHeaders).to.have.string('Summary'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 999d7342f4639..f1ce8b0c4071d 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -27,9 +27,7 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./logs/log_entry_categories_tab')); loadTestFile(require.resolve('./logs/log_entry_rate_tab')); loadTestFile(require.resolve('./logs/logs_source_configuration')); - loadTestFile(require.resolve('./logs/log_stream_date_nano')); loadTestFile(require.resolve('./logs/link_to')); - loadTestFile(require.resolve('./logs/log_stream')); loadTestFile(require.resolve('./logs/ml_job_id_formats/tests')); }); }); diff --git a/x-pack/test/functional/apps/infra/logs/log_stream.ts b/x-pack/test/functional/apps/infra/logs/log_stream.ts deleted file mode 100644 index 16dcc038f7aab..0000000000000 --- a/x-pack/test/functional/apps/infra/logs/log_stream.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; -import { URL } from 'url'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -const SERVICE_ID = '49a18510598271e924253ed2581d7ada'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['common']); - const retry = getService('retry'); - const browser = getService('browser'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - - describe('Log stream', function () { - describe('Legacy URL handling', () => { - describe('Correctly handles legacy versions of logFilter', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics'); - await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); - }); - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/infra/8.0.0/logs_and_metrics' - ); - await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); - }); - it('Expression and kind', async () => { - const location = { - hash: '', - pathname: '/stream', - search: `logFilter=(expression:'service.id:"${SERVICE_ID}"',kind:kuery)`, - state: undefined, - }; - - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'infraLogs', - location.pathname, - location.search, - { - ensureCurrentUrl: false, - } - ); - - await retry.tryForTime(5000, async () => { - const currentUrl = await browser.getCurrentUrl(); - const parsedUrl = new URL(currentUrl); - - expect(parsedUrl.pathname).to.be('/app/logs/stream'); - expect(parsedUrl.searchParams.get('logFilter')).to.contain( - `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')` - ); - }); - }); - it('Top-level query and language', async () => { - const location = { - hash: '', - pathname: '/stream', - search: `logFilter=(query:'service.id:"${SERVICE_ID}"',language:kuery)`, - state: undefined, - }; - - await pageObjects.common.navigateToUrlWithBrowserHistory( - 'infraLogs', - location.pathname, - location.search, - { - ensureCurrentUrl: false, - } - ); - - await retry.tryForTime(5000, async () => { - const currentUrl = await browser.getCurrentUrl(); - const parsedUrl = new URL(currentUrl); - - expect(parsedUrl.pathname).to.be('/app/logs/stream'); - expect(parsedUrl.searchParams.get('logFilter')).to.contain( - `(filters:!(),query:(language:kuery,query:\'service.id:"${SERVICE_ID}"\')` - ); - }); - }); - }); - }); - }); -}; diff --git a/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts b/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts deleted file mode 100644 index 141d1bc38c3d3..0000000000000 --- a/x-pack/test/functional/apps/infra/logs/log_stream_date_nano.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { DATES } from '../constants'; - -export default ({ getPageObjects, getService }: FtrProviderContext) => { - const retry = getService('retry'); - const esArchiver = getService('esArchiver'); - const logsUi = getService('logsUi'); - const find = getService('find'); - const kibanaServer = getService('kibanaServer'); - const logFilter = { - timeRange: { - from: DATES.metricsAndLogs.stream.startWithData, - to: DATES.metricsAndLogs.stream.endWithData, - }, - }; - - describe('Log stream supports nano precision', function () { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/infra/logs_with_nano_date'); - await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: true }); - }); - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/infra/logs_with_nano_date'); - await kibanaServer.uiSettings.update({ [OBSERVABILITY_ENABLE_LOGS_STREAM]: false }); - }); - - it('should display logs entries containing date_nano timestamps properly ', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - - expect(logStreamEntries.length).to.be(4); - }); - - it('should render timestamp column properly', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - await retry.try(async () => { - const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - expect(columnHeaderLabels[0]).to.eql('Oct 17, 2018'); - }); - }); - - it('should render timestamp column values properly', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - - const firstLogStreamEntry = logStreamEntries[0]; - - const entryTimestamp = await logsUi.logStreamPage.getLogEntryColumnValueByName( - firstLogStreamEntry, - 'timestampLogColumn' - ); - - expect(entryTimestamp).to.be('19:43:22.111'); - }); - - it('should properly render timestamp in flyout with nano precision', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - const firstLogStreamEntry = logStreamEntries[0]; - - await logsUi.logStreamPage.openLogEntryDetailsFlyout(firstLogStreamEntry); - - const cells = await find.allByCssSelector('.euiTableCellContent'); - - let isFound = false; - - for (const cell of cells) { - const cellText = await cell.getVisibleText(); - if (cellText === '2018-10-17T19:43:22.111111111Z') { - isFound = true; - return; - } - } - - expect(isFound).to.be(true); - }); - }); -}; diff --git a/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts index 84158051021c3..c173f60f2ba10 100644 --- a/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs/logs_source_configuration.ts @@ -6,19 +6,11 @@ */ import expect from '@kbn/expect'; -import { - ELASTIC_HTTP_VERSION_HEADER, - X_ELASTIC_INTERNAL_ORIGIN_REQUEST, -} from '@kbn/core-http-common'; import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { DATES } from '../constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -const COMMON_REQUEST_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', -}; - export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const browser = getService('browser'); @@ -26,8 +18,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'header', 'infraLogs']); const retry = getService('retry'); - const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dataGrid = getService('dataGrid'); describe('Logs Source Configuration', function () { before(async () => { @@ -46,11 +39,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { to: DATES.metricsAndLogs.stream.endWithData, }, }; - const formattedLocalStart = new Date(logFilter.timeRange.from).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); @@ -93,10 +81,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the no indices screen when no indices match the pattern', async () => { + // We will still navigate to the log stream page, but it should redirect to Log Explorer. + // This way this test, serves 2 purposes: await logsUi.logStreamPage.navigateTo(); - await retry.try(async () => { - await logsUi.logStreamPage.getNoDataPage(); + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.existOrFail('discoverNoResults'); }); }); @@ -119,44 +109,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the default log columns with their headers', async () => { await logsUi.logStreamPage.navigateTo({ logFilter }); - await retry.try(async () => { - const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - - expect(columnHeaderLabels).to.eql([formattedLocalStart, 'event.dataset', 'Message']); - }); - - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); - expect(logStreamEntries.length).to.be.greaterThan(0); + await dataGrid.waitForDataTableToLoad(); + const columnHeaders = await dataGrid.getHeaders(); - const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( - firstLogStreamEntry - ); - - expect(logStreamEntryColumns).to.have.length(3); - }); - - it('records telemetry for logs', async () => { - await logsUi.logStreamPage.navigateTo({ logFilter }); - - await logsUi.logStreamPage.getStreamEntries(); - - const [{ stats }] = await supertest - .post(`/internal/telemetry/clusters/_stats`) - .set(COMMON_REQUEST_HEADERS) - .set(ELASTIC_HTTP_VERSION_HEADER, '2') - .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .set('Accept', 'application/json') - .send({ - unencrypted: true, - refreshCache: true, - }) - .expect(200) - .then((res: any) => res.body); - - expect(stats.stack_stats.kibana.plugins.infraops.last_24_hours.hits.logs).to.be.greaterThan( - 0 - ); + expect(columnHeaders).to.eql([ + 'Select column', + 'Actions columnActionsActions', + '@timestamp ', + 'Summary', + ]); }); it('can change the log columns', async () => { @@ -176,22 +137,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the changed log columns with their headers', async () => { await logsUi.logStreamPage.navigateTo({ logFilter }); - await retry.try(async () => { - const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - - expect(columnHeaderLabels).to.eql([formattedLocalStart, 'host.name']); - }); + await dataGrid.waitForDataTableToLoad(); + const columnHeaders = await dataGrid.getHeaders(); - const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); + expect(columnHeaders).to.eql([ + 'Select column', + 'Actions columnActionsActions', + '@timestamp ', + 'Summary', + ]); - expect(logStreamEntries.length).to.be.greaterThan(0); + await testSubjects.click('field-host.name'); + await testSubjects.click('fieldPopoverHeader_addField-host.name'); - const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( - firstLogStreamEntry - ); + await dataGrid.waitForDataTableToLoad(); + const updatedColumnHeaders = await dataGrid.getHeaders(); - expect(logStreamEntryColumns).to.have.length(2); + expect(updatedColumnHeaders).to.eql([ + 'Select column', + 'Actions columnActionsActions', + '@timestamp ', + 'Keywordhost.name', + ]); }); }); }); diff --git a/x-pack/test/functional/apps/infra/page_not_found.ts b/x-pack/test/functional/apps/infra/page_not_found.ts index eb1fc77b4f9f9..0867958fd17b9 100644 --- a/x-pack/test/functional/apps/infra/page_not_found.ts +++ b/x-pack/test/functional/apps/infra/page_not_found.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { OBSERVABILITY_ENABLE_LOGS_STREAM } from '@kbn/management-settings-ids'; import { FtrProviderContext } from '../../ftr_provider_context'; -const logsPages = ['logs/stream', 'logs/anomalies', 'logs/log-categories', 'logs/settings']; +const logsPages = ['logs/anomalies', 'logs/log-categories', 'logs/settings']; const metricsPages = [ 'metrics/inventory', diff --git a/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts b/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts index f6acf29cb3a9e..123de23bbe6c1 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/infra/hosts_page.ts @@ -189,7 +189,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should load the Logs tab section when clicking on it', async () => { - await testSubjects.existOrFail('hostsView-logs'); + await testSubjects.existOrFail('embeddedSavedSearchDocTable'); }); });