|
4 | 4 | * you may not use this file except in compliance with the Elastic License. |
5 | 5 | */ |
6 | 6 | import { RequestHandler, SavedObjectsClientContract } from 'src/core/server'; |
| 7 | +import { keyBy, keys, merge } from 'lodash'; |
7 | 8 | import { DataStream } from '../../types'; |
8 | 9 | import { GetDataStreamsResponse, KibanaAssetType, KibanaSavedObjectType } from '../../../common'; |
9 | 10 | import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; |
10 | 11 | import { defaultIngestErrorHandler } from '../../errors'; |
11 | 12 |
|
12 | 13 | const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*'; |
13 | 14 |
|
| 15 | +interface ESDataStreamInfoResponse { |
| 16 | + data_streams: Array<{ |
| 17 | + name: string; |
| 18 | + timestamp_field: { |
| 19 | + name: string; |
| 20 | + }; |
| 21 | + indices: Array<{ index_name: string; index_uuid: string }>; |
| 22 | + generation: number; |
| 23 | + _meta?: { |
| 24 | + package?: { |
| 25 | + name: string; |
| 26 | + }; |
| 27 | + managed_by?: string; |
| 28 | + managed?: boolean; |
| 29 | + [key: string]: any; |
| 30 | + }; |
| 31 | + status: string; |
| 32 | + template: string; |
| 33 | + ilm_policy: string; |
| 34 | + hidden: boolean; |
| 35 | + }>; |
| 36 | +} |
| 37 | + |
| 38 | +interface ESDataStreamStatsResponse { |
| 39 | + data_streams: Array<{ |
| 40 | + data_stream: string; |
| 41 | + backing_indices: number; |
| 42 | + store_size_bytes: number; |
| 43 | + maximum_timestamp: number; |
| 44 | + }>; |
| 45 | +} |
| 46 | + |
14 | 47 | export const getListHandler: RequestHandler = async (context, request, response) => { |
15 | 48 | const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; |
| 49 | + const body: GetDataStreamsResponse = { |
| 50 | + data_streams: [], |
| 51 | + }; |
16 | 52 |
|
17 | 53 | try { |
18 | | - // Get stats (size on disk) of all potentially matching indices |
19 | | - const { indices: indexStats } = await callCluster('indices.stats', { |
20 | | - index: DATA_STREAM_INDEX_PATTERN, |
21 | | - metric: ['store'], |
22 | | - }); |
| 54 | + // Get matching data streams, their stats, and package SOs |
| 55 | + const [ |
| 56 | + { data_streams: dataStreamsInfo }, |
| 57 | + { data_streams: dataStreamStats }, |
| 58 | + packageSavedObjects, |
| 59 | + ] = await Promise.all([ |
| 60 | + callCluster('transport.request', { |
| 61 | + method: 'GET', |
| 62 | + path: `/_data_stream/${DATA_STREAM_INDEX_PATTERN}`, |
| 63 | + }) as Promise<ESDataStreamInfoResponse>, |
| 64 | + callCluster('transport.request', { |
| 65 | + method: 'GET', |
| 66 | + path: `/_data_stream/${DATA_STREAM_INDEX_PATTERN}/_stats`, |
| 67 | + }) as Promise<ESDataStreamStatsResponse>, |
| 68 | + getPackageSavedObjects(context.core.savedObjects.client), |
| 69 | + ]); |
| 70 | + const dataStreamsInfoByName = keyBy(dataStreamsInfo, 'name'); |
| 71 | + const dataStreamsStatsByName = keyBy(dataStreamStats, 'data_stream'); |
| 72 | + |
| 73 | + // Combine data stream info |
| 74 | + const dataStreams = merge(dataStreamsInfoByName, dataStreamsStatsByName); |
| 75 | + const dataStreamNames = keys(dataStreams); |
| 76 | + |
| 77 | + // Map package SOs |
| 78 | + const packageSavedObjectsByName = keyBy(packageSavedObjects.saved_objects, 'id'); |
| 79 | + const packageMetadata: any = {}; |
| 80 | + |
| 81 | + // Query additional information for each data stream |
| 82 | + const dataStreamPromises = dataStreamNames.map(async (dataStreamName) => { |
| 83 | + const dataStream = dataStreams[dataStreamName]; |
| 84 | + const dataStreamResponse: DataStream = { |
| 85 | + index: dataStreamName, |
| 86 | + dataset: '', |
| 87 | + namespace: '', |
| 88 | + type: '', |
| 89 | + package: dataStream._meta?.package?.name || '', |
| 90 | + package_version: '', |
| 91 | + last_activity_ms: dataStream.maximum_timestamp, |
| 92 | + size_in_bytes: dataStream.store_size_bytes, |
| 93 | + dashboards: [], |
| 94 | + }; |
23 | 95 |
|
24 | | - // Get all matching indices and info about each |
25 | | - // This returns the top 100,000 indices (as buckets) by last activity |
26 | | - const { aggregations } = await callCluster('search', { |
27 | | - index: DATA_STREAM_INDEX_PATTERN, |
28 | | - body: { |
29 | | - size: 0, |
30 | | - query: { |
31 | | - bool: { |
32 | | - must: [ |
33 | | - { |
34 | | - exists: { |
35 | | - field: 'data_stream.namespace', |
| 96 | + // Query backing indices to extract data stream dataset, namespace, and type values |
| 97 | + const { |
| 98 | + aggregations: { dataset, namespace, type }, |
| 99 | + } = await callCluster('search', { |
| 100 | + index: dataStream.indices.map((index) => index.index_name), |
| 101 | + body: { |
| 102 | + size: 0, |
| 103 | + query: { |
| 104 | + bool: { |
| 105 | + must: [ |
| 106 | + { |
| 107 | + exists: { |
| 108 | + field: 'data_stream.namespace', |
| 109 | + }, |
36 | 110 | }, |
37 | | - }, |
38 | | - { |
39 | | - exists: { |
40 | | - field: 'data_stream.dataset', |
| 111 | + { |
| 112 | + exists: { |
| 113 | + field: 'data_stream.dataset', |
| 114 | + }, |
41 | 115 | }, |
42 | | - }, |
43 | | - ], |
| 116 | + ], |
| 117 | + }, |
44 | 118 | }, |
45 | | - }, |
46 | | - aggs: { |
47 | | - index: { |
48 | | - terms: { |
49 | | - field: '_index', |
50 | | - size: 100000, |
51 | | - order: { |
52 | | - last_activity: 'desc', |
| 119 | + aggs: { |
| 120 | + dataset: { |
| 121 | + terms: { |
| 122 | + field: 'data_stream.dataset', |
| 123 | + size: 1, |
53 | 124 | }, |
54 | 125 | }, |
55 | | - aggs: { |
56 | | - dataset: { |
57 | | - terms: { |
58 | | - field: 'data_stream.dataset', |
59 | | - size: 1, |
60 | | - }, |
61 | | - }, |
62 | | - namespace: { |
63 | | - terms: { |
64 | | - field: 'data_stream.namespace', |
65 | | - size: 1, |
66 | | - }, |
| 126 | + namespace: { |
| 127 | + terms: { |
| 128 | + field: 'data_stream.namespace', |
| 129 | + size: 1, |
67 | 130 | }, |
68 | | - type: { |
69 | | - terms: { |
70 | | - field: 'data_stream.type', |
71 | | - size: 1, |
72 | | - }, |
73 | | - }, |
74 | | - last_activity: { |
75 | | - max: { |
76 | | - field: '@timestamp', |
77 | | - }, |
| 131 | + }, |
| 132 | + type: { |
| 133 | + terms: { |
| 134 | + field: 'data_stream.type', |
| 135 | + size: 1, |
78 | 136 | }, |
79 | 137 | }, |
80 | 138 | }, |
81 | 139 | }, |
82 | | - }, |
83 | | - }); |
84 | | - |
85 | | - const body: GetDataStreamsResponse = { |
86 | | - data_streams: [], |
87 | | - }; |
88 | | - |
89 | | - if (!(aggregations && aggregations.index && aggregations.index.buckets)) { |
90 | | - return response.ok({ |
91 | | - body, |
92 | 140 | }); |
93 | | - } |
94 | 141 |
|
95 | | - const { |
96 | | - index: { buckets: indexResults }, |
97 | | - } = aggregations; |
98 | | - |
99 | | - const packageSavedObjects = await getPackageSavedObjects(context.core.savedObjects.client); |
100 | | - const packageMetadata: any = {}; |
101 | | - |
102 | | - const dataStreamsPromises = (indexResults as any[]).map(async (result) => { |
103 | | - const { |
104 | | - key: indexName, |
105 | | - dataset: { buckets: datasetBuckets }, |
106 | | - namespace: { buckets: namespaceBuckets }, |
107 | | - type: { buckets: typeBuckets }, |
108 | | - last_activity: { value_as_string: lastActivity }, |
109 | | - } = result; |
110 | | - |
111 | | - // We don't have a reliable way to associate index with package ID, so |
112 | | - // this is a hack to extract the package ID from the first part of the dataset name |
113 | | - // with fallback to extraction from index name |
114 | | - const pkg = datasetBuckets.length |
115 | | - ? datasetBuckets[0].key.split('.')[0] |
116 | | - : indexName.split('-')[1].split('.')[0]; |
117 | | - const pkgSavedObject = packageSavedObjects.saved_objects.filter((p) => p.id === pkg); |
118 | | - |
119 | | - // if |
120 | | - // - the datastream is associated with a package |
121 | | - // - and the package has been installed through EPM |
122 | | - // - and we didn't pick the metadata in an earlier iteration of this map() |
123 | | - if (pkg !== '' && pkgSavedObject.length > 0 && !packageMetadata[pkg]) { |
124 | | - // then pick the dashboards from the package saved object |
125 | | - const dashboards = |
126 | | - pkgSavedObject[0].attributes?.installed_kibana?.filter( |
127 | | - (o) => o.type === KibanaSavedObjectType.dashboard |
128 | | - ) || []; |
129 | | - // and then pick the human-readable titles from the dashboard saved objects |
130 | | - const enhancedDashboards = await getEnhancedDashboards( |
131 | | - context.core.savedObjects.client, |
132 | | - dashboards |
133 | | - ); |
134 | | - |
135 | | - packageMetadata[pkg] = { |
136 | | - version: pkgSavedObject[0].attributes?.version || '', |
137 | | - dashboards: enhancedDashboards, |
138 | | - }; |
| 142 | + // Set values from backing indices query |
| 143 | + dataStreamResponse.dataset = dataset.buckets[0]?.key || ''; |
| 144 | + dataStreamResponse.namespace = namespace.buckets[0]?.key || ''; |
| 145 | + dataStreamResponse.type = type.buckets[0]?.key || ''; |
| 146 | + |
| 147 | + // Find package saved object |
| 148 | + const pkgName = dataStreamResponse.package; |
| 149 | + const pkgSavedObject = pkgName ? packageSavedObjectsByName[pkgName] : null; |
| 150 | + |
| 151 | + if (pkgSavedObject) { |
| 152 | + // if |
| 153 | + // - the data stream is associated with a package |
| 154 | + // - and the package has been installed through EPM |
| 155 | + // - and we didn't pick the metadata in an earlier iteration of this map() |
| 156 | + if (!packageMetadata[pkgName]) { |
| 157 | + // then pick the dashboards from the package saved object |
| 158 | + const dashboards = |
| 159 | + pkgSavedObject.attributes?.installed_kibana?.filter( |
| 160 | + (o) => o.type === KibanaSavedObjectType.dashboard |
| 161 | + ) || []; |
| 162 | + // and then pick the human-readable titles from the dashboard saved objects |
| 163 | + const enhancedDashboards = await getEnhancedDashboards( |
| 164 | + context.core.savedObjects.client, |
| 165 | + dashboards |
| 166 | + ); |
| 167 | + |
| 168 | + packageMetadata[pkgName] = { |
| 169 | + version: pkgSavedObject.attributes?.version || '', |
| 170 | + dashboards: enhancedDashboards, |
| 171 | + }; |
| 172 | + } |
| 173 | + |
| 174 | + // Set values from package information |
| 175 | + dataStreamResponse.package = pkgName; |
| 176 | + dataStreamResponse.package_version = packageMetadata[pkgName].version; |
| 177 | + dataStreamResponse.dashboards = packageMetadata[pkgName].dashboards; |
139 | 178 | } |
140 | 179 |
|
141 | | - return { |
142 | | - index: indexName, |
143 | | - dataset: datasetBuckets.length ? datasetBuckets[0].key : '', |
144 | | - namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', |
145 | | - type: typeBuckets.length ? typeBuckets[0].key : '', |
146 | | - package: pkgSavedObject.length ? pkg : '', |
147 | | - package_version: packageMetadata[pkg] ? packageMetadata[pkg].version : '', |
148 | | - last_activity: lastActivity, |
149 | | - size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, |
150 | | - dashboards: packageMetadata[pkg] ? packageMetadata[pkg].dashboards : [], |
151 | | - }; |
| 180 | + return dataStreamResponse; |
152 | 181 | }); |
153 | 182 |
|
154 | | - const dataStreams: DataStream[] = await Promise.all(dataStreamsPromises); |
155 | | - |
156 | | - body.data_streams = dataStreams; |
157 | | - |
| 183 | + // Return final data streams objects sorted by last activity, decending |
| 184 | + // After filtering out data streams that are missing dataset/namespace/type fields |
| 185 | + body.data_streams = (await Promise.all(dataStreamPromises)) |
| 186 | + .filter(({ dataset, namespace, type }) => dataset && namespace && type) |
| 187 | + .sort((a, b) => b.last_activity_ms - a.last_activity_ms); |
158 | 188 | return response.ok({ |
159 | 189 | body, |
160 | 190 | }); |
|
0 commit comments