[Monitoring] Convert Elasticsearch-related server files that read from _source to typescript (#88212)

* A good chunk of server-side ES changes

* CCR files

* More areas where we just pass down the source to the client

* Some more

* Fix tests

* Fix tests and types
This commit is contained in:
Chris Roberson 2021-01-20 13:43:53 -05:00 committed by GitHub
parent f0f192c654
commit c9002a25c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 665 additions and 306 deletions

View file

@ -4,6 +4,31 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
export interface ElasticsearchResponse {
hits?: {
hits: ElasticsearchResponseHit[];
total: {
value: number;
};
};
aggregations?: any;
}
export interface ElasticsearchResponseHit {
_index: string;
_source: ElasticsearchSource;
inner_hits?: {
[field: string]: {
hits?: {
hits: ElasticsearchResponseHit[];
total: {
value: number;
};
};
};
};
}
export interface ElasticsearchSourceKibanaStats { export interface ElasticsearchSourceKibanaStats {
timestamp?: string; timestamp?: string;
kibana?: { kibana?: {
@ -34,9 +59,94 @@ export interface ElasticsearchSourceLogstashPipelineVertex {
}; };
} }
export interface ElasticsearchSource { export interface ElasticsearchNodeStats {
indices?: {
docs?: {
count?: number;
};
store?: {
size_in_bytes?: number;
size?: {
bytes?: number;
};
};
};
fs?: {
total?: {
available_in_bytes?: number;
total_in_bytes?: number;
};
summary?: {
available?: {
bytes?: number;
};
total?: {
bytes?: number;
};
};
};
jvm?: {
mem?: {
heap_used_percent?: number;
heap?: {
used?: {
pct?: number;
};
};
};
};
}
export interface ElasticsearchLegacySource {
timestamp: string; timestamp: string;
cluster_uuid: string;
cluster_stats?: {
nodes?: {
count?: {
total?: number;
};
jvm?: {
max_uptime_in_millis?: number;
mem?: {
heap_used_in_bytes?: number;
heap_max_in_bytes?: number;
};
};
versions?: string[];
};
indices?: {
count?: number;
docs?: {
count?: number;
};
shards?: {
total?: number;
};
store?: {
size_in_bytes?: number;
};
};
};
cluster_state?: {
status?: string;
nodes?: {
[nodeUuid: string]: {};
};
master_node?: boolean;
};
source_node?: {
id?: string;
uuid?: string;
attributes?: {};
transport_address?: string;
name?: string;
type?: string;
};
kibana_stats?: ElasticsearchSourceKibanaStats; kibana_stats?: ElasticsearchSourceKibanaStats;
license?: {
status?: string;
type?: string;
};
logstash_state?: { logstash_state?: {
pipeline?: { pipeline?: {
representation?: { representation?: {
@ -108,4 +218,98 @@ export interface ElasticsearchSource {
}; };
}; };
}; };
stack_stats?: {
xpack?: {
ccr?: {
enabled?: boolean;
available?: boolean;
};
};
};
job_stats?: {
job_id?: number;
state?: string;
data_counts?: {
processed_record_count?: number;
};
model_size_stats?: {
model_bytes?: number;
};
forecasts_stats?: {
total?: number;
};
node?: {
id?: number;
name?: string;
};
};
index_stats?: {
index?: string;
primaries?: {
docs?: {
count?: number;
};
store?: {
size_in_bytes?: number;
};
indexing?: {
index_total?: number;
};
};
total?: {
store?: {
size_in_bytes?: number;
};
search?: {
query_total?: number;
};
};
};
node_stats?: ElasticsearchNodeStats;
service?: {
address?: string;
};
shard?: {
index?: string;
shard?: string;
primary?: boolean;
relocating_node?: string;
node?: string;
};
ccr_stats?: {
leader_index?: string;
follower_index?: string;
shard_id?: number;
read_exceptions?: Array<{
exception?: {
type?: string;
};
}>;
time_since_last_read_millis?: number;
};
index_recovery?: {
shards?: ElasticsearchIndexRecoveryShard[];
};
}
export interface ElasticsearchIndexRecoveryShard {
start_time_in_millis: number;
stop_time_in_millis: number;
}
export interface ElasticsearchMetricbeatNode {
stats?: ElasticsearchNodeStats;
}
export interface ElasticsearchMetricbeatSource {
elasticsearch?: {
node?: ElasticsearchLegacySource['source_node'] & ElasticsearchMetricbeatNode;
};
}
export type ElasticsearchSource = ElasticsearchLegacySource & ElasticsearchMetricbeatSource;
export interface ElasticsearchModifiedSource extends ElasticsearchSource {
ccs?: string;
isSupported?: boolean;
} }

View file

@ -8,7 +8,8 @@
import { createApmQuery } from './create_apm_query'; import { createApmQuery } from './create_apm_query';
// @ts-ignore // @ts-ignore
import { ApmClusterMetric } from '../metrics'; import { ApmClusterMetric } from '../metrics';
import { LegacyRequest, ElasticsearchResponse } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
export async function getTimeOfLastEvent({ export async function getTimeOfLastEvent({
req, req,
@ -58,5 +59,5 @@ export async function getTimeOfLastEvent({
}; };
const response = await callWithRequest(req, 'search', params); const response = await callWithRequest(req, 'search', params);
return response.hits?.hits.length ? response.hits?.hits[0]._source.timestamp : undefined; return response.hits?.hits.length ? response.hits?.hits[0]?._source.timestamp : undefined;
} }

View file

@ -14,7 +14,8 @@ import { getDiffCalculation } from '../beats/_beats_stats';
// @ts-ignore // @ts-ignore
import { ApmMetric } from '../metrics'; import { ApmMetric } from '../metrics';
import { getTimeOfLastEvent } from './_get_time_of_last_event'; import { getTimeOfLastEvent } from './_get_time_of_last_event';
import { LegacyRequest, ElasticsearchResponse } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
export function handleResponse(response: ElasticsearchResponse, apmUuid: string) { export function handleResponse(response: ElasticsearchResponse, apmUuid: string) {
if (!response.hits || response.hits.hits.length === 0) { if (!response.hits || response.hits.hits.length === 0) {

View file

@ -14,7 +14,8 @@ import { createApmQuery } from './create_apm_query';
import { calculateRate } from '../calculate_rate'; import { calculateRate } from '../calculate_rate';
// @ts-ignore // @ts-ignore
import { getDiffCalculation } from './_apm_stats'; import { getDiffCalculation } from './_apm_stats';
import { LegacyRequest, ElasticsearchResponse, ElasticsearchResponseHit } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse, ElasticsearchResponseHit } from '../../../common/types/es';
export function handleResponse(response: ElasticsearchResponse, start: number, end: number) { export function handleResponse(response: ElasticsearchResponse, start: number, end: number) {
const initial = { ids: new Set(), beats: [] }; const initial = { ids: new Set(), beats: [] };

View file

@ -5,7 +5,8 @@
*/ */
import { upperFirst } from 'lodash'; import { upperFirst } from 'lodash';
import { LegacyRequest, ElasticsearchResponse } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
// @ts-ignore // @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore // @ts-ignore

View file

@ -14,7 +14,8 @@ import { createBeatsQuery } from './create_beats_query';
import { calculateRate } from '../calculate_rate'; import { calculateRate } from '../calculate_rate';
// @ts-ignore // @ts-ignore
import { getDiffCalculation } from './_beats_stats'; import { getDiffCalculation } from './_beats_stats';
import { ElasticsearchResponse, LegacyRequest } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
interface Beat { interface Beat {
uuid: string | undefined; uuid: string | undefined;

View file

@ -4,17 +4,18 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { set } from '@elastic/safer-lodash-set'; // @ts-ignore
import { get, find } from 'lodash';
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants';
import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
async function findSupportedBasicLicenseCluster( async function findSupportedBasicLicenseCluster(
req, req: LegacyRequest,
clusters, clusters: ElasticsearchModifiedSource[],
kbnIndexPattern, kbnIndexPattern: string,
kibanaUuid, kibanaUuid: string,
serverLog serverLog: (message: string) => void
) { ) {
checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/findSupportedBasicLicenseCluster'); checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/findSupportedBasicLicenseCluster');
@ -25,7 +26,7 @@ async function findSupportedBasicLicenseCluster(
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const gte = req.payload.timeRange.min; const gte = req.payload.timeRange.min;
const lte = req.payload.timeRange.max; const lte = req.payload.timeRange.max;
const kibanaDataResult = await callWithRequest(req, 'search', { const kibanaDataResult: ElasticsearchResponse = (await callWithRequest(req, 'search', {
index: kbnIndexPattern, index: kbnIndexPattern,
size: 1, size: 1,
ignoreUnavailable: true, ignoreUnavailable: true,
@ -42,11 +43,13 @@ async function findSupportedBasicLicenseCluster(
}, },
}, },
}, },
}); })) as ElasticsearchResponse;
const supportedClusterUuid = get(kibanaDataResult, 'hits.hits[0]._source.cluster_uuid'); const supportedClusterUuid = kibanaDataResult.hits?.hits[0]?._source.cluster_uuid ?? undefined;
const supportedCluster = find(clusters, { cluster_uuid: supportedClusterUuid }); for (const cluster of clusters) {
// only this basic cluster is supported if (cluster.cluster_uuid === supportedClusterUuid) {
set(supportedCluster, 'isSupported', true); cluster.isSupported = true;
}
}
serverLog( serverLog(
`Found basic license admin cluster UUID for Monitoring UI support: ${supportedClusterUuid}.` `Found basic license admin cluster UUID for Monitoring UI support: ${supportedClusterUuid}.`
@ -69,12 +72,12 @@ async function findSupportedBasicLicenseCluster(
* Non-Basic license clusters and any cluster in a single-cluster environment * Non-Basic license clusters and any cluster in a single-cluster environment
* are also flagged as supported in this method. * are also flagged as supported in this method.
*/ */
export function flagSupportedClusters(req, kbnIndexPattern) { export function flagSupportedClusters(req: LegacyRequest, kbnIndexPattern: string) {
checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/flagSupportedClusters'); checkParam(kbnIndexPattern, 'kbnIndexPattern in cluster/flagSupportedClusters');
const config = req.server.config(); const config = req.server.config();
const serverLog = (msg) => req.getLogger('supported-clusters').debug(msg); const serverLog = (message: string) => req.getLogger('supported-clusters').debug(message);
const flagAllSupported = (clusters) => { const flagAllSupported = (clusters: ElasticsearchModifiedSource[]) => {
clusters.forEach((cluster) => { clusters.forEach((cluster) => {
if (cluster.license) { if (cluster.license) {
cluster.isSupported = true; cluster.isSupported = true;
@ -83,7 +86,7 @@ export function flagSupportedClusters(req, kbnIndexPattern) {
return clusters; return clusters;
}; };
return async function (clusters) { return async function (clusters: ElasticsearchModifiedSource[]) {
// Standalone clusters are automatically supported in the UI so ignore those for // Standalone clusters are automatically supported in the UI so ignore those for
// our calculations here // our calculations here
let linkedClusterCount = 0; let linkedClusterCount = 0;
@ -110,7 +113,7 @@ export function flagSupportedClusters(req, kbnIndexPattern) {
// if all linked are basic licenses // if all linked are basic licenses
if (linkedClusterCount === basicLicenseCount) { if (linkedClusterCount === basicLicenseCount) {
const kibanaUuid = config.get('server.uuid'); const kibanaUuid = config.get('server.uuid') as string;
return await findSupportedBasicLicenseCluster( return await findSupportedBasicLicenseCluster(
req, req,
clusters, clusters,

View file

@ -4,12 +4,16 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get } from 'lodash'; // @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore
import { createQuery } from '../create_query'; import { createQuery } from '../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics'; import { ElasticsearchMetric } from '../metrics';
import { ElasticsearchResponse } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
export function getClusterLicense(req, esIndexPattern, clusterUuid) { export function getClusterLicense(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) {
checkParam(esIndexPattern, 'esIndexPattern in getClusterLicense'); checkParam(esIndexPattern, 'esIndexPattern in getClusterLicense');
const params = { const params = {
@ -28,7 +32,7 @@ export function getClusterLicense(req, esIndexPattern, clusterUuid) {
}; };
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
return callWithRequest(req, 'search', params).then((response) => { return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => {
return get(response, 'hits.hits[0]._source.license', {}); return response.hits?.hits[0]?._source.license;
}); });
} }

View file

@ -3,20 +3,20 @@
* or more contributor license agreements. Licensed under the Elastic License; * or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get } from 'lodash'; import { get } from 'lodash';
import { ElasticsearchSource } from '../../../common/types/es';
/* /*
* @param cluster {Object} clusterStats from getClusterStatus * @param cluster {Object} clusterStats from getClusterStatus
* @param unassignedShards {Object} shardStats from getShardStats * @param unassignedShards {Object} shardStats from getShardStats
* @return top-level cluster summary data * @return top-level cluster summary data
*/ */
export function getClusterStatus(cluster, shardStats) { export function getClusterStatus(cluster: ElasticsearchSource, shardStats: unknown) {
const clusterStats = get(cluster, 'cluster_stats', {}); const clusterStats = cluster.cluster_stats ?? {};
const clusterNodes = get(clusterStats, 'nodes', {}); const clusterNodes = clusterStats.nodes ?? {};
const clusterIndices = get(clusterStats, 'indices', {}); const clusterIndices = clusterStats.indices ?? {};
const clusterTotalShards = get(clusterIndices, 'shards.total', 0); const clusterTotalShards = clusterIndices.shards?.total ?? 0;
let unassignedShardsTotal = 0; let unassignedShardsTotal = 0;
const unassignedShards = get(shardStats, 'indicesTotals.unassigned'); const unassignedShards = get(shardStats, 'indicesTotals.unassigned');
if (unassignedShards !== undefined) { if (unassignedShards !== undefined) {
@ -26,17 +26,17 @@ export function getClusterStatus(cluster, shardStats) {
const totalShards = clusterTotalShards + unassignedShardsTotal; const totalShards = clusterTotalShards + unassignedShardsTotal;
return { return {
status: get(cluster, 'cluster_state.status', 'unknown'), status: cluster.cluster_state?.status ?? 'unknown',
// index-based stats // index-based stats
indicesCount: get(clusterIndices, 'count', 0), indicesCount: clusterIndices.count ?? 0,
documentCount: get(clusterIndices, 'docs.count', 0), documentCount: clusterIndices.docs?.count ?? 0,
dataSize: get(clusterIndices, 'store.size_in_bytes', 0), dataSize: clusterIndices.store?.size_in_bytes ?? 0,
// node-based stats // node-based stats
nodesCount: get(clusterNodes, 'count.total', 0), nodesCount: clusterNodes.count?.total ?? 0,
upTime: get(clusterNodes, 'jvm.max_uptime_in_millis', 0), upTime: clusterNodes.jvm?.max_uptime_in_millis ?? 0,
version: get(clusterNodes, 'versions', null), version: clusterNodes.versions ?? null,
memUsed: get(clusterNodes, 'jvm.mem.heap_used_in_bytes', 0), memUsed: clusterNodes.jvm?.mem?.heap_used_in_bytes ?? 0,
memMax: get(clusterNodes, 'jvm.mem.heap_max_in_bytes', 0), memMax: clusterNodes.jvm?.mem?.heap_max_in_bytes ?? 0,
unassignedShards: unassignedShardsTotal, unassignedShards: unassignedShardsTotal,
totalShards, totalShards,
}; };

View file

@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get, find } from 'lodash'; import { find } from 'lodash';
// @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
/** /**
* Augment the {@clusters} with their cluster state's from the {@code response}. * Augment the {@clusters} with their cluster state's from the {@code response}.
@ -15,11 +18,14 @@ import { checkParam } from '../error_missing_required';
* @param {Array} clusters Array of clusters to be augmented * @param {Array} clusters Array of clusters to be augmented
* @return {Array} Always {@code clusters}. * @return {Array} Always {@code clusters}.
*/ */
export function handleResponse(response, clusters) { export function handleResponse(
const hits = get(response, 'hits.hits', []); response: ElasticsearchResponse,
clusters: ElasticsearchModifiedSource[]
) {
const hits = response.hits?.hits ?? [];
hits.forEach((hit) => { hits.forEach((hit) => {
const currentCluster = get(hit, '_source', {}); const currentCluster = hit._source;
if (currentCluster) { if (currentCluster) {
const cluster = find(clusters, { cluster_uuid: currentCluster.cluster_uuid }); const cluster = find(clusters, { cluster_uuid: currentCluster.cluster_uuid });
@ -39,7 +45,11 @@ export function handleResponse(response, clusters) {
* *
* If there is no cluster state available for any cluster, then it will be returned without any cluster state information. * If there is no cluster state available for any cluster, then it will be returned without any cluster state information.
*/ */
export function getClustersState(req, esIndexPattern, clusters) { export function getClustersState(
req: LegacyRequest,
esIndexPattern: string,
clusters: ElasticsearchModifiedSource[]
) {
checkParam(esIndexPattern, 'esIndexPattern in cluster/getClustersHealth'); checkParam(esIndexPattern, 'esIndexPattern in cluster/getClustersHealth');
const clusterUuids = clusters const clusterUuids = clusters

View file

@ -4,12 +4,17 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get } from 'lodash'; // @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore
import { createQuery } from '../create_query'; import { createQuery } from '../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics'; import { ElasticsearchMetric } from '../metrics';
// @ts-ignore
import { parseCrossClusterPrefix } from '../ccs_utils'; import { parseCrossClusterPrefix } from '../ccs_utils';
import { getClustersState } from './get_clusters_state'; import { getClustersState } from './get_clusters_state';
import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
/** /**
* This will fetch the cluster stats and cluster state as a single object per cluster. * This will fetch the cluster stats and cluster state as a single object per cluster.
@ -19,10 +24,10 @@ import { getClustersState } from './get_clusters_state';
* @param {String} clusterUuid (optional) If not undefined, getClusters will filter for a single cluster * @param {String} clusterUuid (optional) If not undefined, getClusters will filter for a single cluster
* @return {Promise} A promise containing an array of clusters. * @return {Promise} A promise containing an array of clusters.
*/ */
export function getClustersStats(req, esIndexPattern, clusterUuid) { export function getClustersStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) {
return ( return (
fetchClusterStats(req, esIndexPattern, clusterUuid) fetchClusterStats(req, esIndexPattern, clusterUuid)
.then((response) => handleClusterStats(response, req.server)) .then((response) => handleClusterStats(response))
// augment older documents (e.g., from 2.x - 5.4) with their cluster_state // augment older documents (e.g., from 2.x - 5.4) with their cluster_state
.then((clusters) => getClustersState(req, esIndexPattern, clusters)) .then((clusters) => getClustersState(req, esIndexPattern, clusters))
); );
@ -36,7 +41,7 @@ export function getClustersStats(req, esIndexPattern, clusterUuid) {
* @param {String} clusterUuid (optional) - if not undefined, getClusters filters for a single clusterUuid * @param {String} clusterUuid (optional) - if not undefined, getClusters filters for a single clusterUuid
* @return {Promise} Object representing each cluster. * @return {Promise} Object representing each cluster.
*/ */
function fetchClusterStats(req, esIndexPattern, clusterUuid) { function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) {
checkParam(esIndexPattern, 'esIndexPattern in getClusters'); checkParam(esIndexPattern, 'esIndexPattern in getClusters');
const config = req.server.config(); const config = req.server.config();
@ -81,15 +86,15 @@ function fetchClusterStats(req, esIndexPattern, clusterUuid) {
* @param {Object} response The response from Elasticsearch. * @param {Object} response The response from Elasticsearch.
* @return {Array} Objects representing each cluster. * @return {Array} Objects representing each cluster.
*/ */
export function handleClusterStats(response) { export function handleClusterStats(response: ElasticsearchResponse) {
const hits = get(response, 'hits.hits', []); const hits = response?.hits?.hits ?? [];
return hits return hits
.map((hit) => { .map((hit) => {
const cluster = get(hit, '_source'); const cluster = hit._source as ElasticsearchModifiedSource;
if (cluster) { if (cluster) {
const indexName = get(hit, '_index', ''); const indexName = hit._index;
const ccs = parseCrossClusterPrefix(indexName); const ccs = parseCrossClusterPrefix(indexName);
// use CCS whenever we come across it so that we can avoid talking to other monitoring clusters whenever possible // use CCS whenever we come across it so that we can avoid talking to other monitoring clusters whenever possible

View file

@ -4,19 +4,24 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get } from 'lodash';
import moment from 'moment'; import moment from 'moment';
// @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics'; import { ElasticsearchMetric } from '../metrics';
// @ts-ignore
import { createQuery } from '../create_query'; import { createQuery } from '../create_query';
import { ElasticsearchResponse } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
export function handleResponse(response) { export function handleResponse(response: ElasticsearchResponse) {
const isEnabled = get(response, 'hits.hits[0]._source.stack_stats.xpack.ccr.enabled'); const isEnabled = response.hits?.hits[0]?._source.stack_stats?.xpack?.ccr?.enabled ?? undefined;
const isAvailable = get(response, 'hits.hits[0]._source.stack_stats.xpack.ccr.available'); const isAvailable =
response.hits?.hits[0]?._source.stack_stats?.xpack?.ccr?.available ?? undefined;
return isEnabled && isAvailable; return isEnabled && isAvailable;
} }
export async function checkCcrEnabled(req, esIndexPattern) { export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string) {
checkParam(esIndexPattern, 'esIndexPattern in getNodes'); checkParam(esIndexPattern, 'esIndexPattern in getNodes');
const start = moment.utc(req.payload.timeRange.min).valueOf(); const start = moment.utc(req.payload.timeRange.min).valueOf();

View file

@ -5,9 +5,14 @@
*/ */
import moment from 'moment'; import moment from 'moment';
import _ from 'lodash'; import _ from 'lodash';
// @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore
import { createQuery } from '../create_query'; import { createQuery } from '../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics'; import { ElasticsearchMetric } from '../metrics';
import { ElasticsearchResponse, ElasticsearchIndexRecoveryShard } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
/** /**
* Filter out shard activity that we do not care about. * Filter out shard activity that we do not care about.
@ -20,8 +25,8 @@ import { ElasticsearchMetric } from '../metrics';
* @param {Number} startMs Start time in milliseconds of the polling window * @param {Number} startMs Start time in milliseconds of the polling window
* @returns {boolean} true to keep * @returns {boolean} true to keep
*/ */
export function filterOldShardActivity(startMs) { export function filterOldShardActivity(startMs: number) {
return (activity) => { return (activity: ElasticsearchIndexRecoveryShard) => {
// either it's still going and there is no stop time, or the stop time happened after we started looking for one // either it's still going and there is no stop time, or the stop time happened after we started looking for one
return !_.isNumber(activity.stop_time_in_millis) || activity.stop_time_in_millis >= startMs; return !_.isNumber(activity.stop_time_in_millis) || activity.stop_time_in_millis >= startMs;
}; };
@ -35,9 +40,9 @@ export function filterOldShardActivity(startMs) {
* @param {Date} start The start time from the request payload (expected to be of type {@code Date}) * @param {Date} start The start time from the request payload (expected to be of type {@code Date})
* @returns {Object[]} An array of shards representing active shard activity from {@code _source.index_recovery.shards}. * @returns {Object[]} An array of shards representing active shard activity from {@code _source.index_recovery.shards}.
*/ */
export function handleLastRecoveries(resp, start) { export function handleLastRecoveries(resp: ElasticsearchResponse, start: number) {
if (resp.hits.hits.length === 1) { if (resp.hits?.hits.length === 1) {
const data = _.get(resp.hits.hits[0], '_source.index_recovery.shards', []).filter( const data = (resp.hits?.hits[0]?._source.index_recovery?.shards ?? []).filter(
filterOldShardActivity(moment.utc(start).valueOf()) filterOldShardActivity(moment.utc(start).valueOf())
); );
data.sort((a, b) => b.start_time_in_millis - a.start_time_in_millis); data.sort((a, b) => b.start_time_in_millis - a.start_time_in_millis);
@ -47,7 +52,7 @@ export function handleLastRecoveries(resp, start) {
return []; return [];
} }
export function getLastRecovery(req, esIndexPattern) { export function getLastRecovery(req: LegacyRequest, esIndexPattern: string) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getLastRecovery'); checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getLastRecovery');
const start = req.payload.timeRange.min; const start = req.payload.timeRange.min;

View file

@ -4,22 +4,26 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import Bluebird from 'bluebird'; import { includes } from 'lodash';
import { includes, get } from 'lodash'; // @ts-ignore
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore
import { createQuery } from '../create_query'; import { createQuery } from '../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics'; import { ElasticsearchMetric } from '../metrics';
import { ML_SUPPORTED_LICENSES } from '../../../common/constants'; import { ML_SUPPORTED_LICENSES } from '../../../common/constants';
import { ElasticsearchResponse, ElasticsearchSource } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
/* /*
* Get a listing of jobs along with some metric data to use for the listing * Get a listing of jobs along with some metric data to use for the listing
*/ */
export function handleResponse(response) { export function handleResponse(response: ElasticsearchResponse) {
const hits = get(response, 'hits.hits', []); const hits = response.hits?.hits;
return hits.map((hit) => get(hit, '_source.job_stats')); return hits?.map((hit) => hit._source.job_stats) ?? [];
} }
export function getMlJobs(req, esIndexPattern) { export function getMlJobs(req: LegacyRequest, esIndexPattern: string) {
checkParam(esIndexPattern, 'esIndexPattern in getMlJobs'); checkParam(esIndexPattern, 'esIndexPattern in getMlJobs');
const config = req.server.config(); const config = req.server.config();
@ -56,8 +60,12 @@ export function getMlJobs(req, esIndexPattern) {
* cardinality isn't guaranteed to be accurate is the issue * cardinality isn't guaranteed to be accurate is the issue
* but it will be as long as the precision threshold is >= the actual value * but it will be as long as the precision threshold is >= the actual value
*/ */
export function getMlJobsForCluster(req, esIndexPattern, cluster) { export function getMlJobsForCluster(
const license = get(cluster, 'license', {}); req: LegacyRequest,
esIndexPattern: string,
cluster: ElasticsearchSource
) {
const license = cluster.license ?? {};
if (license.status === 'active' && includes(ML_SUPPORTED_LICENSES, license.type)) { if (license.status === 'active' && includes(ML_SUPPORTED_LICENSES, license.type)) {
// ML is supported // ML is supported
@ -80,11 +88,11 @@ export function getMlJobsForCluster(req, esIndexPattern, cluster) {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
return callWithRequest(req, 'search', params).then((response) => { return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => {
return get(response, 'aggregations.jobs_count.value', 0); return response.aggregations.jobs_count.value ?? 0;
}); });
} }
// ML is not supported // ML is not supported
return Bluebird.resolve(null); return Promise.resolve(null);
} }

View file

@ -5,22 +5,27 @@
*/ */
import { get } from 'lodash'; import { get } from 'lodash';
import { checkParam } from '../../error_missing_required';
import { createQuery } from '../../create_query';
import { ElasticsearchMetric } from '../../metrics';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
// @ts-ignore
import { checkParam } from '../../error_missing_required';
// @ts-ignore
import { createQuery } from '../../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../../metrics';
import { ElasticsearchResponse } from '../../../../common/types/es';
import { LegacyRequest } from '../../../types';
export function handleResponse(shardStats, indexUuid) { export function handleResponse(shardStats: any, indexUuid: string) {
return (response) => { return (response: ElasticsearchResponse) => {
const indexStats = get(response, 'hits.hits[0]._source.index_stats'); const indexStats = response.hits?.hits[0]?._source.index_stats;
const primaries = get(indexStats, 'primaries'); const primaries = indexStats?.primaries;
const total = get(indexStats, 'total'); const total = indexStats?.total;
const stats = { const stats = {
documents: get(primaries, 'docs.count'), documents: primaries?.docs?.count,
dataSize: { dataSize: {
primaries: get(primaries, 'store.size_in_bytes'), primaries: primaries?.store?.size_in_bytes,
total: get(total, 'store.size_in_bytes'), total: total?.store?.size_in_bytes,
}, },
}; };
@ -55,10 +60,15 @@ export function handleResponse(shardStats, indexUuid) {
} }
export function getIndexSummary( export function getIndexSummary(
req, req: LegacyRequest,
esIndexPattern, esIndexPattern: string,
shardStats, shardStats: any,
{ clusterUuid, indexUuid, start, end } {
clusterUuid,
indexUuid,
start,
end,
}: { clusterUuid: string; indexUuid: string; start: number; end: number }
) { ) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndexSummary'); checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndexSummary');

View file

@ -5,43 +5,54 @@
*/ */
import { get } from 'lodash'; import { get } from 'lodash';
import { checkParam } from '../../error_missing_required';
import { ElasticsearchMetric } from '../../metrics';
import { createQuery } from '../../create_query';
import { calculateRate } from '../../calculate_rate';
import { getUnassignedShards } from '../shards';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
// @ts-ignore
import { checkParam } from '../../error_missing_required';
// @ts-ignore
import { ElasticsearchMetric } from '../../metrics';
// @ts-ignore
import { createQuery } from '../../create_query';
// @ts-ignore
import { calculateRate } from '../../calculate_rate';
// @ts-ignore
import { getUnassignedShards } from '../shards';
import { ElasticsearchResponse } from '../../../../common/types/es';
import { LegacyRequest } from '../../../types';
export function handleResponse(resp, min, max, shardStats) { export function handleResponse(
resp: ElasticsearchResponse,
min: number,
max: number,
shardStats: any
) {
// map the hits // map the hits
const hits = get(resp, 'hits.hits', []); const hits = resp?.hits?.hits ?? [];
return hits.map((hit) => { return hits.map((hit) => {
const stats = get(hit, '_source.index_stats'); const stats = hit._source.index_stats;
const earliestStats = get(hit, 'inner_hits.earliest.hits.hits[0]._source.index_stats'); const earliestStats = hit.inner_hits?.earliest?.hits?.hits[0]?._source.index_stats;
const rateOptions = { const rateOptions = {
hitTimestamp: get(hit, '_source.timestamp'), hitTimestamp: hit._source.timestamp,
earliestHitTimestamp: get(hit, 'inner_hits.earliest.hits.hits[0]._source.timestamp'), earliestHitTimestamp: hit.inner_hits?.earliest?.hits?.hits[0]?._source.timestamp,
timeWindowMin: min, timeWindowMin: min,
timeWindowMax: max, timeWindowMax: max,
}; };
const earliestIndexingHit = get(earliestStats, 'primaries.indexing'); const earliestIndexingHit = earliestStats?.primaries?.indexing;
const { rate: indexRate } = calculateRate({ const { rate: indexRate } = calculateRate({
latestTotal: get(stats, 'primaries.indexing.index_total'), latestTotal: stats?.primaries?.indexing?.index_total,
earliestTotal: get(earliestIndexingHit, 'index_total'), earliestTotal: earliestIndexingHit?.index_total,
...rateOptions, ...rateOptions,
}); });
const earliestSearchHit = get(earliestStats, 'total.search'); const earliestSearchHit = earliestStats?.total?.search;
const { rate: searchRate } = calculateRate({ const { rate: searchRate } = calculateRate({
latestTotal: get(stats, 'total.search.query_total'), latestTotal: stats?.total?.search?.query_total,
earliestTotal: get(earliestSearchHit, 'query_total'), earliestTotal: earliestSearchHit?.query_total,
...rateOptions, ...rateOptions,
}); });
const shardStatsForIndex = get(shardStats, ['indices', stats.index]); const shardStatsForIndex = get(shardStats, ['indices', stats?.index ?? '']);
let status; let status;
let statusSort; let statusSort;
let unassignedShards; let unassignedShards;
@ -65,10 +76,10 @@ export function handleResponse(resp, min, max, shardStats) {
} }
return { return {
name: stats.index, name: stats?.index,
status, status,
doc_count: get(stats, 'primaries.docs.count'), doc_count: stats?.primaries?.docs?.count,
data_size: get(stats, 'total.store.size_in_bytes'), data_size: stats?.total?.store?.size_in_bytes,
index_rate: indexRate, index_rate: indexRate,
search_rate: searchRate, search_rate: searchRate,
unassigned_shards: unassignedShards, unassigned_shards: unassignedShards,
@ -78,9 +89,14 @@ export function handleResponse(resp, min, max, shardStats) {
} }
export function buildGetIndicesQuery( export function buildGetIndicesQuery(
esIndexPattern, esIndexPattern: string,
clusterUuid, clusterUuid: string,
{ start, end, size, showSystemIndices = false } {
start,
end,
size,
showSystemIndices = false,
}: { start: number; end: number; size: number; showSystemIndices: boolean }
) { ) {
const filters = []; const filters = [];
if (!showSystemIndices) { if (!showSystemIndices) {
@ -134,7 +150,12 @@ export function buildGetIndicesQuery(
}; };
} }
export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) { export function getIndices(
req: LegacyRequest,
esIndexPattern: string,
showSystemIndices: boolean = false,
shardStats: any
) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices');
const { min: start, max: end } = req.payload.timeRange; const { min: start, max: end } = req.payload.timeRange;
@ -145,7 +166,7 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard
start, start,
end, end,
showSystemIndices, showSystemIndices,
size: config.get('monitoring.ui.max_bucket_size'), size: parseInt(config.get('monitoring.ui.max_bucket_size') || '', 10),
}); });
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');

View file

@ -5,35 +5,49 @@
*/ */
import { get } from 'lodash'; import { get } from 'lodash';
import { checkParam } from '../../error_missing_required';
import { createQuery } from '../../create_query';
import { ElasticsearchMetric } from '../../metrics';
import { getDefaultNodeFromId } from './get_default_node_from_id';
import { calculateNodeType } from './calculate_node_type';
import { getNodeTypeClassLabel } from './get_node_type_class_label';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
// @ts-ignore
import { checkParam } from '../../error_missing_required';
// @ts-ignore
import { createQuery } from '../../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../../metrics';
// @ts-ignore
import { getDefaultNodeFromId } from './get_default_node_from_id';
// @ts-ignore
import { calculateNodeType } from './calculate_node_type';
// @ts-ignore
import { getNodeTypeClassLabel } from './get_node_type_class_label';
import {
ElasticsearchSource,
ElasticsearchResponse,
ElasticsearchLegacySource,
ElasticsearchMetricbeatNode,
} from '../../../../common/types/es';
import { LegacyRequest } from '../../../types';
export function handleResponse(clusterState, shardStats, nodeUuid) { export function handleResponse(
return (response) => { clusterState: ElasticsearchSource['cluster_state'],
shardStats: any,
nodeUuid: string
) {
return (response: ElasticsearchResponse) => {
let nodeSummary = {}; let nodeSummary = {};
const nodeStatsHits = get(response, 'hits.hits', []); const nodeStatsHits = response.hits?.hits ?? [];
const nodes = nodeStatsHits.map((hit) => const nodes: Array<
get(hit, '_source.elasticsearch.node', hit._source.source_node) ElasticsearchLegacySource['source_node'] | ElasticsearchMetricbeatNode
); // using [0] value because query results are sorted desc per timestamp > = nodeStatsHits.map((hit) => hit._source.elasticsearch?.node || hit._source.source_node); // using [0] value because query results are sorted desc per timestamp
const node = nodes[0] || getDefaultNodeFromId(nodeUuid); const node = nodes[0] || getDefaultNodeFromId(nodeUuid);
const sourceStats = const sourceStats =
get(response, 'hits.hits[0]._source.elasticsearch.node.stats') || response.hits?.hits[0]?._source.elasticsearch?.node?.stats ||
get(response, 'hits.hits[0]._source.node_stats'); response.hits?.hits[0]?._source.node_stats;
const clusterNode = get(clusterState, ['nodes', nodeUuid]); const clusterNode =
clusterState && clusterState.nodes ? clusterState.nodes[nodeUuid] : undefined;
const stats = { const stats = {
resolver: nodeUuid, resolver: nodeUuid,
node_ids: nodes.map((node) => node.id || node.uuid), node_ids: nodes.map((_node) => node.id || node.uuid),
attributes: node.attributes, attributes: node.attributes,
transport_address: get( transport_address: response.hits?.hits[0]?._source.service?.address || node.transport_address,
response,
'hits.hits[0]._source.service.address',
node.transport_address
),
name: node.name, name: node.name,
type: node.type, type: node.type,
}; };
@ -48,22 +62,19 @@ export function handleResponse(clusterState, shardStats, nodeUuid) {
nodeSummary = { nodeSummary = {
type: nodeType, type: nodeType,
nodeTypeLabel: nodeTypeLabel, nodeTypeLabel,
nodeTypeClass: nodeTypeClass, nodeTypeClass,
totalShards: _shardStats.shardCount, totalShards: _shardStats.shardCount,
indexCount: _shardStats.indexCount, indexCount: _shardStats.indexCount,
documents: get(sourceStats, 'indices.docs.count'), documents: sourceStats?.indices?.docs?.count,
dataSize: dataSize:
get(sourceStats, 'indices.store.size_in_bytes') || sourceStats?.indices?.store?.size_in_bytes || sourceStats?.indices?.store?.size?.bytes,
get(sourceStats, 'indices.store.size.bytes'),
freeSpace: freeSpace:
get(sourceStats, 'fs.total.available_in_bytes') || sourceStats?.fs?.total?.available_in_bytes || sourceStats?.fs?.summary?.available?.bytes,
get(sourceStats, 'fs.summary.available.bytes'),
totalSpace: totalSpace:
get(sourceStats, 'fs.total.total_in_bytes') || get(sourceStats, 'fs.summary.total.bytes'), sourceStats?.fs?.total?.total_in_bytes || sourceStats?.fs?.summary?.total?.bytes,
usedHeap: usedHeap:
get(sourceStats, 'jvm.mem.heap_used_percent') || sourceStats?.jvm?.mem?.heap_used_percent || sourceStats?.jvm?.mem?.heap?.used?.pct,
get(sourceStats, 'jvm.mem.heap.used.pct'),
status: i18n.translate('xpack.monitoring.es.nodes.onlineStatusLabel', { status: i18n.translate('xpack.monitoring.es.nodes.onlineStatusLabel', {
defaultMessage: 'Online', defaultMessage: 'Online',
}), }),
@ -89,11 +100,16 @@ export function handleResponse(clusterState, shardStats, nodeUuid) {
} }
export function getNodeSummary( export function getNodeSummary(
req, req: LegacyRequest,
esIndexPattern, esIndexPattern: string,
clusterState, clusterState: ElasticsearchSource['cluster_state'],
shardStats, shardStats: any,
{ clusterUuid, nodeUuid, start, end } {
clusterUuid,
nodeUuid,
start,
end,
}: { clusterUuid: string; nodeUuid: string; start: number; end: number }
) { ) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getNodeSummary'); checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getNodeSummary');

View file

@ -5,13 +5,21 @@
*/ */
import moment from 'moment'; import moment from 'moment';
// @ts-ignore
import { checkParam } from '../../../error_missing_required'; import { checkParam } from '../../../error_missing_required';
// @ts-ignore
import { createQuery } from '../../../create_query'; import { createQuery } from '../../../create_query';
// @ts-ignore
import { calculateAuto } from '../../../calculate_auto'; import { calculateAuto } from '../../../calculate_auto';
// @ts-ignore
import { ElasticsearchMetric } from '../../../metrics'; import { ElasticsearchMetric } from '../../../metrics';
// @ts-ignore
import { getMetricAggs } from './get_metric_aggs'; import { getMetricAggs } from './get_metric_aggs';
import { handleResponse } from './handle_response'; import { handleResponse } from './handle_response';
// @ts-ignore
import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_metrics'; import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_metrics';
import { LegacyRequest } from '../../../../types';
import { ElasticsearchModifiedSource } from '../../../../../common/types/es';
/* Run an aggregation on node_stats to get stat data for the selected time /* Run an aggregation on node_stats to get stat data for the selected time
* range for all the active nodes. Every option is a key to a configuration * range for all the active nodes. Every option is a key to a configuration
@ -30,7 +38,13 @@ import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_me
* @param {Object} nodesShardCount: per-node information about shards * @param {Object} nodesShardCount: per-node information about shards
* @return {Array} node info combined with metrics for each node from handle_response * @return {Array} node info combined with metrics for each node from handle_response
*/ */
export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, nodesShardCount) { export async function getNodes(
req: LegacyRequest,
esIndexPattern: string,
pageOfNodes: Array<{ uuid: string }>,
clusterStats: ElasticsearchModifiedSource,
nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } }
) {
checkParam(esIndexPattern, 'esIndexPattern in getNodes'); checkParam(esIndexPattern, 'esIndexPattern in getNodes');
const start = moment.utc(req.payload.timeRange.min).valueOf(); const start = moment.utc(req.payload.timeRange.min).valueOf();
@ -45,7 +59,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n
const min = start; const min = start;
const bucketSize = Math.max( const bucketSize = Math.max(
config.get('monitoring.ui.min_interval_seconds'), parseInt(config.get('monitoring.ui.min_interval_seconds') as string, 10),
calculateAuto(100, duration).asSeconds() calculateAuto(100, duration).asSeconds()
); );

View file

@ -6,8 +6,11 @@
import { get } from 'lodash'; import { get } from 'lodash';
import { mapNodesInfo } from './map_nodes_info'; import { mapNodesInfo } from './map_nodes_info';
// @ts-ignore
import { mapNodesMetrics } from './map_nodes_metrics'; import { mapNodesMetrics } from './map_nodes_metrics';
// @ts-ignore
import { uncovertMetricNames } from '../../convert_metric_names'; import { uncovertMetricNames } from '../../convert_metric_names';
import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../../../common/types/es';
/* /*
* Process the response from the get_nodes query * Process the response from the get_nodes query
@ -18,18 +21,18 @@ import { uncovertMetricNames } from '../../convert_metric_names';
* @return {Array} node info combined with metrics for each node * @return {Array} node info combined with metrics for each node
*/ */
export function handleResponse( export function handleResponse(
response, response: ElasticsearchResponse,
clusterStats, clusterStats: ElasticsearchModifiedSource | undefined,
nodesShardCount, nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } } | undefined,
pageOfNodes, pageOfNodes: Array<{ uuid: string }>,
timeOptions = {} timeOptions = {}
) { ) {
if (!get(response, 'hits.hits')) { if (!get(response, 'hits.hits')) {
return []; return [];
} }
const nodeHits = get(response, 'hits.hits', []); const nodeHits = response.hits?.hits ?? [];
const nodesInfo = mapNodesInfo(nodeHits, clusterStats, nodesShardCount); const nodesInfo: { [key: string]: any } = mapNodesInfo(nodeHits, clusterStats, nodesShardCount);
/* /*
* Every node bucket is an object with a field for nodeId and fields for * Every node bucket is an object with a field for nodeId and fields for
@ -37,19 +40,29 @@ export function handleResponse(
* with a sub-object for all the metrics buckets * with a sub-object for all the metrics buckets
*/ */
const nodeBuckets = get(response, 'aggregations.nodes.buckets', []); const nodeBuckets = get(response, 'aggregations.nodes.buckets', []);
const metricsForNodes = nodeBuckets.reduce((accum, { key: nodeId, by_date: byDate }) => { const metricsForNodes = nodeBuckets.reduce(
return { (
...accum, accum: { [nodeId: string]: any },
[nodeId]: uncovertMetricNames(byDate), { key: nodeId, by_date: byDate }: { key: string; by_date: any }
}; ) => {
}, {}); return {
const nodesMetrics = mapNodesMetrics(metricsForNodes, nodesInfo, timeOptions); // summarize the metrics of online nodes ...accum,
[nodeId]: uncovertMetricNames(byDate),
};
},
{}
);
const nodesMetrics: { [key: string]: any } = mapNodesMetrics(
metricsForNodes,
nodesInfo,
timeOptions
); // summarize the metrics of online nodes
// nodesInfo is the source of truth for the nodeIds, where nodesMetrics will lack metrics for offline nodes // nodesInfo is the source of truth for the nodeIds, where nodesMetrics will lack metrics for offline nodes
const nodes = pageOfNodes.map((node) => ({ const nodes = pageOfNodes.map((node) => ({
...node, ...node,
...nodesInfo[node.uuid], ...(nodesInfo && nodesInfo[node.uuid] ? nodesInfo[node.uuid] : {}),
...nodesMetrics[node.uuid], ...(nodesMetrics && nodesMetrics[node.uuid] ? nodesMetrics[node.uuid] : {}),
resolver: node.uuid, resolver: node.uuid,
})); }));

View file

@ -1,46 +0,0 @@
/*
* 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, isUndefined } from 'lodash';
import { calculateNodeType, getNodeTypeClassLabel } from '../';
/**
* @param {Array} nodeHits: info about each node from the hits in the get_nodes query
* @param {Object} clusterStats: cluster stats from cluster state document
* @param {Object} nodesShardCount: per-node information about shards
* @return {Object} summarized info about each node keyed by nodeId
*/
export function mapNodesInfo(nodeHits, clusterStats, nodesShardCount) {
const clusterState = get(clusterStats, 'cluster_state', { nodes: {} });
return nodeHits.reduce((prev, node) => {
const sourceNode = get(node, '_source.source_node') || get(node, '_source.elasticsearch.node');
const calculatedNodeType = calculateNodeType(sourceNode, get(clusterState, 'master_node'));
const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(
sourceNode,
calculatedNodeType
);
const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid || sourceNode.id]));
return {
...prev,
[sourceNode.uuid || sourceNode.id]: {
name: sourceNode.name,
transport_address: sourceNode.transport_address,
type: nodeType,
isOnline,
nodeTypeLabel: nodeTypeLabel,
nodeTypeClass: nodeTypeClass,
shardCount: get(
nodesShardCount,
`nodes[${sourceNode.uuid || sourceNode.id}].shardCount`,
0
),
},
};
}, {});
}

View file

@ -0,0 +1,57 @@
/*
* 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 { isUndefined } from 'lodash';
// @ts-ignore
import { calculateNodeType } from '../calculate_node_type';
// @ts-ignore
import { getNodeTypeClassLabel } from '../get_node_type_class_label';
import {
ElasticsearchResponseHit,
ElasticsearchModifiedSource,
} from '../../../../../common/types/es';
/**
* @param {Array} nodeHits: info about each node from the hits in the get_nodes query
* @param {Object} clusterStats: cluster stats from cluster state document
* @param {Object} nodesShardCount: per-node information about shards
* @return {Object} summarized info about each node keyed by nodeId
*/
export function mapNodesInfo(
nodeHits: ElasticsearchResponseHit[],
clusterStats?: ElasticsearchModifiedSource,
nodesShardCount?: { nodes: { [nodeId: string]: { shardCount: number } } }
) {
const clusterState = clusterStats?.cluster_state ?? { nodes: {} };
return nodeHits.reduce((prev, node) => {
const sourceNode = node._source.source_node || node._source.elasticsearch?.node;
const calculatedNodeType = calculateNodeType(sourceNode, clusterState.master_node);
const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(
sourceNode,
calculatedNodeType
);
const uuid = sourceNode?.uuid ?? sourceNode?.id ?? undefined;
if (!uuid) {
return prev;
}
const isOnline = !isUndefined(clusterState.nodes ? clusterState.nodes[uuid] : undefined);
return {
...prev,
[uuid]: {
name: sourceNode?.name,
transport_address: sourceNode?.transport_address,
type: nodeType,
isOnline,
nodeTypeLabel,
nodeTypeClass,
shardCount: nodesShardCount?.nodes[uuid]?.shardCount ?? 0,
},
};
}, {});
}

View file

@ -4,22 +4,26 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get } from 'lodash'; // @ts-ignore
import { checkParam } from '../../error_missing_required'; import { checkParam } from '../../error_missing_required';
// @ts-ignore
import { createQuery } from '../../create_query'; import { createQuery } from '../../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../../metrics'; import { ElasticsearchMetric } from '../../metrics';
import { ElasticsearchResponse, ElasticsearchLegacySource } from '../../../../common/types/es';
import { LegacyRequest } from '../../../types';
export function handleResponse(response) { export function handleResponse(response: ElasticsearchResponse) {
const hits = get(response, 'hits.hits'); const hits = response.hits?.hits;
if (!hits) { if (!hits) {
return []; return [];
} }
// deduplicate any shards from earlier days with the same cluster state state_uuid // deduplicate any shards from earlier days with the same cluster state state_uuid
const uniqueShards = new Set(); const uniqueShards = new Set<string>();
// map into object with shard and source properties // map into object with shard and source properties
return hits.reduce((shards, hit) => { return hits.reduce((shards: Array<ElasticsearchLegacySource['shard']>, hit) => {
const shard = hit._source.shard; const shard = hit._source.shard;
if (shard) { if (shard) {
@ -37,9 +41,13 @@ export function handleResponse(response) {
} }
export function getShardAllocation( export function getShardAllocation(
req, req: LegacyRequest,
esIndexPattern, esIndexPattern: string,
{ shardFilter, stateUuid, showSystemIndices = false } {
shardFilter,
stateUuid,
showSystemIndices = false,
}: { shardFilter: any; stateUuid: string; showSystemIndices: boolean }
) { ) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardAllocation'); checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardAllocation');

View file

@ -9,10 +9,11 @@ import { merge } from 'lodash';
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore // @ts-ignore
import { calculateAvailability } from '../calculate_availability'; import { calculateAvailability } from '../calculate_availability';
import { LegacyRequest, ElasticsearchResponse } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
export function handleResponse(resp: ElasticsearchResponse) { export function handleResponse(resp: ElasticsearchResponse) {
const source = resp.hits?.hits[0]._source.kibana_stats; const source = resp.hits?.hits[0]?._source.kibana_stats;
const kibana = source?.kibana; const kibana = source?.kibana;
return merge(kibana, { return merge(kibana, {
availability: calculateAvailability(source?.timestamp), availability: calculateAvailability(source?.timestamp),

View file

@ -9,7 +9,8 @@ import { merge } from 'lodash';
import { checkParam } from '../error_missing_required'; import { checkParam } from '../error_missing_required';
// @ts-ignore // @ts-ignore
import { calculateAvailability } from '../calculate_availability'; import { calculateAvailability } from '../calculate_availability';
import { LegacyRequest, ElasticsearchResponse } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
export function handleResponse(resp: ElasticsearchResponse) { export function handleResponse(resp: ElasticsearchResponse) {
const source = resp.hits?.hits[0]?._source?.logstash_stats; const source = resp.hits?.hits[0]?._source?.logstash_stats;

View file

@ -8,7 +8,8 @@
import { createQuery } from '../create_query'; import { createQuery } from '../create_query';
// @ts-ignore // @ts-ignore
import { LogstashMetric } from '../metrics'; import { LogstashMetric } from '../metrics';
import { LegacyRequest, ElasticsearchResponse } from '../../types'; import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
export async function getPipelineStateDocument( export async function getPipelineStateDocument(
req: LegacyRequest, req: LegacyRequest,

View file

@ -7,11 +7,15 @@
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import moment from 'moment'; import moment from 'moment';
import { get, groupBy } from 'lodash'; import { get, groupBy } from 'lodash';
// @ts-ignore
import { handleError } from '../../../../lib/errors/handle_error'; import { handleError } from '../../../../lib/errors/handle_error';
// @ts-ignore
import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { prefixIndexPattern } from '../../../../lib/ccs_utils';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants';
import { ElasticsearchResponse, ElasticsearchSource } from '../../../../../common/types/es';
import { LegacyRequest } from '../../../../types';
function getBucketScript(max, min) { function getBucketScript(max: string, min: string) {
return { return {
bucket_script: { bucket_script: {
buckets_path: { buckets_path: {
@ -23,7 +27,13 @@ function getBucketScript(max, min) {
}; };
} }
function buildRequest(req, config, esIndexPattern) { function buildRequest(
req: LegacyRequest,
config: {
get: (key: string) => string | undefined;
},
esIndexPattern: string
) {
const min = moment.utc(req.payload.timeRange.min).valueOf(); const min = moment.utc(req.payload.timeRange.min).valueOf();
const max = moment.utc(req.payload.timeRange.max).valueOf(); const max = moment.utc(req.payload.timeRange.max).valueOf();
const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); const maxBucketSize = config.get('monitoring.ui.max_bucket_size');
@ -168,7 +178,12 @@ function buildRequest(req, config, esIndexPattern) {
}; };
} }
export function ccrRoute(server) { export function ccrRoute(server: {
route: (p: any) => void;
config: () => {
get: (key: string) => string | undefined;
};
}) {
server.route({ server.route({
method: 'POST', method: 'POST',
path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr', path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr',
@ -186,14 +201,14 @@ export function ccrRoute(server) {
}), }),
}, },
}, },
async handler(req) { async handler(req: LegacyRequest) {
const config = server.config(); const config = server.config();
const ccs = req.payload.ccs; const ccs = req.payload.ccs;
const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs);
try { try {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const response = await callWithRequest( const response: ElasticsearchResponse = await callWithRequest(
req, req,
'search', 'search',
buildRequest(req, config, esIndexPattern) buildRequest(req, config, esIndexPattern)
@ -203,50 +218,72 @@ export function ccrRoute(server) {
return { data: [] }; return { data: [] };
} }
const fullStats = get(response, 'hits.hits').reduce((accum, hit) => { const fullStats: {
const innerHits = get(hit, 'inner_hits.by_shard.hits.hits'); [key: string]: Array<NonNullable<ElasticsearchSource['ccr_stats']>>;
const innerHitsSource = innerHits.map((innerHit) => get(innerHit, '_source.ccr_stats')); } =
const grouped = groupBy( response.hits?.hits.reduce((accum, hit) => {
innerHitsSource, const innerHits = hit.inner_hits?.by_shard.hits?.hits ?? [];
(stat) => `${stat.follower_index}:${stat.shard_id}` const innerHitsSource = innerHits.map(
); (innerHit) =>
innerHit._source.ccr_stats as NonNullable<ElasticsearchSource['ccr_stats']>
);
const grouped = groupBy(
innerHitsSource,
(stat) => `${stat.follower_index}:${stat.shard_id}`
);
return { return {
...accum, ...accum,
...grouped, ...grouped,
}; };
}, {}); }, {}) ?? {};
const buckets = get(response, 'aggregations.by_follower_index.buckets'); const buckets = response.aggregations.by_follower_index.buckets;
const data = buckets.reduce((accum, bucket) => { const data = buckets.reduce((accum: any, bucket: any) => {
const leaderIndex = get(bucket, 'leader_index.buckets[0].key'); const leaderIndex = get(bucket, 'leader_index.buckets[0].key');
const remoteCluster = get( const remoteCluster = get(
bucket, bucket,
'leader_index.buckets[0].remote_cluster.buckets[0].key' 'leader_index.buckets[0].remote_cluster.buckets[0].key'
); );
const follows = remoteCluster ? `${leaderIndex} on ${remoteCluster}` : leaderIndex; const follows = remoteCluster ? `${leaderIndex} on ${remoteCluster}` : leaderIndex;
const stat = { const stat: {
[key: string]: any;
shards: Array<{
error?: string;
opsSynced: number;
syncLagTime: number;
syncLagOps: number;
}>;
} = {
id: bucket.key, id: bucket.key,
index: bucket.key, index: bucket.key,
follows, follows,
shards: [],
error: undefined,
opsSynced: undefined,
syncLagTime: undefined,
syncLagOps: undefined,
}; };
stat.shards = get(bucket, 'by_shard_id.buckets').reduce((accum, shardBucket) => { stat.shards = get(bucket, 'by_shard_id.buckets').reduce(
const fullStat = get(fullStats[`${bucket.key}:${shardBucket.key}`], '[0]', {}); (accum2: any, shardBucket: any) => {
const shardStat = { const fullStat = fullStats[`${bucket.key}:${shardBucket.key}`][0] ?? {};
shardId: shardBucket.key, const shardStat = {
error: fullStat.read_exceptions.length shardId: shardBucket.key,
? fullStat.read_exceptions[0].exception.type error: fullStat.read_exceptions?.length
: null, ? fullStat.read_exceptions[0].exception?.type
opsSynced: get(shardBucket, 'ops_synced.value'), : null,
syncLagTime: fullStat.time_since_last_read_millis, opsSynced: get(shardBucket, 'ops_synced.value'),
syncLagOps: get(shardBucket, 'lag_ops.value'), syncLagTime: fullStat.time_since_last_read_millis,
syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'), syncLagOps: get(shardBucket, 'lag_ops.value'),
syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'), syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'),
}; syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'),
accum.push(shardStat); };
return accum; accum2.push(shardStat);
}, []); return accum2;
},
[]
);
stat.error = (stat.shards.find((shard) => shard.error) || {}).error; stat.error = (stat.shards.find((shard) => shard.error) || {}).error;
stat.opsSynced = stat.shards.reduce((sum, { opsSynced }) => sum + opsSynced, 0); stat.opsSynced = stat.shards.reduce((sum, { opsSynced }) => sum + opsSynced, 0);

View file

@ -4,15 +4,19 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { get } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
// @ts-ignore
import { handleError } from '../../../../lib/errors/handle_error'; import { handleError } from '../../../../lib/errors/handle_error';
// @ts-ignore
import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { prefixIndexPattern } from '../../../../lib/ccs_utils';
// @ts-ignore
import { getMetrics } from '../../../../lib/details/get_metrics'; import { getMetrics } from '../../../../lib/details/get_metrics';
import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants';
import { ElasticsearchResponse } from '../../../../../common/types/es';
import { LegacyRequest } from '../../../../types';
function getFormattedLeaderIndex(leaderIndex) { function getFormattedLeaderIndex(leaderIndex: string) {
let leader = leaderIndex; let leader = leaderIndex;
if (leader.includes(':')) { if (leader.includes(':')) {
const leaderSplit = leader.split(':'); const leaderSplit = leader.split(':');
@ -21,7 +25,7 @@ function getFormattedLeaderIndex(leaderIndex) {
return leader; return leader;
} }
async function getCcrStat(req, esIndexPattern, filters) { async function getCcrStat(req: LegacyRequest, esIndexPattern: string, filters: unknown[]) {
const min = moment.utc(req.payload.timeRange.min).valueOf(); const min = moment.utc(req.payload.timeRange.min).valueOf();
const max = moment.utc(req.payload.timeRange.max).valueOf(); const max = moment.utc(req.payload.timeRange.max).valueOf();
@ -68,7 +72,7 @@ async function getCcrStat(req, esIndexPattern, filters) {
return await callWithRequest(req, 'search', params); return await callWithRequest(req, 'search', params);
} }
export function ccrShardRoute(server) { export function ccrShardRoute(server: { route: (p: any) => void; config: () => {} }) {
server.route({ server.route({
method: 'POST', method: 'POST',
path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr/{index}/shard/{shardId}', path: '/api/monitoring/v1/clusters/{clusterUuid}/elasticsearch/ccr/{index}/shard/{shardId}',
@ -88,7 +92,7 @@ export function ccrShardRoute(server) {
}), }),
}, },
}, },
async handler(req) { async handler(req: LegacyRequest) {
const config = server.config(); const config = server.config();
const index = req.params.index; const index = req.params.index;
const shardId = req.params.shardId; const shardId = req.params.shardId;
@ -120,7 +124,7 @@ export function ccrShardRoute(server) {
]; ];
try { try {
const [metrics, ccrResponse] = await Promise.all([ const [metrics, ccrResponse]: [unknown, ElasticsearchResponse] = await Promise.all([
getMetrics( getMetrics(
req, req,
esIndexPattern, esIndexPattern,
@ -133,18 +137,15 @@ export function ccrShardRoute(server) {
getCcrStat(req, esIndexPattern, filters), getCcrStat(req, esIndexPattern, filters),
]); ]);
const stat = get(ccrResponse, 'hits.hits[0]._source.ccr_stats', {}); const stat = ccrResponse.hits?.hits[0]?._source.ccr_stats ?? {};
const oldestStat = get( const oldestStat =
ccrResponse, ccrResponse.hits?.hits[0].inner_hits?.oldest.hits?.hits[0]?._source.ccr_stats ?? {};
'hits.hits[0].inner_hits.oldest.hits.hits[0]._source.ccr_stats',
{}
);
return { return {
metrics, metrics,
stat, stat,
formattedLeader: getFormattedLeaderIndex(stat.leader_index), formattedLeader: getFormattedLeaderIndex(stat.leader_index ?? ''),
timestamp: get(ccrResponse, 'hits.hits[0]._source.timestamp'), timestamp: ccrResponse.hits?.hits[0]?._source.timestamp,
oldestStat, oldestStat,
}; };
} catch (err) { } catch (err) {

View file

@ -17,7 +17,6 @@ import { LicensingPluginSetup } from '../../licensing/server';
import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server';
import { CloudSetup } from '../../cloud/server'; import { CloudSetup } from '../../cloud/server';
import { ElasticsearchSource } from '../common/types/es';
export interface MonitoringLicenseService { export interface MonitoringLicenseService {
refresh: () => Promise<any>; refresh: () => Promise<any>;
@ -118,26 +117,3 @@ export interface LegacyServer {
}; };
}; };
} }
export interface ElasticsearchResponse {
hits?: {
hits: ElasticsearchResponseHit[];
total: {
value: number;
};
};
}
export interface ElasticsearchResponseHit {
_source: ElasticsearchSource;
inner_hits?: {
[field: string]: {
hits?: {
hits: ElasticsearchResponseHit[];
total: {
value: number;
};
};
};
};
}