[Infra UI] Adding links from Infra UI to Uptime (#35993)
* [Infra UI] Adding from Infra UI to Uptime * Removed unused variable * Adding tests for link generation * Sometimes the IP address will be an array with [IPv4, IPv6] * Ensuring only IPv4 addresses are returned; adding tests; * only showing uptime link for host's with ip addresses
This commit is contained in:
parent
3dacef2901
commit
69bbca490b
|
@ -262,6 +262,8 @@ export interface InfraSnapshotNodePath {
|
|||
value: string;
|
||||
|
||||
label: string;
|
||||
|
||||
ip?: string | null;
|
||||
}
|
||||
|
||||
export interface InfraSnapshotNodeMetric {
|
||||
|
@ -868,6 +870,8 @@ export namespace WaffleNodesQuery {
|
|||
value: string;
|
||||
|
||||
label: string;
|
||||
|
||||
ip?: string | null;
|
||||
};
|
||||
|
||||
export type Metric = {
|
||||
|
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createUptimeLink } from './create_uptime_link';
|
||||
import {
|
||||
InfraWaffleMapOptions,
|
||||
InfraWaffleMapLegendMode,
|
||||
InfraFormatterType,
|
||||
} from '../../../lib/lib';
|
||||
import { InfraNodeType } from '../../../../common/graphql/types';
|
||||
import { InfraSnapshotMetricType } from '../../../graphql/types';
|
||||
|
||||
const options: InfraWaffleMapOptions = {
|
||||
fields: {
|
||||
container: 'container.id',
|
||||
pod: 'kubernetes.pod.uid',
|
||||
host: 'host.name',
|
||||
message: ['@message'],
|
||||
timestamp: '@timestanp',
|
||||
tiebreaker: '@timestamp',
|
||||
},
|
||||
formatter: InfraFormatterType.percent,
|
||||
formatTemplate: '{{value}}',
|
||||
metric: { type: InfraSnapshotMetricType.cpu },
|
||||
groupBy: [],
|
||||
legend: {
|
||||
type: InfraWaffleMapLegendMode.gradient,
|
||||
rules: [],
|
||||
},
|
||||
};
|
||||
|
||||
describe('createUptimeLink()', () => {
|
||||
it('should work for hosts with ip', () => {
|
||||
const node = {
|
||||
pathId: 'host-01',
|
||||
id: 'host-01',
|
||||
name: 'host-01',
|
||||
ip: '10.0.1.2',
|
||||
path: [],
|
||||
metric: {
|
||||
name: InfraSnapshotMetricType.cpu,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
};
|
||||
expect(createUptimeLink(options, InfraNodeType.host, node)).toBe(
|
||||
'../app/uptime#/?search=host.ip:"10.0.1.2"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for hosts without ip', () => {
|
||||
const node = {
|
||||
pathId: 'host-01',
|
||||
id: 'host-01',
|
||||
name: 'host-01',
|
||||
path: [],
|
||||
metric: {
|
||||
name: InfraSnapshotMetricType.cpu,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
};
|
||||
expect(createUptimeLink(options, InfraNodeType.host, node)).toBe(
|
||||
'../app/uptime#/?search=host.name:"host-01"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for pods', () => {
|
||||
const node = {
|
||||
pathId: 'pod-01',
|
||||
id: '29193-pod-02939',
|
||||
name: 'pod-01',
|
||||
path: [],
|
||||
metric: {
|
||||
name: InfraSnapshotMetricType.cpu,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
};
|
||||
expect(createUptimeLink(options, InfraNodeType.pod, node)).toBe(
|
||||
'../app/uptime#/?search=kubernetes.pod.uid:"29193-pod-02939"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for container', () => {
|
||||
const node = {
|
||||
pathId: 'docker-01',
|
||||
id: 'docker-1234',
|
||||
name: 'docker-01',
|
||||
path: [],
|
||||
metric: {
|
||||
name: InfraSnapshotMetricType.cpu,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
};
|
||||
expect(createUptimeLink(options, InfraNodeType.container, node)).toBe(
|
||||
'../app/uptime#/?search=container.id:"docker-1234"'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { InfraNodeType } from '../../../graphql/types';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../lib/lib';
|
||||
|
||||
const BASE_URL = '../app/uptime#/?search=';
|
||||
|
||||
export const createUptimeLink = (
|
||||
options: InfraWaffleMapOptions,
|
||||
nodeType: InfraNodeType,
|
||||
node: InfraWaffleMapNode
|
||||
) => {
|
||||
if (nodeType === InfraNodeType.host && node.ip) {
|
||||
return `${BASE_URL}host.ip:"${node.ip}"`;
|
||||
}
|
||||
const field = get(options, ['fields', nodeType], '');
|
||||
return `${BASE_URL}${field ? field + ':' : ''}"${node.id}"`;
|
||||
};
|
|
@ -17,6 +17,7 @@ import { injectUICapabilities } from 'ui/capabilities/react';
|
|||
import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
|
||||
import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to';
|
||||
import { createUptimeLink } from './lib/create_uptime_link';
|
||||
|
||||
interface Props {
|
||||
options: InfraWaffleMapOptions;
|
||||
|
@ -89,6 +90,19 @@ export const NodeContextMenu = injectUICapabilities(
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const uptimeUrl = node.ip
|
||||
? {
|
||||
name: intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.infra.nodeContextMenu.viewUptimeLink',
|
||||
defaultMessage: 'View {nodeType} in Uptime',
|
||||
},
|
||||
{ nodeType }
|
||||
),
|
||||
href: createUptimeLink(options, nodeType, node),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
|
@ -118,6 +132,7 @@ export const NodeContextMenu = injectUICapabilities(
|
|||
]
|
||||
: []),
|
||||
...(apmTracesUrl ? [apmTracesUrl] : []),
|
||||
...(uptimeUrl ? [uptimeUrl] : []),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -94,6 +94,7 @@ export function createWaffleMapNode(node: InfraSnapshotNode): InfraWaffleMapNode
|
|||
pathId: node.path.map(p => p.value).join('/'),
|
||||
path: node.path,
|
||||
id: nodePathItem.value,
|
||||
ip: nodePathItem.ip,
|
||||
name: nodePathItem.label || nodePathItem.value,
|
||||
metric: node.metric,
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ export const waffleNodesQuery = gql`
|
|||
path {
|
||||
value
|
||||
label
|
||||
ip
|
||||
}
|
||||
metric {
|
||||
name
|
||||
|
|
|
@ -2006,6 +2006,14 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ip",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
|
@ -262,6 +262,8 @@ export interface InfraSnapshotNodePath {
|
|||
value: string;
|
||||
|
||||
label: string;
|
||||
|
||||
ip?: string | null;
|
||||
}
|
||||
|
||||
export interface InfraSnapshotNodeMetric {
|
||||
|
@ -868,6 +870,8 @@ export namespace WaffleNodesQuery {
|
|||
value: string;
|
||||
|
||||
label: string;
|
||||
|
||||
ip?: string | null;
|
||||
};
|
||||
|
||||
export type Metric = {
|
||||
|
|
|
@ -102,6 +102,7 @@ export interface InfraWaffleMapNode {
|
|||
pathId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ip?: string | null;
|
||||
path: InfraSnapshotNodePath[];
|
||||
metric: InfraSnapshotNodeMetric;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export const snapshotSchema: any = gql`
|
|||
type InfraSnapshotNodePath {
|
||||
value: String!
|
||||
label: String!
|
||||
ip: String
|
||||
}
|
||||
|
||||
type InfraSnapshotNode {
|
||||
|
|
|
@ -290,6 +290,8 @@ export interface InfraSnapshotNodePath {
|
|||
value: string;
|
||||
|
||||
label: string;
|
||||
|
||||
ip?: string | null;
|
||||
}
|
||||
|
||||
export interface InfraSnapshotNodeMetric {
|
||||
|
@ -1455,6 +1457,8 @@ export namespace InfraSnapshotNodePathResolvers {
|
|||
value?: ValueResolver<string, TypeParent, Context>;
|
||||
|
||||
label?: LabelResolver<string, TypeParent, Context>;
|
||||
|
||||
ip?: IpResolver<string | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type ValueResolver<
|
||||
|
@ -1467,6 +1471,11 @@ export namespace InfraSnapshotNodePathResolvers {
|
|||
Parent = InfraSnapshotNodePath,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type IpResolver<
|
||||
R = string | null,
|
||||
Parent = InfraSnapshotNodePath,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace InfraSnapshotNodeMetricResolvers {
|
||||
|
|
|
@ -15,3 +15,8 @@ export const NAME_FIELDS = {
|
|||
[InfraNodeType.pod]: 'kubernetes.pod.name',
|
||||
[InfraNodeType.container]: 'container.name',
|
||||
};
|
||||
export const IP_FIELDS = {
|
||||
[InfraNodeType.host]: 'host.ip',
|
||||
[InfraNodeType.pod]: 'kubernetes.pod.ip',
|
||||
[InfraNodeType.container]: 'container.ip_address',
|
||||
};
|
||||
|
|
|
@ -14,7 +14,9 @@ export const getGroupedNodesSources = (options: InfraSnapshotRequestOptions) =>
|
|||
return { [`${gb.field}`]: { terms: { field: gb.field } } };
|
||||
});
|
||||
sources.push({
|
||||
id: { terms: { field: options.sourceConfiguration.fields[options.nodeType] } },
|
||||
id: {
|
||||
terms: { field: options.sourceConfiguration.fields[options.nodeType] },
|
||||
},
|
||||
});
|
||||
sources.push({
|
||||
name: { terms: { field: NAME_FIELDS[options.nodeType] } },
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isIPv4, getIPFromBucket, InfraSnapshotNodeGroupByBucket } from './response_helpers';
|
||||
import { InfraNodeType } from '../../graphql/types';
|
||||
|
||||
describe('InfraOps ResponseHelpers', () => {
|
||||
describe('isIPv4', () => {
|
||||
it('should return true for IPv4', () => {
|
||||
expect(isIPv4('192.168.2.4')).toBe(true);
|
||||
});
|
||||
it('should return false for anything else', () => {
|
||||
expect(isIPv4('0:0:0:0:0:0:0:1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIPFromBucket', () => {
|
||||
it('should return IPv4 address', () => {
|
||||
const bucket: InfraSnapshotNodeGroupByBucket = {
|
||||
key: {
|
||||
id: 'example-01',
|
||||
name: 'example-01',
|
||||
},
|
||||
ip: {
|
||||
hits: {
|
||||
total: { value: 1 },
|
||||
hits: [
|
||||
{
|
||||
_index: 'metricbeat-2019-01-01',
|
||||
_type: '_doc',
|
||||
_id: '29392939',
|
||||
_score: null,
|
||||
sort: [],
|
||||
_source: {
|
||||
host: {
|
||||
ip: ['2001:db8:85a3::8a2e:370:7334', '192.168.1.4'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getIPFromBucket(InfraNodeType.host, bucket)).toBe('192.168.1.4');
|
||||
});
|
||||
it('should NOT return ipv6 address', () => {
|
||||
const bucket: InfraSnapshotNodeGroupByBucket = {
|
||||
key: {
|
||||
id: 'example-01',
|
||||
name: 'example-01',
|
||||
},
|
||||
ip: {
|
||||
hits: {
|
||||
total: { value: 1 },
|
||||
hits: [
|
||||
{
|
||||
_index: 'metricbeat-2019-01-01',
|
||||
_type: '_doc',
|
||||
_id: '29392939',
|
||||
_score: null,
|
||||
sort: [],
|
||||
_source: {
|
||||
host: {
|
||||
ip: ['2001:db8:85a3::8a2e:370:7334'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getIPFromBucket(InfraNodeType.host, bucket)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,16 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isNumber, last, max, sum } from 'lodash';
|
||||
import { isNumber, last, max, sum, get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
InfraSnapshotMetricType,
|
||||
InfraSnapshotNodePath,
|
||||
InfraSnapshotNodeMetric,
|
||||
InfraNodeType,
|
||||
} from '../../graphql/types';
|
||||
import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
|
||||
import { InfraSnapshotRequestOptions } from './snapshot';
|
||||
import { IP_FIELDS } from '../constants';
|
||||
|
||||
export interface InfraSnapshotNodeMetricsBucket {
|
||||
key: { id: string };
|
||||
|
@ -38,23 +40,56 @@ export interface InfraSnapshotBucketWithValues {
|
|||
|
||||
export type InfraSnapshotMetricsBucket = InfraSnapshotBucketWithKey & InfraSnapshotBucketWithValues;
|
||||
|
||||
interface InfraSnapshotIpHit {
|
||||
_index: string;
|
||||
_type: string;
|
||||
_id: string;
|
||||
_score: number | null;
|
||||
_source: {
|
||||
host: {
|
||||
ip: string[] | string;
|
||||
};
|
||||
};
|
||||
sort: number[];
|
||||
}
|
||||
|
||||
export interface InfraSnapshotNodeGroupByBucket {
|
||||
key: {
|
||||
id: string;
|
||||
name: string;
|
||||
[groupByField: string]: string;
|
||||
};
|
||||
ip: {
|
||||
hits: {
|
||||
total: { value: number };
|
||||
hits: InfraSnapshotIpHit[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject);
|
||||
|
||||
export const getIPFromBucket = (
|
||||
nodeType: InfraNodeType,
|
||||
bucket: InfraSnapshotNodeGroupByBucket
|
||||
): string | null => {
|
||||
const ip = get(bucket, `ip.hits.hits[0]._source.${IP_FIELDS[nodeType]}`, null);
|
||||
if (Array.isArray(ip)) {
|
||||
return ip.find(isIPv4) || null;
|
||||
}
|
||||
return ip;
|
||||
};
|
||||
|
||||
export const getNodePath = (
|
||||
groupBucket: InfraSnapshotNodeGroupByBucket,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): InfraSnapshotNodePath[] => {
|
||||
const node = groupBucket.key;
|
||||
const path = options.groupBy.map(gb => {
|
||||
return { value: node[`${gb.field}`], label: node[`${gb.field}`] };
|
||||
return { value: node[`${gb.field}`], label: node[`${gb.field}`] } as InfraSnapshotNodePath;
|
||||
});
|
||||
path.push({ value: node.id, label: node.name });
|
||||
const ip = getIPFromBucket(options.nodeType, groupBucket);
|
||||
path.push({ value: node.id, label: node.name, ip });
|
||||
return path;
|
||||
};
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
InfraSnapshotNodeGroupByBucket,
|
||||
InfraSnapshotNodeMetricsBucket,
|
||||
} from './response_helpers';
|
||||
import { IP_FIELDS } from '../constants';
|
||||
|
||||
export interface InfraSnapshotRequestOptions {
|
||||
nodeType: InfraNodeType;
|
||||
|
@ -96,6 +97,17 @@ const requestGroupedNodes = async (
|
|||
size: SNAPSHOT_COMPOSITE_REQUEST_SIZE,
|
||||
sources: getGroupedNodesSources(options),
|
||||
},
|
||||
aggs: {
|
||||
ip: {
|
||||
top_hits: {
|
||||
sort: [{ [options.sourceConfiguration.fields.timestamp]: { order: 'desc' } }],
|
||||
_source: {
|
||||
includes: [IP_FIELDS[options.nodeType]],
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue