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 c656e54ee8f5f..3f17d66656ae6 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -17101,7 +17101,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 157391ad9a6ff..1c6e69343b28c 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -17114,7 +17114,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 192297f53e6ec..b0a78d859fb97 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -17114,7 +17114,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": "今日",