Skip to content

Commit c87e881

Browse files
authored
[Monitoring/Telemetry] Force collectors to indicate when they are ready (#36153)
* Initial code to force collectors to indicate when they are ready * Add and fix tests * Remove debug * Add ready check in api call * Fix prettier complaints * Return 503 if not all collectors are ready * PR feedback * Add retry logic for usage collection in the reporting tests * Fix incorrect boomify usage * Fix more issues with the tests * Just add debug I guess * More debug * Try and handle this exception * Try and make the tests more defensive and remove console logs * Retry logic here too * Debug for the reporting tests failure * I don't like this, but lets see if it works * Move the retry logic into the collector set directly * Add support for this new collector * Localize this * This shouldn't be static on the class, but rather static for the entire runtime
1 parent 54f53d1 commit c87e881

File tree

31 files changed

+279
-62
lines changed

31 files changed

+279
-62
lines changed

src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/make_kql_usage_collector.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function makeKQLUsageCollector(server) {
2525
const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({
2626
type: 'kql',
2727
fetch,
28+
isReady: () => true,
2829
});
2930

3031
server.usage.collectorSet.register(kqlUsageCollector);

src/legacy/core_plugins/ui_metric/server/usage/collector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function registerUiMetricUsageCollector(server: any) {
5252

5353
return uiMetricsByAppName;
5454
},
55+
isReady: () => true,
5556
});
5657

5758
server.usage.collectorSet.register(collector);

src/legacy/server/config/schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ export default () => Joi.object({
175175
pollInterval: Joi.number().default(1500),
176176
}).default(),
177177

178+
stats: Joi.object({
179+
maximumWaitTimeForAllCollectorsInS: Joi.number().default(60)
180+
}).default(),
181+
178182
optimize: Joi.object({
179183
enabled: Joi.boolean().default(true),
180184
bundleFilter: Joi.string().default('!tests'),

src/legacy/server/sample_data/usage/collector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function makeSampleDataUsageCollector(server: KbnServer) {
3636
server.usage.collectorSet.makeUsageCollector({
3737
type: 'sample-data',
3838
fetch: fetchProvider(index),
39+
isReady: () => true,
3940
})
4041
);
4142
}

src/legacy/server/status/collectors/get_ops_stats_collector.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function getOpsStatsCollector(server, kbnServer) {
4545
...kbnServer.metrics // latest metrics captured from the ops event listener in src/legacy/server/status/index
4646
};
4747
},
48+
isReady: () => true,
4849
ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there.
4950
});
5051
}

src/legacy/server/status/routes/api/register_stats.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818
*/
1919

2020
import Joi from 'joi';
21-
import { boomify } from 'boom';
21+
import boom from 'boom';
22+
import { i18n } from '@kbn/i18n';
2223
import { wrapAuthConfig } from '../../wrap_auth_config';
2324
import { KIBANA_STATS_TYPE } from '../../constants';
2425

26+
const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', {
27+
defaultMessage: 'Stats are not ready yet. Please try again later.',
28+
});
29+
2530
/*
2631
* API for Kibana meta info and accumulated operations stats
2732
* Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data
@@ -69,6 +74,11 @@ export function registerStatsApi(kbnServer, server, config) {
6974
if (isExtended) {
7075
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin');
7176
const callCluster = (...args) => callWithRequest(req, ...args);
77+
const collectorsReady = await collectorSet.areAllCollectorsReady();
78+
79+
if (shouldGetUsage && !collectorsReady) {
80+
return boom.serverUnavailable(STATS_NOT_READY_MESSAGE);
81+
}
7282

7383
const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve();
7484
try {
@@ -77,7 +87,6 @@ export function registerStatsApi(kbnServer, server, config) {
7787
getClusterUuid(callCluster),
7888
]);
7989

80-
8190
let modifiedUsage = usage;
8291
if (isLegacy) {
8392
// In an effort to make telemetry more easily augmented, we need to ensure
@@ -123,14 +132,17 @@ export function registerStatsApi(kbnServer, server, config) {
123132
});
124133
}
125134
} catch (e) {
126-
throw boomify(e);
135+
throw boom.boomify(e);
127136
}
128137
}
129138

130139
/* kibana_stats gets singled out from the collector set as it is used
131140
* for health-checking Kibana and fetch does not rely on fetching data
132141
* from ES */
133142
const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE);
143+
if (!await kibanaStatsCollector.isReady()) {
144+
return boom.serverUnavailable(STATS_NOT_READY_MESSAGE);
145+
}
134146
let kibanaStats = await kibanaStatsCollector.fetch();
135147
kibanaStats = collectorSet.toApiFieldNames(kibanaStats);
136148

src/legacy/server/usage/classes/collector.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class Collector {
2828
* @param {Function} options.formatForBulkUpload - optional
2929
* @param {Function} options.rest - optional other properties
3030
*/
31-
constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) {
31+
constructor(server, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) {
3232
if (type === undefined) {
3333
throw new Error('Collector must be instantiated with a options.type string property');
3434
}
@@ -49,6 +49,9 @@ export class Collector {
4949

5050
const defaultFormatterForBulkUpload = result => ({ type, payload: result });
5151
this._formatForBulkUpload = formatForBulkUpload || defaultFormatterForBulkUpload;
52+
if (typeof isReady === 'function') {
53+
this.isReady = isReady;
54+
}
5255
}
5356

5457
/*
@@ -69,4 +72,8 @@ export class Collector {
6972
formatForBulkUpload(result) {
7073
return this._formatForBulkUpload(result);
7174
}
75+
76+
isReady() {
77+
throw `isReady() must be implemented in ${this.type} collector`;
78+
}
7279
}

src/legacy/server/usage/classes/collector_set.js

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,19 @@ import { getCollectorLogger } from '../lib';
2323
import { Collector } from './collector';
2424
import { UsageCollector } from './usage_collector';
2525

26+
let _waitingForAllCollectorsTimestamp = null;
27+
2628
/*
2729
* A collector object has types registered into it with the register(type)
2830
* function. Each type that gets registered defines how to fetch its own data
2931
* and optionally, how to combine it into a unified payload for bulk upload.
3032
*/
3133
export class CollectorSet {
32-
3334
/*
3435
* @param {Object} server - server object
3536
* @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance
3637
*/
37-
constructor(server, collectors = []) {
38+
constructor(server, collectors = [], config = null) {
3839
this._log = getCollectorLogger(server);
3940
this._collectors = collectors;
4041

@@ -44,7 +45,9 @@ export class CollectorSet {
4445
*/
4546
this.makeStatsCollector = options => new Collector(server, options);
4647
this.makeUsageCollector = options => new UsageCollector(server, options);
47-
this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray);
48+
this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray, config);
49+
50+
this._maximumWaitTimeForAllCollectorsInS = config ? config.get('stats.maximumWaitTimeForAllCollectorsInS') : 60;
4851
}
4952

5053
/*
@@ -73,6 +76,40 @@ export class CollectorSet {
7376
return x instanceof UsageCollector;
7477
}
7578

79+
async areAllCollectorsReady(collectorSet = this) {
80+
if (!(collectorSet instanceof CollectorSet)) {
81+
throw new Error(`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet);
82+
}
83+
84+
const collectorTypesNotReady = [];
85+
let allReady = true;
86+
await collectorSet.asyncEach(async collector => {
87+
if (!await collector.isReady()) {
88+
allReady = false;
89+
collectorTypesNotReady.push(collector.type);
90+
}
91+
});
92+
93+
if (!allReady && this._maximumWaitTimeForAllCollectorsInS >= 0) {
94+
const nowTimestamp = +new Date();
95+
_waitingForAllCollectorsTimestamp = _waitingForAllCollectorsTimestamp || nowTimestamp;
96+
const timeWaitedInMS = nowTimestamp - _waitingForAllCollectorsTimestamp;
97+
const timeLeftInMS = (this._maximumWaitTimeForAllCollectorsInS * 1000) - timeWaitedInMS;
98+
if (timeLeftInMS <= 0) {
99+
this._log.debug(`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) `
100+
+ `but we have waited the required `
101+
+ `${this._maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`);
102+
return true;
103+
} else {
104+
this._log.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`);
105+
}
106+
} else {
107+
_waitingForAllCollectorsTimestamp = null;
108+
}
109+
110+
return allReady;
111+
}
112+
76113
/*
77114
* Call a bunch of fetch methods and then do them in bulk
78115
* @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors
@@ -155,4 +192,14 @@ export class CollectorSet {
155192
map(mapFn) {
156193
return this._collectors.map(mapFn);
157194
}
195+
196+
some(someFn) {
197+
return this._collectors.some(someFn);
198+
}
199+
200+
async asyncEach(eachFn) {
201+
for (const collector of this._collectors) {
202+
await eachFn(collector);
203+
}
204+
}
158205
}

src/legacy/server/usage/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
import { CollectorSet } from './classes';
2121

22-
export function usageMixin(kbnServer, server) {
23-
const collectorSet = new CollectorSet(server);
22+
export function usageMixin(kbnServer, server, config) {
23+
const collectorSet = new CollectorSet(server, undefined, config);
2424

2525
/*
2626
* expose the collector set object on the server

x-pack/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export function makeApmUsageCollector(core: CoreSetupWithUsageCollector) {
3737
} catch (err) {
3838
return createApmTelementry();
3939
}
40-
}
40+
},
41+
isReady: () => true
4142
});
4243
server.usage.collectorSet.register(apmUsageCollector);
4344
}

0 commit comments

Comments
 (0)