mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Display latest agent version in agent explorer (#153643)
Closes https://github.com/elastic/kibana/issues/152326. ### Changes - `fetchWithTimeout` function was added, so we can fetch the external bucket where the versions are with a timeout. This is mostly useful for air-gapped environments. - `fetchAgentsLatestVersion` was introduced an it's in charge of fetching the bucket and handling the errors accordingly. - `getAgentsItems` now returns `latestVersion` property for each agent. - New column was created in the UI to list the latestVersion per agent. When no timing out https://user-images.githubusercontent.com/1313018/227519796-e5569475-451d-4c04-8243-d18c8e7126c3.mov When timing out https://user-images.githubusercontent.com/1313018/227520011-ae616a07-e87b-4d0f-bd29-4b3338aa5df2.mov ### Pending - [ ] Replace bucket URL with production bucket url --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e29265e51c
commit
628db34d8a
18 changed files with 486 additions and 20 deletions
|
@ -101,4 +101,7 @@ Matcher for all source map indices. Defaults to `apm-*`.
|
|||
`xpack.apm.autoCreateApmDataView` {ess-icon}::
|
||||
Set to `false` to disable the automatic creation of the APM data view when the APM app is opened. Defaults to `true`.
|
||||
|
||||
`xpack.apm.latestAgentVersionsUrl` {ess-icon}::
|
||||
Specifies the URL of a self hosted file that contains latest agent versions. Defaults to `https://apm-agent-versions.elastic.co/versions.json`. Set to `''` to disable requesting latest agent versions.
|
||||
|
||||
// end::general-apm-settings[]
|
||||
|
|
|
@ -165,6 +165,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.apm.serviceMapEnabled (boolean)',
|
||||
'xpack.apm.ui.enabled (boolean)',
|
||||
'xpack.apm.ui.maxTraceItems (number)',
|
||||
'xpack.apm.latestAgentVersionsUrl (string)',
|
||||
'xpack.cases.files.allowedMimeTypes (array)',
|
||||
'xpack.cases.files.maxSize (number)',
|
||||
'xpack.cases.markdownPlugins.lens (boolean)',
|
||||
|
|
|
@ -14,3 +14,12 @@ export enum AgentExplorerFieldName {
|
|||
AgentDocsPageUrl = 'agentDocsPageUrl',
|
||||
Instances = 'instances',
|
||||
}
|
||||
|
||||
export interface ElasticApmAgentLatestVersion {
|
||||
latest_version: string;
|
||||
}
|
||||
|
||||
export interface OtelAgentLatestVersion {
|
||||
sdk_latest_version: string;
|
||||
auto_latest_version?: string;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
@ -18,6 +19,7 @@ import { StickyProperties } from '../../../../../shared/sticky_properties';
|
|||
import { getComparisonEnabled } from '../../../../../shared/time_comparison/get_comparison_enabled';
|
||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||
import { AgentExplorerDocsLink } from '../../agent_explorer_docs_link';
|
||||
import { AgentLatestVersion } from '../../agent_latest_version';
|
||||
|
||||
const serviceLabel = i18n.translate(
|
||||
'xpack.apm.agentInstancesDetails.serviceLabel',
|
||||
|
@ -40,6 +42,13 @@ const instancesLabel = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const latestVersionLabel = i18n.translate(
|
||||
'xpack.apm.agentInstancesDetails.latestVersionLabel',
|
||||
{
|
||||
defaultMessage: 'Latest agent version',
|
||||
}
|
||||
);
|
||||
|
||||
const agentDocsLabel = i18n.translate(
|
||||
'xpack.apm.agentInstancesDetails.agentDocsUrlLabel',
|
||||
{
|
||||
|
@ -52,17 +61,25 @@ export function AgentContextualInformation({
|
|||
serviceName,
|
||||
agentDocsPageUrl,
|
||||
instances,
|
||||
latestVersion,
|
||||
query,
|
||||
isLatestVersionsLoading,
|
||||
latestVersionsFailed,
|
||||
}: {
|
||||
agentName: AgentName;
|
||||
serviceName: string;
|
||||
agentDocsPageUrl?: string;
|
||||
instances: number;
|
||||
latestVersion?: string;
|
||||
query: TypeOf<ApmRoutes, '/settings/agent-explorer'>['query'];
|
||||
isLatestVersionsLoading: boolean;
|
||||
latestVersionsFailed: boolean;
|
||||
}) {
|
||||
const { core } = useApmPluginContext();
|
||||
const { core, config } = useApmPluginContext();
|
||||
const latestAgentVersionEnabled = !isEmpty(config.latestAgentVersionsUrl);
|
||||
const comparisonEnabled = getComparisonEnabled({ core });
|
||||
const { rangeFrom, rangeTo } = useDefaultTimeRange();
|
||||
const width = latestAgentVersionEnabled ? '20%' : '25%';
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
|
@ -88,7 +105,7 @@ export function AgentContextualInformation({
|
|||
}
|
||||
/>
|
||||
),
|
||||
width: '25%',
|
||||
width,
|
||||
},
|
||||
{
|
||||
label: agentNameLabel,
|
||||
|
@ -100,7 +117,7 @@ export function AgentContextualInformation({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
width: '25%',
|
||||
width,
|
||||
},
|
||||
{
|
||||
label: instancesLabel,
|
||||
|
@ -112,8 +129,25 @@ export function AgentContextualInformation({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
width: '25%',
|
||||
width,
|
||||
},
|
||||
...(latestAgentVersionEnabled
|
||||
? [
|
||||
{
|
||||
label: latestVersionLabel,
|
||||
fieldName: latestVersionLabel,
|
||||
val: (
|
||||
<AgentLatestVersion
|
||||
agentName={agentName}
|
||||
isLoading={isLatestVersionsLoading}
|
||||
latestVersion={latestVersion}
|
||||
failed={latestVersionsFailed}
|
||||
/>
|
||||
),
|
||||
width,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: agentDocsLabel,
|
||||
fieldName: agentDocsLabel,
|
||||
|
@ -129,7 +163,7 @@ export function AgentContextualInformation({
|
|||
}
|
||||
/>
|
||||
),
|
||||
width: '25%',
|
||||
width,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -58,10 +58,17 @@ function useAgentInstancesFetcher({ serviceName }: { serviceName: string }) {
|
|||
|
||||
interface Props {
|
||||
agent: AgentExplorerItem;
|
||||
isLatestVersionsLoading: boolean;
|
||||
latestVersionsFailed: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AgentInstances({ agent, onClose }: Props) {
|
||||
export function AgentInstances({
|
||||
agent,
|
||||
isLatestVersionsLoading,
|
||||
latestVersionsFailed,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const { query } = useApmParams('/settings/agent-explorer');
|
||||
|
||||
const instances = useAgentInstancesFetcher({
|
||||
|
@ -95,7 +102,10 @@ export function AgentInstances({ agent, onClose }: Props) {
|
|||
serviceName={agent.serviceName}
|
||||
agentDocsPageUrl={agent.agentDocsPageUrl}
|
||||
instances={agent.instances}
|
||||
latestVersion={agent.latestVersion}
|
||||
query={query}
|
||||
isLatestVersionsLoading={isLatestVersionsLoading}
|
||||
latestVersionsFailed={latestVersionsFailed}
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { EuiSkeletonRectangle, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../../common/i18n';
|
||||
import { AgentName } from '../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
|
||||
export function AgentLatestVersion({
|
||||
agentName,
|
||||
isLoading,
|
||||
latestVersion,
|
||||
failed,
|
||||
}: {
|
||||
agentName: AgentName;
|
||||
isLoading: boolean;
|
||||
latestVersion?: string;
|
||||
failed: boolean;
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const latestVersionElement = latestVersion ? (
|
||||
<>{latestVersion}</>
|
||||
) : (
|
||||
<>{NOT_AVAILABLE_LABEL}</>
|
||||
);
|
||||
|
||||
const failedLatestVersionsElement = (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.agentExplorer.agentLatestVersion.airGappedMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The latest version of {agentName} agent could not be fetched from the repository. Please contact your administrator to check the server logs.',
|
||||
values: { agentName },
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>{NOT_AVAILABLE_LABEL}</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiSkeletonRectangle
|
||||
width="60px"
|
||||
height={euiTheme.size.l}
|
||||
borderRadius="m"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{!failed ? latestVersionElement : failedLatestVersionsElement}
|
||||
</EuiSkeletonRectangle>
|
||||
);
|
||||
}
|
|
@ -9,13 +9,16 @@ import {
|
|||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { AgentExplorerFieldName } from '../../../../../../common/agent_explorer';
|
||||
import { AgentName } from '../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
|
||||
import { AgentIcon } from '../../../../shared/agent_icon';
|
||||
import { EnvironmentBadge } from '../../../../shared/environment_badge';
|
||||
|
@ -24,6 +27,7 @@ import { ITableColumn, ManagedTable } from '../../../../shared/managed_table';
|
|||
import { TruncateWithTooltip } from '../../../../shared/truncate_with_tooltip';
|
||||
import { AgentExplorerDocsLink } from '../agent_explorer_docs_link';
|
||||
import { AgentInstances } from '../agent_instances';
|
||||
import { AgentLatestVersion } from '../agent_latest_version';
|
||||
|
||||
export type AgentExplorerItem = ValuesType<
|
||||
APIReturnType<'GET /internal/apm/get_agents_per_service'>['items']
|
||||
|
@ -31,9 +35,15 @@ export type AgentExplorerItem = ValuesType<
|
|||
|
||||
export function getAgentsColumns({
|
||||
selectedAgent,
|
||||
isLatestVersionsLoading,
|
||||
latestAgentVersionEnabled,
|
||||
latestVersionsFailed,
|
||||
onAgentSelected,
|
||||
}: {
|
||||
selectedAgent?: AgentExplorerItem;
|
||||
isLatestVersionsLoading: boolean;
|
||||
latestAgentVersionEnabled: boolean;
|
||||
latestVersionsFailed: boolean;
|
||||
onAgentSelected: (agent: AgentExplorerItem) => void;
|
||||
}): Array<ITableColumn<AgentExplorerItem>> {
|
||||
return [
|
||||
|
@ -153,6 +163,51 @@ export function getAgentsColumns({
|
|||
/>
|
||||
),
|
||||
},
|
||||
...(latestAgentVersionEnabled
|
||||
? [
|
||||
{
|
||||
field: AgentExplorerFieldName.AgentLastVersion,
|
||||
name: (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.agentLatestVersionColumnTooltip',
|
||||
{
|
||||
defaultMessage: 'The latest released version of the agent.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.agentLatestVersionColumnLabel',
|
||||
{ defaultMessage: 'Latest Agent Version' }
|
||||
)}
|
||||
|
||||
<EuiIcon
|
||||
size="s"
|
||||
color="subdued"
|
||||
type="questionInCircle"
|
||||
className="eui-alignCenter"
|
||||
/>
|
||||
</>
|
||||
</EuiToolTip>
|
||||
),
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
truncateText: true,
|
||||
render: (
|
||||
_: any,
|
||||
{ agentName, latestVersion }: AgentExplorerItem
|
||||
) => (
|
||||
<AgentLatestVersion
|
||||
agentName={agentName}
|
||||
isLoading={isLatestVersionsLoading}
|
||||
latestVersion={latestVersion}
|
||||
failed={latestVersionsFailed}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
field: AgentExplorerFieldName.AgentDocsPageUrl,
|
||||
name: i18n.translate(
|
||||
|
@ -177,9 +232,20 @@ interface Props {
|
|||
items: AgentExplorerItem[];
|
||||
noItemsMessage: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
isLatestVersionsLoading: boolean;
|
||||
latestVersionsFailed: boolean;
|
||||
}
|
||||
|
||||
export function AgentList({ items, noItemsMessage, isLoading }: Props) {
|
||||
export function AgentList({
|
||||
items,
|
||||
noItemsMessage,
|
||||
isLoading,
|
||||
isLatestVersionsLoading,
|
||||
latestVersionsFailed,
|
||||
}: Props) {
|
||||
const { config } = useApmPluginContext();
|
||||
const latestAgentVersionEnabled = !isEmpty(config.latestAgentVersionsUrl);
|
||||
|
||||
const [selectedAgent, setSelectedAgent] = useState<AgentExplorerItem>();
|
||||
|
||||
const onAgentSelected = (agent: AgentExplorerItem) => {
|
||||
|
@ -191,14 +257,31 @@ export function AgentList({ items, noItemsMessage, isLoading }: Props) {
|
|||
};
|
||||
|
||||
const agentColumns = useMemo(
|
||||
() => getAgentsColumns({ selectedAgent, onAgentSelected }),
|
||||
[selectedAgent]
|
||||
() =>
|
||||
getAgentsColumns({
|
||||
selectedAgent,
|
||||
isLatestVersionsLoading,
|
||||
latestAgentVersionEnabled,
|
||||
latestVersionsFailed,
|
||||
onAgentSelected,
|
||||
}),
|
||||
[
|
||||
selectedAgent,
|
||||
latestAgentVersionEnabled,
|
||||
isLatestVersionsLoading,
|
||||
latestVersionsFailed,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedAgent && (
|
||||
<AgentInstances agent={selectedAgent} onClose={onCloseFlyout} />
|
||||
<AgentInstances
|
||||
agent={selectedAgent}
|
||||
isLatestVersionsLoading={isLatestVersionsLoading}
|
||||
latestVersionsFailed={latestVersionsFailed}
|
||||
onClose={onCloseFlyout}
|
||||
/>
|
||||
)}
|
||||
<ManagedTable
|
||||
columns={agentColumns}
|
||||
|
|
|
@ -17,13 +17,20 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ElasticApmAgentLatestVersion,
|
||||
OtelAgentLatestVersion,
|
||||
} from '../../../../../common/agent_explorer';
|
||||
import { isOpenTelemetryAgentName } from '../../../../../common/agent_name';
|
||||
import {
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../../common/es_fields/apm';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { EnvironmentsContextProvider } from '../../../../context/environments_context/environments_context';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { ApmEnvironmentFilter } from '../../../shared/environment_filter';
|
||||
|
@ -33,6 +40,15 @@ import { SuggestionsSelect } from '../../../shared/suggestions_select';
|
|||
import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge';
|
||||
import { AgentList } from './agent_list';
|
||||
|
||||
const getOtelLatestAgentVersion = (
|
||||
agentTelemetryAutoVersion: string[],
|
||||
otelLatestVersion?: OtelAgentLatestVersion
|
||||
) => {
|
||||
return agentTelemetryAutoVersion.length > 0
|
||||
? otelLatestVersion?.auto_latest_version
|
||||
: otelLatestVersion?.sdk_latest_version;
|
||||
};
|
||||
|
||||
function useAgentExplorerFetcher({
|
||||
start,
|
||||
end,
|
||||
|
@ -63,6 +79,17 @@ function useAgentExplorerFetcher({
|
|||
);
|
||||
}
|
||||
|
||||
function useLatestAgentVersionsFetcher(latestAgentVersionEnabled: boolean) {
|
||||
return useFetcher(
|
||||
(callApmApi) => {
|
||||
if (latestAgentVersionEnabled) {
|
||||
return callApmApi('GET /internal/apm/get_latest_agent_versions');
|
||||
}
|
||||
},
|
||||
[latestAgentVersionEnabled]
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentExplorer() {
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -74,9 +101,30 @@ export function AgentExplorer() {
|
|||
const rangeTo = 'now';
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const { config } = useApmPluginContext();
|
||||
const latestAgentVersionEnabled = !isEmpty(config.latestAgentVersionsUrl);
|
||||
|
||||
const agents = useAgentExplorerFetcher({ start, end });
|
||||
const { data: latestAgentVersions, status: latestAgentVersionsStatus } =
|
||||
useLatestAgentVersionsFetcher(latestAgentVersionEnabled);
|
||||
|
||||
const isLoading = agents.status === FETCH_STATUS.LOADING;
|
||||
const isLatestAgentVersionsLoading =
|
||||
latestAgentVersionsStatus === FETCH_STATUS.LOADING;
|
||||
|
||||
const agentItems = (agents.data?.items ?? []).map((agent) => ({
|
||||
...agent,
|
||||
latestVersion: isOpenTelemetryAgentName(agent.agentName)
|
||||
? getOtelLatestAgentVersion(
|
||||
agent.agentTelemetryAutoVersion,
|
||||
latestAgentVersions?.data?.[agent.agentName] as OtelAgentLatestVersion
|
||||
)
|
||||
: (
|
||||
latestAgentVersions?.data?.[
|
||||
agent.agentName
|
||||
] as ElasticApmAgentLatestVersion
|
||||
)?.latest_version,
|
||||
}));
|
||||
|
||||
const noItemsMessage = (
|
||||
<EuiEmptyPrompt
|
||||
|
@ -199,8 +247,10 @@ export function AgentExplorer() {
|
|||
<EuiFlexItem>
|
||||
<AgentList
|
||||
isLoading={isLoading}
|
||||
items={agents.data?.items ?? []}
|
||||
items={agentItems}
|
||||
noItemsMessage={noItemsMessage}
|
||||
isLatestVersionsLoading={isLatestAgentVersionsLoading}
|
||||
latestVersionsFailed={!!latestAgentVersions?.error}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -66,6 +66,7 @@ const mockConfig: ConfigSchema = {
|
|||
ui: {
|
||||
enabled: false,
|
||||
},
|
||||
latestAgentVersionsUrl: '',
|
||||
};
|
||||
|
||||
const urlService = new UrlService({
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface ConfigSchema {
|
|||
ui: {
|
||||
enabled: boolean;
|
||||
};
|
||||
latestAgentVersionsUrl: string;
|
||||
}
|
||||
|
||||
export const plugin: PluginInitializer<ApmPluginSetup, ApmPluginStart> = (
|
||||
|
|
|
@ -53,6 +53,9 @@ const configSchema = schema.object({
|
|||
onboarding: schema.string({ defaultValue: 'apm-*' }),
|
||||
}),
|
||||
forceSyntheticSource: schema.boolean({ defaultValue: false }),
|
||||
latestAgentVersionsUrl: schema.string({
|
||||
defaultValue: 'https://apm-agent-versions.elastic.co/versions.json',
|
||||
}),
|
||||
});
|
||||
|
||||
// plugin config
|
||||
|
@ -110,6 +113,7 @@ export const config: PluginConfigDescriptor<APMConfig> = {
|
|||
exposeToBrowser: {
|
||||
serviceMapEnabled: true,
|
||||
ui: true,
|
||||
latestAgentVersionsUrl: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export class ErrorWithStatusCode extends Error {
|
||||
constructor(message: string, public readonly statusCode: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('node-fetch');
|
||||
import Boom from '@hapi/boom';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { fetchAgentsLatestVersion } from './fetch_agents_latest_version';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fetchMock = require('node-fetch') as jest.Mock;
|
||||
const logger = loggerMock.create();
|
||||
|
||||
describe('ApmFetchAgentslatestsVersion', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('when url is empty should not fetch latest versions', async () => {
|
||||
const boom = Boom.notImplemented(
|
||||
'To use latest agent versions you must set xpack.apm.latestAgentVersionsUrl.'
|
||||
);
|
||||
|
||||
await expect(fetchAgentsLatestVersion(logger, '')).rejects.toThrow(boom);
|
||||
expect(fetchMock).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('when url is defined', () => {
|
||||
it('should handle errors gracefully', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
text: () => 'Request Timeout',
|
||||
status: 408,
|
||||
ok: false,
|
||||
});
|
||||
|
||||
const { data, error } = await fetchAgentsLatestVersion(logger, 'my-url');
|
||||
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
expect(data).toEqual({});
|
||||
expect(error?.statusCode).toEqual('408');
|
||||
});
|
||||
|
||||
it('should return latest agents version', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
json: () => ({
|
||||
java: '1.1.0',
|
||||
}),
|
||||
status: 200,
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const { data, error } = await fetchAgentsLatestVersion(logger, 'my-url');
|
||||
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
expect(data).toEqual({ java: '1.1.0' });
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import fetch from 'node-fetch';
|
||||
import {
|
||||
ElasticApmAgentLatestVersion,
|
||||
OtelAgentLatestVersion,
|
||||
} from '../../../common/agent_explorer';
|
||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
import { ErrorWithStatusCode } from './error_with_status_code';
|
||||
|
||||
const MISSING_CONFIGURATION = i18n.translate(
|
||||
'xpack.apm.agent_explorer.error.missing_configuration',
|
||||
{
|
||||
defaultMessage:
|
||||
'To use latest agent versions you must set xpack.apm.latestAgentVersionsUrl.',
|
||||
}
|
||||
);
|
||||
|
||||
export interface AgentLatestVersionsResponse {
|
||||
data: AgentLatestVersions;
|
||||
error?: { message: string; type?: string; statusCode?: string };
|
||||
}
|
||||
|
||||
type AgentLatestVersions = Record<
|
||||
AgentName,
|
||||
ElasticApmAgentLatestVersion | OtelAgentLatestVersion
|
||||
>;
|
||||
|
||||
export const fetchAgentsLatestVersion = async (
|
||||
logger: Logger,
|
||||
latestAgentVersionsUrl: string
|
||||
): Promise<AgentLatestVersionsResponse> => {
|
||||
if (isEmpty(latestAgentVersionsUrl)) {
|
||||
throw Boom.notImplemented(MISSING_CONFIGURATION);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(latestAgentVersionsUrl);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorWithStatusCode(
|
||||
`${response.status} - ${await response.text()}`,
|
||||
`${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return { data };
|
||||
} catch (error) {
|
||||
const message = `Failed to retrieve latest APM Agent versions due to ${error}`;
|
||||
logger.warn(message);
|
||||
|
||||
return {
|
||||
data: {} as AgentLatestVersions,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
|
@ -9,8 +9,8 @@ import { isOpenTelemetryAgentName } from '../../../common/agent_name';
|
|||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { RandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { getAgentsItems } from './get_agents_items';
|
||||
import { getAgentDocsPageUrl } from './get_agent_url_repository';
|
||||
import { getAgentsItems } from './get_agents_items';
|
||||
|
||||
const getOtelAgentVersion = (item: {
|
||||
agentTelemetryAutoVersion: string[];
|
||||
|
@ -29,7 +29,9 @@ export interface AgentExplorerAgentsResponse {
|
|||
environments: string[];
|
||||
agentName: AgentName;
|
||||
agentVersion: string[];
|
||||
agentTelemetryAutoVersion: string[];
|
||||
instances: number;
|
||||
latestVersion?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
@ -65,16 +67,19 @@ export async function getAgents({
|
|||
|
||||
return {
|
||||
items: items.map((item) => {
|
||||
const agentVersion = isOpenTelemetryAgentName(item.agentName)
|
||||
? getOtelAgentVersion(item)
|
||||
: item.agentVersion;
|
||||
const agentDocsPageUrl = getAgentDocsPageUrl(item.agentName);
|
||||
|
||||
const { agentTelemetryAutoVersion, ...rest } = item;
|
||||
if (isOpenTelemetryAgentName(item.agentName)) {
|
||||
return {
|
||||
...item,
|
||||
agentVersion: getOtelAgentVersion(item),
|
||||
agentDocsPageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
agentVersion,
|
||||
agentDocsPageUrl: getAgentDocsPageUrl(item.agentName as AgentName),
|
||||
...item,
|
||||
agentDocsPageUrl,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -20,6 +20,10 @@ import {
|
|||
AgentExplorerAgentInstancesResponse,
|
||||
getAgentInstances,
|
||||
} from './get_agent_instances';
|
||||
import {
|
||||
AgentLatestVersionsResponse,
|
||||
fetchAgentsLatestVersion,
|
||||
} from './fetch_agents_latest_version';
|
||||
|
||||
const agentExplorerRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/get_agents_per_service',
|
||||
|
@ -71,6 +75,16 @@ const agentExplorerRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const latestAgentVersionsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/get_latest_agent_versions',
|
||||
options: { tags: ['access:apm'] },
|
||||
async handler(resources): Promise<AgentLatestVersionsResponse> {
|
||||
const { logger, config } = resources;
|
||||
|
||||
return fetchAgentsLatestVersion(logger, config.latestAgentVersionsUrl);
|
||||
},
|
||||
});
|
||||
|
||||
const agentExplorerInstanceRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/agent_instances',
|
||||
options: { tags: ['access:apm'] },
|
||||
|
@ -104,5 +118,6 @@ const agentExplorerInstanceRoute = createApmServerRoute({
|
|||
|
||||
export const agentExplorerRouteRepository = {
|
||||
...agentExplorerRoute,
|
||||
...latestAgentVersionsRoute,
|
||||
...agentExplorerInstanceRoute,
|
||||
};
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"@kbn/shared-ux-router",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/exploratory-view-plugin",
|
||||
"@kbn/logging-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { ElasticApmAgentLatestVersion } from '@kbn/apm-plugin/common/agent_explorer';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
|
||||
const nodeAgentName = 'nodejs';
|
||||
const unlistedAgentName = 'unlistedAgent';
|
||||
|
||||
async function callApi() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/get_latest_agent_versions',
|
||||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Agent latest versions when configuration is defined',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('returns a version when agent is listed in the file', async () => {
|
||||
const { status, body } = await callApi();
|
||||
expect(status).to.be(200);
|
||||
|
||||
const agents = body.data;
|
||||
|
||||
const nodeAgent = agents[nodeAgentName] as ElasticApmAgentLatestVersion;
|
||||
expect(nodeAgent?.latest_version).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('returns undefined when agent is not listed in the file', async () => {
|
||||
const { status, body } = await callApi();
|
||||
expect(status).to.be(200);
|
||||
|
||||
const agents = body.data;
|
||||
|
||||
// @ts-ignore
|
||||
const unlistedAgent = agents[unlistedAgentName] as ElasticApmAgentLatestVersion;
|
||||
expect(unlistedAgent?.latest_version).to.be(undefined);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue