diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index dea8d18bb65b1..c7e24193a124a 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -21,14 +21,26 @@ import { RouteInit } from './route_init'; import { NoDataPage } from './pages/no_data'; import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview'; import { BeatsOverviewPage } from './pages/beats/overview'; -import { BeatsInstancesPage } from './pages/beats/instances'; +import { + CODE_PATH_ELASTICSEARCH, + CODE_PATH_BEATS, + CODE_PATH_LOGSTASH, +} from '../../common/constants'; import { BeatsInstancePage } from './pages/beats/instance'; -import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants'; import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page'; import { ElasticsearchIndicesPage } from './pages/elasticsearch/indices_page'; import { ElasticsearchNodePage } from './pages/elasticsearch/node_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; +import { LogStashOverviewPage } from './pages/logstash/overview'; +import { LogStashNodesPage } from './pages/logstash/nodes'; +import { LogStashPipelinesPage } from './pages/logstash/pipelines'; +import { LogStashPipelinePage } from './pages/logstash/pipeline'; +import { BeatsInstancesPage } from './pages/beats/instances'; +import { LogStashNodeAdvancedPage } from './pages/logstash/advanced'; +// import { LogStashNodePipelinesPage } from './pages/logstash/node_pipelines'; +import { LogStashNodePage } from './pages/logstash/node'; +import { LogStashNodePipelinesPage } from './pages/logstash/node_pipelines'; export const renderApp = ( core: CoreStart, @@ -133,6 +145,56 @@ const MonitoringApp: React.FC<{ fetchAllClusters={false} /> + {/* Logstash Routes */} + + + + + + + + + + + + + + = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const match = useRouteMatch<{ uuid: string | undefined }>(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const { zoomInfo, onBrush } = useCharts(); + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.logstash.node.advanced.routeTitle', { + defaultMessage: 'Logstash - {nodeName} - Advanced', + values: { + nodeName: data.nodeSummary ? data.nodeSummary.name : '', + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.node.advanced.pageTitle', { + defaultMessage: 'Logstash node: {nodeName}', + values: { + nodeName: data.nodeSummary ? data.nodeSummary.name : '', + }, + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + + setData(response); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + match.params.uuid, + ]); + + const metricsToShow = useMemo(() => { + if (!data.metrics) return []; + + return [ + data.metrics.logstash_node_cpu_utilization, + data.metrics.logstash_queue_events_count, + data.metrics.logstash_node_cgroup_cpu, + data.metrics.logstash_pipeline_queue_size, + data.metrics.logstash_node_cgroup_stats, + ]; + }, [data.metrics]); + + return ( + + + + {data.nodeSummary && } + + + + + {metricsToShow.map((metric, index) => ( + + + + + ))} + + + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/logstash_template.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/logstash_template.tsx new file mode 100644 index 0000000000000..d1b3c5e5ec374 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/logstash_template.tsx @@ -0,0 +1,85 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { PageTemplate } from '../page_template'; +import { TabMenuItem, PageTemplateProps } from '../page_template'; + +interface LogstashTemplateProps extends PageTemplateProps { + cluster: any; + instance?: any; + pipelineId?: string; + pipelineVersions?: string[]; + tabsDisabled?: boolean; +} + +export const LogstashTemplate: React.FC = ({ + cluster, + instance, + pipelineId, + pipelineVersions, + tabsDisabled, + ...props +}) => { + const tabs: TabMenuItem[] = []; + if (!tabsDisabled) { + if (!instance && !pipelineId) { + tabs.push({ + id: 'overview', + label: i18n.translate('xpack.monitoring.logstashNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/logstash', + }); + tabs.push({ + id: 'nodes', + label: i18n.translate('xpack.monitoring.logstashNavigation.nodesLinkText', { + defaultMessage: 'Nodes', + }), + route: '/logstash/nodes', + }); + tabs.push({ + id: 'pipelines', + label: i18n.translate('xpack.monitoring.logstashNavigation.pipelinesLinkText', { + defaultMessage: 'Pipelines', + }), + route: '/logstash/pipelines', + }); + } else if (instance) { + tabs.push({ + id: 'overview', + label: i18n.translate('xpack.monitoring.logstashNavigation.instance.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: `/logstash/node/${instance.nodeSummary?.uuid}`, // IDK if this is right + }); + tabs.push({ + id: 'pipeline', + label: i18n.translate('xpack.monitoring.logstashNavigation.instance.pipelinesLinkText', { + defaultMessage: 'Pipelines', + }), + route: `/logstash/node/${instance.nodeSummary?.uuid}/pipelines`, // IDK if this is right + }); + tabs.push({ + id: 'advanced', + label: i18n.translate('xpack.monitoring.logstashNavigation.instance.advancedLinkText', { + defaultMessage: 'Advanced', + }), + route: `/logstash/node/${instance.nodeSummary?.uuid}/advanced`, // IDK if this is right + }); + } + } + + if (pipelineVersions && pipelineVersions.length) { + // todo add this in: https://github.com/elastic/kibana/blob/4584a8b570402aa07832cf3e5b520e5d2cfa7166/x-pack/plugins/monitoring/public/directives/main/index.js#L36, https://github.com/elastic/kibana/blob/c07a512e4647a40d8e891eb24f5912783b561fba/x-pack/plugins/monitoring/public/directives/main/index.html#L293-L298 + // tabs.push({ + // id: 'dropdown-elm', + // }) + } + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx new file mode 100644 index 0000000000000..301d3c45dedb5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx @@ -0,0 +1,122 @@ +/* + * 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 React, { useContext, useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { useRouteMatch } from 'react-router-dom'; +import { + EuiPage, + EuiPageBody, + EuiPanel, + EuiSpacer, + EuiPageContent, + EuiFlexGrid, + EuiFlexItem, +} from '@elastic/eui'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +// @ts-ignore +import { List } from '../../../components/logstash/pipeline_viewer/models/list'; +// @ts-ignore +import { LogstashTemplate } from './logstash_template'; +// @ts-ignore +import { DetailStatus } from '../../../components/logstash/detail_status'; +// @ts-ignore +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +import { AlertsCallout } from '../../../alerts/callout'; +import { useCharts } from '../../hooks/use_charts'; + +export const LogStashNodePage: React.FC = ({ clusters }) => { + const match = useRouteMatch<{ uuid: string | undefined }>(); + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState({} as any); + const { zoomInfo, onBrush } = useCharts(); + const title = i18n.translate('xpack.monitoring.logstash.node.routeTitle', { + defaultMessage: 'Logstash - {nodeName}', + values: { + nodeName: data.nodeSummary ? data.nodeSummary.name : '', + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.node.pageTitle', { + defaultMessage: 'Logstash node: {nodeName}', + values: { + nodeName: data.nodeSummary ? data.nodeSummary.name : '', + }, + }); + + const getPageData = useCallback(async () => { + const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, match.params]); + + const metricsToShow = useMemo(() => { + if (!data.metrics) return []; + + return [ + data.metrics.logstash_events_input_rate, + data.metrics.logstash_jvm_usage, + data.metrics.logstash_events_output_rate, + data.metrics.logstash_node_cpu_metric, + data.metrics.logstash_events_latency, + data.metrics.logstash_os_load, + ]; + }, [data.metrics]); + + return ( + + + + {data.nodeSummary && } + + + + + {metricsToShow.map((metric, index) => ( + + + + + ))} + + + + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx new file mode 100644 index 0000000000000..1c956603f99bd --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx @@ -0,0 +1,122 @@ +/* + * 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 React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +// @ts-ignore +import { useRouteMatch } from 'react-router-dom'; +// @ts-ignore +import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +// @ts-ignore +import { Listing } from '../../../components/logstash/listing'; +import { LogstashTemplate } from './logstash_template'; +// @ts-ignore +import { DetailStatus } from '../../../components/logstash/detail_status'; +// @ts-ignore +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +import { useTable } from '../../hooks/use_table'; +// @ts-ignore +import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; +import { useCharts } from '../../hooks/use_charts'; + +export const LogStashNodePipelinesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const match = useRouteMatch<{ uuid: string | undefined }>(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const { onBrush, zoomInfo } = useCharts(); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + + const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = + useTable('logstash.pipelines'); + + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.logstash.node.pipelines.routeTitle', { + defaultMessage: 'Logstash - {nodeName} - Pipelines', + values: { + nodeName: data.nodeSummary ? data.nodeSummary.name : '', + }, + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.node.pipelines.pageTitle', { + defaultMessage: 'Logstash node pipelines: {nodeName}', + values: { + nodeName: data.nodeSummary ? data.nodeSummary.name : '', + }, + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const options: any = getPaginationRouteOptions(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}/pipelines`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + pagination: options.pagination, + queryText: options.queryText, + }), + }); + + setData(response); + updateTotalItemCount(response.totalPipelineCount); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + getPaginationRouteOptions, + updateTotalItemCount, + match.params.uuid, + ]); + + return ( + + {data.pipelines && ( + + )} + + ); +}; + +function makeUpgradeMessage(logstashVersions: any) { + if ( + !Array.isArray(logstashVersions) || + logstashVersions.length === 0 || + logstashVersions.some(isPipelineMonitoringSupportedInVersion) + ) { + return null; + } + + return 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher.'; +} diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx new file mode 100644 index 0000000000000..633e47339f467 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx @@ -0,0 +1,89 @@ +/* + * 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 React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +// @ts-ignore +import { Listing } from '../../../components/logstash/listing'; +import { LogstashTemplate } from './logstash_template'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { useTable } from '../../hooks/use_table'; + +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} + +export const LogStashNodesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState({} as any); + const { getPaginationTableProps } = useTable('logstash.nodes'); + + const title = i18n.translate('xpack.monitoring.logstash.nodes.routeTitle', { + defaultMessage: 'Logstash - Nodes', + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.nodes.pageTitle', { + defaultMessage: 'Logstash nodes', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/nodes`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + return ( + +
+ ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> +
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx new file mode 100644 index 0000000000000..1edbe5cf71e7d --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx @@ -0,0 +1,84 @@ +/* + * 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 React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +import { useCharts } from '../../hooks/use_charts'; +// @ts-ignore +import { Overview } from '../../../components/logstash/overview'; +import { LogstashTemplate } from './logstash_template'; + +export const LogStashOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState(null); + // const [showShardActivityHistory, setShowShardActivityHistory] = useState(false); + + const title = i18n.translate('xpack.monitoring.logstash.overview.title', { + defaultMessage: 'Logstash', + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.overview.pageTitle', { + defaultMessage: 'Logstash overview', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + const renderOverview = (overviewData: any) => { + if (overviewData === null) { + return null; + } + const { clusterStatus, metrics, logs } = overviewData || {}; + + return ( + + ); + }; + + return ( + +
{renderOverview(data)}
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx new file mode 100644 index 0000000000000..abff0ab17b992 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx @@ -0,0 +1,175 @@ +/* + * 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 React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import moment from 'moment'; +import { useRouteMatch } from 'react-router-dom'; +import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +// @ts-ignore +import { List } from '../../../components/logstash/pipeline_viewer/models/list'; +// @ts-ignore +import { PipelineViewer } from '../../../components/logstash/pipeline_viewer'; +// @ts-ignore +import { Pipeline } from '../../../components/logstash/pipeline_viewer/models/pipeline'; +// @ts-ignore +import { PipelineState } from '../../../components/logstash/pipeline_viewer/models/pipeline_state'; +// @ts-ignore +import { vertexFactory } from '../../../components/logstash/pipeline_viewer/models/graph/vertex_factory'; +import { LogstashTemplate } from './logstash_template'; +import { useTable } from '../../hooks/use_table'; +import { ExternalConfigContext } from '../../external_config_context'; +import { formatTimestampToDuration } from '../../../../common'; +import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { PipelineVersions } from './pipeline_versions_dropdown'; + +export const LogStashPipelinePage: React.FC = ({ clusters }) => { + const match = useRouteMatch<{ id: string | undefined; hash: string | undefined }>(); + const { hash: pipelineHash, id: pipelineId } = match.params; + const globalState = useContext(GlobalStateContext); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const { minIntervalSeconds } = useContext(ExternalConfigContext); + + const dateFormat = useUiSetting('dateFormat'); + const [pipelineState, setPipelineState] = useState(null); + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState({} as any); + const [detailVertexId, setDetailVertexId] = useState(null); + const { updateTotalItemCount } = useTable('logstash.pipelines'); + + const title = i18n.translate('xpack.monitoring.logstash.pipeline.routeTitle', { + defaultMessage: 'Logstash - Pipeline', + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.pipeline.pageTitle', { + defaultMessage: 'Logstash pipeline: {pipeline}', + values: { + pipeline: data.pipeline ? data.pipeline.id : '', + }, + }); + + const getPageData = useCallback(async () => { + const url = pipelineHash + ? `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}/${pipelineHash}` + : `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + detailVertexId: detailVertexId || undefined, + }), + }); + const myData = response; + + myData.versions = myData.versions.map((version: any) => { + const relativeFirstSeen = formatTimestampToDuration( + version.firstSeen, + CALCULATE_DURATION_SINCE + ); + const relativeLastSeen = formatTimestampToDuration( + version.lastSeen, + CALCULATE_DURATION_SINCE + ); + + const fudgeFactorSeconds = 2 * minIntervalSeconds; + const isLastSeenCloseToNow = Date.now() - version.lastSeen <= fudgeFactorSeconds * 1000; + + return { + ...version, + relativeFirstSeen: i18n.translate( + 'xpack.monitoring.logstash.pipeline.relativeFirstSeenAgoLabel', + { + defaultMessage: '{relativeFirstSeen} ago', + values: { relativeFirstSeen }, + } + ), + relativeLastSeen: isLastSeenCloseToNow + ? i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenNowLabel', { + defaultMessage: 'now', + }) + : i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenAgoLabel', { + defaultMessage: 'until {relativeLastSeen} ago', + values: { relativeLastSeen }, + }), + }; + }); + setData(myData); + updateTotalItemCount(response.totalNodeCount); + }, [ + ccs, + clusterUuid, + services.http, + updateTotalItemCount, + detailVertexId, + minIntervalSeconds, + pipelineHash, + pipelineId, + ]); + + useEffect(() => { + if (data.pipeline) { + setPipelineState(new PipelineState(data.pipeline)); + } + }, [data]); + + const timeseriesTooltipXValueFormatter = (xValue: any) => moment(xValue).format(dateFormat); + + const onVertexChange = useCallback( + (vertex: any) => { + if (!vertex) { + setDetailVertexId(null); + } else { + setDetailVertexId(vertex.id); + } + + getPageData(); + }, + [getPageData] + ); + + const onChangePipelineHash = useCallback(() => { + window.location.hash = getSafeForExternalLink( + `#/logstash/pipelines/${pipelineId}/${pipelineHash}` + ); + }, [pipelineId, pipelineHash]); + + return ( + +
+ +
+
+ {pipelineState && ( + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline_versions_dropdown.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline_versions_dropdown.tsx new file mode 100644 index 0000000000000..021b8ad3b6a28 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline_versions_dropdown.tsx @@ -0,0 +1,47 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface Props { + pipelineVersions: any[]; + pipelineHash?: string; + onChangePipelineHash: () => void; +} + +export const PipelineVersions = (props: Props) => { + const { pipelineHash, pipelineVersions, onChangePipelineHash } = props; + + return ( + + + { + return { + text: i18n.translate( + 'xpack.monitoring.logstashNavigation.pipelineVersionDescription', + { + defaultMessage: + 'Version active {relativeLastSeen} and first seen {relativeFirstSeen}', + values: { + relativeLastSeen: option.relativeLastSeen, + relativeFirstSeen: option.relativeFirstSeen, + }, + } + ), + value: option.hash, + }; + })} + onChange={onChangePipelineHash} + /> + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx new file mode 100644 index 0000000000000..5f4fe634177de --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx @@ -0,0 +1,117 @@ +/* + * 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 React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ComponentProps } from '../../route_init'; +import { useCharts } from '../../hooks/use_charts'; +// @ts-ignore +import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; +// @ts-ignore +import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; +import { LogstashTemplate } from './logstash_template'; +import { useTable } from '../../hooks/use_table'; + +export const LogStashPipelinesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const dateFormat = useUiSetting('dateFormat'); + + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState(null); + const { getPaginationTableProps, getPaginationRouteOptions, updateTotalItemCount } = + useTable('logstash.pipelines'); + + const title = i18n.translate('xpack.monitoring.logstash.overview.title', { + defaultMessage: 'Logstash', + }); + + const pageTitle = i18n.translate('xpack.monitoring.logstash.overview.pageTitle', { + defaultMessage: 'Logstash overview', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipelines`; + + const options: any = getPaginationRouteOptions(); + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + pagination: options.pagination, + queryText: options.queryText, + }), + }); + + setData(response); + updateTotalItemCount(response.totalPipelineCount); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + getPaginationRouteOptions, + updateTotalItemCount, + ]); + + const renderOverview = (pageData: any) => { + if (pageData === null) { + return null; + } + const { clusterStatus, pipelines } = pageData || {}; + + const upgradeMessage = pageData ? makeUpgradeMessage(clusterStatus.versions) : null; + return ( + onBrush({ xaxis })} + stats={clusterStatus} + data={pipelines} + {...getPaginationTableProps()} + upgradeMessage={upgradeMessage} + dateFormat={dateFormat} + /> + ); + }; + + return ( + +
{renderOverview(data)}
+
+ ); +}; + +function makeUpgradeMessage(logstashVersions: any) { + if ( + !Array.isArray(logstashVersions) || + logstashVersions.length === 0 || + logstashVersions.some(isPipelineMonitoringSupportedInVersion) + ) { + return null; + } + + return 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher.'; +} diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 5c2bdc1424f93..ddbfd480a9f4e 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -51,7 +51,7 @@ export const configSchema = schema.object({ }), min_interval_seconds: schema.number({ defaultValue: 10 }), show_license_expiration: schema.boolean({ defaultValue: true }), - render_react_app: schema.boolean({ defaultValue: false }), + render_react_app: schema.boolean({ defaultValue: true }), }), kibana: schema.object({ collection: schema.object({