[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:
parent
f0f192c654
commit
c9002a25c5
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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: [] };
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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,
|
||||||
};
|
};
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
|
@ -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;
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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');
|
||||||
|
|
|
@ -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');
|
|
@ -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');
|
||||||
|
|
|
@ -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()
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}
|
|
@ -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');
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
|
@ -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) {
|
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue