Skip to content

Commit

Permalink
[8.15] [Fleet] Display view in logs button when logs app is available (
Browse files Browse the repository at this point in the history
…#187871) (#188000)

# Backport

This will backport the following commits from `main` to `8.15`:
- [[Fleet] Display view in logs button when logs app is available
(#187871)](#187871)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Cristina
Amico","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-07-10T15:17:31Z","message":"[Fleet]
Display view in logs button when logs app is available
(#187871)\n\nCloses
https://github.com/elastic/kibana/issues/185711\r\n\r\n##
Summary\r\nThis change fixes
#185711, but\r\nwhile working on
that I also realised that we should move away from\r\nusing hardcoded
urls. So this PR does two things:\r\n- Displays the button only when the
user has `authz.fleet.readAgents`\r\nprivilege\r\n- Refactors the button
functionality to use the new locators that take\r\ncare of linking to
the observability logs/discover app\r\n\r\n### Why the refactor\r\nWhile
testing this button, I noticed that the functionality was broken\r\nin
some cases, that's because we were manually routing the urls to
Logs\r\nUI/Discover apps based if we are in serverless or not.\r\n\r\nI
found a PR that already implements this
functionality:\r\nhttps://github.com//pull/155156\r\nI
also found #154145 that
takes\r\ncare of the redirect to the correct app.\r\nSo I'm replacing
the current manual functionality with these utilities\r\nso that
`getLogsLocatorsFromUrlService` takes care of where the open in\r\nlogs
button should link.\r\n\r\n\r\n\r\n###
ESS\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/3f0760c9-3afb-4793-a3af-317f625b36d7\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/3436cf5a-36c9-425d-a114-e116ddaa1a03\r\n\r\n###
Serverless\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/84176f09-96a4-4932-9508-5f7682d03aae\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"696bb88d7c33eebfeabec6064ea8a97a2e2bb1bb","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Fleet","v8.15.0","v8.16.0"],"title":"[Fleet]
Display view in logs button when logs app is
available","number":187871,"url":"https://github.com/elastic/kibana/pull/187871","mergeCommit":{"message":"[Fleet]
Display view in logs button when logs app is available
(#187871)\n\nCloses
https://github.com/elastic/kibana/issues/185711\r\n\r\n##
Summary\r\nThis change fixes
#185711, but\r\nwhile working on
that I also realised that we should move away from\r\nusing hardcoded
urls. So this PR does two things:\r\n- Displays the button only when the
user has `authz.fleet.readAgents`\r\nprivilege\r\n- Refactors the button
functionality to use the new locators that take\r\ncare of linking to
the observability logs/discover app\r\n\r\n### Why the refactor\r\nWhile
testing this button, I noticed that the functionality was broken\r\nin
some cases, that's because we were manually routing the urls to
Logs\r\nUI/Discover apps based if we are in serverless or not.\r\n\r\nI
found a PR that already implements this
functionality:\r\nhttps://github.com//pull/155156\r\nI
also found #154145 that
takes\r\ncare of the redirect to the correct app.\r\nSo I'm replacing
the current manual functionality with these utilities\r\nso that
`getLogsLocatorsFromUrlService` takes care of where the open in\r\nlogs
button should link.\r\n\r\n\r\n\r\n###
ESS\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/3f0760c9-3afb-4793-a3af-317f625b36d7\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/3436cf5a-36c9-425d-a114-e116ddaa1a03\r\n\r\n###
Serverless\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/84176f09-96a4-4932-9508-5f7682d03aae\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"696bb88d7c33eebfeabec6064ea8a97a2e2bb1bb"}},"sourceBranch":"main","suggestedTargetBranches":["8.15"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/187871","number":187871,"mergeCommit":{"message":"[Fleet]
Display view in logs button when logs app is available
(#187871)\n\nCloses
https://github.com/elastic/kibana/issues/185711\r\n\r\n##
Summary\r\nThis change fixes
#185711, but\r\nwhile working on
that I also realised that we should move away from\r\nusing hardcoded
urls. So this PR does two things:\r\n- Displays the button only when the
user has `authz.fleet.readAgents`\r\nprivilege\r\n- Refactors the button
functionality to use the new locators that take\r\ncare of linking to
the observability logs/discover app\r\n\r\n### Why the refactor\r\nWhile
testing this button, I noticed that the functionality was broken\r\nin
some cases, that's because we were manually routing the urls to
Logs\r\nUI/Discover apps based if we are in serverless or not.\r\n\r\nI
found a PR that already implements this
functionality:\r\nhttps://github.com//pull/155156\r\nI
also found #154145 that
takes\r\ncare of the redirect to the correct app.\r\nSo I'm replacing
the current manual functionality with these utilities\r\nso that
`getLogsLocatorsFromUrlService` takes care of where the open in\r\nlogs
button should link.\r\n\r\n\r\n\r\n###
ESS\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/3f0760c9-3afb-4793-a3af-317f625b36d7\r\n\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/3436cf5a-36c9-425d-a114-e116ddaa1a03\r\n\r\n###
Serverless\r\n\r\nhttps://github.com/elastic/kibana/assets/16084106/84176f09-96a4-4932-9508-5f7682d03aae\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"696bb88d7c33eebfeabec6064ea8a97a2e2bb1bb"}}]}]
BACKPORT-->

Co-authored-by: Cristina Amico <[email protected]>
  • Loading branch information
kibanamachine and criamico authored Jul 10, 2024
1 parent c30c0ea commit 494668e
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 112 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/constants/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
export const LOCATORS_IDS = {
APM_LOCATOR: 'APM_LOCATOR',
DASHBOARD_APP: 'DASHBOARD_APP_LOCATOR',
DISCOVER_APP_LOCATOR: 'DISCOVER_APP_LOCATOR',
} as const;

// Dashboards ids
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { createFleetTestRendererMock } from '../../../../../../../mock';

import { AgentLogsUI } from './agent_logs';

jest.mock('../../../../../../../hooks/use_authz');

jest.mock('@kbn/kibana-utils-plugin/public', () => {
return {
...jest.requireActual('@kbn/kibana-utils-plugin/public'),
Expand All @@ -28,6 +26,13 @@ jest.mock('@kbn/logs-shared-plugin/public', () => {
LogStream: () => <div />,
};
});
jest.mock('@kbn/logs-shared-plugin/common', () => {
return {
getLogsLocatorsFromUrlService: jest.fn().mockReturnValue({
logsLocator: { getRedirectUrl: jest.fn(() => 'https://discover-redirect-url') },
}),
};
});

jest.mock('@kbn/shared-ux-link-redirect-app', () => {
return {
Expand All @@ -52,6 +57,13 @@ jest.mock('../../../../../hooks', () => {
...jest.requireActual('../../../../../hooks'),
useLink: jest.fn(),
useStartServices: jest.fn(),
useAuthz: jest.fn(),
useDiscoverLocator: jest.fn().mockImplementation(() => {
return {
id: 'DISCOVER_APP_LOCATOR',
getRedirectUrl: jest.fn().mockResolvedValue('app/discover/logs/someview'),
};
}),
};
});

Expand All @@ -62,6 +74,7 @@ describe('AgentLogsUI', () => {
jest.mocked(useAuthz).mockReturnValue({
fleet: {
allAgents: true,
readAgents: true,
},
} as any);
});
Expand Down Expand Up @@ -100,34 +113,36 @@ describe('AgentLogsUI', () => {
},
},
},
http: {
basePath: {
prepend: (url: string) => 'http://localhost:5620' + url,
share: {
url: {
locators: {
get: () => ({
useUrl: () => 'https://locator.url',
}),
},
},
},
cloud: {
isServerlessEnabled,
},
});
};

it('should render Open in Logs UI if capabilities not set', () => {
it('should render Open in Logs button if privileges are set', () => {
mockStartServices();
const result = renderComponent();
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)`
`https://discover-redirect-url`
);
});

it('should render Open in Discover if serverless enabled', () => {
mockStartServices(true);
it('should not render Open in Logs button if privileges are not set', () => {
jest.mocked(useAuthz).mockReturnValue({
fleet: {
readAgents: false,
},
} as any);
mockStartServices();
const result = renderComponent();
const viewInDiscover = result.getByTestId('viewInDiscoverBtn');
expect(viewInDiscover).toHaveAttribute(
'href',
`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)'))`
);
expect(result.queryByTestId('viewInLogsBtn')).not.toBeInTheDocument();
});

it('should show log level dropdown with correct value', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +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';
import { ViewLogsButton, getFormattedRange } from './view_logs_button';

const WrapperFlexGroup = styled(EuiFlexGroup)`
height: 100%;
Expand Down Expand Up @@ -112,9 +112,8 @@ const AgentPolicyLogsNotEnabledCallout: React.FunctionComponent<{ agentPolicy: A

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

// Util to convert date expressions (returned by datepicker) to timestamps (used by LogStream)
const getDateRangeTimestamps = useCallback(
Expand Down Expand Up @@ -321,10 +320,9 @@ export const AgentLogsUI: React.FunctionComponent<AgentLogsProps> = memo(
}}
>
<ViewLogsButton
viewInLogs={isLogsUIAvailable}
logStreamQuery={logStreamQuery}
startTime={state.start}
endTime={state.end}
startTime={getFormattedRange(state.start)}
endTime={getFormattedRange(state.end)}
/>
</RedirectAppLinks>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,81 +5,61 @@
* 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';
import { getLogsLocatorsFromUrlService } from '@kbn/logs-shared-plugin/common';

import moment from 'moment';

import { useDiscoverLocator, useStartServices, useAuthz } from '../../../../../hooks';

interface ViewLogsProps {
viewInLogs: boolean;
logStreamQuery: string;
startTime: string;
endTime: string;
startTime: number;
endTime: number;
}

export const getFormattedRange = (date: string) => new Date(date).getTime();

/*
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
Button that takes to the Logs view UI or the Discover logs, depending on what's available
If none is available, don't display the button at all
*/
export const ViewLogsButton: React.FunctionComponent<ViewLogsProps> = ({
viewInLogs,
logStreamQuery,
startTime,
endTime,
}) => {
const { http } = useStartServices();
const discoverLocator = useDiscoverLocator();

// 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 { share } = useStartServices();
const { logsLocator } = getLogsLocatorsFromUrlService(share.url);
const authz = useAuthz();

const viewInDiscoverUrl = useMemo(() => {
const index = 'logs-*';
const query = encode({
query: logStreamQuery,
language: 'kuery',
const logsUrl = useMemo(() => {
const now = moment().toISOString();
const oneDayAgo = moment().subtract(1, 'day').toISOString();
const defaultStartTime = getFormattedRange(oneDayAgo);
const defaultEndTime = getFormattedRange(now);

return logsLocator.getRedirectUrl({
time: endTime ? endTime : defaultEndTime,
timeRange: {
startTime: startTime ? startTime : defaultStartTime,
endTime: endTime ? endTime : defaultEndTime,
},
filter: logStreamQuery,
});
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]);
}, [endTime, logStreamQuery, logsLocator, startTime]);

return viewInLogs ? (
<EuiButton href={viewInLogsUrl} iconType="popout" data-test-subj="viewInLogsBtn">
return authz.fleet.readAgents && (logsLocator || discoverLocator) ? (
<EuiButton href={logsUrl} 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>
);
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { act, render, fireEvent } from '@testing-library/react';
import { IntlProvider } from 'react-intl';

import { useActionStatus } from '../../hooks';
import { useGetAgentPolicies, useStartServices } from '../../../../../hooks';
import { useGetAgentPolicies, useStartServices, useAuthz } from '../../../../../hooks';

import { AgentActivityFlyout } from '.';

Expand All @@ -25,6 +25,15 @@ jest.mock('@kbn/shared-ux-link-redirect-app', () => ({
const mockUseActionStatus = useActionStatus as jest.Mock;
const mockUseGetAgentPolicies = useGetAgentPolicies as jest.Mock;
const mockUseStartServices = useStartServices as jest.Mock;
const mockedUseAuthz = useAuthz as jest.Mock;

jest.mock('@kbn/logs-shared-plugin/common', () => {
return {
getLogsLocatorsFromUrlService: jest.fn().mockReturnValue({
logsLocator: { getRedirectUrl: jest.fn(() => 'https://discover-redirect-url') },
}),
};
});

describe('AgentActivityFlyout', () => {
const mockOnClose = jest.fn();
Expand Down Expand Up @@ -65,7 +74,22 @@ describe('AgentActivityFlyout', () => {
docLinks: { links: { fleet: { upgradeElasticAgent: 'https://elastic.co' } } },
application: { navigateToUrl: jest.fn() },
http: { basePath: { prepend: jest.fn() } },
share: {
url: {
locators: {
get: () => ({
useUrl: () => 'https://locator.url',
}),
},
},
},
});
mockedUseAuthz.mockReturnValue({
fleet: {
readAgents: true,
allAgents: true,
},
} as any);
});

beforeEach(() => {
Expand Down
Loading

0 comments on commit 494668e

Please sign in to comment.