diff --git a/x-pack/platform/plugins/shared/dataset_quality/common/api_types.ts b/x-pack/platform/plugins/shared/dataset_quality/common/api_types.ts index 7c64375e43fd5..b156368f9d15b 100644 --- a/x-pack/platform/plugins/shared/dataset_quality/common/api_types.ts +++ b/x-pack/platform/plugins/shared/dataset_quality/common/api_types.ts @@ -32,6 +32,7 @@ export const dataStreamStatRt = rt.intersection([ lastActivity: rt.number, integration: rt.string, totalDocs: rt.number, + creationDate: rt.number, }), ]); diff --git a/x-pack/platform/plugins/shared/dataset_quality/public/index.ts b/x-pack/platform/plugins/shared/dataset_quality/public/index.ts index 339be1ec1de9b..943707b252ce0 100644 --- a/x-pack/platform/plugins/shared/dataset_quality/public/index.ts +++ b/x-pack/platform/plugins/shared/dataset_quality/public/index.ts @@ -9,8 +9,12 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import { DatasetQualityConfig } from '../common/plugin_config'; import { DatasetQualityPlugin } from './plugin'; +export type { DataStreamStatServiceResponse } from '../common/data_streams_stats'; export type { DatasetQualityPluginSetup, DatasetQualityPluginStart } from './types'; +export { DataStreamsStatsService } from './services/data_streams_stats/data_streams_stats_service'; +export type { IDataStreamsStatsClient } from './services/data_streams_stats/types'; + export function plugin(context: PluginInitializerContext) { return new DatasetQualityPlugin(context); } diff --git a/x-pack/platform/plugins/shared/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts b/x-pack/platform/plugins/shared/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts index deb97678b65ac..f389bbe80c8b2 100644 --- a/x-pack/platform/plugins/shared/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts +++ b/x-pack/platform/plugins/shared/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts @@ -41,12 +41,15 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { public async getDataStreamsStats( params: GetDataStreamsStatsQuery ): Promise { - const types = params.types.length === 0 ? KNOWN_TYPES : params.types; + const types = + 'types' in params + ? rison.encodeArray(params.types.length === 0 ? KNOWN_TYPES : params.types) + : undefined; const response = await this.http .get('/internal/dataset_quality/data_streams/stats', { query: { ...params, - types: rison.encodeArray(types), + types, }, }) .catch((error) => { diff --git a/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/get_data_streams_creation_date.ts b/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/get_data_streams_creation_date.ts new file mode 100644 index 0000000000000..c86b7ffad02fb --- /dev/null +++ b/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/get_data_streams_creation_date.ts @@ -0,0 +1,45 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { dataStreamService } from '../../services'; + +export async function getDataStreamsCreationDate({ + esClient, + dataStreams, +}: { + esClient: ElasticsearchClient; + dataStreams: string[]; +}) { + const matchingStreams = await dataStreamService.getMatchingDataStreams(esClient, dataStreams); + const streamByIndex = matchingStreams.reduce((acc, { name, indices }) => { + if (indices[0]) acc[indices[0].index_name] = name; + return acc; + }, {} as Record); + + const indices = Object.keys(streamByIndex); + if (indices.length === 0) { + return {}; + } + // While _cat api is not recommended for application use this is the only way + // to retrieve the creation date in serverless for now. We should change this + // once a proper approach exists (see elastic/elasticsearch-serverless#3010) + const catIndices = await esClient.cat.indices({ + index: indices, + h: ['creation.date', 'index'], + format: 'json', + }); + + return catIndices.reduce((acc, index) => { + const creationDate = index['creation.date']; + const indexName = index.index!; + const stream = streamByIndex[indexName]; + + acc[stream] = creationDate ? Number(creationDate) : undefined; + return acc; + }, {} as Record); +} diff --git a/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/routes.ts b/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/routes.ts index a12048d9235d8..a986321125980 100644 --- a/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/routes.ts +++ b/x-pack/platform/plugins/shared/dataset_quality/server/routes/data_streams/routes.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { toBooleanRt } from '@kbn/io-ts-utils'; import { CheckAndLoadIntegrationResponse, DataStreamDetails, @@ -38,15 +39,14 @@ import { getDegradedFieldValues } from './get_degraded_field_values'; import { getDegradedFields } from './get_degraded_fields'; import { getNonAggregatableDataStreams } from './get_non_aggregatable_data_streams'; import { updateFieldLimit } from './update_field_limit'; +import { getDataStreamsCreationDate } from './get_data_streams_creation_date'; const statsRoute = createDatasetQualityServerRoute({ endpoint: 'GET /internal/dataset_quality/data_streams/stats', params: t.type({ query: t.intersection([ - t.type({ types: typesRt }), - t.partial({ - datasetQuery: t.string, - }), + t.union([t.type({ types: typesRt }), t.type({ datasetQuery: t.string })]), + t.partial({ includeCreationDate: toBooleanRt }), ]), }), options: { @@ -81,15 +81,25 @@ const statsRoute = createDatasetQualityServerRoute({ return dataStream.userPrivileges.canMonitor; }); - const dataStreamsStats = isServerless - ? await getDataStreamsMeteringStats({ - esClient: esClientAsSecondaryAuthUser, - dataStreams: privilegedDataStreams.map((stream) => stream.name), - }) - : await getDataStreamsStats({ - esClient, - dataStreams: privilegedDataStreams.map((stream) => stream.name), - }); + const dataStreamsNames = privilegedDataStreams.map((stream) => stream.name); + const [dataStreamsStats, dataStreamsCreationDate] = await Promise.all([ + isServerless + ? getDataStreamsMeteringStats({ + esClient: esClientAsSecondaryAuthUser, + dataStreams: dataStreamsNames, + }) + : getDataStreamsStats({ + esClient, + dataStreams: dataStreamsNames, + }), + + params.query.includeCreationDate + ? getDataStreamsCreationDate({ + esClient: esClientAsSecondaryAuthUser, + dataStreams: dataStreamsNames, + }) + : ({} as Record), + ]); return { datasetUserPrivileges, @@ -97,6 +107,7 @@ const statsRoute = createDatasetQualityServerRoute({ dataStream.size = dataStreamsStats[dataStream.name]?.size; dataStream.sizeBytes = dataStreamsStats[dataStream.name]?.sizeBytes; dataStream.totalDocs = dataStreamsStats[dataStream.name]?.totalDocs; + dataStream.creationDate = dataStreamsCreationDate[dataStream.name]; return dataStream; }), diff --git a/x-pack/platform/plugins/shared/dataset_quality/server/services/data_stream.ts b/x-pack/platform/plugins/shared/dataset_quality/server/services/data_stream.ts index e54f0f33f375a..857768853b4cd 100644 --- a/x-pack/platform/plugins/shared/dataset_quality/server/services/data_stream.ts +++ b/x-pack/platform/plugins/shared/dataset_quality/server/services/data_stream.ts @@ -15,7 +15,7 @@ import { reduceAsyncChunks } from '../utils/reduce_async_chunks'; class DataStreamService { public async getMatchingDataStreams( esClient: ElasticsearchClient, - datasetName: string + datasetName: string | string[] ): Promise { try { const { data_streams: dataStreamsInfo } = await esClient.indices.getDataStream({ diff --git a/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx b/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx index d86c0723e038a..5e57ea3250907 100644 --- a/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx @@ -15,6 +15,7 @@ import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; import { NavigationPublicStart } from '@kbn/navigation-plugin/public/types'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks'; +import { DataStreamsStatsClient } from '@kbn/dataset-quality-plugin/public/services/data_streams_stats/data_streams_stats_client'; import type { StreamsAppKibanaContext } from '../public/hooks/use_kibana'; export function getMockStreamsAppContext(): StreamsAppKibanaContext { @@ -38,7 +39,7 @@ export function getMockStreamsAppContext(): StreamsAppKibanaContext { }, }, services: { - query: jest.fn(), + dataStreamsClient: Promise.resolve({} as unknown as DataStreamsStatsClient), }, isServerless: false, }; diff --git a/x-pack/solutions/observability/plugins/streams_app/kibana.jsonc b/x-pack/solutions/observability/plugins/streams_app/kibana.jsonc index 8967a468aa81a..716376423337c 100644 --- a/x-pack/solutions/observability/plugins/streams_app/kibana.jsonc +++ b/x-pack/solutions/observability/plugins/streams_app/kibana.jsonc @@ -19,6 +19,7 @@ "savedObjectsTagging", "navigation", "fieldsMetadata", + "datasetQuality" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/helpers/format_bytes.ts b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/helpers/format_bytes.ts new file mode 100644 index 0000000000000..4cc666058b677 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/helpers/format_bytes.ts @@ -0,0 +1,10 @@ +/* + * 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 { formatNumber } from '@elastic/eui'; + +export const formatBytes = (value: number) => formatNumber(value, '0.0 b'); diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/helpers/ingestion_rate_query.ts b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/helpers/ingestion_rate_query.ts new file mode 100644 index 0000000000000..3e752562be7f7 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/helpers/ingestion_rate_query.ts @@ -0,0 +1,55 @@ +/* + * 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 datemath from '@kbn/datemath'; + +export const ingestionRateQuery = ({ + index, + start, + end, + timestampField = '@timestamp', + bucketCount = 10, +}: { + index: string; + start: string; + end: string; + timestampField?: string; + bucketCount?: number; +}) => { + const startDate = datemath.parse(start); + const endDate = datemath.parse(end); + if (!startDate || !endDate) { + throw new Error(`Expected a valid start and end date but got [start: ${start} | end: ${end}]`); + } + + const intervalInSeconds = Math.max( + Math.round(endDate.diff(startDate, 'seconds') / bucketCount), + 1 + ); + + return { + index, + track_total_hits: false, + body: { + size: 0, + query: { + bool: { + filter: [{ range: { [timestampField]: { gte: start, lte: end } } }], + }, + }, + aggs: { + docs_count: { + date_histogram: { + field: timestampField, + fixed_interval: `${intervalInSeconds}s`, + min_doc_count: 0, + }, + }, + }, + }, + }; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/hooks/use_data_stream_stats.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/hooks/use_data_stream_stats.tsx new file mode 100644 index 0000000000000..9125aba411d93 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/hooks/use_data_stream_stats.tsx @@ -0,0 +1,58 @@ +/* + * 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 { IngestStreamGetResponse } from '@kbn/streams-schema'; +import { DataStreamStatServiceResponse } from '@kbn/dataset-quality-plugin/public'; +import { useKibana } from '../../../hooks/use_kibana'; +import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch'; + +export type DataStreamStats = DataStreamStatServiceResponse['dataStreamsStats'][number] & { + bytesPerDoc: number; + bytesPerDay: number; +}; + +export const useDataStreamStats = ({ definition }: { definition?: IngestStreamGetResponse }) => { + const { + services: { dataStreamsClient }, + } = useKibana(); + + const statsFetch = useStreamsAppFetch(async () => { + if (!definition) { + return; + } + + const client = await dataStreamsClient; + const { + dataStreamsStats: [dsStats], + } = await client.getDataStreamsStats({ + datasetQuery: definition.stream.name, + includeCreationDate: true, + }); + + if (!dsStats || !dsStats.creationDate || !dsStats.sizeBytes) { + return undefined; + } + const daysSinceCreation = Math.max( + 1, + Math.round(moment().diff(moment(dsStats.creationDate), 'days')) + ); + + return { + ...dsStats, + bytesPerDay: dsStats.sizeBytes / daysSinceCreation, + bytesPerDoc: dsStats.totalDocs ? dsStats.sizeBytes / dsStats.totalDocs : 0, + }; + }, [dataStreamsClient, definition]); + + return { + stats: statsFetch.value, + isLoading: statsFetch.loading, + refresh: statsFetch.refresh, + error: statsFetch.error, + }; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/index.tsx index fd6801e456ab6..372f130c24253 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/index.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import React, { useMemo, useState } from 'react'; import { IngestStreamGetResponse, @@ -27,6 +27,8 @@ import { useKibana } from '../../hooks/use_kibana'; import { EditLifecycleModal, LifecycleEditAction } from './modal'; import { RetentionSummary } from './summary'; import { RetentionMetadata } from './metadata'; +import { IngestionRate } from './ingestion_rate'; +import { useDataStreamStats } from './hooks/use_data_stream_stats'; import { getFormattedError } from '../../util/errors'; function useLifecycleState({ @@ -112,6 +114,13 @@ export function StreamDetailLifecycle({ setUpdateInProgress, } = useLifecycleState({ definition, isServerless }); + const { + stats, + isLoading: isLoadingStats, + refresh: refreshStats, + error: statsError, + } = useDataStreamStats({ definition }); + const { signal } = useAbortController(); if (!definition) { @@ -176,24 +185,38 @@ export function StreamDetailLifecycle({ ilmLocator={ilmLocator} /> - + + + + + + + + setOpenEditModal(action)} + isLoadingStats={isLoadingStats} + stats={stats} + statsError={statsError} + /> + + + + + + + - - - - - - - setOpenEditModal(action)} - /> - - + - + ); } diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/ingestion_rate.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/ingestion_rate.tsx new file mode 100644 index 0000000000000..3ffda07c2be46 --- /dev/null +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/ingestion_rate.tsx @@ -0,0 +1,169 @@ +/* + * 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 React from 'react'; +import { lastValueFrom } from 'rxjs'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/search-types'; +import { i18n } from '@kbn/i18n'; +import { IngestStreamGetResponse } from '@kbn/streams-schema'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingChart, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { AreaSeries, Axis, Chart, Settings } from '@elastic/charts'; +import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range'; +import { useKibana } from '../../hooks/use_kibana'; +import { DataStreamStats } from './hooks/use_data_stream_stats'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; +import { ingestionRateQuery } from './helpers/ingestion_rate_query'; +import { formatBytes } from './helpers/format_bytes'; +import { StreamsAppSearchBar } from '../streams_app_search_bar'; + +export function IngestionRate({ + definition, + stats, + isLoadingStats, + refreshStats, +}: { + definition?: IngestStreamGetResponse; + stats?: DataStreamStats; + isLoadingStats: boolean; + refreshStats: () => void; +}) { + const { + dependencies: { + start: { data }, + }, + } = useKibana(); + const { timeRange, setTimeRange } = useDateRange({ data }); + + const { + loading: isLoadingIngestionRate, + value: ingestionRate, + error: ingestionRateError, + } = useStreamsAppFetch( + async ({ signal }) => { + if (!definition || isLoadingStats || !stats?.bytesPerDay) { + return; + } + + const { rawResponse } = await lastValueFrom( + data.search.search< + IKibanaSearchRequest, + IKibanaSearchResponse<{ + aggregations: { docs_count: { buckets: Array<{ key: string; doc_count: number }> } }; + }> + >( + { + params: ingestionRateQuery({ + start: timeRange.from, + end: timeRange.to, + index: definition.stream.name, + }), + }, + { abortSignal: signal } + ) + ); + + return rawResponse.aggregations.docs_count.buckets.map(({ key, doc_count: docCount }) => ({ + key, + value: docCount * stats.bytesPerDoc, + })); + }, + [data.search, definition, stats, isLoadingStats, timeRange] + ); + + return ( + <> + + + + +
+ {i18n.translate('xpack.streams.streamDetailLifecycle.ingestionRatePanel', { + defaultMessage: 'Ingestion rate', + })} +
+
+
+ + + { + if (!isUpdate) { + refreshStats(); + return; + } + + if (dateRange) { + setTimeRange({ + from: dateRange.from, + to: dateRange?.to, + mode: dateRange.mode, + }); + } + }} + /> + +
+
+ + + + {ingestionRateError ? ( + + Failed to load ingestion rate + + ) : isLoadingIngestionRate || isLoadingStats || !ingestionRate ? ( + + + + ) : ( + + + + + moment(value).format('YYYY-MM-DD')} + gridLine={{ visible: false }} + /> + formatBytes(value)} + gridLine={{ visible: true }} + /> + + )} + + ); +} diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/metadata.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/metadata.tsx index 3de494f070d5e..67e6b2f4acc7c 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/metadata.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/metadata.tsx @@ -26,6 +26,7 @@ import { EuiFlexItem, EuiHorizontalRule, EuiLink, + EuiLoadingSpinner, EuiPanel, EuiPopover, EuiText, @@ -33,21 +34,28 @@ import { import { i18n } from '@kbn/i18n'; import { LifecycleEditAction } from './modal'; import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; +import { DataStreamStats } from './hooks/use_data_stream_stats'; +import { formatBytes } from './helpers/format_bytes'; export function RetentionMetadata({ definition, ilmLocator, lifecycleActions, openEditModal, + stats, + isLoadingStats, + statsError, }: { definition: IngestStreamGetResponse; ilmLocator?: LocatorPublic; lifecycleActions: Array<{ name: string; action: LifecycleEditAction }>; openEditModal: (action: LifecycleEditAction) => void; + stats?: DataStreamStats; + isLoadingStats: boolean; + statsError?: Error; }) { const [isMenuOpen, { toggle: toggleMenu, off: closeMenu }] = useBoolean(false); const router = useStreamsAppRouter(); - const lifecycle = definition.effective_lifecycle; const contextualMenu = @@ -171,6 +179,38 @@ export function RetentionMetadata({ } /> + + + ) : stats.bytesPerDay ? ( + formatIngestionRate(stats.bytesPerDay) + ) : ( + '-' + ) + } + /> + + + ) : ( + stats.totalDocs + ) + } + /> ); } @@ -197,3 +237,9 @@ function MetadataRow({ ); } + +const formatIngestionRate = (bytesPerDay: number) => { + const perDay = formatBytes(bytesPerDay); + const perMonth = formatBytes(bytesPerDay * 30); + return `${perDay} / Day - ${perMonth} / Month`; +}; diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/summary.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/summary.tsx index 7b3c37649dacb..c867e317d239c 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/summary.tsx +++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_lifecycle/summary.tsx @@ -21,7 +21,7 @@ export function RetentionSummary({ definition }: { definition: IngestStreamGetRe const summary = useMemo(() => summaryText(definition), [definition]); return ( - +
{i18n.translate('xpack.streams.streamDetailLifecycle.retentionSummaryLabel', { diff --git a/x-pack/solutions/observability/plugins/streams_app/public/plugin.ts b/x-pack/solutions/observability/plugins/streams_app/public/plugin.ts index c6aec1005cc09..dbe50f39b6308 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/plugin.ts +++ b/x-pack/solutions/observability/plugins/streams_app/public/plugin.ts @@ -18,6 +18,7 @@ import { } from '@kbn/core/public'; import type { Logger } from '@kbn/logging'; import { STREAMS_APP_ID } from '@kbn/deeplinks-observability/constants'; +import { DataStreamsStatsService } from '@kbn/dataset-quality-plugin/public'; import type { ConfigSchema, StreamsAppPublicSetup, @@ -119,7 +120,11 @@ export class StreamsAppPlugin coreSetup.getStartServices(), ]); - const services: StreamsAppServices = {}; + const services: StreamsAppServices = { + dataStreamsClient: new DataStreamsStatsService() + .start({ http: coreStart.http }) + .getClient(), + }; return renderApp({ coreStart, diff --git a/x-pack/solutions/observability/plugins/streams_app/public/services/types.ts b/x-pack/solutions/observability/plugins/streams_app/public/services/types.ts index 7f75493d2525c..69f51c04df1e6 100644 --- a/x-pack/solutions/observability/plugins/streams_app/public/services/types.ts +++ b/x-pack/solutions/observability/plugins/streams_app/public/services/types.ts @@ -5,5 +5,8 @@ * 2.0. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StreamsAppServices {} +import { IDataStreamsStatsClient } from '@kbn/dataset-quality-plugin/public'; + +export interface StreamsAppServices { + dataStreamsClient: Promise; +} diff --git a/x-pack/solutions/observability/plugins/streams_app/tsconfig.json b/x-pack/solutions/observability/plugins/streams_app/tsconfig.json index f43409d674c59..3f33c3ddc109c 100644 --- a/x-pack/solutions/observability/plugins/streams_app/tsconfig.json +++ b/x-pack/solutions/observability/plugins/streams_app/tsconfig.json @@ -57,5 +57,8 @@ "@kbn/actions-plugin", "@kbn/object-utils", "@kbn/traced-es-client", + "@kbn/datemath", + "@kbn/dataset-quality-plugin", + "@kbn/search-types" ] } diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts index c7d44c26b230b..c4f183e0dc8e5 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts @@ -21,13 +21,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { async function callApiAs( user: DatasetQualityApiClientKey, - types: Array<'logs' | 'metrics' | 'traces' | 'synthetics'> = ['logs'] + types: Array<'logs' | 'metrics' | 'traces' | 'synthetics'> = ['logs'], + includeCreationDate = false ) { return await datasetQualityApiClient[user]({ endpoint: 'GET /internal/dataset_quality/data_streams/stats', params: { query: { types: rison.encodeArray(types), + includeCreationDate, }, }, }); @@ -152,6 +154,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(stats.body.dataStreamsStats[0].totalDocs).greaterThan(0); }); + it('does not return creation date by default', async () => { + const stats = await callApiAs('datasetQualityMonitorUser'); + expect(stats.body.dataStreamsStats[0].size).not.empty(); + expect(stats.body.dataStreamsStats[0].creationDate).to.be(undefined); + }); + + it('returns creation date when specified', async () => { + const stats = await callApiAs('datasetQualityMonitorUser', ['logs'], true); + expect(stats.body.dataStreamsStats[0].size).not.empty(); + expect(stats.body.dataStreamsStats[0].creationDate).greaterThan(0); + }); + after(async () => { await logsSynthtrace.clean(); await cleanLogIndexTemplate({ esClient: es });