From 011a04fac6e15602ebc99e698f4c2f407b16d171 Mon Sep 17 00:00:00 2001 From: Milton Hultgren Date: Tue, 31 May 2022 20:53:26 +0200 Subject: [PATCH] [Stack Monitoring] Add stale status reporting for Kibana (#126386) --- docs/settings/monitoring-settings.asciidoc | 4 + .../contexts/external_config_context.tsx | 1 + .../cluster/overview/kibana_panel.js | 90 +++++++++--- .../components/kibana/cluster_status/index.js | 110 ++++++++++++++- .../components/kibana/detail_status/index.js | 89 +++++++++++- .../kibana/format_last_seen_timestamp.ts | 25 ++++ .../components/kibana/instances/instances.tsx | 132 ++++++++++++++---- .../public/components/summary_status/index.js | 2 +- .../summary_status/summary_status.js | 53 +++---- x-pack/plugins/monitoring/public/plugin.ts | 4 + x-pack/plugins/monitoring/server/config.ts | 5 + x-pack/plugins/monitoring/server/index.ts | 5 + .../server/lib/kibana/get_kibana_info.ts | 17 ++- .../server/lib/kibana/get_kibanas.ts | 23 +-- .../lib/kibana/get_kibanas_for_clusters.ts | 32 ++++- .../lib/kibana/is_kibana_status_stale.ts | 18 +++ 16 files changed, 495 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/components/kibana/format_last_seen_timestamp.ts create mode 100644 x-pack/plugins/monitoring/server/lib/kibana/is_kibana_status_stale.ts diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index e78aeb6bac00bb..33e46b98ad40dd 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -137,6 +137,10 @@ represent. Defaults to 10. If you modify the `monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same value in this setting. +`monitoring.ui.kibana.reporting.stale_status_threshold_seconds`:: +Specifies how many seconds can pass before the Kibana status reports are considered stale. +Defaults to `120`. + [float] [[monitoring-ui-cgroup-settings]] ===== Monitoring UI container settings diff --git a/x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx index e710032ff1aef5..805a39e2bf69ec 100644 --- a/x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx +++ b/x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx @@ -12,6 +12,7 @@ export interface ExternalConfig { showCgroupMetricsElasticsearch: boolean; showCgroupMetricsLogstash: boolean; renderReactApp: boolean; + staleStatusThresholdSeconds: number; } export const ExternalConfigContext = createContext({} as ExternalConfig); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index ca2eefe440a0a8..dd25053a25a447 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -5,37 +5,40 @@ * 2.0. */ -import React from 'react'; -import { formatNumber } from '../../../lib/format_number'; -import { - ClusterItemContainer, - HealthStatusIndicator, - BytesPercentageUsage, - DisabledIfNoDataAndInSetupModeLink, -} from './helpers'; -import { get } from 'lodash'; import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, EuiFlexGrid, EuiFlexGroup, EuiFlexItem, + EuiHorizontalRule, EuiLink, - EuiTitle, EuiPanel, - EuiDescriptionList, - EuiDescriptionListTitle, - EuiDescriptionListDescription, - EuiHorizontalRule, + EuiTitle, + EuiIcon, + EuiToolTip, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { SetupModeTooltip } from '../../setup_mode/tooltip'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { get } from 'lodash'; +import React from 'react'; import { KIBANA_SYSTEM_ID, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { SetupModeFeature } from '../../../../common/enums'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; +import { formatNumber } from '../../../lib/format_number'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; -import { SetupModeFeature } from '../../../../common/enums'; +import { ExternalConfigContext } from '../../../application/contexts/external_config_context'; import { SetupModeContext } from '../../setup_mode/setup_mode_context'; +import { SetupModeTooltip } from '../../setup_mode/tooltip'; +import { + BytesPercentageUsage, + ClusterItemContainer, + DisabledIfNoDataAndInSetupModeLink, + HealthStatusIndicator, +} from './helpers'; const INSTANCES_PANEL_ALERTS = [RULE_KIBANA_VERSION_MISMATCH]; @@ -43,14 +46,13 @@ export function KibanaPanel(props) { const setupMode = props.setupMode; const alerts = props.alerts; const setupModeContext = React.useContext(SetupModeContext); + const { staleStatusThresholdSeconds } = React.useContext(ExternalConfigContext); const showDetectedKibanas = setupMode.enabled && get(setupMode.data, 'kibana.detected.doesExist', false); if (!props.count && !showDetectedKibanas) { return null; } - const statusIndicator = ; - const goToKibana = () => getSafeForExternalLink('#/kibana'); const goToInstances = () => getSafeForExternalLink('#/kibana/instances'); @@ -78,7 +80,12 @@ export function KibanaPanel(props) { return ( ); } + +function statusIndicator(status, someStatusIsStale, instancesHref, staleStatusThresholdSeconds) { + if (!someStatusIsStale) { + return ; + } + + const staleMessage = i18n.translate( + 'xpack.monitoring.cluster.overview.kibanaPanel.staleStatusTooltip', + { + defaultMessage: + "It's been more than {staleStatusThresholdSeconds} seconds since we heard from some instances.", + values: { + staleStatusThresholdSeconds, + }, + } + ); + + return ( + <> +
+ + <> + +   + {i18n.translate('xpack.monitoring.cluster.overview.kibanaPanel.staleStatusLabel', { + defaultMessage: 'Stale', + })} + + +
+ + {i18n.translate( + 'xpack.monitoring.cluster.overview.kibanaPanel.staleStatusLinkToInstancesLabel', + { + defaultMessage: 'View all instances', + } + )} + + + ); +} diff --git a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js index b3aa60e7c5c696..f36db34c761115 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js @@ -5,11 +5,15 @@ * 2.0. */ +import { EuiLink, EuiToolTip, EuiIcon, EuiStat } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React from 'react'; -import { SummaryStatus } from '../../summary_status'; -import { KibanaStatusIcon } from '../status_icon'; +import { useLocation } from 'react-router-dom'; import { formatMetric } from '../../../lib/format_number'; -import { i18n } from '@kbn/i18n'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { ExternalConfigContext } from '../../../application/contexts/external_config_context'; +import { SummaryStatus, DefaultStatusIndicator } from '../../summary_status'; +import { KibanaStatusIcon } from '../status_icon'; export function ClusterStatus({ stats, alerts }) { const { @@ -20,8 +24,12 @@ export function ClusterStatus({ stats, alerts }) { requests_total: requests, response_time_max: maxResponseTime, status, + some_status_is_stale: someStatusIsStale, } = stats; + const { staleStatusThresholdSeconds } = React.useContext(ExternalConfigContext); + const location = useLocation(); + const metrics = [ { label: i18n.translate('xpack.monitoring.kibana.clusterStatus.instancesLabel', { @@ -60,15 +68,103 @@ export function ClusterStatus({ stats, alerts }) { }, ]; - const IconComponent = ({ status }) => ; + const StatusIndicator = () => { + if (!someStatusIsStale) { + return ( + + ); + } + + const staleMessage = i18n.translate( + 'xpack.monitoring.kibana.clusterStatus.staleStatusTooltip', + { + defaultMessage: + "It's been more than {staleStatusThresholdSeconds} seconds since we heard from some instances.", + values: { + staleStatusThresholdSeconds, + }, + } + ); + + if (location.pathname === '/kibana') { + return ; + } + + return ; + }; return ( ); } + +function OverviewPageStatusIndicator({ staleMessage }) { + const instancesHref = getSafeForExternalLink('#/kibana/instances'); + + const title = ( + <> +
+ + <> + +   + {i18n.translate('xpack.monitoring.kibana.clusterStatus.staleStatusInstancesLabel', { + defaultMessage: 'Stale', + })} + + +
+ + {i18n.translate( + 'xpack.monitoring.cluster.overview.kibanaPanel.staleStatusLinkToInstancesLabel', + { + defaultMessage: 'View all instances', + } + )} + + + ); + + return ( + + ); +} + +function InstancesPageStatusIndicator({ staleMessage }) { + const title = ( + + + +   + {i18n.translate('xpack.monitoring.kibana.clusterStatus.staleStatusInstancesLabel', { + defaultMessage: 'Stale', + })} + + + ); + + return ( + + ); +} diff --git a/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js index cb55782818e279..d12782d2080ef3 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js @@ -5,11 +5,16 @@ * 2.0. */ +import { EuiIconTip, EuiStat } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUiSetting } from '@kbn/kibana-react-plugin/public'; +import { capitalize } from 'lodash'; import React from 'react'; -import { SummaryStatus } from '../../summary_status'; -import { KibanaStatusIcon } from '../status_icon'; +import { ExternalConfigContext } from '../../../application/contexts/external_config_context'; import { formatMetric } from '../../../lib/format_number'; -import { i18n } from '@kbn/i18n'; +import { DefaultStatusIndicator, SummaryStatus } from '../../summary_status'; +import { formatLastSeenTimestamp } from '../format_last_seen_timestamp'; +import { KibanaStatusIcon } from '../status_icon'; export function DetailStatus({ stats }) { const { @@ -18,8 +23,13 @@ export function DetailStatus({ stats }) { version, uptime, status, + statusIsStale, + lastSeenTimestamp, } = stats; + const { staleStatusThresholdSeconds } = React.useContext(ExternalConfigContext); + const dateFormat = useUiSetting('dateFormat'); + const metrics = [ { label: i18n.translate('xpack.monitoring.kibana.detailStatus.transportAddressLabel', { @@ -51,14 +61,81 @@ export function DetailStatus({ stats }) { }, ]; - const IconComponent = ({ status }) => ; + const StatusIndicator = () => { + if (!statusIsStale) { + return ( + + ); + } + + const { description, title } = prepareStaleMessage( + status, + lastSeenTimestamp, + staleStatusThresholdSeconds, + dateFormat + ); + + return ( + + ); + }; return ( ); } + +function prepareStaleMessage(status, lastSeenTimestamp, staleStatusThresholdSeconds, dateFormat) { + const { shouldShowRelativeTime, relativeTime, formattedTimestamp } = formatLastSeenTimestamp( + lastSeenTimestamp, + dateFormat + ); + + const staleMessage = i18n.translate('xpack.monitoring.kibana.listing.staleStatusTooltip', { + defaultMessage: + "It's been more than {staleStatusThresholdSeconds} seconds since we heard from this instance. Last seen: {lastSeenTimestamp}", + values: { + staleStatusThresholdSeconds, + lastSeenTimestamp: shouldShowRelativeTime ? relativeTime : formattedTimestamp, + }, + }); + + const description = ( + <> + {i18n.translate('xpack.monitoring.kibana.detailStatus.staleStatusMetricDescription', { + defaultMessage: 'Last Reported Status', + })} +   + + + ); + + const title = ( + <> + +   + {capitalize(status)} + + ); + + return { + description, + title, + }; +} diff --git a/x-pack/plugins/monitoring/public/components/kibana/format_last_seen_timestamp.ts b/x-pack/plugins/monitoring/public/components/kibana/format_last_seen_timestamp.ts new file mode 100644 index 00000000000000..3da3c4b9ee3504 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kibana/format_last_seen_timestamp.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { capitalize } from 'lodash'; +import moment from 'moment'; + +export function formatLastSeenTimestamp(lastSeenTimestampRaw: string, dateFormat: string) { + const lastSeenTimestamp = moment(lastSeenTimestampRaw); + + const formattedTimestamp = lastSeenTimestamp.format(dateFormat); + const relativeTime = capitalize(lastSeenTimestamp.fromNow()); + + const sixHoursAgo = moment().subtract(6, 'hours'); + const shouldShowRelativeTime = !sixHoursAgo.isAfter(lastSeenTimestamp); + + return { + shouldShowRelativeTime, + formattedTimestamp, + relativeTime, + }; +} diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx index e0290bbdb13f8f..51134d4d46384f 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx @@ -5,42 +5,51 @@ * 2.0. */ -import React, { Fragment } from 'react'; import { + EuiCallOut, + EuiHealth, + EuiIconTip, + EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiPanel, - EuiSpacer, - EuiLink, - EuiCallOut, EuiScreenReaderOnly, + EuiSpacer, EuiToolTip, - EuiHealth, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useUiSetting } from '@kbn/kibana-react-plugin/public'; import { capitalize, get } from 'lodash'; -// @ts-ignore -import { ClusterStatus } from '../cluster_status'; -// @ts-ignore -import { EuiMonitoringTable } from '../../table'; -import { STATUS_ICON_TYPES } from '../../status_icon'; +import React, { Fragment } from 'react'; +import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; +import { SetupModeFeature } from '../../../../common/enums'; +import { CommonAlertStatus } from '../../../../common/types/alerts'; +import { ElasticsearchSourceKibanaStats } from '../../../../common/types/es'; +import { AlertsStatus } from '../../../alerts/status'; +import { ExternalConfigContext } from '../../../application/contexts/external_config_context'; // @ts-ignore import { formatMetric, formatNumber } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; // @ts-ignore import { SetupModeBadge } from '../../setup_mode/badge'; -import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; -import { CommonAlertStatus } from '../../../../common/types/alerts'; -import { ElasticsearchSourceKibanaStats } from '../../../../common/types/es'; // @ts-ignore import { ListingCallOut } from '../../setup_mode/listing_callout'; -import { AlertsStatus } from '../../../alerts/status'; -import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; -import { SetupModeFeature } from '../../../../common/enums'; +import { STATUS_ICON_TYPES } from '../../status_icon'; +// @ts-ignore +import { EuiMonitoringTable } from '../../table'; +// @ts-ignore +import { ClusterStatus } from '../cluster_status'; +import { formatLastSeenTimestamp } from '../format_last_seen_timestamp'; -const getColumns = (setupMode: any, alerts: { [alertTypeId: string]: CommonAlertStatus[] }) => { +const getColumns = ( + setupMode: any, + alerts: { [alertTypeId: string]: CommonAlertStatus[] }, + dateFormat: string, + staleStatusThresholdSeconds: number +) => { const columns = [ { name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', { @@ -95,29 +104,57 @@ const getColumns = (setupMode: any, alerts: { [alertTypeId: string]: CommonAlert name: i18n.translate('xpack.monitoring.kibana.listing.alertsColumnTitle', { defaultMessage: 'Alerts', }), - field: 'isOnline', + field: 'alerts_column', width: '175px', sortable: true, render: () => , }, { - name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', { - defaultMessage: 'Status', + name: i18n.translate('xpack.monitoring.kibana.listing.lastReportedStatusColumnTitle', { + defaultMessage: 'Last Reported Status', }), field: 'status', + render: (status: string) => { + return ( + + {capitalize(status)} + + ); + }, + }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.lastSeenColumnTitle', { + defaultMessage: 'Last Seen', + }), + field: 'lastSeenTimestamp', render: ( - status: string, - kibana: Pick & { availability: boolean } + lastSeenTimestampRaw: string, + kibana: Pick & { statusIsStale: boolean } ) => { + const lastSeenTimestamp = prepareLastSeenTimestamp(lastSeenTimestampRaw, dateFormat); + const staleMessage = i18n.translate('xpack.monitoring.kibana.listing.staleStatusTooltip', { + defaultMessage: + "It's been more than {staleStatusThresholdSeconds} seconds since we heard from this instance.", + values: { + staleStatusThresholdSeconds, + }, + }); return ( - - - {capitalize(status)} - - + + {lastSeenTimestamp} + {kibana.statusIsStale && ( + <> +   + + + )} + ); }, }, @@ -183,6 +220,9 @@ interface Props { export const KibanaInstances: React.FC = (props: Props) => { const { clusterStatus, alerts, setupMode, sorting, pagination, onTableChange } = props; + const { staleStatusThresholdSeconds } = React.useContext(ExternalConfigContext); + const dateFormat = useUiSetting('dateFormat'); + let setupModeCallOut = null; // Merge the instances data with the setup data if enabled const instances = props.instances || []; @@ -286,7 +326,7 @@ export const KibanaInstances: React.FC = (props: Props) => { = (props: Props) => { ); }; + +function statusIconColor(status: string) { + switch (status) { + case 'red': + return 'danger'; + case 'yellow': + return 'warning'; + case 'green': + return 'success'; + default: + return 'subdued'; + } +} + +function prepareLastSeenTimestamp(lastSeenTimestampRaw: string, dateFormat: string) { + const { shouldShowRelativeTime, formattedTimestamp, relativeTime } = formatLastSeenTimestamp( + lastSeenTimestampRaw, + dateFormat + ); + + if (shouldShowRelativeTime) { + return ( + + {relativeTime} + + ); + } + + return formattedTimestamp; +} diff --git a/x-pack/plugins/monitoring/public/components/summary_status/index.js b/x-pack/plugins/monitoring/public/components/summary_status/index.js index 7105abe08f87f1..15e01b4c444dfc 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/index.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/index.js @@ -5,4 +5,4 @@ * 2.0. */ -export { SummaryStatus } from './summary_status'; +export { SummaryStatus, DefaultStatusIndicator } from './summary_status'; diff --git a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js index 951031b25da8b1..ef1416881ffaa8 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js @@ -54,49 +54,50 @@ const DefaultIconComponent = ({ status }) => ( ); -const StatusIndicator = ({ status, isOnline, IconComponent }) => { +export const DefaultStatusIndicator = ({ status, isOnline, IconComponent }) => { if (isEmpty(status)) { return null; } return ( - - - -   - {capitalize(status)} - - } - titleSize="xxxs" - textAlign="left" - className="monSummaryStatusNoWrap__stat" - description={i18n.translate('xpack.monitoring.summaryStatus.statusDescription', { - defaultMessage: 'Status', - })} - /> - + + +   + {capitalize(status)} + + } + titleSize="xxxs" + textAlign="left" + className="monSummaryStatusNoWrap__stat" + description={i18n.translate('xpack.monitoring.summaryStatus.statusDescription', { + defaultMessage: 'Status', + })} + /> ); }; export function SummaryStatus({ - metrics, + StatusIndicator = DefaultStatusIndicator, status, - alerts, isOnline, IconComponent = DefaultIconComponent, + alerts, + metrics, ...props }) { return (
- + + + {alerts ? ( > = { ccs: { enabled: true, }, + kibana: { + reporting: { + stale_status_threshold_seconds: true, + }, + }, }, kibana: true, }, diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts index 9b939ad83c0898..b338829c9e48f6 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibana_info.ts @@ -6,27 +6,26 @@ */ import { merge } from 'lodash'; -// @ts-ignore -import { MissingRequiredError } from '../error_missing_required'; -// @ts-ignore -import { calculateAvailability } from '../calculate_availability'; -import { LegacyRequest } from '../../types'; import { ElasticsearchResponse } from '../../../common/types/es'; -import { getNewIndexPatterns } from '../cluster/get_index_patterns'; import { Globals } from '../../static_globals'; +import { LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; +import { MissingRequiredError } from '../error_missing_required'; import { buildKibanaInfo } from './build_kibana_info'; +import { isKibanaStatusStale } from './is_kibana_status_stale'; export function handleResponse(resp: ElasticsearchResponse) { const hit = resp.hits?.hits[0]; const legacySource = hit?._source.kibana_stats; const mbSource = hit?._source.kibana?.stats; - const availabilityTimestamp = hit?._source['@timestamp'] ?? legacySource?.timestamp; - if (!availabilityTimestamp) { + const lastSeenTimestamp = hit?._source['@timestamp'] ?? legacySource?.timestamp; + if (!lastSeenTimestamp) { throw new MissingRequiredError('timestamp'); } return merge(buildKibanaInfo(hit!), { - availability: calculateAvailability(availabilityTimestamp), + statusIsStale: isKibanaStatusStale(lastSeenTimestamp), + lastSeenTimestamp, os_memory_free: mbSource?.os?.memory?.free_in_bytes ?? legacySource?.os?.memory?.free_in_bytes, uptime: mbSource?.process?.uptime?.ms ?? legacySource?.process?.uptime_in_millis, }); diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts index 7f6f45cc985003..69cda62ae852cf 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas.ts @@ -6,17 +6,14 @@ */ import moment from 'moment'; -// @ts-ignore -import { createQuery } from '../create_query'; -// @ts-ignore -import { calculateAvailability } from '../calculate_availability'; -// @ts-ignore -import { KibanaMetric } from '../metrics'; +import { ElasticsearchResponse, ElasticsearchResponseHit } from '../../../common/types/es'; +import { Globals } from '../../static_globals'; import { LegacyRequest } from '../../types'; import { getNewIndexPatterns } from '../cluster/get_index_patterns'; -import { Globals } from '../../static_globals'; -import { ElasticsearchResponse, ElasticsearchResponseHit } from '../../../common/types/es'; -import { KibanaInfo, buildKibanaInfo } from './build_kibana_info'; +import { createQuery } from '../create_query'; +import { KibanaMetric } from '../metrics'; +import { buildKibanaInfo, KibanaInfo } from './build_kibana_info'; +import { isKibanaStatusStale } from './is_kibana_status_stale'; interface Kibana { process?: { @@ -38,7 +35,8 @@ interface Kibana { }; concurrent_connections?: number; kibana?: KibanaInfo; - availability: boolean; + statusIsStale: boolean; + lastSeenTimestamp: string; } /* @@ -120,6 +118,8 @@ export async function getKibanas(req: LegacyRequest, { clusterUuid }: { clusterU const legacyStats = hit._source.kibana_stats; const mbStats = hit._source.kibana?.stats; + const lastSeenTimestamp = hit._source['@timestamp'] ?? hit._source.timestamp; + const kibana: Kibana = { kibana: buildKibanaInfo(hit), concurrent_connections: @@ -143,7 +143,8 @@ export async function getKibanas(req: LegacyRequest, { clusterUuid }: { clusterU requests: { total: mbStats?.request?.total ?? legacyStats?.requests?.total, }, - availability: calculateAvailability(hit._source['@timestamp'] ?? hit._source.timestamp), + statusIsStale: isKibanaStatusStale(lastSeenTimestamp), + lastSeenTimestamp, }; return kibana; }); diff --git a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts index 4b3acd4e937db9..17ccff4a9ea7da 100644 --- a/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/kibana/get_kibanas_for_clusters.ts @@ -6,11 +6,12 @@ */ import { chain, find } from 'lodash'; -import { LegacyRequest, Cluster, Bucket } from '../../types'; +import { Globals } from '../../static_globals'; +import { Bucket, Cluster, LegacyRequest } from '../../types'; +import { getNewIndexPatterns } from '../cluster/get_index_patterns'; import { createQuery } from '../create_query'; import { KibanaClusterMetric } from '../metrics'; -import { getNewIndexPatterns } from '../cluster/get_index_patterns'; -import { Globals } from '../../static_globals'; +import { isKibanaStatusStale } from './is_kibana_status_stale'; /* * Get high-level info for Kibanas in a set of clusters @@ -74,6 +75,11 @@ export function getKibanasForClusters(req: LegacyRequest, clusters: Cluster[], c }, }, aggs: { + last_seen: { + max: { + field: 'kibana_stats.timestamp', + }, + }, response_time_max: { max: { field: 'kibana_stats.response_times.max', @@ -185,6 +191,7 @@ export function getKibanasForClusters(req: LegacyRequest, clusters: Cluster[], c let responseTime = 0; let memorySize = 0; let memoryLimit = 0; + let someStatusIsStale = true; // if the cluster has kibana instances at all if (kibanaUuids.length) { @@ -204,6 +211,8 @@ export function getKibanasForClusters(req: LegacyRequest, clusters: Cluster[], c responseTime = aggregations.response_time_max?.value; memorySize = aggregations.memory_rss?.value; memoryLimit = aggregations.memory_heap_size_limit?.value; + + someStatusIsStale = kibanaUuids.some(hasStaleStatus); } return { @@ -211,6 +220,7 @@ export function getKibanasForClusters(req: LegacyRequest, clusters: Cluster[], c stats: { uuids: kibanaUuids.map(({ key }: Bucket) => key), status, + some_status_is_stale: someStatusIsStale, requests_total: requestsTotal, concurrent_connections: connections, response_time_max: responseTime, @@ -223,3 +233,19 @@ export function getKibanasForClusters(req: LegacyRequest, clusters: Cluster[], c }) ); } + +function hasStaleStatus(kibana: any) { + const buckets: any[] = kibana?.latest_report?.buckets ?? []; + + if (buckets.length === 0) { + return true; + } + + const lastSeenTimestamp: string | null = buckets[0]?.last_seen?.value_as_string ?? null; + + if (lastSeenTimestamp === null) { + return true; + } + + return isKibanaStatusStale(lastSeenTimestamp); +} diff --git a/x-pack/plugins/monitoring/server/lib/kibana/is_kibana_status_stale.ts b/x-pack/plugins/monitoring/server/lib/kibana/is_kibana_status_stale.ts new file mode 100644 index 00000000000000..841fd61ad65f46 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/kibana/is_kibana_status_stale.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { Globals } from '../../static_globals'; + +export function isKibanaStatusStale(lastSeenTimestamp: string) { + const lastSeen = moment(lastSeenTimestamp); + const staleThreshold = moment().subtract( + Globals.app.config.ui.kibana.reporting.stale_status_threshold_seconds, + 'seconds' + ); + return staleThreshold.isAfter(lastSeen); +}