mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM] Agent explorer (PoC) (#143844)
Closes [142218](https://github.com/elastic/kibana/issues/142218) - Introducing the Agent explorer view https://user-images.githubusercontent.com/1313018/198403801-bd9aab9c-1f7e-4775-b3ed-e0e488eef513.mov Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
fa69b424bc
commit
9c27f3d798
32 changed files with 1890 additions and 69 deletions
|
@ -81,6 +81,7 @@ export type ApmFields = Fields &
|
|||
'service.name': string;
|
||||
'service.version': string;
|
||||
'service.environment': string;
|
||||
'service.language.name': string;
|
||||
'service.node.name': string;
|
||||
'service.runtime.name': string;
|
||||
'service.runtime.version': string;
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { random } from 'lodash';
|
||||
import { flatten, random } from 'lodash';
|
||||
|
||||
import { apm, timerange } from '../..';
|
||||
import { Instance } from '../lib/apm/instance';
|
||||
import { Scenario } from '../cli/scenario';
|
||||
import { getLogger } from '../cli/utils/get_common_services';
|
||||
import { RunOptions } from '../cli/utils/parse_run_cli_flags';
|
||||
import { ApmFields } from '../lib/apm/apm_fields';
|
||||
import { Instance } from '../lib/apm/instance';
|
||||
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
|
||||
|
||||
const ENVIRONMENT = getSynthtraceEnvironment(__filename);
|
||||
|
@ -24,6 +24,12 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
const numServices = 500;
|
||||
const languages = ['go', 'dotnet', 'java', 'python'];
|
||||
const services = ['web', 'order-processing', 'api-backend', 'proxy'];
|
||||
const agentVersions: Record<string, string[]> = {
|
||||
go: ['2.1.0', '2.0.0', '1.15.0', '1.14.0', '1.13.1'],
|
||||
dotnet: ['1.18.0', '1.17.0', '1.16.1', '1.16.0', '1.15.0'],
|
||||
java: ['1.34.1', '1.34.0', '1.33.0', '1.32.0', '1.32.0'],
|
||||
python: ['6.12.0', '6.11.0', '6.10.2', '6.10.1', '6.10.0'],
|
||||
};
|
||||
|
||||
return {
|
||||
generate: ({ from, to }) => {
|
||||
|
@ -31,16 +37,27 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
|
||||
const successfulTimestamps = range.ratePerMinute(180);
|
||||
|
||||
const instances = [...Array(numServices).keys()].map((index) =>
|
||||
apm
|
||||
.service({
|
||||
name: `${services[index % services.length]}-${
|
||||
languages[index % languages.length]
|
||||
}-${index}`,
|
||||
environment: ENVIRONMENT,
|
||||
agentName: languages[index % languages.length],
|
||||
})
|
||||
.instance(`instance-${index}`)
|
||||
const instances = flatten(
|
||||
[...Array(numServices).keys()].map((index) => {
|
||||
const language = languages[index % languages.length];
|
||||
const agentLanguageVersions = agentVersions[language];
|
||||
|
||||
const numOfInstances = (index % 3) + 1;
|
||||
|
||||
return [...Array(numOfInstances).keys()].map((instanceIndex) =>
|
||||
apm
|
||||
.service({
|
||||
name: `${services[index % services.length]}-${language}-${index}`,
|
||||
environment: ENVIRONMENT,
|
||||
agentName: language,
|
||||
})
|
||||
.instance(`instance-${index}-${instanceIndex}`)
|
||||
.defaults({
|
||||
'agent.version': agentLanguageVersions[index % agentLanguageVersions.length],
|
||||
'service.language.name': language,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const urls = ['GET /order/{id}', 'POST /basket/{id}', 'DELETE /basket', 'GET /products'];
|
||||
|
|
|
@ -442,6 +442,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:apmAgentExplorerView': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:apmAWSLambdaPriceFactor': {
|
||||
type: 'text',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
|
|
|
@ -46,6 +46,7 @@ export interface UsageStats {
|
|||
'observability:apmAWSLambdaPriceFactor': string;
|
||||
'observability:apmAWSLambdaRequestCostPerMillion': number;
|
||||
'observability:enableInfrastructureHostsView': boolean;
|
||||
'observability:apmAgentExplorerView': boolean;
|
||||
'visualize:enableLabs': boolean;
|
||||
'visualization:heatmap:maxBuckets': number;
|
||||
'visualization:colorMapping': string;
|
||||
|
|
|
@ -8916,6 +8916,12 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:apmAgentExplorerView": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:apmAWSLambdaPriceFactor": {
|
||||
"type": "text",
|
||||
"_meta": {
|
||||
|
|
16
x-pack/plugins/apm/common/agent_explorer.ts
Normal file
16
x-pack/plugins/apm/common/agent_explorer.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 enum AgentExplorerFieldName {
|
||||
ServiceName = 'serviceName',
|
||||
Environments = 'environments',
|
||||
AgentName = 'agentName',
|
||||
AgentVersion = 'agentVersion',
|
||||
AgentLastVersion = 'agentLastVersion',
|
||||
AgentDocsPageUrl = 'agentDocsPageUrl',
|
||||
Instances = 'instances',
|
||||
}
|
|
@ -45,6 +45,9 @@ export const AGENT_NAMES: AgentName[] = [
|
|||
...OPEN_TELEMETRY_AGENT_NAMES,
|
||||
];
|
||||
|
||||
export const isOpenTelemetryAgentName = (agentName: AgentName) =>
|
||||
OPEN_TELEMETRY_AGENT_NAMES.includes(agentName);
|
||||
|
||||
export const JAVA_AGENT_NAMES: AgentName[] = ['java', 'opentelemetry/java'];
|
||||
|
||||
export function isJavaAgentName(
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { EuiIcon, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { isOpenTelemetryAgentName } from '../../../../../../common/agent_name';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../../common/i18n';
|
||||
import { AgentName } from '../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
|
||||
interface AgentExplorerDocsLinkProps {
|
||||
agentName: AgentName;
|
||||
repositoryUrl?: string;
|
||||
}
|
||||
|
||||
export function AgentExplorerDocsLink({
|
||||
agentName,
|
||||
repositoryUrl,
|
||||
}: AgentExplorerDocsLinkProps) {
|
||||
if (!repositoryUrl) {
|
||||
return <>{NOT_AVAILABLE_LABEL}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj={`agentExplorerDocsLink_${agentName}`}
|
||||
href={repositoryUrl}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
{isOpenTelemetryAgentName(agentName) ? (
|
||||
<EuiIcon
|
||||
type="documentation"
|
||||
size="m"
|
||||
title={i18n.translate('xpack.apm.agentExplorer.docsLink.otel.logo', {
|
||||
defaultMessage: 'Opentelemetry logo',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<EuiIcon
|
||||
type="logoElastic"
|
||||
size="m"
|
||||
title={i18n.translate(
|
||||
'xpack.apm.agentExplorer.docsLink.elastic.logo',
|
||||
{
|
||||
defaultMessage: 'Elastic logo',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}{' '}
|
||||
{i18n.translate('xpack.apm.agentExplorer.docsLink.message', {
|
||||
defaultMessage: 'Docs',
|
||||
})}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||
import React from 'react';
|
||||
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 { useDefaultTimeRange } from '../../../../../../hooks/use_default_time_range';
|
||||
import { ApmRoutes } from '../../../../../routing/apm_route_config';
|
||||
import { ServiceLink } from '../../../../../shared/service_link';
|
||||
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';
|
||||
|
||||
export function AgentContextualInformation({
|
||||
agentName,
|
||||
serviceName,
|
||||
agentDocsPageUrl,
|
||||
instances,
|
||||
query,
|
||||
}: {
|
||||
agentName: AgentName;
|
||||
serviceName: string;
|
||||
agentDocsPageUrl?: string;
|
||||
instances: number;
|
||||
query: TypeOf<ApmRoutes, '/settings/agent-explorer'>['query'];
|
||||
}) {
|
||||
const { core } = useApmPluginContext();
|
||||
const comparisonEnabled = getComparisonEnabled({ core });
|
||||
const { rangeFrom, rangeTo } = useDefaultTimeRange();
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: i18n.translate('xpack.apm.agentInstancesDetails.serviceLabel', {
|
||||
defaultMessage: 'Service',
|
||||
}),
|
||||
fieldName: AgentExplorerFieldName.ServiceName,
|
||||
val: (
|
||||
<TruncateWithTooltip
|
||||
data-test-subj="apmAgentExplorerListServiceLink"
|
||||
text={serviceName}
|
||||
content={
|
||||
<ServiceLink
|
||||
agentName={agentName}
|
||||
query={{
|
||||
kuery: query.kuery,
|
||||
serviceGroup: '',
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment: query.environment,
|
||||
comparisonEnabled,
|
||||
}}
|
||||
serviceName={serviceName}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.apm.agentInstancesDetails.agentNameLabel', {
|
||||
defaultMessage: 'Agent Name',
|
||||
}),
|
||||
fieldName: AgentExplorerFieldName.AgentName,
|
||||
val: (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<span className="eui-textTruncate">{agentName}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.apm.agentInstancesDetails.intancesLabel', {
|
||||
defaultMessage: 'Instances',
|
||||
}),
|
||||
fieldName: 'instances',
|
||||
val: (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<span className="eui-textTruncate">{instances}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'xpack.apm.agentInstancesDetails.agentDocsUrlLabel',
|
||||
{
|
||||
defaultMessage: 'Agent documentation',
|
||||
}
|
||||
),
|
||||
fieldName: AgentExplorerFieldName.AgentDocsPageUrl,
|
||||
val: (
|
||||
<TruncateWithTooltip
|
||||
data-test-subj="apmAgentExplorerListDocsLink"
|
||||
text={`${agentName} agent docs`}
|
||||
content={
|
||||
<AgentExplorerDocsLink
|
||||
agentName={agentName}
|
||||
repositoryUrl={agentDocsPageUrl}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
width: '25%',
|
||||
},
|
||||
];
|
||||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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 { EuiLink, EuiLoadingContent, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { AgentExplorerFieldName } from '../../../../../../../common/agent_explorer';
|
||||
import { isOpenTelemetryAgentName } from '../../../../../../../common/agent_name';
|
||||
import {
|
||||
getServiceNodeName,
|
||||
SERVICE_NODE_NAME_MISSING,
|
||||
} from '../../../../../../../common/service_nodes';
|
||||
import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
import { APIReturnType } from '../../../../../../services/rest/create_call_apm_api';
|
||||
import { unit } from '../../../../../../utils/style';
|
||||
import { EnvironmentBadge } from '../../../../../shared/environment_badge';
|
||||
import { ItemsBadge } from '../../../../../shared/item_badge';
|
||||
import { ServiceNodeMetricOverviewLink } from '../../../../../shared/links/apm/service_node_metric_overview_link';
|
||||
import {
|
||||
ITableColumn,
|
||||
ManagedTable,
|
||||
} from '../../../../../shared/managed_table';
|
||||
import { PopoverTooltip } from '../../../../../shared/popover_tooltip';
|
||||
import { TimestampTooltip } from '../../../../../shared/timestamp_tooltip';
|
||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||
|
||||
type AgentExplorerInstance = ValuesType<
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/agent_instances'>['items']
|
||||
>;
|
||||
|
||||
enum AgentExplorerInstanceFieldName {
|
||||
InstanceName = 'serviceNode',
|
||||
Environments = 'environments',
|
||||
AgentVersion = 'agentVersion',
|
||||
LastReport = 'lastReport',
|
||||
}
|
||||
|
||||
export function getInstanceColumns(
|
||||
serviceName: string,
|
||||
agentName: AgentName,
|
||||
agentDocsPageUrl?: string
|
||||
): Array<ITableColumn<AgentExplorerInstance>> {
|
||||
return [
|
||||
{
|
||||
field: AgentExplorerInstanceFieldName.InstanceName,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.InstanceColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Instance',
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (_, { serviceNode }) => {
|
||||
const displayedName = getServiceNodeName(serviceNode);
|
||||
|
||||
return serviceNode === SERVICE_NODE_NAME_MISSING ? (
|
||||
<>
|
||||
{displayedName}
|
||||
<PopoverTooltip
|
||||
ariaLabel={i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.noServiceNodeName.tooltip',
|
||||
{
|
||||
defaultMessage: 'Tooltip for missing serviceNodeName',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiText style={{ width: `${unit * 24}px` }} size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="You can configure the service node name through {seeDocs}."
|
||||
id="xpack.apm.agentExplorerInstanceTable.noServiceNodeName.tooltip.linkToDocs"
|
||||
values={{
|
||||
seeDocs: (
|
||||
<EuiLink
|
||||
href={`${agentDocsPageUrl}${
|
||||
!isOpenTelemetryAgentName(agentName)
|
||||
? 'configuration.html#service-node-name'
|
||||
: ''
|
||||
}`}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.noServiceNodeName.configurationOptions',
|
||||
{
|
||||
defaultMessage: 'configuration options',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</PopoverTooltip>
|
||||
</>
|
||||
) : (
|
||||
<TruncateWithTooltip
|
||||
data-test-subj="apmAgentExplorerInstanceListServiceLink"
|
||||
text={displayedName}
|
||||
content={
|
||||
<>
|
||||
{serviceNode ? (
|
||||
<ServiceNodeMetricOverviewLink
|
||||
serviceName={serviceName}
|
||||
serviceNodeName={serviceNode}
|
||||
>
|
||||
<span className="eui-textTruncate">{displayedName}</span>
|
||||
</ServiceNodeMetricOverviewLink>
|
||||
) : (
|
||||
<span className="eui-textTruncate">{displayedName}</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: AgentExplorerInstanceFieldName.Environments,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.environmentColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Environment',
|
||||
}
|
||||
),
|
||||
width: `${unit * 16}px`,
|
||||
sortable: true,
|
||||
render: (_, { environments }) => (
|
||||
<EnvironmentBadge environments={environments} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: AgentExplorerInstanceFieldName.AgentVersion,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.agentVersionColumnLabel',
|
||||
{ defaultMessage: 'Agent Version' }
|
||||
),
|
||||
width: `${unit * 16}px`,
|
||||
sortable: true,
|
||||
render: (_, { agentVersion }) => {
|
||||
const versions = [agentVersion];
|
||||
return (
|
||||
<ItemsBadge
|
||||
items={versions}
|
||||
multipleItemsMessage={i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.agentVersionColumnLabel.multipleVersions',
|
||||
{
|
||||
values: { versionsCount: versions.length },
|
||||
defaultMessage:
|
||||
'{versionsCount, plural, one {1 version} other {# versions}}',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: AgentExplorerInstanceFieldName.LastReport,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerInstanceTable.lastReportColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Last report',
|
||||
}
|
||||
),
|
||||
width: `${unit * 16}px`,
|
||||
sortable: true,
|
||||
render: (_, { lastReport }) => <TimestampTooltip time={lastReport} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
agentName: AgentName;
|
||||
agentDocsPageUrl?: string;
|
||||
items: AgentExplorerInstance[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function AgentInstancesDetails({
|
||||
serviceName,
|
||||
agentName,
|
||||
agentDocsPageUrl,
|
||||
items,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ width: '50%' }}>
|
||||
<EuiLoadingContent data-test-subj="loadingSpinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ManagedTable
|
||||
columns={getInstanceColumns(serviceName, agentName, agentDocsPageUrl)}
|
||||
items={items}
|
||||
noItemsMessage={i18n.translate(
|
||||
'xpack.apm.agentExplorer.table.noResults',
|
||||
{
|
||||
defaultMessage: 'No data found',
|
||||
}
|
||||
)}
|
||||
initialSortField={AgentExplorerFieldName.AgentVersion}
|
||||
initialSortDirection="desc"
|
||||
isLoading={isLoading}
|
||||
initialPageSize={25}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiPortal,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useApmParams } from '../../../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS } from '../../../../../hooks/use_fetcher';
|
||||
import { useProgressiveFetcher } from '../../../../../hooks/use_progressive_fetcher';
|
||||
import { useTimeRange } from '../../../../../hooks/use_time_range';
|
||||
import { ResponsiveFlyout } from '../../../transaction_details/waterfall_with_summary/waterfall_container/waterfall/responsive_flyout';
|
||||
import { AgentExplorerItem } from '../agent_list';
|
||||
import { AgentContextualInformation } from './agent_contextual_information';
|
||||
import { AgentInstancesDetails } from './agent_instances_details';
|
||||
|
||||
function useAgentInstancesFetcher({ serviceName }: { serviceName: string }) {
|
||||
const {
|
||||
query: { environment, kuery },
|
||||
} = useApmParams('/settings/agent-explorer');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom: 'now-24h', rangeTo: 'now' });
|
||||
|
||||
return useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/agent_instances',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[start, end, serviceName, environment, kuery]
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
agent: AgentExplorerItem;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AgentInstances({ agent, onClose }: Props) {
|
||||
const { query } = useApmParams('/settings/agent-explorer');
|
||||
|
||||
const instances = useAgentInstancesFetcher({
|
||||
serviceName: agent.serviceName,
|
||||
});
|
||||
|
||||
const isLoading = instances.status === FETCH_STATUS.LOADING;
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<ResponsiveFlyout onClose={onClose} ownFocus={true} maxWidth={false}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.apm.agentExplorer.instancesFlyout.title',
|
||||
{
|
||||
defaultMessage: 'Agent Instances',
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<AgentContextualInformation
|
||||
agentName={agent.agentName}
|
||||
serviceName={agent.serviceName}
|
||||
agentDocsPageUrl={agent.agentDocsPageUrl}
|
||||
instances={agent.instances}
|
||||
query={query}
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiSpacer size="m" />
|
||||
<AgentInstancesDetails
|
||||
serviceName={agent.serviceName}
|
||||
agentName={agent.agentName}
|
||||
agentDocsPageUrl={agent.agentDocsPageUrl}
|
||||
isLoading={isLoading}
|
||||
items={instances.data?.items ?? []}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</ResponsiveFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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 { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
|
||||
import { unit } from '../../../../../utils/style';
|
||||
import { AgentIcon } from '../../../../shared/agent_icon';
|
||||
import { EnvironmentBadge } from '../../../../shared/environment_badge';
|
||||
import { ItemsBadge } from '../../../../shared/item_badge';
|
||||
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';
|
||||
|
||||
export type AgentExplorerItem = ValuesType<
|
||||
APIReturnType<'GET /internal/apm/get_agents_per_service'>['items']
|
||||
>;
|
||||
|
||||
export function getAgentsColumns({
|
||||
selectedAgent,
|
||||
onAgentSelected,
|
||||
}: {
|
||||
selectedAgent?: AgentExplorerItem;
|
||||
onAgentSelected: (agent: AgentExplorerItem) => void;
|
||||
}): Array<ITableColumn<AgentExplorerItem>> {
|
||||
return [
|
||||
{
|
||||
field: AgentExplorerFieldName.ServiceName,
|
||||
name: '',
|
||||
width: `${unit * 3}px`,
|
||||
render: (_, agent) => {
|
||||
const isSelected = selectedAgent === agent;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.viewAgentInstances',
|
||||
{
|
||||
defaultMessage: 'Toggle agent instances view',
|
||||
}
|
||||
)}
|
||||
delay="long"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
iconSize="s"
|
||||
aria-label="Toggle agent instances view"
|
||||
data-test-subj="apmAgentExplorerListToggle"
|
||||
onClick={() => onAgentSelected(agent)}
|
||||
display={isSelected ? 'base' : 'empty'}
|
||||
iconType={isSelected ? 'minimize' : 'expand'}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: AgentExplorerFieldName.ServiceName,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.serviceNameColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Service Name',
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (_, { serviceName, agentName }) => (
|
||||
<TruncateWithTooltip
|
||||
data-test-subj="apmAgentExplorerListServiceLink"
|
||||
text={serviceName}
|
||||
content={
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AgentIcon agentName={agentName} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<span className="eui-textTruncate">{serviceName}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: AgentExplorerFieldName.Environments,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.environmentColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Environment',
|
||||
}
|
||||
),
|
||||
width: `${unit * 16}px`,
|
||||
sortable: true,
|
||||
render: (_, { environments }) => (
|
||||
<EnvironmentBadge environments={environments} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: AgentExplorerFieldName.Instances,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.instancesColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Instances',
|
||||
}
|
||||
),
|
||||
width: `${unit * 8}px`,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: AgentExplorerFieldName.AgentName,
|
||||
width: `${unit * 12}px`,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.agentNameColumnLabel',
|
||||
{ defaultMessage: 'Agent Name' }
|
||||
),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: AgentExplorerFieldName.AgentVersion,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.agentVersionColumnLabel',
|
||||
{ defaultMessage: 'Agent Version' }
|
||||
),
|
||||
width: `${unit * 8}px`,
|
||||
render: (_, { agentVersion }) => (
|
||||
<ItemsBadge
|
||||
items={agentVersion}
|
||||
multipleItemsMessage={i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.agentVersionColumnLabel.multipleVersions',
|
||||
{
|
||||
values: { versionsCount: agentVersion.length },
|
||||
defaultMessage:
|
||||
'{versionsCount, plural, one {1 version} other {# versions}}',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: AgentExplorerFieldName.AgentDocsPageUrl,
|
||||
name: i18n.translate(
|
||||
'xpack.apm.agentExplorerTable.agentDocsColumnLabel',
|
||||
{ defaultMessage: 'Agent Docs' }
|
||||
),
|
||||
width: `${unit * 8}px`,
|
||||
render: (_, { agentName, agentDocsPageUrl }) => (
|
||||
<EuiToolTip content={`${agentName} agent docs`}>
|
||||
<AgentExplorerDocsLink
|
||||
agentName={agentName as AgentName}
|
||||
repositoryUrl={agentDocsPageUrl}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: AgentExplorerItem[];
|
||||
noItemsMessage: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function AgentList({ items, noItemsMessage, isLoading }: Props) {
|
||||
const [selectedAgent, setSelectedAgent] = useState<AgentExplorerItem>();
|
||||
|
||||
const onAgentSelected = (agent: AgentExplorerItem) => {
|
||||
setSelectedAgent(agent);
|
||||
};
|
||||
|
||||
const onCloseFlyout = () => {
|
||||
setSelectedAgent(undefined);
|
||||
};
|
||||
|
||||
const agentColumns = useMemo(
|
||||
() => getAgentsColumns({ selectedAgent, onAgentSelected }),
|
||||
[selectedAgent]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedAgent && (
|
||||
<AgentInstances agent={selectedAgent} onClose={onCloseFlyout} />
|
||||
)}
|
||||
<ManagedTable
|
||||
columns={agentColumns}
|
||||
items={items}
|
||||
noItemsMessage={noItemsMessage}
|
||||
initialSortField={AgentExplorerFieldName.Instances}
|
||||
initialSortDirection="desc"
|
||||
isLoading={isLoading}
|
||||
initialPageSize={25}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCallOut,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { KueryBar } from '../../../shared/kuery_bar';
|
||||
import * as urlHelpers from '../../../shared/links/url_helpers';
|
||||
import { SuggestionsSelect } from '../../../shared/suggestions_select';
|
||||
import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge';
|
||||
import { AgentList } from './agent_list';
|
||||
|
||||
function useAgentExplorerFetcher({
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
}) {
|
||||
const {
|
||||
query: { environment, serviceName, agentLanguage, kuery },
|
||||
} = useApmParams('/settings/agent-explorer');
|
||||
|
||||
return useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/get_agents_per_service', {
|
||||
params: {
|
||||
query: {
|
||||
environment,
|
||||
serviceName,
|
||||
agentLanguage,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[environment, serviceName, agentLanguage, kuery, start, end]
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentExplorer() {
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
query: { serviceName, agentLanguage },
|
||||
} = useApmParams('/settings/agent-explorer');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom: 'now-24h', rangeTo: 'now' });
|
||||
const agents = useAgentExplorerFetcher({ start, end });
|
||||
|
||||
const isLoading = agents.status === FETCH_STATUS.LOADING;
|
||||
|
||||
const noItemsMessage = (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<div>
|
||||
{i18n.translate('xpack.apm.agentExplorer.notFoundLabel', {
|
||||
defaultMessage: 'No Agents found',
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
titleSize="s"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('xpack.apm.settings.agentExplorer.descriptionText', {
|
||||
defaultMessage:
|
||||
'Agent Explorer Technical Preview provides an inventory and details of deployed Agents.',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.settings.agentExplorer.title', {
|
||||
defaultMessage: 'Agent explorer',
|
||||
})}
|
||||
</h2>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TechnicalPreviewBadge icon="beaker" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
<EuiFlexItem grow={false}>
|
||||
<KueryBar />
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SuggestionsSelect
|
||||
prepend={i18n.translate(
|
||||
'xpack.apm.agentExplorer.serviceNameSelect.label',
|
||||
{
|
||||
defaultMessage: 'Service name',
|
||||
}
|
||||
)}
|
||||
defaultValue={serviceName}
|
||||
fieldName={SERVICE_NAME}
|
||||
onChange={(value) => {
|
||||
urlHelpers.push(history, {
|
||||
query: { serviceName: value ?? '' },
|
||||
});
|
||||
}}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.agentExplorer.serviceNameSelect.placeholder',
|
||||
{
|
||||
defaultMessage: 'All',
|
||||
}
|
||||
)}
|
||||
start={start}
|
||||
end={end}
|
||||
dataTestSubj="agentExplorerServiceNameSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SuggestionsSelect
|
||||
prepend={i18n.translate(
|
||||
'xpack.apm.agentExplorer.agentLanguageSelect.label',
|
||||
{
|
||||
defaultMessage: 'Agent language',
|
||||
}
|
||||
)}
|
||||
defaultValue={agentLanguage}
|
||||
fieldName={SERVICE_LANGUAGE_NAME}
|
||||
onChange={(value) => {
|
||||
urlHelpers.push(history, {
|
||||
query: { agentLanguage: value ?? '' },
|
||||
});
|
||||
}}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.apm.agentExplorer.agentLanguageSelect.placeholder',
|
||||
{
|
||||
defaultMessage: 'All',
|
||||
}
|
||||
)}
|
||||
start={start}
|
||||
end={end}
|
||||
dataTestSubj="agentExplorerAgentLanguageSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={i18n.translate('xpack.apm.agentExplorer.callout.24hoursData', {
|
||||
defaultMessage: 'Information based on the lastest 24h',
|
||||
})}
|
||||
iconType="clock"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
<EuiFlexItem>
|
||||
<AgentList
|
||||
isLoading={isLoading}
|
||||
items={agents.data?.items ?? []}
|
||||
noItemsMessage={noItemsMessage}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -4,23 +4,25 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as t from 'io-ts';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { agentConfigurationPageStepRt } from '../../../../common/agent_configuration/constants';
|
||||
import { environmentRt } from '../../../../common/environment_rt';
|
||||
import { Breadcrumb } from '../../app/breadcrumb';
|
||||
import { SettingsTemplate } from '../templates/settings_template';
|
||||
import { AgentConfigurations } from '../../app/settings/agent_configurations';
|
||||
import { CreateAgentConfigurationRouteView } from './create_agent_configuration_route_view';
|
||||
import { EditAgentConfigurationRouteView } from './edit_agent_configuration_route_view';
|
||||
import { AgentExplorer } from '../../app/settings/agent_explorer';
|
||||
import { AgentKeys } from '../../app/settings/agent_keys';
|
||||
import { AnomalyDetection } from '../../app/settings/anomaly_detection';
|
||||
import { ApmIndices } from '../../app/settings/apm_indices';
|
||||
import { CustomLinkOverview } from '../../app/settings/custom_link';
|
||||
import { Schema } from '../../app/settings/schema';
|
||||
import { AnomalyDetection } from '../../app/settings/anomaly_detection';
|
||||
import { AgentKeys } from '../../app/settings/agent_keys';
|
||||
import { GeneralSettings } from '../../app/settings/general_settings';
|
||||
import { Schema } from '../../app/settings/schema';
|
||||
import { SettingsTemplate } from '../templates/settings_template';
|
||||
import { CreateAgentConfigurationRouteView } from './create_agent_configuration_route_view';
|
||||
import { EditAgentConfigurationRouteView } from './edit_agent_configuration_route_view';
|
||||
|
||||
function page({
|
||||
title,
|
||||
|
@ -141,6 +143,28 @@ export const settings = {
|
|||
element: <AgentKeys />,
|
||||
tab: 'agent-keys',
|
||||
}),
|
||||
'/settings/agent-explorer': {
|
||||
...page({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.views.settings.agentExplorer.title',
|
||||
{
|
||||
defaultMessage: 'Agent explorer',
|
||||
}
|
||||
),
|
||||
element: <AgentExplorer />,
|
||||
tab: 'agent-explorer',
|
||||
}),
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
environmentRt,
|
||||
t.type({
|
||||
kuery: t.string,
|
||||
agentLanguage: t.string,
|
||||
serviceName: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
},
|
||||
'/settings': {
|
||||
element: <Redirect to="/settings/general-settings" />,
|
||||
},
|
||||
|
|
|
@ -6,13 +6,17 @@
|
|||
*/
|
||||
|
||||
import { EuiPageHeaderProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { ApmMainTemplate } from './apm_main_template';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { enableAgentExplorerView } from '@kbn/observability-plugin/public';
|
||||
import React from 'react';
|
||||
import { useDefaultEnvironment } from '../../../hooks/use_default_environment';
|
||||
import { Environment } from '../../../../common/environment_rt';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge';
|
||||
import { ApmRouter } from '../apm_route_config';
|
||||
import { ApmMainTemplate } from './apm_main_template';
|
||||
|
||||
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||
key:
|
||||
|
@ -22,7 +26,8 @@ type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
|||
| 'apm-indices'
|
||||
| 'custom-links'
|
||||
| 'schema'
|
||||
| 'general-settings';
|
||||
| 'general-settings'
|
||||
| 'agent-explorer';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
|
@ -34,7 +39,9 @@ interface Props {
|
|||
export function SettingsTemplate({ children, selectedTab }: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const router = useApmRouter();
|
||||
const tabs = getTabs({ core, selectedTab, router });
|
||||
const defaultEnvironment = useDefaultEnvironment();
|
||||
|
||||
const tabs = getTabs({ core, selectedTab, router, defaultEnvironment });
|
||||
|
||||
return (
|
||||
<ApmMainTemplate
|
||||
|
@ -55,13 +62,20 @@ function getTabs({
|
|||
core,
|
||||
selectedTab,
|
||||
router,
|
||||
defaultEnvironment,
|
||||
}: {
|
||||
core: CoreStart;
|
||||
selectedTab: Tab['key'];
|
||||
router: ApmRouter;
|
||||
defaultEnvironment: Environment;
|
||||
}) {
|
||||
const canReadMlJobs = !!core.application.capabilities.ml?.canGetJobs;
|
||||
|
||||
const agentExplorerEnabled = core.uiSettings.get<boolean>(
|
||||
enableAgentExplorerView,
|
||||
false
|
||||
);
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
key: 'general-settings',
|
||||
|
@ -77,6 +91,22 @@ function getTabs({
|
|||
}),
|
||||
href: router.link('/settings/agent-configuration'),
|
||||
},
|
||||
{
|
||||
key: 'agent-explorer',
|
||||
label: i18n.translate('xpack.apm.settings.agentExplorer', {
|
||||
defaultMessage: 'Agent Explorer',
|
||||
}),
|
||||
href: router.link('/settings/agent-explorer', {
|
||||
query: {
|
||||
environment: defaultEnvironment,
|
||||
kuery: '',
|
||||
agentLanguage: '',
|
||||
serviceName: '',
|
||||
},
|
||||
}),
|
||||
append: <TechnicalPreviewBadge icon="beaker" />,
|
||||
hidden: !agentExplorerEnabled,
|
||||
},
|
||||
{
|
||||
key: 'agent-keys',
|
||||
label: i18n.translate('xpack.apm.settings.agentKeys', {
|
||||
|
@ -117,9 +147,10 @@ function getTabs({
|
|||
|
||||
return tabs
|
||||
.filter((t) => !t.hidden)
|
||||
.map(({ href, key, label }) => ({
|
||||
.map(({ href, key, label, append }) => ({
|
||||
href,
|
||||
label,
|
||||
append,
|
||||
isSelected: key === selectedTab,
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -7,40 +7,23 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
import { ItemsBadge } from '../item_badge';
|
||||
|
||||
interface Props {
|
||||
environments: string[];
|
||||
}
|
||||
export function EnvironmentBadge({ environments = [] }: Props) {
|
||||
if (environments.length < 2) {
|
||||
return (
|
||||
<>
|
||||
{environments.map((env) => (
|
||||
<EuiBadge color="hollow" key={env}>
|
||||
{env}
|
||||
</EuiBadge>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content={environments.map((env) => (
|
||||
<React.Fragment key={env}>
|
||||
{env}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
>
|
||||
<EuiBadge>
|
||||
{i18n.translate('xpack.apm.servicesTable.environmentCount', {
|
||||
<ItemsBadge
|
||||
items={environments ?? []}
|
||||
multipleItemsMessage={i18n.translate(
|
||||
'xpack.apm.servicesTable.environmentCount',
|
||||
{
|
||||
values: { environmentCount: environments.length },
|
||||
defaultMessage:
|
||||
'{environmentCount, plural, one {1 environment} other {# environments}}',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
items: string[];
|
||||
multipleItemsMessage?: string;
|
||||
}
|
||||
export function ItemsBadge({
|
||||
items = [],
|
||||
multipleItemsMessage = i18n.translate('xpack.apm.itemsBadge.placeholder', {
|
||||
values: { itemsCount: items.length },
|
||||
defaultMessage: '{itemsCount, plural, one {1 item} other {# items}}',
|
||||
}),
|
||||
}: Props) {
|
||||
if (items.length < 2) {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<EuiBadge color="hollow" key={item}>
|
||||
{item}
|
||||
</EuiBadge>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content={items.map((item) => (
|
||||
<React.Fragment key={item}>
|
||||
{item}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
>
|
||||
<EuiBadge>{multipleItemsMessage}</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiPopover } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface PopoverTooltipProps {
|
||||
ariaLabel?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PopoverTooltip({ ariaLabel, children }: PopoverTooltipProps) {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition={'upCenter'}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={ariaLabel}
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
event.stopPropagation();
|
||||
}}
|
||||
size="xs"
|
||||
color="primary"
|
||||
iconType="questionInCircle"
|
||||
style={{ height: 'auto' }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
21
x-pack/plugins/apm/public/hooks/use_default_time_range.ts
Normal file
21
x-pack/plugins/apm/public/hooks/use_default_time_range.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import { TimePickerTimeDefaults } from '../components/shared/date_picker/typings';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
export function useDefaultTimeRange() {
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
const { from: rangeFrom, to: rangeTo } =
|
||||
core.uiSettings.get<TimePickerTimeDefaults>(
|
||||
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
|
||||
);
|
||||
|
||||
return { rangeFrom, rangeTo };
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
termQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
AGENT_VERSION,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
const MAX_NUMBER_OF_SERVICE_NODES = 500;
|
||||
|
||||
export async function getAgentInstances({
|
||||
environment,
|
||||
serviceName,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
environment: string;
|
||||
serviceName?: string;
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const response = await apmEventClient.search('get_agent_instances', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
exists: {
|
||||
field: AGENT_NAME,
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: AGENT_VERSION,
|
||||
},
|
||||
},
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...(serviceName ? termQuery(SERVICE_NAME, serviceName) : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
serviceNodes: {
|
||||
terms: {
|
||||
field: SERVICE_NODE_NAME,
|
||||
missing: SERVICE_NODE_NAME_MISSING,
|
||||
size: MAX_NUMBER_OF_SERVICE_NODES,
|
||||
},
|
||||
aggs: {
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
},
|
||||
sample: {
|
||||
top_metrics: {
|
||||
metrics: [{ field: AGENT_VERSION } as const],
|
||||
sort: {
|
||||
'@timestamp': 'desc' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
response.aggregations?.serviceNodes.buckets.map((agentInstance) => ({
|
||||
serviceNode: agentInstance.key as string,
|
||||
environments: agentInstance.environments.buckets.map(
|
||||
(environmentBucket) => environmentBucket.key as string
|
||||
),
|
||||
agentVersion: agentInstance.sample.top[0].metrics[
|
||||
AGENT_VERSION
|
||||
] as string,
|
||||
lastReport: agentInstance.sample.top[0].sort[0] as string,
|
||||
})) ?? []
|
||||
);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { isOpenTelemetryAgentName } from '../../../common/agent_name';
|
||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
|
||||
const agentsDocPageName: Partial<Record<AgentName, string>> = {
|
||||
go: 'go',
|
||||
java: 'java',
|
||||
'js-base': 'rum-js',
|
||||
'iOS/swift': 'swift',
|
||||
'rum-js': 'rum-js',
|
||||
nodejs: 'nodejs',
|
||||
python: 'python',
|
||||
dotnet: 'dotnet',
|
||||
ruby: 'ruby',
|
||||
php: 'php',
|
||||
'opentelemetry/cpp': 'cpp',
|
||||
'opentelemetry/dotnet': 'net',
|
||||
'opentelemetry/erlang': 'erlang',
|
||||
'opentelemetry/go': 'go',
|
||||
'opentelemetry/java': 'java',
|
||||
'opentelemetry/nodejs': 'js',
|
||||
'opentelemetry/php': 'php',
|
||||
'opentelemetry/python': 'python',
|
||||
'opentelemetry/ruby': 'ruby',
|
||||
'opentelemetry/swift': 'swift',
|
||||
'opentelemetry/webjs': 'js',
|
||||
};
|
||||
|
||||
export const getAgentDocsPageUrl = (agentName: AgentName) => {
|
||||
const agentDocsPageName = agentsDocPageName[agentName];
|
||||
|
||||
if (!agentDocsPageName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isOpenTelemetryAgentName(agentName)) {
|
||||
return `https://opentelemetry.io/docs/instrumentation/${agentDocsPageName}`;
|
||||
}
|
||||
|
||||
return `https://www.elastic.co/guide/en/apm/agent/${agentDocsPageName}/current/`;
|
||||
};
|
|
@ -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 { 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';
|
||||
|
||||
export async function getAgents({
|
||||
environment,
|
||||
serviceName,
|
||||
agentLanguage,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
randomSampler,
|
||||
}: {
|
||||
environment: string;
|
||||
serviceName?: string;
|
||||
agentLanguage?: string;
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
randomSampler: RandomSampler;
|
||||
}) {
|
||||
const items = await getAgentsItems({
|
||||
environment,
|
||||
serviceName,
|
||||
agentLanguage,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
randomSampler,
|
||||
});
|
||||
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
...item,
|
||||
agentDocsPageUrl: getAgentDocsPageUrl(item.agentName as AgentName),
|
||||
})),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 { ProcessorEvent } from '@kbn/observability-plugin/common/processor_event';
|
||||
import {
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
termQuery,
|
||||
} from '@kbn/observability-plugin/server/utils/queries';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
AGENT_VERSION,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
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 { MAX_NUMBER_OF_SERVICES } from '../services/get_services/get_services_items';
|
||||
|
||||
interface AggregationParams {
|
||||
environment: string;
|
||||
serviceName?: string;
|
||||
agentLanguage?: string;
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
randomSampler: RandomSampler;
|
||||
}
|
||||
|
||||
export async function getAgentsItems({
|
||||
environment,
|
||||
agentLanguage,
|
||||
serviceName,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
randomSampler,
|
||||
}: AggregationParams) {
|
||||
const response = await apmEventClient.search('get_agent_details', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
exists: {
|
||||
field: AGENT_NAME,
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: AGENT_VERSION,
|
||||
},
|
||||
},
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...(serviceName ? termQuery(SERVICE_NAME, serviceName) : []),
|
||||
...(agentLanguage
|
||||
? termQuery(SERVICE_LANGUAGE_NAME, agentLanguage)
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
random_sampler: randomSampler,
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: MAX_NUMBER_OF_SERVICES,
|
||||
},
|
||||
aggs: {
|
||||
instances: {
|
||||
cardinality: {
|
||||
field: SERVICE_NODE_NAME,
|
||||
},
|
||||
},
|
||||
agentVersions: {
|
||||
terms: {
|
||||
field: AGENT_VERSION,
|
||||
},
|
||||
},
|
||||
sample: {
|
||||
top_metrics: {
|
||||
metrics: [{ field: AGENT_NAME } as const],
|
||||
sort: {
|
||||
'@timestamp': 'desc' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
response.aggregations?.sample.services.buckets.map((bucket) => {
|
||||
return {
|
||||
serviceName: bucket.key as string,
|
||||
environments: bucket.environments.buckets.map(
|
||||
(env) => env.key as string
|
||||
),
|
||||
agentName: bucket.sample.top[0].metrics[AGENT_NAME] as AgentName,
|
||||
agentVersion: bucket.agentVersions.buckets.map(
|
||||
(version) => version.key as string
|
||||
),
|
||||
// service.node.name is set by the server only if a container.id or host.name are set. Otherwise should be explicitly set by agents.
|
||||
instances: (bucket.instances.value as number) || 1,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
119
x-pack/plugins/apm/server/routes/agent_explorer/route.ts
Normal file
119
x-pack/plugins/apm/server/routes/agent_explorer/route.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import {
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
probabilityRt,
|
||||
rangeRt,
|
||||
} from '../default_api_types';
|
||||
import { getAgents } from './get_agents';
|
||||
import { getAgentInstances } from './get_agent_instances';
|
||||
|
||||
const agentExplorerRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/get_agents_per_service',
|
||||
options: { tags: ['access:apm'] },
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
probabilityRt,
|
||||
t.partial({
|
||||
serviceName: t.string,
|
||||
agentLanguage: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
async handler(resources): Promise<{
|
||||
items: Array<{
|
||||
serviceName: string;
|
||||
environments: string[];
|
||||
agentName: import('./../../../typings/es_schemas/ui/fields/agent').AgentName;
|
||||
agentVersion: string[];
|
||||
agentDocsPageUrl?: string;
|
||||
instances: number;
|
||||
}>;
|
||||
}> {
|
||||
const {
|
||||
params,
|
||||
request,
|
||||
plugins: { security },
|
||||
} = resources;
|
||||
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
probability,
|
||||
serviceName,
|
||||
agentLanguage,
|
||||
} = params.query;
|
||||
|
||||
const [apmEventClient, randomSampler] = await Promise.all([
|
||||
getApmEventClient(resources),
|
||||
getRandomSampler({ security, request, probability }),
|
||||
]);
|
||||
|
||||
return getAgents({
|
||||
environment,
|
||||
serviceName,
|
||||
agentLanguage,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
randomSampler,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const agentExplorerInstanceRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/agent_instances',
|
||||
options: { tags: ['access:apm'] },
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([environmentRt, kueryRt, rangeRt, probabilityRt]),
|
||||
}),
|
||||
async handler(resources): Promise<{
|
||||
items: Array<{
|
||||
serviceNode?: string;
|
||||
environments: string[];
|
||||
agentVersion: string;
|
||||
lastReport: string;
|
||||
}>;
|
||||
}> {
|
||||
const { params } = resources;
|
||||
|
||||
const { environment, kuery, start, end } = params.query;
|
||||
|
||||
const { serviceName } = params.path;
|
||||
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
|
||||
return {
|
||||
items: await getAgentInstances({
|
||||
environment,
|
||||
serviceName,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const agentExplorerRouteRepository = {
|
||||
...agentExplorerRoute,
|
||||
...agentExplorerInstanceRoute,
|
||||
};
|
|
@ -10,12 +10,13 @@ import type {
|
|||
ServerRouteRepository,
|
||||
} from '@kbn/server-route-repository';
|
||||
import { PickByValue } from 'utility-types';
|
||||
import { agentExplorerRouteRepository } from '../agent_explorer/route';
|
||||
import { agentKeysRouteRepository } from '../agent_keys/route';
|
||||
import { alertsChartPreviewRouteRepository } from '../alerts/route';
|
||||
import { dependencisRouteRepository } from '../dependencies/route';
|
||||
import { correlationsRouteRepository } from '../correlations/route';
|
||||
import { dataViewRouteRepository } from '../data_view/route';
|
||||
import { debugTelemetryRoute } from '../debug_telemetry/route';
|
||||
import { dependencisRouteRepository } from '../dependencies/route';
|
||||
import { environmentsRouteRepository } from '../environments/route';
|
||||
import { errorsRouteRepository } from '../errors/route';
|
||||
import { eventMetadataRouteRepository } from '../event_metadata/route';
|
||||
|
@ -25,6 +26,7 @@ import { historicalDataRouteRepository } from '../historical_data/route';
|
|||
import { infrastructureRouteRepository } from '../infrastructure/route';
|
||||
import { latencyDistributionRouteRepository } from '../latency_distribution/route';
|
||||
import { metricsRouteRepository } from '../metrics/route';
|
||||
import { mobileRouteRepository } from '../mobile/route';
|
||||
import { observabilityOverviewRouteRepository } from '../observability_overview/route';
|
||||
import { serviceRouteRepository } from '../services/route';
|
||||
import { serviceGroupRouteRepository } from '../service_groups/route';
|
||||
|
@ -33,15 +35,14 @@ import { agentConfigurationRouteRepository } from '../settings/agent_configurati
|
|||
import { anomalyDetectionRouteRepository } from '../settings/anomaly_detection/route';
|
||||
import { apmIndicesRouteRepository } from '../settings/apm_indices/route';
|
||||
import { customLinkRouteRepository } from '../settings/custom_link/route';
|
||||
import { labsRouteRepository } from '../settings/labs/route';
|
||||
import { sourceMapsRouteRepository } from '../source_maps/route';
|
||||
import { spanLinksRouteRepository } from '../span_links/route';
|
||||
import { storageExplorerRouteRepository } from '../storage_explorer/route';
|
||||
import { suggestionsRouteRepository } from '../suggestions/route';
|
||||
import { timeRangeMetadataRoute } from '../time_range_metadata/route';
|
||||
import { traceRouteRepository } from '../traces/route';
|
||||
import { transactionRouteRepository } from '../transactions/route';
|
||||
import { storageExplorerRouteRepository } from '../storage_explorer/route';
|
||||
import { labsRouteRepository } from '../settings/labs/route';
|
||||
import { mobileRouteRepository } from '../mobile/route';
|
||||
|
||||
function getTypedGlobalApmServerRouteRepository() {
|
||||
const repository = {
|
||||
|
@ -76,6 +77,7 @@ function getTypedGlobalApmServerRouteRepository() {
|
|||
...debugTelemetryRoute,
|
||||
...timeRangeMetadataRoute,
|
||||
...labsRouteRepository,
|
||||
...agentExplorerRouteRepository,
|
||||
...mobileRouteRepository,
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import { ServiceGroup } from '../../../../common/service_groups';
|
|||
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
const MAX_NUMBER_OF_SERVICES = 500;
|
||||
export const MAX_NUMBER_OF_SERVICES = 500;
|
||||
|
||||
export async function getServicesItems({
|
||||
environment,
|
||||
|
|
|
@ -26,6 +26,7 @@ export {
|
|||
enableInfrastructureHostsView,
|
||||
enableServiceMetrics,
|
||||
enableAwsLambdaMetrics,
|
||||
enableAgentExplorerView,
|
||||
apmAWSLambdaPriceFactor,
|
||||
apmAWSLambdaRequestCostPerMillion,
|
||||
enableCriticalPath,
|
||||
|
|
|
@ -21,6 +21,7 @@ export const apmLabsButton = 'observability:apmLabsButton';
|
|||
export const enableInfrastructureHostsView = 'observability:enableInfrastructureHostsView';
|
||||
export const enableAwsLambdaMetrics = 'observability:enableAwsLambdaMetrics';
|
||||
export const enableServiceMetrics = 'observability:apmEnableServiceMetrics';
|
||||
export const enableAgentExplorerView = 'observability:apmAgentExplorerView';
|
||||
export const apmAWSLambdaPriceFactor = 'observability:apmAWSLambdaPriceFactor';
|
||||
export const apmAWSLambdaRequestCostPerMillion = 'observability:apmAWSLambdaRequestCostPerMillion';
|
||||
export const enableCriticalPath = 'observability:apmEnableCriticalPath';
|
||||
|
|
|
@ -30,6 +30,7 @@ export {
|
|||
enableNewSyntheticsView,
|
||||
apmServiceGroupMaxNumberOfServices,
|
||||
enableInfrastructureHostsView,
|
||||
enableAgentExplorerView,
|
||||
} from '../common/ui_settings_keys';
|
||||
export { uptimeOverviewLocatorID } from '../common';
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiSettingsParams } from '@kbn/core/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { observabilityFeatureId, ProgressiveLoadingQuality } from '../common';
|
||||
import {
|
||||
enableComparisonByDefault,
|
||||
|
@ -21,12 +21,13 @@ import {
|
|||
apmTraceExplorerTab,
|
||||
apmOperationsTab,
|
||||
apmLabsButton,
|
||||
enableInfrastructureHostsView,
|
||||
enableServiceMetrics,
|
||||
enableAgentExplorerView,
|
||||
enableAwsLambdaMetrics,
|
||||
apmAWSLambdaPriceFactor,
|
||||
apmAWSLambdaRequestCostPerMillion,
|
||||
enableCriticalPath,
|
||||
enableInfrastructureHostsView,
|
||||
enableServiceMetrics,
|
||||
} from '../common/ui_settings_keys';
|
||||
|
||||
const technicalPreviewLabel = i18n.translate(
|
||||
|
@ -294,6 +295,23 @@ export const uiSettings: Record<string, UiSettings> = {
|
|||
type: 'boolean',
|
||||
showInLabs: true,
|
||||
},
|
||||
[enableAgentExplorerView]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.enableAgentExplorer', {
|
||||
defaultMessage: 'Agent explorer',
|
||||
}),
|
||||
description: i18n.translate('xpack.observability.enableAgentExplorerDescription', {
|
||||
defaultMessage: '{technicalPreviewLabel} Enables Agent explorer view.',
|
||||
values: {
|
||||
technicalPreviewLabel: `<em>[${technicalPreviewLabel}]</em>`,
|
||||
},
|
||||
}),
|
||||
schema: schema.boolean(),
|
||||
value: false,
|
||||
requiresPageReload: true,
|
||||
type: 'boolean',
|
||||
showInLabs: true,
|
||||
},
|
||||
[apmAWSLambdaPriceFactor]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.apmAWSLambdaPricePerGbSeconds', {
|
||||
|
|
|
@ -5,20 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
import supertest from 'supertest';
|
||||
import { format, UrlObject } from 'url';
|
||||
import {
|
||||
ApmUsername,
|
||||
APM_TEST_PASSWORD,
|
||||
} from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication';
|
||||
import { createApmUsers } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/create_apm_users';
|
||||
import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
import supertest from 'supertest';
|
||||
import { format, UrlObject } from 'url';
|
||||
import { MachineLearningAPIProvider } from '../../functional/services/ml/api';
|
||||
import { APMFtrConfigName } from '../configs';
|
||||
import { createApmApiClient } from './apm_api_supertest';
|
||||
import { RegistryProvider } from './registry';
|
||||
import { bootstrapApmSynthtrace } from './bootstrap_apm_synthtrace';
|
||||
import { MachineLearningAPIProvider } from '../../functional/services/ml/api';
|
||||
import {
|
||||
FtrProviderContext,
|
||||
InheritedFtrProviderContext,
|
||||
InheritedServices,
|
||||
} from './ftr_provider_context';
|
||||
import { RegistryProvider } from './registry';
|
||||
|
||||
export interface ApmFtrConfig {
|
||||
name: APMFtrConfigName;
|
||||
|
@ -43,7 +48,37 @@ async function getApmApiClient({
|
|||
|
||||
export type CreateTestConfig = ReturnType<typeof createTestConfig>;
|
||||
|
||||
export function createTestConfig(config: ApmFtrConfig) {
|
||||
type ApmApiClientKey =
|
||||
| 'noAccessUser'
|
||||
| 'readUser'
|
||||
| 'writeUser'
|
||||
| 'annotationWriterUser'
|
||||
| 'noMlAccessUser'
|
||||
| 'manageOwnAgentKeysUser'
|
||||
| 'createAndAllAgentKeysUser'
|
||||
| 'monitorClusterAndIndicesUser';
|
||||
|
||||
export interface CreateTest {
|
||||
testFiles: string[];
|
||||
servers: any;
|
||||
servicesRequiredForTestAnalysis: string[];
|
||||
services: InheritedServices & {
|
||||
apmFtrConfig: () => ApmFtrConfig;
|
||||
registry: ({ getService }: FtrProviderContext) => ReturnType<typeof RegistryProvider>;
|
||||
synthtraceEsClient: (context: InheritedFtrProviderContext) => Promise<ApmSynthtraceEsClient>;
|
||||
apmApiClient: (
|
||||
context: InheritedFtrProviderContext
|
||||
) => Record<ApmApiClientKey, Awaited<ReturnType<typeof getApmApiClient>>>;
|
||||
ml: ({ getService }: FtrProviderContext) => ReturnType<typeof MachineLearningAPIProvider>;
|
||||
};
|
||||
junit: { reportName: string };
|
||||
esTestCluster: any;
|
||||
kbnTestServer: any;
|
||||
}
|
||||
|
||||
export function createTestConfig(
|
||||
config: ApmFtrConfig
|
||||
): ({ readConfigFile }: FtrConfigProviderContext) => Promise<CreateTest> {
|
||||
const { license, name, kibanaConfig } = config;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
|
@ -51,7 +86,7 @@ export function createTestConfig(config: ApmFtrConfig) {
|
|||
require.resolve('../../api_integration/config.ts')
|
||||
);
|
||||
|
||||
const services = xPackAPITestsConfig.get('services') as InheritedServices;
|
||||
const services = xPackAPITestsConfig.get('services');
|
||||
const servers = xPackAPITestsConfig.get('servers');
|
||||
const kibanaServer = servers.kibana as UrlObject;
|
||||
const kibanaServerUrl = format(kibanaServer);
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace';
|
||||
import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
|
||||
import { keyBy } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
const goServiceName = 'opbeans-go';
|
||||
const nodeServiceName = 'opbeans-node';
|
||||
|
||||
async function callApi(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/get_agents_per_service'>['params']
|
||||
>
|
||||
) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/get_agents_per_service',
|
||||
params: {
|
||||
query: {
|
||||
probability: 1,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('Agent explorer when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body.items).to.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
registry.when('Agent explorer', { config: 'basic', archives: [] }, () => {
|
||||
describe('when data is loaded', () => {
|
||||
before(async () => {
|
||||
const serviceGo = apm
|
||||
.service({
|
||||
name: goServiceName,
|
||||
environment: 'production',
|
||||
agentName: 'go',
|
||||
})
|
||||
.instance('instance-go')
|
||||
.defaults({
|
||||
'agent.version': '5.1.2',
|
||||
'service.language.name': 'go',
|
||||
});
|
||||
|
||||
const serviceNodeStaging = apm
|
||||
.service({
|
||||
name: nodeServiceName,
|
||||
environment: 'staging',
|
||||
agentName: 'nodejs',
|
||||
})
|
||||
.instance('instance-node-staging')
|
||||
.defaults({
|
||||
'agent.version': '1.0.0',
|
||||
'service.language.name': 'javascript',
|
||||
});
|
||||
|
||||
const serviceNodeDev = apm
|
||||
.service({
|
||||
name: nodeServiceName,
|
||||
environment: 'dev',
|
||||
agentName: 'nodejs',
|
||||
})
|
||||
.instance('instance-node-dev')
|
||||
.defaults({
|
||||
'agent.version': '1.0.3',
|
||||
'service.language.name': 'javascript',
|
||||
});
|
||||
|
||||
await synthtraceEsClient.index([
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
serviceGo
|
||||
.transaction({ transactionName: 'GET /api/product/list' })
|
||||
.duration(2000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
serviceNodeStaging
|
||||
.transaction({ transactionName: 'GET /api/users/list' })
|
||||
.duration(2000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
serviceNodeDev
|
||||
.transaction({ transactionName: 'GET /api/users/list' })
|
||||
.duration(2000)
|
||||
.timestamp(timestamp)
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct agents information', async () => {
|
||||
const { status, body } = await callApi();
|
||||
expect(status).to.be(200);
|
||||
expect(body.items).to.have.length(2);
|
||||
|
||||
const agents = keyBy(body.items, 'serviceName');
|
||||
|
||||
const goAgent = agents[goServiceName];
|
||||
expect(goAgent?.environments).to.have.length(1);
|
||||
expect(goAgent?.environments).to.contain('production');
|
||||
expect(goAgent?.agentName).to.be('go');
|
||||
expect(goAgent?.agentVersion).to.contain('5.1.2');
|
||||
expect(goAgent?.agentDocsPageUrl).to.be(
|
||||
'https://www.elastic.co/guide/en/apm/agent/go/current/'
|
||||
);
|
||||
|
||||
const nodeAgent = agents[nodeServiceName];
|
||||
expect(nodeAgent?.environments).to.have.length(2);
|
||||
expect(nodeAgent?.environments).to.contain('staging');
|
||||
expect(nodeAgent?.environments).to.contain('dev');
|
||||
expect(nodeAgent?.agentName).to.be('nodejs');
|
||||
expect(nodeAgent?.agentVersion).to.contain('1.0.0');
|
||||
expect(nodeAgent?.agentVersion).to.contain('1.0.3');
|
||||
expect(nodeAgent?.agentDocsPageUrl).to.be(
|
||||
'https://www.elastic.co/guide/en/apm/agent/nodejs/current/'
|
||||
);
|
||||
});
|
||||
|
||||
const matchingFilterTests = [
|
||||
['environment', 'dev', nodeServiceName],
|
||||
['serviceName', nodeServiceName, nodeServiceName],
|
||||
['agentLanguage', 'go', goServiceName],
|
||||
['kuery', `service.name : ${goServiceName}`, goServiceName],
|
||||
];
|
||||
|
||||
matchingFilterTests.forEach(([filterName, filterValue, expectedService]) => {
|
||||
it(`returns only agents matching selected ${filterName}`, async () => {
|
||||
const { status, body } = await callApi({
|
||||
query: {
|
||||
[filterName]: filterValue,
|
||||
},
|
||||
});
|
||||
expect(status).to.be(200);
|
||||
expect(body.items).to.have.length(1);
|
||||
expect(body.items[0]?.serviceName).to.be(expectedService);
|
||||
});
|
||||
});
|
||||
|
||||
const notMatchingFilterTests = [
|
||||
['serviceName', 'my-service'],
|
||||
['agentLanguage', 'my-language'],
|
||||
];
|
||||
|
||||
notMatchingFilterTests.forEach(([filterName, filterValue]) => {
|
||||
it(`returns empty agents when there is no matching ${filterName}`, async () => {
|
||||
const { status, body } = await callApi({
|
||||
query: {
|
||||
[filterName]: filterValue,
|
||||
},
|
||||
});
|
||||
expect(status).to.be(200);
|
||||
expect(body.items).to.be.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue