From 10ec7132fa031b60ee8733a077521dee12368d12 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 21 Nov 2023 09:14:57 +0100 Subject: [PATCH] [Fleet] Fix links to Logs view to point to Discover in Serverless (#171525) Fixes https://github.com/elastic/kibana/issues/168349 ## Summary Fix links to Logs view to point to Discover in Serverless. As the Logs view UI is not available in serverless, the "Open in logs" buttons should point to Discover instead. Rather than hardcode the url in each of the places where is needed, I extracted a small component that builds the two urls and allows switching in an easier way. If in the future on of the two links will go away, it will be easier to find those occurrences. ### Testing Test for serverless following [these instructions](https://github.com/elastic/kibana/pull/167976) **Error logs in agent activity flyout** - Enroll an agent and try to cause some error - for instance upgrading an agent that is not upgradeable - Click on "Agent Activity" and find the error and a button besides it - On stateful the button says "Open in Logs" ![Screenshot 2023-11-20 at 13 07 08](https://github.com/elastic/kibana/assets/16084106/704cf0e2-c7ee-4751-9e7f-7dcd263a5aa4) - On serverless is "Open in discover" ![Screenshot 2023-11-20 at 13 08 02](https://github.com/elastic/kibana/assets/16084106/3902f09e-93dc-48d3-867e-1f80d977f437) - Check that both show the same logs: ![Screenshot 2023-11-16 at 11 49 24](https://github.com/elastic/kibana/assets/16084106/d863d99f-0c70-45e5-9316-a37645464c34) ![Screenshot 2023-11-16 at 11 48 54](https://github.com/elastic/kibana/assets/16084106/7cbd0a5f-3b31-4c4d-a4b7-4eb7390983c8) **Agent logs** (Same test as above) - Enroll an agent - Click on the agent and go to the "Logs" tab - On stateful the button says "Open in Logs" ![Screenshot 2023-11-20 at 13 04 41](https://github.com/elastic/kibana/assets/16084106/6a43a062-37db-47ea-819f-acd170439395) - On serverless is "Open in discover" ![Screenshot 2023-11-20 at 13 04 11](https://github.com/elastic/kibana/assets/16084106/e15fdc8b-8780-4ac6-afc6-bff3d3a96be5) - Check that both show the same logs **Custom Logs UI** There is also a link to logs on custom logs UI but I just linked to discover for that one: https://github.com/elastic/kibana/pull/171525/files#diff-e337aa916d60d0d1033e3298c8c9c33c6a6fcd87a8ded971a4a87f5ccfc0981fR20-R22 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/agent_logs/agent_logs.test.tsx | 9 +- .../components/agent_logs/agent_logs.tsx | 71 ++-------------- .../agent_logs/view_logs_button.tsx | 85 +++++++++++++++++++ .../components/view_errors.test.tsx | 73 +++++++++++++--- .../components/view_errors.tsx | 48 ++++------- .../public/custom_logs_assets_extension.tsx | 8 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 9 files changed, 185 insertions(+), 112 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx index 6de1a3caaa2c1..6e965dc9eca6a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.test.tsx @@ -63,6 +63,8 @@ describe('AgentLogsUI', () => { const state = { datasets: ['elastic_agent'], logLevels: ['info', 'error'], + start: '2023-20-04T14:00:00.340Z', + end: '2023-20-04T14:20:00.340Z', query: '', } as any; return render(); @@ -97,7 +99,10 @@ describe('AgentLogsUI', () => { it('should render Open in Logs UI if capabilities not set', () => { mockStartServices(); const result = renderComponent(); - expect(result.getByTestId('viewInLogsBtn')).not.toBeNull(); + expect(result.getByTestId('viewInLogsBtn')).toHaveAttribute( + 'href', + `http://localhost:5620/app/logs/stream?logPosition=(end%3A'2023-20-04T14%3A20%3A00.340Z'%2Cstart%3A'2023-20-04T14%3A00%3A00.340Z'%2CstreamLive%3A!f)&logFilter=(expression%3A'elastic_agent.id%3Aagent1%20and%20(data_stream.dataset%3Aelastic_agent)%20and%20(log.level%3Ainfo%20or%20log.level%3Aerror)'%2Ckind%3Akuery)` + ); }); it('should render Open in Discover if serverless enabled', () => { @@ -106,7 +111,7 @@ describe('AgentLogsUI', () => { const viewInDiscover = result.getByTestId('viewInDiscoverBtn'); expect(viewInDiscover).toHaveAttribute( 'href', - `http://localhost:5620/app/discover#/?_a=(index:'logs-*',query:(language:kuery,query:'data_stream.dataset:elastic_agent%20AND%20elastic_agent.id:agent1'))` + `http://localhost:5620/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2023-20-04T14:00:00.340Z',to:'2023-20-04T14:20:00.340Z'))&_a=(columns:!(event.dataset,message),index:'logs-*',query:(language:kuery,query:'elastic_agent.id:agent1 and (data_stream.dataset:elastic_agent) and (log.level:info or log.level:error)'))` ); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 8efef6592f870..05efb3abfdcaa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -4,21 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import url from 'url'; -import { stringify } from 'querystring'; - import React, { memo, useMemo, useState, useCallback, useEffect } from 'react'; import styled from 'styled-components'; -import { encode } from '@kbn/rison'; import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiFilterGroup, EuiPanel, - EuiButton, - EuiButtonEmpty, EuiCallOut, EuiLink, } from '@elastic/eui'; @@ -42,6 +35,7 @@ import { LogLevelFilter } from './filter_log_level'; import { LogQueryBar } from './query_bar'; import { buildQuery } from './build_query'; import { SelectLogLevel } from './select_log_level'; +import { ViewLogsButton } from './view_logs_button'; const WrapperFlexGroup = styled(EuiFlexGroup)` height: 100%; @@ -118,7 +112,7 @@ const AgentPolicyLogsNotEnabledCallout: React.FunctionComponent<{ agentPolicy: A export const AgentLogsUI: React.FunctionComponent = memo( ({ agent, agentPolicy, state }) => { - const { data, application, http, cloud } = useStartServices(); + const { data, application, cloud } = useStartServices(); const { update: updateState } = AgentLogsUrlStateHelper.useTransitions(); const isLogsUIAvailable = !cloud?.isServerlessEnabled; @@ -218,37 +212,6 @@ export const AgentLogsUI: React.FunctionComponent = memo( [agent.id, state.datasets, state.logLevels, state.query] ); - // Generate URL to pass page state to Logs UI - const viewInLogsUrl = useMemo( - () => - http.basePath.prepend( - url.format({ - pathname: '/app/logs/stream', - search: stringify({ - logPosition: encode({ - start: state.start, - end: state.end, - streamLive: false, - }), - logFilter: encode({ - expression: logStreamQuery, - kind: 'kuery', - }), - }), - }) - ), - [http.basePath, state.start, state.end, logStreamQuery] - ); - - const viewInDiscoverUrl = useMemo(() => { - const index = 'logs-*'; - const datasetQuery = 'data_stream.dataset:elastic_agent'; - const agentIdQuery = `elastic_agent.id:${agent.id}`; - return http.basePath.prepend( - `/app/discover#/?_a=(index:'${index}',query:(language:kuery,query:'${datasetQuery}%20AND%20${agentIdQuery}'))` - ); - }, [http.basePath, agent.id]); - const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { if (!agentVersion) { @@ -357,30 +320,12 @@ export const AgentLogsUI: React.FunctionComponent = memo( application, }} > - {isLogsUIAvailable ? ( - - - - ) : ( - - - - )} + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx new file mode 100644 index 0000000000000..762c34ad7bc36 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/view_logs_button.tsx @@ -0,0 +1,85 @@ +/* + * 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 url from 'url'; +import { stringify } from 'querystring'; + +import React, { useMemo } from 'react'; +import { encode } from '@kbn/rison'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { useStartServices } from '../../../../../hooks'; + +interface ViewLogsProps { + viewInLogs: boolean; + logStreamQuery: string; + startTime: string; + endTime: string; +} + +/* + Button that takes to the Logs view Ui when that is available, otherwise fallback to the Discover UI + The urls are built using same logStreamQuery (provided by a prop), startTime and endTime, ensuring that they'll both will target same log lines +*/ +export const ViewLogsButton: React.FunctionComponent = ({ + viewInLogs, + logStreamQuery, + startTime, + endTime, +}) => { + const { http } = useStartServices(); + + // Generate URL to pass page state to Logs UI + const viewInLogsUrl = useMemo( + () => + http.basePath.prepend( + url.format({ + pathname: '/app/logs/stream', + search: stringify({ + logPosition: encode({ + start: startTime, + end: endTime, + streamLive: false, + }), + logFilter: encode({ + expression: logStreamQuery, + kind: 'kuery', + }), + }), + }) + ), + [http.basePath, startTime, endTime, logStreamQuery] + ); + + const viewInDiscoverUrl = useMemo(() => { + const index = 'logs-*'; + const query = encode({ + query: logStreamQuery, + language: 'kuery', + }); + return http.basePath.prepend( + `/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'${startTime}',to:'${endTime}'))&_a=(columns:!(event.dataset,message),index:'${index}',query:${query})` + ); + }, [logStreamQuery, http.basePath, startTime, endTime]); + + return viewInLogs ? ( + + + + ) : ( + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx index f907edd52e2b4..b5018f812da4e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.test.tsx @@ -12,25 +12,51 @@ import { I18nProvider } from '@kbn/i18n-react'; import type { ActionStatus } from '../../../../../../../common/types'; +import { useStartServices } from '../../../../hooks'; + import { ViewErrors } from './view_errors'; +jest.mock('../../../../hooks', () => { + return { + ...jest.requireActual('../../../../hooks'), + useLink: jest.fn(), + useStartServices: jest.fn(), + }; +}); + +const mockUseStartServices = useStartServices as jest.Mock; + jest.mock('@kbn/shared-ux-link-redirect-app', () => ({ RedirectAppLinks: (props: any) => { return
{props.children}
; }, })); -jest.mock('../../../../hooks', () => { - return { - useStartServices: jest.fn().mockReturnValue({ - http: { - basePath: { - prepend: jest.fn().mockImplementation((str) => 'http://localhost' + str), +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', + }), + }, }, }, - }), - }; -}); + }, + http: { + basePath: { + prepend: (url: string) => 'http://localhost:5620' + url, + }, + }, + cloud: { + isServerlessEnabled, + }, + }); +}; describe('ViewErrors', () => { const renderComponent = (action: ActionStatus) => { @@ -41,7 +67,30 @@ describe('ViewErrors', () => { ); }; - it('should render error message with btn to logs', () => { + it('should render error message with btn to Logs view if serverless not enabled', () => { + mockStartServices(); + const result = renderComponent({ + actionId: 'action1', + latestErrors: [ + { + agentId: 'agent1', + error: 'Agent agent1 is not upgradeable', + timestamp: '2023-03-06T14:51:24.709Z', + }, + ], + } as any); + + const errorText = result.getByTestId('errorText'); + expect(errorText.textContent).toEqual('Agent agent1 is not upgradeable'); + + const viewErrorBtn = result.getByTestId('viewInLogsBtn'); + expect(viewErrorBtn.getAttribute('href')).toEqual( + `http://localhost:5620/app/logs/stream?logPosition=(end%3A'2023-03-06T14%3A56%3A24.709Z'%2Cstart%3A'2023-03-06T14%3A46%3A24.709Z'%2CstreamLive%3A!f)&logFilter=(expression%3A'elastic_agent.id%3Aagent1%20and%20(data_stream.dataset%3Aelastic_agent)%20and%20(log.level%3Aerror)'%2Ckind%3Akuery)` + ); + }); + + it('should render error message with btn to Discover view if serverless enabled', () => { + mockStartServices(true); const result = renderComponent({ actionId: 'action1', latestErrors: [ @@ -56,9 +105,9 @@ describe('ViewErrors', () => { const errorText = result.getByTestId('errorText'); expect(errorText.textContent).toEqual('Agent agent1 is not upgradeable'); - const viewErrorBtn = result.getByTestId('viewLogsBtn'); + const viewErrorBtn = result.getByTestId('viewInDiscoverBtn'); expect(viewErrorBtn.getAttribute('href')).toEqual( - `http://localhost/app/logs/stream?logPosition=(position%3A(time%3A1678114284709)%2CstreamLive%3A!f)&logFilter=(expression%3A'elastic_agent.id%3Aagent1%20and%20(data_stream.dataset%3Aelastic_agent)%20and%20(log.level%3Aerror)'%2Ckind%3Akuery)` + `http://localhost:5620/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2023-03-06T14:46:24.709Z',to:'2023-03-06T14:56:24.709Z'))&_a=(columns:!(event.dataset,message),index:'logs-*',query:(language:kuery,query:'elastic_agent.id:agent1 and (data_stream.dataset:elastic_agent) and (log.level:error)'))` ); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx index d49be8d4cacda..4d43c9a60a618 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/view_errors.tsx @@ -5,21 +5,19 @@ * 2.0. */ -import { stringify } from 'querystring'; - import styled from 'styled-components'; import React from 'react'; -import { encode } from '@kbn/rison'; import type { EuiBasicTableProps } from '@elastic/eui'; -import { EuiButton, EuiAccordion, EuiToolTip, EuiText, EuiBasicTable } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiAccordion, EuiToolTip, EuiText, EuiBasicTable } from '@elastic/eui'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import moment from 'moment'; import { i18n } from '@kbn/i18n'; import type { ActionErrorResult } from '../../../../../../../common/types'; import { buildQuery } from '../../agent_details_page/components/agent_logs/build_query'; +import { ViewLogsButton } from '../../agent_details_page/components/agent_logs/view_logs_button'; import type { ActionStatus } from '../../../../types'; import { useStartServices } from '../../../../hooks'; @@ -32,27 +30,26 @@ const TruncatedEuiText = styled(EuiText)` export const ViewErrors: React.FunctionComponent<{ action: ActionStatus }> = ({ action }) => { const coreStart = useStartServices(); + const isLogsUIAvailable = !coreStart.cloud?.isServerlessEnabled; + + const getLogsButton = (agentId: string, timestamp: string, viewInLogs: boolean) => { + const startTime = moment(timestamp).subtract(5, 'm').toISOString(); + const endTime = moment(timestamp).add(5, 'm').toISOString(); - const logStreamQuery = (agentId: string) => - buildQuery({ + const logStreamQuery = buildQuery({ agentId, datasets: ['elastic_agent'], logLevels: ['error'], userQuery: '', }); - - const getErrorLogsUrl = (agentId: string, timestamp: string) => { - const queryParams = stringify({ - logPosition: encode({ - position: { time: Date.parse(timestamp) }, - streamLive: false, - }), - logFilter: encode({ - expression: logStreamQuery(agentId), - kind: 'kuery', - }), - }); - return coreStart.http.basePath.prepend(`/app/logs/stream?${queryParams}`); + return ( + + ); }; const columns: EuiBasicTableProps['columns'] = [ @@ -89,16 +86,7 @@ export const ViewErrors: React.FunctionComponent<{ action: ActionStatus }> = ({ const errorItem = (action.latestErrors ?? []).find((item) => item.agentId === agentId); return ( - - - + {getLogsButton(agentId, errorItem!.timestamp, !!isLogsUIAvailable)} ); }, diff --git a/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx b/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx index 4090c4520bf2a..26668c4062981 100644 --- a/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx +++ b/x-pack/plugins/fleet/public/custom_logs_assets_extension.tsx @@ -14,8 +14,12 @@ import { useStartServices } from './hooks'; import type { PackageAssetsComponent } from './types'; export const CustomLogsAssetsExtension: PackageAssetsComponent = () => { - const { http } = useStartServices(); - const logStreamUrl = http.basePath.prepend('/app/logs/stream'); + const { http, cloud } = useStartServices(); + const isLogsUIAvailable = !cloud?.isServerlessEnabled; + // if logs ui is not available, link to discover + const logStreamUrl = isLogsUIAvailable + ? http.basePath.prepend('/app/logs/stream') + : http.basePath.prepend('/app/discover'); const views: CustomAssetsAccordionProps['views'] = [ { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f55b85b97b539..22771a5feae0b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -17100,7 +17100,6 @@ "xpack.fleet.agentActivityFlyout.inProgressTitle": "En cours", "xpack.fleet.agentActivityFlyout.noActivityDescription": "Le fil d'activités s'affichera ici au fur et à mesure que les agents seront réaffectés, mis à niveau ou désenregistrés.", "xpack.fleet.agentActivityFlyout.noActivityText": "Aucune activité à afficher", - "xpack.fleet.agentActivityFlyout.reviewErrorLogs": "Vérifier les logs d'erreur", "xpack.fleet.agentActivityFlyout.scheduledDescription": "Planifié pour ", "xpack.fleet.agentActivityFlyout.title": "Activité des agents", "xpack.fleet.agentActivityFlyout.todayTitle": "Aujourd'hui", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a04e7f46cf676..8cea00c9c0579 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17113,7 +17113,6 @@ "xpack.fleet.agentActivityFlyout.inProgressTitle": "進行中", "xpack.fleet.agentActivityFlyout.noActivityDescription": "エージェントが再割り当て、アップグレード、または登録解除されたときに、アクティビティフィードがここに表示されます。", "xpack.fleet.agentActivityFlyout.noActivityText": "表示するアクティビティがありません", - "xpack.fleet.agentActivityFlyout.reviewErrorLogs": "エラーログを確認", "xpack.fleet.agentActivityFlyout.scheduledDescription": "スケジュール済み ", "xpack.fleet.agentActivityFlyout.title": "エージェントアクティビティ", "xpack.fleet.agentActivityFlyout.todayTitle": "今日", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5077a6c337332..b92382260e44e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17113,7 +17113,6 @@ "xpack.fleet.agentActivityFlyout.inProgressTitle": "进行中", "xpack.fleet.agentActivityFlyout.noActivityDescription": "重新分配、升级或取消注册代理时,活动源将在此处显示。", "xpack.fleet.agentActivityFlyout.noActivityText": "没有可显示的活动", - "xpack.fleet.agentActivityFlyout.reviewErrorLogs": "查看错误日志", "xpack.fleet.agentActivityFlyout.scheduledDescription": "计划进行 ", "xpack.fleet.agentActivityFlyout.title": "代理活动", "xpack.fleet.agentActivityFlyout.todayTitle": "今日",