Skip to content

Commit

Permalink
[Fleet] Fix links to Logs view to point to Discover in Serverless (el…
Browse files Browse the repository at this point in the history
…astic#171525)

Fixes elastic#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](elastic#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 <[email protected]>
  • Loading branch information
criamico and kibanamachine authored Nov 21, 2023
1 parent 3bb16c7 commit 10ec713
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(<AgentLogsUI agent={agent} state={state} />);
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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)'))`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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%;
Expand Down Expand Up @@ -118,7 +112,7 @@ const AgentPolicyLogsNotEnabledCallout: React.FunctionComponent<{ agentPolicy: A

export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = memo(
({ agent, agentPolicy, state }) => {
const { data, application, http, cloud } = useStartServices();
const { data, application, cloud } = useStartServices();
const { update: updateState } = AgentLogsUrlStateHelper.useTransitions();
const isLogsUIAvailable = !cloud?.isServerlessEnabled;

Expand Down Expand Up @@ -218,37 +212,6 @@ export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = 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) {
Expand Down Expand Up @@ -357,30 +320,12 @@ export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = memo(
application,
}}
>
{isLogsUIAvailable ? (
<EuiButtonEmpty
href={viewInLogsUrl}
iconType="popout"
flush="both"
data-test-subj="viewInLogsBtn"
>
<FormattedMessage
id="xpack.fleet.agentLogs.openInLogsUiLinkText"
defaultMessage="Open in Logs"
/>
</EuiButtonEmpty>
) : (
<EuiButton
href={viewInDiscoverUrl}
iconType="popout"
data-test-subj="viewInDiscoverBtn"
>
<FormattedMessage
id="xpack.fleet.agentLogs.openInDiscoverUiLinkText"
defaultMessage="Open in Discover"
/>
</EuiButton>
)}
<ViewLogsButton
viewInLogs={isLogsUIAvailable}
logStreamQuery={logStreamQuery}
startTime={state.start}
endTime={state.end}
/>
</RedirectAppLinks>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ViewLogsProps> = ({
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 ? (
<EuiButton href={viewInLogsUrl} iconType="popout" data-test-subj="viewInLogsBtn">
<FormattedMessage
id="xpack.fleet.agentLogs.openInLogsUiLinkText"
defaultMessage="Open in Logs"
/>
</EuiButton>
) : (
<EuiButton href={viewInDiscoverUrl} iconType="popout" data-test-subj="viewInDiscoverBtn">
<FormattedMessage
id="xpack.fleet.agentLogs.openInDiscoverUiLinkText"
defaultMessage="Open in Discover"
/>
</EuiButton>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>{props.children}</div>;
},
}));

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) => {
Expand All @@ -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: [
Expand All @@ -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)'))`
);
});
});
Loading

0 comments on commit 10ec713

Please sign in to comment.