[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.
*/
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 {
timestamp?: string;
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;
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;
license?: {
status?: string;
type?: string;
};
logstash_state?: {
pipeline?: {
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';
// @ts-ignore
import { ApmClusterMetric } from '../metrics';
import { LegacyRequest, ElasticsearchResponse } from '../../types';
import { LegacyRequest } from '../../types';
import { ElasticsearchResponse } from '../../../common/types/es';
export async function getTimeOfLastEvent({
req,
@ -58,5 +59,5 @@ export async function getTimeOfLastEvent({
};
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
import { ApmMetric } from '../metrics';
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) {
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';
// @ts-ignore
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) {
const initial = { ids: new Set(), beats: [] };

View file

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

View file

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

View file

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

View file

@ -4,12 +4,16 @@
* 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';
// @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 getClusterLicense(req, esIndexPattern, clusterUuid) {
export function getClusterLicense(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) {
checkParam(esIndexPattern, 'esIndexPattern in getClusterLicense');
const params = {
@ -28,7 +32,7 @@ export function getClusterLicense(req, esIndexPattern, clusterUuid) {
};
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
return callWithRequest(req, 'search', params).then((response) => {
return get(response, 'hits.hits[0]._source.license', {});
return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => {
return response.hits?.hits[0]?._source.license;
});
}

View file

@ -3,20 +3,20 @@
* 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 { ElasticsearchSource } from '../../../common/types/es';
/*
* @param cluster {Object} clusterStats from getClusterStatus
* @param unassignedShards {Object} shardStats from getShardStats
* @return top-level cluster summary data
*/
export function getClusterStatus(cluster, shardStats) {
const clusterStats = get(cluster, 'cluster_stats', {});
const clusterNodes = get(clusterStats, 'nodes', {});
const clusterIndices = get(clusterStats, 'indices', {});
export function getClusterStatus(cluster: ElasticsearchSource, shardStats: unknown) {
const clusterStats = cluster.cluster_stats ?? {};
const clusterNodes = clusterStats.nodes ?? {};
const clusterIndices = clusterStats.indices ?? {};
const clusterTotalShards = get(clusterIndices, 'shards.total', 0);
const clusterTotalShards = clusterIndices.shards?.total ?? 0;
let unassignedShardsTotal = 0;
const unassignedShards = get(shardStats, 'indicesTotals.unassigned');
if (unassignedShards !== undefined) {
@ -26,17 +26,17 @@ export function getClusterStatus(cluster, shardStats) {
const totalShards = clusterTotalShards + unassignedShardsTotal;
return {
status: get(cluster, 'cluster_state.status', 'unknown'),
status: cluster.cluster_state?.status ?? 'unknown',
// index-based stats
indicesCount: get(clusterIndices, 'count', 0),
documentCount: get(clusterIndices, 'docs.count', 0),
dataSize: get(clusterIndices, 'store.size_in_bytes', 0),
indicesCount: clusterIndices.count ?? 0,
documentCount: clusterIndices.docs?.count ?? 0,
dataSize: clusterIndices.store?.size_in_bytes ?? 0,
// node-based stats
nodesCount: get(clusterNodes, 'count.total', 0),
upTime: get(clusterNodes, 'jvm.max_uptime_in_millis', 0),
version: get(clusterNodes, 'versions', null),
memUsed: get(clusterNodes, 'jvm.mem.heap_used_in_bytes', 0),
memMax: get(clusterNodes, 'jvm.mem.heap_max_in_bytes', 0),
nodesCount: clusterNodes.count?.total ?? 0,
upTime: clusterNodes.jvm?.max_uptime_in_millis ?? 0,
version: clusterNodes.versions ?? null,
memUsed: clusterNodes.jvm?.mem?.heap_used_in_bytes ?? 0,
memMax: clusterNodes.jvm?.mem?.heap_max_in_bytes ?? 0,
unassignedShards: unassignedShardsTotal,
totalShards,
};

View file

@ -4,8 +4,11 @@
* 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 { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
/**
* 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
* @return {Array} Always {@code clusters}.
*/
export function handleResponse(response, clusters) {
const hits = get(response, 'hits.hits', []);
export function handleResponse(
response: ElasticsearchResponse,
clusters: ElasticsearchModifiedSource[]
) {
const hits = response.hits?.hits ?? [];
hits.forEach((hit) => {
const currentCluster = get(hit, '_source', {});
const currentCluster = hit._source;
if (currentCluster) {
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.
*/
export function getClustersState(req, esIndexPattern, clusters) {
export function getClustersState(
req: LegacyRequest,
esIndexPattern: string,
clusters: ElasticsearchModifiedSource[]
) {
checkParam(esIndexPattern, 'esIndexPattern in cluster/getClustersHealth');
const clusterUuids = clusters

View file

@ -4,12 +4,17 @@
* 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';
// @ts-ignore
import { createQuery } from '../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics';
// @ts-ignore
import { parseCrossClusterPrefix } from '../ccs_utils';
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.
@ -19,10 +24,10 @@ import { getClustersState } from './get_clusters_state';
* @param {String} clusterUuid (optional) If not undefined, getClusters will filter for a single cluster
* @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 (
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
.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
* @return {Promise} Object representing each cluster.
*/
function fetchClusterStats(req, esIndexPattern, clusterUuid) {
function fetchClusterStats(req: LegacyRequest, esIndexPattern: string, clusterUuid: string) {
checkParam(esIndexPattern, 'esIndexPattern in getClusters');
const config = req.server.config();
@ -81,15 +86,15 @@ function fetchClusterStats(req, esIndexPattern, clusterUuid) {
* @param {Object} response The response from Elasticsearch.
* @return {Array} Objects representing each cluster.
*/
export function handleClusterStats(response) {
const hits = get(response, 'hits.hits', []);
export function handleClusterStats(response: ElasticsearchResponse) {
const hits = response?.hits?.hits ?? [];
return hits
.map((hit) => {
const cluster = get(hit, '_source');
const cluster = hit._source as ElasticsearchModifiedSource;
if (cluster) {
const indexName = get(hit, '_index', '');
const indexName = hit._index;
const ccs = parseCrossClusterPrefix(indexName);
// 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.
*/
import { get } from 'lodash';
import moment from 'moment';
// @ts-ignore
import { checkParam } from '../error_missing_required';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics';
// @ts-ignore
import { createQuery } from '../create_query';
import { ElasticsearchResponse } from '../../../common/types/es';
import { LegacyRequest } from '../../types';
export function handleResponse(response) {
const isEnabled = get(response, 'hits.hits[0]._source.stack_stats.xpack.ccr.enabled');
const isAvailable = get(response, 'hits.hits[0]._source.stack_stats.xpack.ccr.available');
export function handleResponse(response: ElasticsearchResponse) {
const isEnabled = response.hits?.hits[0]?._source.stack_stats?.xpack?.ccr?.enabled ?? undefined;
const isAvailable =
response.hits?.hits[0]?._source.stack_stats?.xpack?.ccr?.available ?? undefined;
return isEnabled && isAvailable;
}
export async function checkCcrEnabled(req, esIndexPattern) {
export async function checkCcrEnabled(req: LegacyRequest, esIndexPattern: string) {
checkParam(esIndexPattern, 'esIndexPattern in getNodes');
const start = moment.utc(req.payload.timeRange.min).valueOf();

View file

@ -5,9 +5,14 @@
*/
import moment from 'moment';
import _ from 'lodash';
// @ts-ignore
import { checkParam } from '../error_missing_required';
// @ts-ignore
import { createQuery } from '../create_query';
// @ts-ignore
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.
@ -20,8 +25,8 @@ import { ElasticsearchMetric } from '../metrics';
* @param {Number} startMs Start time in milliseconds of the polling window
* @returns {boolean} true to keep
*/
export function filterOldShardActivity(startMs) {
return (activity) => {
export function filterOldShardActivity(startMs: number) {
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
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})
* @returns {Object[]} An array of shards representing active shard activity from {@code _source.index_recovery.shards}.
*/
export function handleLastRecoveries(resp, start) {
if (resp.hits.hits.length === 1) {
const data = _.get(resp.hits.hits[0], '_source.index_recovery.shards', []).filter(
export function handleLastRecoveries(resp: ElasticsearchResponse, start: number) {
if (resp.hits?.hits.length === 1) {
const data = (resp.hits?.hits[0]?._source.index_recovery?.shards ?? []).filter(
filterOldShardActivity(moment.utc(start).valueOf())
);
data.sort((a, b) => b.start_time_in_millis - a.start_time_in_millis);
@ -47,7 +52,7 @@ export function handleLastRecoveries(resp, start) {
return [];
}
export function getLastRecovery(req, esIndexPattern) {
export function getLastRecovery(req: LegacyRequest, esIndexPattern: string) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getLastRecovery');
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.
*/
import Bluebird from 'bluebird';
import { includes, get } from 'lodash';
import { includes } from 'lodash';
// @ts-ignore
import { checkParam } from '../error_missing_required';
// @ts-ignore
import { createQuery } from '../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../metrics';
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
*/
export function handleResponse(response) {
const hits = get(response, 'hits.hits', []);
return hits.map((hit) => get(hit, '_source.job_stats'));
export function handleResponse(response: ElasticsearchResponse) {
const hits = response.hits?.hits;
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');
const config = req.server.config();
@ -56,8 +60,12 @@ export function getMlJobs(req, esIndexPattern) {
* cardinality isn't guaranteed to be accurate is the issue
* but it will be as long as the precision threshold is >= the actual value
*/
export function getMlJobsForCluster(req, esIndexPattern, cluster) {
const license = get(cluster, 'license', {});
export function getMlJobsForCluster(
req: LegacyRequest,
esIndexPattern: string,
cluster: ElasticsearchSource
) {
const license = cluster.license ?? {};
if (license.status === 'active' && includes(ML_SUPPORTED_LICENSES, license.type)) {
// ML is supported
@ -80,11 +88,11 @@ export function getMlJobsForCluster(req, esIndexPattern, cluster) {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
return callWithRequest(req, 'search', params).then((response) => {
return get(response, 'aggregations.jobs_count.value', 0);
return callWithRequest(req, 'search', params).then((response: ElasticsearchResponse) => {
return response.aggregations.jobs_count.value ?? 0;
});
}
// ML is not supported
return Bluebird.resolve(null);
return Promise.resolve(null);
}

View file

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

View file

@ -5,43 +5,54 @@
*/
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';
// @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
const hits = get(resp, 'hits.hits', []);
const hits = resp?.hits?.hits ?? [];
return hits.map((hit) => {
const stats = get(hit, '_source.index_stats');
const earliestStats = get(hit, 'inner_hits.earliest.hits.hits[0]._source.index_stats');
const stats = hit._source.index_stats;
const earliestStats = hit.inner_hits?.earliest?.hits?.hits[0]?._source.index_stats;
const rateOptions = {
hitTimestamp: get(hit, '_source.timestamp'),
earliestHitTimestamp: get(hit, 'inner_hits.earliest.hits.hits[0]._source.timestamp'),
hitTimestamp: hit._source.timestamp,
earliestHitTimestamp: hit.inner_hits?.earliest?.hits?.hits[0]?._source.timestamp,
timeWindowMin: min,
timeWindowMax: max,
};
const earliestIndexingHit = get(earliestStats, 'primaries.indexing');
const earliestIndexingHit = earliestStats?.primaries?.indexing;
const { rate: indexRate } = calculateRate({
latestTotal: get(stats, 'primaries.indexing.index_total'),
earliestTotal: get(earliestIndexingHit, 'index_total'),
latestTotal: stats?.primaries?.indexing?.index_total,
earliestTotal: earliestIndexingHit?.index_total,
...rateOptions,
});
const earliestSearchHit = get(earliestStats, 'total.search');
const earliestSearchHit = earliestStats?.total?.search;
const { rate: searchRate } = calculateRate({
latestTotal: get(stats, 'total.search.query_total'),
earliestTotal: get(earliestSearchHit, 'query_total'),
latestTotal: stats?.total?.search?.query_total,
earliestTotal: earliestSearchHit?.query_total,
...rateOptions,
});
const shardStatsForIndex = get(shardStats, ['indices', stats.index]);
const shardStatsForIndex = get(shardStats, ['indices', stats?.index ?? '']);
let status;
let statusSort;
let unassignedShards;
@ -65,10 +76,10 @@ export function handleResponse(resp, min, max, shardStats) {
}
return {
name: stats.index,
name: stats?.index,
status,
doc_count: get(stats, 'primaries.docs.count'),
data_size: get(stats, 'total.store.size_in_bytes'),
doc_count: stats?.primaries?.docs?.count,
data_size: stats?.total?.store?.size_in_bytes,
index_rate: indexRate,
search_rate: searchRate,
unassigned_shards: unassignedShards,
@ -78,9 +89,14 @@ export function handleResponse(resp, min, max, shardStats) {
}
export function buildGetIndicesQuery(
esIndexPattern,
clusterUuid,
{ start, end, size, showSystemIndices = false }
esIndexPattern: string,
clusterUuid: string,
{
start,
end,
size,
showSystemIndices = false,
}: { start: number; end: number; size: number; showSystemIndices: boolean }
) {
const filters = [];
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');
const { min: start, max: end } = req.payload.timeRange;
@ -145,7 +166,7 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard
start,
end,
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');

View file

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

View file

@ -5,13 +5,21 @@
*/
import moment from 'moment';
// @ts-ignore
import { checkParam } from '../../../error_missing_required';
// @ts-ignore
import { createQuery } from '../../../create_query';
// @ts-ignore
import { calculateAuto } from '../../../calculate_auto';
// @ts-ignore
import { ElasticsearchMetric } from '../../../metrics';
// @ts-ignore
import { getMetricAggs } from './get_metric_aggs';
import { handleResponse } from './handle_response';
// @ts-ignore
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
* 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
* @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');
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 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()
);

View file

@ -6,8 +6,11 @@
import { get } from 'lodash';
import { mapNodesInfo } from './map_nodes_info';
// @ts-ignore
import { mapNodesMetrics } from './map_nodes_metrics';
// @ts-ignore
import { uncovertMetricNames } from '../../convert_metric_names';
import { ElasticsearchResponse, ElasticsearchModifiedSource } from '../../../../../common/types/es';
/*
* 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
*/
export function handleResponse(
response,
clusterStats,
nodesShardCount,
pageOfNodes,
response: ElasticsearchResponse,
clusterStats: ElasticsearchModifiedSource | undefined,
nodesShardCount: { nodes: { [nodeId: string]: { shardCount: number } } } | undefined,
pageOfNodes: Array<{ uuid: string }>,
timeOptions = {}
) {
if (!get(response, 'hits.hits')) {
return [];
}
const nodeHits = get(response, 'hits.hits', []);
const nodesInfo = mapNodesInfo(nodeHits, clusterStats, nodesShardCount);
const nodeHits = response.hits?.hits ?? [];
const nodesInfo: { [key: string]: any } = mapNodesInfo(nodeHits, clusterStats, nodesShardCount);
/*
* 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
*/
const nodeBuckets = get(response, 'aggregations.nodes.buckets', []);
const metricsForNodes = nodeBuckets.reduce((accum, { key: nodeId, by_date: byDate }) => {
return {
...accum,
[nodeId]: uncovertMetricNames(byDate),
};
}, {});
const nodesMetrics = mapNodesMetrics(metricsForNodes, nodesInfo, timeOptions); // summarize the metrics of online nodes
const metricsForNodes = nodeBuckets.reduce(
(
accum: { [nodeId: string]: any },
{ key: nodeId, by_date: byDate }: { key: string; by_date: any }
) => {
return {
...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
const nodes = pageOfNodes.map((node) => ({
...node,
...nodesInfo[node.uuid],
...nodesMetrics[node.uuid],
...(nodesInfo && nodesInfo[node.uuid] ? nodesInfo[node.uuid] : {}),
...(nodesMetrics && nodesMetrics[node.uuid] ? nodesMetrics[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.
*/
import { get } from 'lodash';
// @ts-ignore
import { checkParam } from '../../error_missing_required';
// @ts-ignore
import { createQuery } from '../../create_query';
// @ts-ignore
import { ElasticsearchMetric } from '../../metrics';
import { ElasticsearchResponse, ElasticsearchLegacySource } from '../../../../common/types/es';
import { LegacyRequest } from '../../../types';
export function handleResponse(response) {
const hits = get(response, 'hits.hits');
export function handleResponse(response: ElasticsearchResponse) {
const hits = response.hits?.hits;
if (!hits) {
return [];
}
// 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
return hits.reduce((shards, hit) => {
return hits.reduce((shards: Array<ElasticsearchLegacySource['shard']>, hit) => {
const shard = hit._source.shard;
if (shard) {
@ -37,9 +41,13 @@ export function handleResponse(response) {
}
export function getShardAllocation(
req,
esIndexPattern,
{ shardFilter, stateUuid, showSystemIndices = false }
req: LegacyRequest,
esIndexPattern: string,
{
shardFilter,
stateUuid,
showSystemIndices = false,
}: { shardFilter: any; stateUuid: string; showSystemIndices: boolean }
) {
checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getShardAllocation');

View file

@ -9,10 +9,11 @@ import { merge } from 'lodash';
import { checkParam } from '../error_missing_required';
// @ts-ignore
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) {
const source = resp.hits?.hits[0]._source.kibana_stats;
const source = resp.hits?.hits[0]?._source.kibana_stats;
const kibana = source?.kibana;
return merge(kibana, {
availability: calculateAvailability(source?.timestamp),

View file

@ -9,7 +9,8 @@ import { merge } from 'lodash';
import { checkParam } from '../error_missing_required';
// @ts-ignore
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) {
const source = resp.hits?.hits[0]?._source?.logstash_stats;

View file

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

View file

@ -7,11 +7,15 @@
import { schema } from '@kbn/config-schema';
import moment from 'moment';
import { get, groupBy } from 'lodash';
// @ts-ignore
import { handleError } from '../../../../lib/errors/handle_error';
// @ts-ignore
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
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 {
bucket_script: {
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 max = moment.utc(req.payload.timeRange.max).valueOf();
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({
method: 'POST',
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 ccs = req.payload.ccs;
const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs);
try {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
const response = await callWithRequest(
const response: ElasticsearchResponse = await callWithRequest(
req,
'search',
buildRequest(req, config, esIndexPattern)
@ -203,50 +218,72 @@ export function ccrRoute(server) {
return { data: [] };
}
const fullStats = get(response, 'hits.hits').reduce((accum, hit) => {
const innerHits = get(hit, 'inner_hits.by_shard.hits.hits');
const innerHitsSource = innerHits.map((innerHit) => get(innerHit, '_source.ccr_stats'));
const grouped = groupBy(
innerHitsSource,
(stat) => `${stat.follower_index}:${stat.shard_id}`
);
const fullStats: {
[key: string]: Array<NonNullable<ElasticsearchSource['ccr_stats']>>;
} =
response.hits?.hits.reduce((accum, hit) => {
const innerHits = hit.inner_hits?.by_shard.hits?.hits ?? [];
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 {
...accum,
...grouped,
};
}, {});
return {
...accum,
...grouped,
};
}, {}) ?? {};
const buckets = get(response, 'aggregations.by_follower_index.buckets');
const data = buckets.reduce((accum, bucket) => {
const buckets = response.aggregations.by_follower_index.buckets;
const data = buckets.reduce((accum: any, bucket: any) => {
const leaderIndex = get(bucket, 'leader_index.buckets[0].key');
const remoteCluster = get(
bucket,
'leader_index.buckets[0].remote_cluster.buckets[0].key'
);
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,
index: bucket.key,
follows,
shards: [],
error: undefined,
opsSynced: undefined,
syncLagTime: undefined,
syncLagOps: undefined,
};
stat.shards = get(bucket, 'by_shard_id.buckets').reduce((accum, shardBucket) => {
const fullStat = get(fullStats[`${bucket.key}:${shardBucket.key}`], '[0]', {});
const shardStat = {
shardId: shardBucket.key,
error: fullStat.read_exceptions.length
? fullStat.read_exceptions[0].exception.type
: null,
opsSynced: get(shardBucket, 'ops_synced.value'),
syncLagTime: fullStat.time_since_last_read_millis,
syncLagOps: get(shardBucket, 'lag_ops.value'),
syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'),
syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'),
};
accum.push(shardStat);
return accum;
}, []);
stat.shards = get(bucket, 'by_shard_id.buckets').reduce(
(accum2: any, shardBucket: any) => {
const fullStat = fullStats[`${bucket.key}:${shardBucket.key}`][0] ?? {};
const shardStat = {
shardId: shardBucket.key,
error: fullStat.read_exceptions?.length
? fullStat.read_exceptions[0].exception?.type
: null,
opsSynced: get(shardBucket, 'ops_synced.value'),
syncLagTime: fullStat.time_since_last_read_millis,
syncLagOps: get(shardBucket, 'lag_ops.value'),
syncLagOpsLeader: get(shardBucket, 'leader_lag_ops.value'),
syncLagOpsFollower: get(shardBucket, 'follower_lag_ops.value'),
};
accum2.push(shardStat);
return accum2;
},
[]
);
stat.error = (stat.shards.find((shard) => shard.error) || {}).error;
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.
*/
import { get } from 'lodash';
import moment from 'moment';
import { schema } from '@kbn/config-schema';
// @ts-ignore
import { handleError } from '../../../../lib/errors/handle_error';
// @ts-ignore
import { prefixIndexPattern } from '../../../../lib/ccs_utils';
// @ts-ignore
import { getMetrics } from '../../../../lib/details/get_metrics';
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;
if (leader.includes(':')) {
const leaderSplit = leader.split(':');
@ -21,7 +25,7 @@ function getFormattedLeaderIndex(leaderIndex) {
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 max = moment.utc(req.payload.timeRange.max).valueOf();
@ -68,7 +72,7 @@ async function getCcrStat(req, esIndexPattern, filters) {
return await callWithRequest(req, 'search', params);
}
export function ccrShardRoute(server) {
export function ccrShardRoute(server: { route: (p: any) => void; config: () => {} }) {
server.route({
method: 'POST',
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 index = req.params.index;
const shardId = req.params.shardId;
@ -120,7 +124,7 @@ export function ccrShardRoute(server) {
];
try {
const [metrics, ccrResponse] = await Promise.all([
const [metrics, ccrResponse]: [unknown, ElasticsearchResponse] = await Promise.all([
getMetrics(
req,
esIndexPattern,
@ -133,18 +137,15 @@ export function ccrShardRoute(server) {
getCcrStat(req, esIndexPattern, filters),
]);
const stat = get(ccrResponse, 'hits.hits[0]._source.ccr_stats', {});
const oldestStat = get(
ccrResponse,
'hits.hits[0].inner_hits.oldest.hits.hits[0]._source.ccr_stats',
{}
);
const stat = ccrResponse.hits?.hits[0]?._source.ccr_stats ?? {};
const oldestStat =
ccrResponse.hits?.hits[0].inner_hits?.oldest.hits?.hits[0]?._source.ccr_stats ?? {};
return {
metrics,
stat,
formattedLeader: getFormattedLeaderIndex(stat.leader_index),
timestamp: get(ccrResponse, 'hits.hits[0]._source.timestamp'),
formattedLeader: getFormattedLeaderIndex(stat.leader_index ?? ''),
timestamp: ccrResponse.hits?.hits[0]?._source.timestamp,
oldestStat,
};
} catch (err) {

View file

@ -17,7 +17,6 @@ import { LicensingPluginSetup } from '../../licensing/server';
import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server';
import { CloudSetup } from '../../cloud/server';
import { ElasticsearchSource } from '../common/types/es';
export interface MonitoringLicenseService {
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;
};
};
};
};
}