[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:
Chris Cowan 2019-05-10 07:32:18 -07:00 committed by GitHub
parent 3dacef2901
commit 69bbca490b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 311 additions and 4 deletions

View file

@ -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 = {

View file

@ -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"'
);
});
});

View file

@ -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}"`;
};

View file

@ -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] : []),
],
},
];

View file

@ -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,
};

View file

@ -22,6 +22,7 @@ export const waffleNodesQuery = gql`
path {
value
label
ip
}
metric {
name

View file

@ -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,

View file

@ -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 = {

View file

@ -102,6 +102,7 @@ export interface InfraWaffleMapNode {
pathId: string;
id: string;
name: string;
ip?: string | null;
path: InfraSnapshotNodePath[];
metric: InfraSnapshotNodeMetric;
}

View file

@ -17,6 +17,7 @@ export const snapshotSchema: any = gql`
type InfraSnapshotNodePath {
value: String!
label: String!
ip: String
}
type InfraSnapshotNode {

View file

@ -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 {

View file

@ -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',
};

View file

@ -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] } },

View file

@ -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);
});
});
});

View file

@ -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;
};

View file

@ -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,
},
},
},
},
},
},