[8.17] [Infra][ObsUX] Hosts & Container Logs only overview (#202992) (#203815)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[Infra][ObsUX] Hosts & Container Logs only overview
(#202992)](https://github.com/elastic/kibana/pull/202992)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Gonçalo Rica Pais da
Silva","email":"goncalo.rica@elastic.co"},"sourceCommit":{"committedDate":"2024-12-10T16:38:19Z","message":"[Infra][ObsUX]
Hosts & Container Logs only overview (#202992)\n\n##
Summary\r\n\r\nEnables a logs only overview for hosts & containers.
Disables the\r\nmetrics tab as there's no data incoming for metrics, and
provides Logs\r\ncharts on the overview page detailing the Log Rate (all
logs generated)\r\nand Log Error Rate (all recorded
errors).\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ced14b6d-dd08-4514-9066-6c02c62d5ff8\r\n\r\nCloses
#201752\r\n\r\n## How to test\r\n\r\nThis is tested using synthtrace
data, loading the scenario below:\r\n\r\n```\r\nnode scripts/synthtrace
traces_logs_entities.ts --live\r\n```\r\n\r\nThis loads a logs-only
host, though if other scenarios contain logs only\r\ncontainers, feel
free to use those as well.\r\n\r\n* Go to Inventory page. Click on a
host or container.\r\n* If it is a logs only host/container, no metrics
tab should be shown.\r\nMetrics KPI charts should be replaced with Logs
KPI charts (Log Rate and\r\nLog Error Rate).\r\n* If the host/container
contains metrics, the metrics tab should be\r\nvisible and the normal
Metrics KPI charts should be
present.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"305bb1b8879dd41077afa79195ac920e7d8080e1","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services","v8.17.0"],"number":202992,"url":"https://github.com/elastic/kibana/pull/202992","mergeCommit":{"message":"[Infra][ObsUX]
Hosts & Container Logs only overview (#202992)\n\n##
Summary\r\n\r\nEnables a logs only overview for hosts & containers.
Disables the\r\nmetrics tab as there's no data incoming for metrics, and
provides Logs\r\ncharts on the overview page detailing the Log Rate (all
logs generated)\r\nand Log Error Rate (all recorded
errors).\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ced14b6d-dd08-4514-9066-6c02c62d5ff8\r\n\r\nCloses
#201752\r\n\r\n## How to test\r\n\r\nThis is tested using synthtrace
data, loading the scenario below:\r\n\r\n```\r\nnode scripts/synthtrace
traces_logs_entities.ts --live\r\n```\r\n\r\nThis loads a logs-only
host, though if other scenarios contain logs only\r\ncontainers, feel
free to use those as well.\r\n\r\n* Go to Inventory page. Click on a
host or container.\r\n* If it is a logs only host/container, no metrics
tab should be shown.\r\nMetrics KPI charts should be replaced with Logs
KPI charts (Log Rate and\r\nLog Error Rate).\r\n* If the host/container
contains metrics, the metrics tab should be\r\nvisible and the normal
Metrics KPI charts should be
present.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"305bb1b8879dd41077afa79195ac920e7d8080e1"}},"sourceBranch":"main","suggestedTargetBranches":["8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202992","number":202992,"mergeCommit":{"message":"[Infra][ObsUX]
Hosts & Container Logs only overview (#202992)\n\n##
Summary\r\n\r\nEnables a logs only overview for hosts & containers.
Disables the\r\nmetrics tab as there's no data incoming for metrics, and
provides Logs\r\ncharts on the overview page detailing the Log Rate (all
logs generated)\r\nand Log Error Rate (all recorded
errors).\r\n\r\n\r\nhttps://github.com/user-attachments/assets/ced14b6d-dd08-4514-9066-6c02c62d5ff8\r\n\r\nCloses
#201752\r\n\r\n## How to test\r\n\r\nThis is tested using synthtrace
data, loading the scenario below:\r\n\r\n```\r\nnode scripts/synthtrace
traces_logs_entities.ts --live\r\n```\r\n\r\nThis loads a logs-only
host, though if other scenarios contain logs only\r\ncontainers, feel
free to use those as well.\r\n\r\n* Go to Inventory page. Click on a
host or container.\r\n* If it is a logs only host/container, no metrics
tab should be shown.\r\nMetrics KPI charts should be replaced with Logs
KPI charts (Log Rate and\r\nLog Error Rate).\r\n* If the host/container
contains metrics, the metrics tab should be\r\nvisible and the normal
Metrics KPI charts should be
present.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"305bb1b8879dd41077afa79195ac920e7d8080e1"}},{"branch":"8.17","label":"v8.17.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"url":"https://github.com/elastic/kibana/pull/203644","number":203644,"branch":"8.x","state":"OPEN"}]}]
BACKPORT-->
This commit is contained in:
Gonçalo Rica Pais da Silva 2024-12-17 17:12:55 +01:00 committed by GitHub
parent ec8c5fa1f9
commit b2f5821788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 278 additions and 54 deletions

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { Kpi } from './kpi';
@ -79,10 +79,8 @@ const DockerKpiCharts = ({
searchSessionId,
loading = false,
}: ContainerKpiChartsProps) => {
const { euiTheme } = useEuiTheme();
const charts = useDockerContainerKpiCharts({
dataViewId: dataView?.id,
seriesColor: euiTheme.colors.lightestShade,
});
return (
@ -112,10 +110,8 @@ const KubernetesKpiCharts = ({
searchSessionId,
loading = false,
}: ContainerKpiChartsProps) => {
const { euiTheme } = useEuiTheme();
const charts = useK8sContainerKpiCharts({
dataViewId: dataView?.id,
seriesColor: euiTheme.colors.lightestShade,
});
return (

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { Kpi } from './kpi';
@ -31,11 +31,9 @@ export const HostKpiCharts = ({
searchSessionId,
loading = false,
}: HostKpiChartsProps) => {
const { euiTheme } = useEuiTheme();
const charts = useHostKpiCharts({
dataViewId: dataView?.id,
getSubtitle,
seriesColor: euiTheme.colors.lightestShade,
});
return (

View file

@ -0,0 +1,43 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
import { renderHook } from '@testing-library/react';
import { useChartSeriesColor } from './use_chart_series_color';
describe('useChartSeriesColor', () => {
let seriesDefaultColor: string;
beforeEach(() => {
const { result } = renderHook(() => useEuiTheme());
// Don't try to test a hardcoded value, just use what is provided by EUI.
// If in the future this value changes, the tests won't break.
seriesDefaultColor = result.current.euiTheme.colors.lightestShade;
});
it('returns a default color value if given no input', () => {
const { result } = renderHook(() => useChartSeriesColor());
expect(result.current).not.toBe('');
expect(result.current).toBe(seriesDefaultColor);
});
it('returns a default color value if given an empty string', () => {
const { result } = renderHook(() => useChartSeriesColor(''));
expect(result.current).not.toBe('');
expect(result.current).toBe(seriesDefaultColor);
});
it('returns the provided color input', () => {
const { result } = renderHook(() => useChartSeriesColor('#fff'));
expect(result.current).not.toBe(seriesDefaultColor);
expect(result.current).toBe('#fff');
});
});

View file

@ -0,0 +1,21 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
/**
* Provides either the input color, or yields the default EUI theme
* color for use as the KPI chart series color.
* @param seriesColor A user-defined color value
* @returns Either the input `seriesColor` or the default color from EUI
*/
export const useChartSeriesColor = (seriesColor?: string): string => {
const { euiTheme } = useEuiTheme();
// Prevent empty string being used as a valid color
return seriesColor || euiTheme.colors.lightestShade;
};

View file

@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common';
import useAsync from 'react-use/lib/useAsync';
import { ContainerMetricTypes } from '../charts/types';
import { useChartSeriesColor } from './use_chart_series_color';
const getSubtitleFromFormula = (value: string) =>
value.startsWith('max')
@ -106,6 +107,8 @@ export const useDockerContainerKpiCharts = ({
dataViewId?: string;
seriesColor?: string;
}) => {
seriesColor = useChartSeriesColor(seriesColor);
const { value: charts = [] } = useAsync(async () => {
const model = findInventoryModel('container');
const { cpu, memory } = await model.metrics.getCharts();
@ -134,6 +137,8 @@ export const useK8sContainerKpiCharts = ({
dataViewId?: string;
seriesColor?: string;
}) => {
seriesColor = useChartSeriesColor(seriesColor);
const { value: charts = [] } = useAsync(async () => {
const model = findInventoryModel('container');
const { cpu, memory } = await model.metrics.getCharts();

View file

@ -10,6 +10,7 @@ import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common';
import { useMemo } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { HostMetricTypes } from '../charts/types';
import { useChartSeriesColor } from './use_chart_series_color';
export const useHostCharts = ({
metric,
@ -87,6 +88,8 @@ export const useHostKpiCharts = ({
seriesColor?: string;
getSubtitle?: (formulaValue: string) => string;
}) => {
seriesColor = useChartSeriesColor(seriesColor);
const { value: charts = [] } = useAsync(async () => {
const model = findInventoryModel('host');
const { cpu, memory, disk } = await model.metrics.getCharts();

View file

@ -0,0 +1,77 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { useMemo } from 'react';
import { LensConfig } from '@kbn/lens-embeddable-utils/config_builder';
import { useChartSeriesColor } from './use_chart_series_color';
const LOG_RATE = i18n.translate('xpack.infra.assetDetails.charts.logRate', {
defaultMessage: 'Log Rate',
});
const LOG_ERROR_RATE = i18n.translate('xpack.infra.assetDetails.charts.logErrorRate', {
defaultMessage: 'Log Error Rate',
});
const logRateMetric: LensConfig & { id: string } = {
id: 'logMetric',
chartType: 'metric',
title: LOG_RATE,
label: LOG_RATE,
trendLine: true,
value: 'count()',
format: 'number',
decimals: 1,
normalizeByUnit: 's',
};
const logErrorRateMetric: LensConfig & { id: string } = {
id: 'logErrorMetric',
chartType: 'metric',
title: LOG_ERROR_RATE,
label: LOG_ERROR_RATE,
trendLine: true,
value:
'count(kql=\'log.level: "error" OR log.level: "ERROR" OR error.log.level: "error" OR error.log.level: "ERROR"\')',
format: 'number',
decimals: 1,
normalizeByUnit: 's',
};
export const useLogsCharts = ({
dataViewId,
seriesColor,
}: {
dataViewId?: string;
seriesColor?: string;
}) => {
seriesColor = useChartSeriesColor(seriesColor);
return useMemo(() => {
const dataset = dataViewId && {
dataset: {
index: dataViewId,
},
};
return {
charts: [
{
...logRateMetric,
...dataset,
seriesColor,
},
{
...logErrorRateMetric,
...dataset,
seriesColor,
},
],
};
}, [dataViewId, seriesColor]);
};

View file

@ -26,6 +26,8 @@ import { LinkToNodeDetails } from '../links';
import { ContentTabIds, type LinkOptions, type Tab, type TabIds } from '../types';
import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props';
import { useTabSwitcherContext } from './use_tab_switcher';
import { useEntitySummary } from './use_entity_summary';
import { isMetricsSignal } from '../utils/get_data_stream_types';
type TabItem = NonNullable<Pick<EuiPageHeaderProps, 'tabs'>['tabs']>[number];
@ -140,9 +142,31 @@ const useFeatureFlagTabs = () => {
};
};
const useMetricsTabs = () => {
const { asset } = useAssetDetailsRenderPropsContext();
const { dataStreams } = useEntitySummary({
entityType: asset.type,
entityId: asset.id,
});
const isMetrics = isMetricsSignal(dataStreams);
const hasMetricsTab = useCallback(
(tabItem: Tab) => {
return isMetrics || tabItem.id !== ContentTabIds.METRICS;
},
[isMetrics]
);
return {
hasMetricsTab,
};
};
const useTabs = (tabs: Tab[]) => {
const { showTab, activeTabId } = useTabSwitcherContext();
const { isTabEnabled } = useFeatureFlagTabs();
const { hasMetricsTab } = useMetricsTabs();
const onTabClick = useCallback(
(tabId: TabIds) => {
@ -153,16 +177,18 @@ const useTabs = (tabs: Tab[]) => {
const tabEntries: TabItem[] = useMemo(
() =>
tabs.filter(isTabEnabled).map(({ name, ...tab }) => {
return {
...tab,
'data-test-subj': `infraAssetDetails${capitalize(tab.id)}Tab`,
onClick: () => onTabClick(tab.id),
isSelected: tab.id === activeTabId,
label: name,
};
}),
[activeTabId, isTabEnabled, onTabClick, tabs]
tabs
.filter((tab) => isTabEnabled(tab) && hasMetricsTab(tab))
.map(({ name, ...tab }) => {
return {
...tab,
'data-test-subj': `infraAssetDetails${capitalize(tab.id)}Tab`,
onClick: () => onTabClick(tab.id),
isSelected: tab.id === activeTabId,
label: name,
};
}),
[activeTabId, isTabEnabled, hasMetricsTab, onTabClick, tabs]
);
return { tabEntries };

View file

@ -0,0 +1,59 @@
/*
* 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, { useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {
findInventoryFields,
type InventoryItemType,
} from '@kbn/metrics-data-access-plugin/common';
import { buildCombinedAssetFilter } from '../../../../utils/filters/build';
import { useSearchSessionContext } from '../../../../hooks/use_search_session';
import { useLogsCharts } from '../../hooks/use_log_charts';
import { Kpi } from '../../components/kpis/kpi';
interface Props {
dataView?: DataView;
assetId: string;
assetType: InventoryItemType;
dateRange: TimeRange;
}
export const LogsContent = ({ assetId, assetType, dataView, dateRange }: Props) => {
const { searchSessionId } = useSearchSessionContext();
const filters = useMemo(() => {
return [
buildCombinedAssetFilter({
field: findInventoryFields(assetType).id,
values: [assetId],
dataView,
}),
];
}, [dataView, assetId, assetType]);
const { charts } = useLogsCharts({
dataViewId: dataView?.id,
});
return (
<EuiFlexGroup direction="row" gutterSize="s" data-test-subj="infraAssetDetailsLogsGrid">
{charts.map((chartProps, index) => (
<EuiFlexItem key={index}>
<Kpi
{...chartProps}
dateRange={dateRange}
filters={filters}
searchSessionId={searchSessionId}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};

View file

@ -26,9 +26,8 @@ import { MetricsContent } from './metrics/metrics';
import { AddMetricsCallout } from '../../add_metrics_callout';
import { AddMetricsCalloutKey } from '../../add_metrics_callout/constants';
import { useEntitySummary } from '../../hooks/use_entity_summary';
import { isMetricsSignal } from '../../utils/get_data_stream_types';
import { INTEGRATIONS } from '../../constants';
import { useIntegrationCheck } from '../../hooks/use_integration_check';
import { isMetricsSignal, isLogsSignal } from '../../utils/get_data_stream_types';
import { LogsContent } from './logs';
export const Overview = () => {
const { dateRange } = useDatePickerContext();
@ -38,7 +37,7 @@ export const Overview = () => {
loading: metadataLoading,
error: fetchMetadataError,
} = useMetadataStateContext();
const { metrics } = useDataViewsContext();
const { metrics, logs } = useDataViewsContext();
const isFullPageView = renderMode.mode === 'page';
const { dataStreams, status: dataStreamsStatus } = useEntitySummary({
entityType: asset.type,
@ -50,10 +49,6 @@ export const Overview = () => {
`infra.dismissedAddMetricsCallout.${addMetricsCalloutId}`,
false
);
const isDockerContainer = useIntegrationCheck({ dependsOn: INTEGRATIONS.docker });
const isKubernetesContainer = useIntegrationCheck({
dependsOn: INTEGRATIONS.kubernetesContainer,
});
const metadataSummarySection = isFullPageView ? (
<MetadataSummaryList metadata={metadata} loading={metadataLoading} assetType={asset.type} />
@ -65,6 +60,10 @@ export const Overview = () => {
/>
);
const isMetrics = isMetricsSignal(dataStreams);
const isLogs = isLogsSignal(dataStreams);
const isLogsOnly = !isMetrics && isLogs;
const shouldShowCallout = () => {
if (
dataStreamsStatus !== 'success' ||
@ -74,20 +73,14 @@ export const Overview = () => {
return false;
}
const { type } = asset;
const baseCondition = !isMetricsSignal(dataStreams);
const isRelevantContainer =
type === 'container' && (isDockerContainer || isKubernetesContainer);
return baseCondition && (type === 'host' || isRelevantContainer);
return !isMetrics;
};
const showAddMetricsCallout = shouldShowCallout();
return (
<EuiFlexGroup direction="column" gutterSize="m">
{showAddMetricsCallout ? (
{showAddMetricsCallout && (
<EuiFlexItem grow={false}>
<AddMetricsCallout
id={addMetricsCalloutId}
@ -96,7 +89,18 @@ export const Overview = () => {
}}
/>
</EuiFlexItem>
) : (
)}
{isLogsOnly ? (
<EuiFlexItem grow={false}>
<LogsContent
assetId={asset.id}
assetType={asset.type}
dateRange={dateRange}
dataView={logs.dataView}
/>
</EuiFlexItem>
) : null}
{!showAddMetricsCallout && isMetrics ? (
<EuiFlexItem grow={false}>
<KPIGrid
assetId={asset.id}
@ -106,7 +110,7 @@ export const Overview = () => {
/>
{asset.type === 'host' ? <CpuProfilingPrompt /> : null}
</EuiFlexItem>
)}
) : null}
<EuiFlexItem grow={false}>
{fetchMetadataError && !metadataLoading ? <MetadataErrorCallout /> : metadataSummarySection}
<SectionSeparator />
@ -123,14 +127,16 @@ export const Overview = () => {
<SectionSeparator />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<MetricsContent
assetId={asset.id}
assetType={asset.type}
dateRange={dateRange}
dataView={metrics.dataView}
/>
</EuiFlexItem>
{isMetrics ? (
<EuiFlexItem grow={false}>
<MetricsContent
assetId={asset.id}
assetType={asset.type}
dateRange={dateRange}
dataView={metrics.dataView}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
};

View file

@ -671,16 +671,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
describe('Metrics Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickMetricsTab();
});
it('should show add metrics callout', async () => {
await pageObjects.assetDetails.addMetricsCalloutExists();
});
});
describe('Processes Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickProcessesTab();