mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Telemetry] Add telemetry around the time it is taking for grabbing the telemetry stats (#132233)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f7c6b77317
commit
7e7f862a79
11 changed files with 937 additions and 165 deletions
|
@ -9610,6 +9610,142 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"usage_collector_stats": {
|
||||
"properties": {
|
||||
"total_duration": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The total execution duration to grab usage stats for all collectors in milliseconds"
|
||||
}
|
||||
},
|
||||
"total_is_ready_duration": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The total execution duration of the isReady function for all collectors in milliseconds"
|
||||
}
|
||||
},
|
||||
"total_fetch_duration": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The total execution duration of the fetch function for all ready collectors in milliseconds"
|
||||
}
|
||||
},
|
||||
"is_ready_duration_breakdown": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The name of the collector"
|
||||
}
|
||||
},
|
||||
"duration": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The execution duration of the isReady function for the collector in milliseconds"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch_duration_breakdown": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The name of the collector"
|
||||
}
|
||||
},
|
||||
"duration": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "The execution duration of the fetch function for the collector in milliseconds"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"not_ready": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short",
|
||||
"_meta": {
|
||||
"description": "The number of collectors that returned false from the isReady function"
|
||||
}
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The name of the of collectors that returned false from the isReady function"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"not_ready_timeout": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short",
|
||||
"_meta": {
|
||||
"description": "The number of collectors that timedout during the isReady function"
|
||||
}
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The name of collectors that timedout during the isReady function"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"succeeded": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short",
|
||||
"_meta": {
|
||||
"description": "The number of collectors that returned true from the fetch function"
|
||||
}
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The name of the of collectors that returned true from the fetch function"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"failed": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short",
|
||||
"_meta": {
|
||||
"description": "The number of collectors that threw an error from the fetch function"
|
||||
}
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The name of the of collectors that threw an error from the fetch function"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vis_type_table": {
|
||||
"properties": {
|
||||
"total": {
|
||||
|
|
|
@ -194,62 +194,6 @@
|
|||
"properties": {
|
||||
"kibana_config_usage": {
|
||||
"type": "pass_through"
|
||||
},
|
||||
"usage_collector_stats": {
|
||||
"properties": {
|
||||
"not_ready": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short"
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"not_ready_timeout": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short"
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"succeeded": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short"
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"failed": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "short"
|
||||
},
|
||||
"names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
107
src/plugins/usage_collection/server/collector/__snapshots__/collector_set.test.ts.snap
generated
Normal file
107
src/plugins/usage_collection/server/collector/__snapshots__/collector_set.test.ts.snap
generated
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollectorSet bulkFetch skips collectors that are not ready 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"result": Object {},
|
||||
"type": "ready_col",
|
||||
},
|
||||
Object {
|
||||
"result": Object {
|
||||
"failed": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"fetch_duration_breakdown": Array [
|
||||
Object {
|
||||
"duration": 0,
|
||||
"name": "ready_col",
|
||||
},
|
||||
],
|
||||
"is_ready_duration_breakdown": Array [
|
||||
Object {
|
||||
"duration": 0,
|
||||
"name": "ready_col",
|
||||
},
|
||||
Object {
|
||||
"duration": 0,
|
||||
"name": "not_ready_col",
|
||||
},
|
||||
],
|
||||
"not_ready": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"not_ready_col",
|
||||
],
|
||||
},
|
||||
"not_ready_timeout": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"succeeded": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"ready_col",
|
||||
],
|
||||
},
|
||||
"total_duration": 0,
|
||||
"total_fetch_duration": 0,
|
||||
"total_is_ready_duration": 0,
|
||||
},
|
||||
"type": "usage_collector_stats",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`CollectorSet bulkFetch skips collectors that have timed out 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"result": Object {},
|
||||
"type": "ready_col",
|
||||
},
|
||||
Object {
|
||||
"result": Object {
|
||||
"failed": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"fetch_duration_breakdown": Array [
|
||||
Object {
|
||||
"duration": 0,
|
||||
"name": "ready_col",
|
||||
},
|
||||
],
|
||||
"is_ready_duration_breakdown": Array [
|
||||
Object {
|
||||
"duration": Any<Number>,
|
||||
"name": "ready_col",
|
||||
},
|
||||
Object {
|
||||
"duration": Any<Number>,
|
||||
"name": "timeout_col",
|
||||
},
|
||||
],
|
||||
"not_ready": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"not_ready_timeout": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"timeout_col",
|
||||
],
|
||||
},
|
||||
"succeeded": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"ready_col",
|
||||
],
|
||||
},
|
||||
"total_duration": Any<Number>,
|
||||
"total_fetch_duration": 0,
|
||||
"total_is_ready_duration": Any<Number>,
|
||||
},
|
||||
"type": "usage_collector_stats",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -102,6 +102,11 @@ describe('CollectorSet', () => {
|
|||
not_ready_timeout: { count: 0, names: [] },
|
||||
succeeded: { count: 1, names: ['MY_TEST_COLLECTOR'] },
|
||||
failed: { count: 0, names: [] },
|
||||
fetch_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
is_ready_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
total_duration: 0,
|
||||
total_fetch_duration: 0,
|
||||
total_is_ready_duration: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -132,6 +137,11 @@ describe('CollectorSet', () => {
|
|||
not_ready_timeout: { count: 0, names: [] },
|
||||
succeeded: { count: 0, names: [] },
|
||||
failed: { count: 1, names: ['MY_TEST_COLLECTOR'] },
|
||||
fetch_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
is_ready_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
total_duration: 0,
|
||||
total_fetch_duration: 0,
|
||||
total_is_ready_duration: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -161,6 +171,11 @@ describe('CollectorSet', () => {
|
|||
not_ready_timeout: { count: 0, names: [] },
|
||||
succeeded: { count: 1, names: ['MY_TEST_COLLECTOR'] },
|
||||
failed: { count: 0, names: [] },
|
||||
fetch_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
is_ready_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
total_duration: 0,
|
||||
total_fetch_duration: 0,
|
||||
total_is_ready_duration: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -189,6 +204,11 @@ describe('CollectorSet', () => {
|
|||
not_ready_timeout: { count: 0, names: [] },
|
||||
succeeded: { count: 1, names: ['MY_TEST_COLLECTOR'] },
|
||||
failed: { count: 0, names: [] },
|
||||
fetch_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
is_ready_duration_breakdown: [{ name: 'MY_TEST_COLLECTOR', duration: 0 }],
|
||||
total_duration: 0,
|
||||
total_fetch_duration: 0,
|
||||
total_is_ready_duration: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -354,39 +374,52 @@ describe('CollectorSet', () => {
|
|||
expect(mockIsNotReady).toBeCalledTimes(1);
|
||||
expect(mockNonReadyFetch).toBeCalledTimes(0);
|
||||
|
||||
expect(results).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"result": Object {},
|
||||
"type": "ready_col",
|
||||
},
|
||||
Object {
|
||||
"result": Object {
|
||||
"failed": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"not_ready": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"not_ready_col",
|
||||
],
|
||||
},
|
||||
"not_ready_timeout": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"succeeded": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"ready_col",
|
||||
],
|
||||
},
|
||||
expect(results).toMatchSnapshot([
|
||||
{
|
||||
result: {},
|
||||
type: 'ready_col',
|
||||
},
|
||||
{
|
||||
result: {
|
||||
failed: {
|
||||
count: 0,
|
||||
names: [],
|
||||
},
|
||||
"type": "usage_collector_stats",
|
||||
fetch_duration_breakdown: [
|
||||
{
|
||||
name: 'ready_col',
|
||||
duration: 0,
|
||||
},
|
||||
],
|
||||
is_ready_duration_breakdown: [
|
||||
{
|
||||
name: 'ready_col',
|
||||
duration: 0,
|
||||
},
|
||||
{
|
||||
name: 'not_ready_col',
|
||||
duration: 0,
|
||||
},
|
||||
],
|
||||
not_ready: {
|
||||
count: 1,
|
||||
names: ['not_ready_col'],
|
||||
},
|
||||
not_ready_timeout: {
|
||||
count: 0,
|
||||
names: [],
|
||||
},
|
||||
succeeded: {
|
||||
count: 1,
|
||||
names: ['ready_col'],
|
||||
},
|
||||
total_duration: 0,
|
||||
total_fetch_duration: 0,
|
||||
total_is_ready_duration: 0,
|
||||
},
|
||||
]
|
||||
`);
|
||||
type: 'usage_collector_stats',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips collectors that have timed out', async () => {
|
||||
|
@ -428,39 +461,52 @@ describe('CollectorSet', () => {
|
|||
expect(mockTimedOutReady).toBeCalledTimes(1);
|
||||
expect(mockNonReadyFetch).toBeCalledTimes(0);
|
||||
|
||||
expect(results).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"result": Object {},
|
||||
"type": "ready_col",
|
||||
},
|
||||
Object {
|
||||
"result": Object {
|
||||
"failed": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"not_ready": Object {
|
||||
"count": 0,
|
||||
"names": Array [],
|
||||
},
|
||||
"not_ready_timeout": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"timeout_col",
|
||||
],
|
||||
},
|
||||
"succeeded": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"ready_col",
|
||||
],
|
||||
},
|
||||
expect(results).toMatchSnapshot([
|
||||
{
|
||||
result: {},
|
||||
type: 'ready_col',
|
||||
},
|
||||
{
|
||||
result: {
|
||||
failed: {
|
||||
count: 0,
|
||||
names: [],
|
||||
},
|
||||
"type": "usage_collector_stats",
|
||||
fetch_duration_breakdown: [
|
||||
{
|
||||
name: 'ready_col',
|
||||
duration: 0,
|
||||
},
|
||||
],
|
||||
is_ready_duration_breakdown: [
|
||||
{
|
||||
name: 'ready_col',
|
||||
duration: expect.any(Number),
|
||||
},
|
||||
{
|
||||
name: 'timeout_col',
|
||||
duration: expect.any(Number),
|
||||
},
|
||||
],
|
||||
not_ready: {
|
||||
count: 0,
|
||||
names: [],
|
||||
},
|
||||
not_ready_timeout: {
|
||||
count: 1,
|
||||
names: ['timeout_col'],
|
||||
},
|
||||
succeeded: {
|
||||
count: 1,
|
||||
names: ['ready_col'],
|
||||
},
|
||||
total_duration: expect.any(Number),
|
||||
total_fetch_duration: 0,
|
||||
total_is_ready_duration: expect.any(Number),
|
||||
},
|
||||
]
|
||||
`);
|
||||
type: 'usage_collector_stats',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('passes context to fetch', async () => {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
import { withTimeout } from '@kbn/std';
|
||||
import { snakeCase } from 'lodash';
|
||||
|
||||
import type {
|
||||
Logger,
|
||||
ElasticsearchClient,
|
||||
|
@ -15,10 +16,13 @@ import type {
|
|||
ExecutionContextSetup,
|
||||
} from '@kbn/core/server';
|
||||
import { Collector } from './collector';
|
||||
import type { ICollector, CollectorOptions } from './types';
|
||||
import type { ICollector, CollectorOptions, CollectorFetchContext } from './types';
|
||||
import { UsageCollector, UsageCollectorOptions } from './usage_collector';
|
||||
import { DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S } from '../../common/constants';
|
||||
import { createPerformanceObsHook, perfTimerify } from './measure_duration';
|
||||
import { usageCollectorsStatsCollector } from './collector_stats';
|
||||
|
||||
const SECOND_IN_MS = 1000;
|
||||
// Needed for the general array containing all the collectors. We don't really care about their types here
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyCollector = ICollector<any, any>;
|
||||
|
@ -34,14 +38,6 @@ export interface CollectorSetConfig {
|
|||
collectors?: AnyCollector[];
|
||||
}
|
||||
|
||||
// Schema manually added in src/plugins/telemetry/schema/oss_root.json under `stack_stats.kibana.plugins.usage_collector_stats`
|
||||
interface CollectorStats {
|
||||
not_ready: { count: number; names: string[] };
|
||||
not_ready_timeout: { count: number; names: string[] };
|
||||
succeeded: { count: number; names: string[] };
|
||||
failed: { count: number; names: string[] };
|
||||
}
|
||||
|
||||
export class CollectorSet {
|
||||
private readonly logger: Logger;
|
||||
private readonly executionContext: ExecutionContextSetup;
|
||||
|
@ -115,22 +111,37 @@ export class CollectorSet {
|
|||
);
|
||||
}
|
||||
|
||||
const secondInMs = 1000;
|
||||
const timeoutMs = this.maximumWaitTimeForAllCollectorsInS * SECOND_IN_MS;
|
||||
const collectorsWithStatus: CollectorWithStatus[] = await Promise.all(
|
||||
[...collectors.values()].map(async (collector) => {
|
||||
const isReadyWithTimeout = await withTimeout<boolean>({
|
||||
promise: (async (): Promise<boolean> => {
|
||||
const wrappedPromise = perfTimerify(
|
||||
`is_ready_${collector.type}`,
|
||||
async (): Promise<boolean> => {
|
||||
try {
|
||||
return await collector.isReady();
|
||||
} catch (err) {
|
||||
this.logger.debug(`Collector ${collector.type} failed to get ready. ${err}`);
|
||||
return false;
|
||||
}
|
||||
})(),
|
||||
timeoutMs: this.maximumWaitTimeForAllCollectorsInS * secondInMs,
|
||||
}
|
||||
);
|
||||
|
||||
const isReadyWithTimeout = await withTimeout<boolean>({
|
||||
promise: wrappedPromise(),
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
return { isReadyWithTimeout, collector };
|
||||
if (isReadyWithTimeout.timedout) {
|
||||
return { isReadyWithTimeout, collector };
|
||||
}
|
||||
|
||||
return {
|
||||
isReadyWithTimeout: {
|
||||
value: isReadyWithTimeout.value,
|
||||
timedout: isReadyWithTimeout.timedout,
|
||||
},
|
||||
collector,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -176,55 +187,113 @@ export class CollectorSet {
|
|||
};
|
||||
};
|
||||
|
||||
private fetchCollector = async (
|
||||
collector: AnyCollector,
|
||||
context: CollectorFetchContext
|
||||
): Promise<{
|
||||
result?: unknown;
|
||||
status: 'failed' | 'success';
|
||||
type: string;
|
||||
}> => {
|
||||
const { type } = collector;
|
||||
this.logger.debug(`Fetching data from ${type} collector`);
|
||||
const executionContext: KibanaExecutionContext = {
|
||||
type: 'usage_collection',
|
||||
name: 'collector.fetch',
|
||||
id: type,
|
||||
description: `Fetch method in the Collector "${type}"`,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await this.executionContext.withContext(executionContext, () =>
|
||||
collector.fetch(context)
|
||||
);
|
||||
return { type, result, status: 'success' as const };
|
||||
} catch (err) {
|
||||
this.logger.warn(err);
|
||||
this.logger.warn(`Unable to fetch data from ${type} collector`);
|
||||
return { type, status: 'failed' as const };
|
||||
}
|
||||
};
|
||||
|
||||
public bulkFetch = async (
|
||||
esClient: ElasticsearchClient,
|
||||
soClient: SavedObjectsClientContract,
|
||||
collectors: Map<string, AnyCollector> = this.collectors
|
||||
) => {
|
||||
this.logger.debug(`Getting ready collectors`);
|
||||
const getMarks = createPerformanceObsHook();
|
||||
const { readyCollectors, nonReadyCollectorTypes, timedOutCollectorsTypes } =
|
||||
await this.getReadyCollectors(collectors);
|
||||
|
||||
const collectorStats: CollectorStats = {
|
||||
not_ready: { count: nonReadyCollectorTypes.length, names: nonReadyCollectorTypes },
|
||||
not_ready_timeout: { count: timedOutCollectorsTypes.length, names: timedOutCollectorsTypes },
|
||||
succeeded: { count: 0, names: [] },
|
||||
failed: { count: 0, names: [] },
|
||||
};
|
||||
// freeze object to prevent collectors from mutating it.
|
||||
const context = Object.freeze({ esClient, soClient });
|
||||
|
||||
const responses = await Promise.all(
|
||||
const fetchExecutions = await Promise.all(
|
||||
readyCollectors.map(async (collector) => {
|
||||
this.logger.debug(`Fetching data from ${collector.type} collector`);
|
||||
try {
|
||||
const context = { esClient, soClient };
|
||||
const executionContext: KibanaExecutionContext = {
|
||||
type: 'usage_collection',
|
||||
name: 'collector.fetch',
|
||||
id: collector.type,
|
||||
description: `Fetch method in the Collector "${collector.type}"`,
|
||||
};
|
||||
const result = await this.executionContext.withContext(executionContext, () =>
|
||||
collector.fetch(context)
|
||||
);
|
||||
collectorStats.succeeded.names.push(collector.type);
|
||||
return { type: collector.type, result };
|
||||
} catch (err) {
|
||||
this.logger.warn(err);
|
||||
this.logger.warn(`Unable to fetch data from ${collector.type} collector`);
|
||||
collectorStats.failed.names.push(collector.type);
|
||||
}
|
||||
const wrappedPromise = perfTimerify(
|
||||
`fetch_${collector.type}`,
|
||||
async () => await this.fetchCollector(collector, context)
|
||||
);
|
||||
|
||||
return await wrappedPromise();
|
||||
})
|
||||
);
|
||||
const durationMarks = getMarks();
|
||||
|
||||
collectorStats.succeeded.count = collectorStats.succeeded.names.length;
|
||||
collectorStats.failed.count = collectorStats.failed.names.length;
|
||||
const isReadyExecutionDurationByType = [
|
||||
...readyCollectors.map(({ type }) => {
|
||||
// should always find a duration, fallback to 0 in case something unexpected happened
|
||||
const duration = durationMarks[`is_ready_${type}`] || 0;
|
||||
return { duration, type };
|
||||
}),
|
||||
...nonReadyCollectorTypes.map((type) => {
|
||||
// should always find a duration, fallback to 0 in case something unexpected happened
|
||||
const duration = durationMarks[`is_ready_${type}`] || 0;
|
||||
return { duration, type };
|
||||
}),
|
||||
...timedOutCollectorsTypes.map((type) => {
|
||||
const timeoutMs = this.maximumWaitTimeForAllCollectorsInS * SECOND_IN_MS;
|
||||
// if undefined default to timeoutMs since the collector timedout
|
||||
const duration = durationMarks[`is_ready_${type}`] || timeoutMs;
|
||||
return { duration, type };
|
||||
}),
|
||||
];
|
||||
|
||||
// Treat it as just another "collector"
|
||||
responses.push({ type: 'usage_collector_stats', result: collectorStats });
|
||||
const fetchExecutionDurationByType = fetchExecutions.map(({ type, status }) => {
|
||||
// should always find a duration, fallback to 0 in case something unexpected happened
|
||||
const duration = durationMarks[`fetch_${type}`] || 0;
|
||||
return { duration, type, status };
|
||||
});
|
||||
|
||||
return responses.filter(
|
||||
(response): response is { type: string; result: unknown } => typeof response !== 'undefined'
|
||||
const usageCollectorStats = usageCollectorsStatsCollector(
|
||||
// pass `this` as `usageCollection` to the collector to mimic
|
||||
// registering a collector via usageCollection.SetupContract
|
||||
this,
|
||||
{
|
||||
// isReady stats
|
||||
nonReadyCollectorTypes,
|
||||
timedOutCollectorsTypes,
|
||||
isReadyExecutionDurationByType,
|
||||
|
||||
// fetch stats
|
||||
fetchExecutionDurationByType,
|
||||
}
|
||||
);
|
||||
|
||||
return [
|
||||
...fetchExecutions
|
||||
// pluck type and result from collector object
|
||||
.map(({ type, result }) => ({ type, result }))
|
||||
// only keep data of collectors thar returned a result
|
||||
.filter(
|
||||
(response): response is { type: string; result: unknown } =>
|
||||
typeof response?.result !== 'undefined'
|
||||
),
|
||||
|
||||
// Treat collector stats as just another "collector"
|
||||
{ type: usageCollectorStats.type, result: usageCollectorStats.fetch(context) },
|
||||
];
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
## Collector Stats Collector
|
||||
|
||||
The `usage_collector_stats` collector adds telemetry around the execution duration grabbing usage and the status of the collectors:
|
||||
- Total number and names of collectors that return `true` from `isReady`
|
||||
- Total number and names of collectors that return `false` from from `isReady`
|
||||
- Total number and names of collectors that timeout from from `isReady`
|
||||
- Total number and names of ready collectors that successfully return data from `fetch`
|
||||
- Total number and names of ready collectors that fail to return data from `fetch`
|
||||
- Total execution duration to grab all collectors
|
||||
- Total execution duration to get the `isReady` state of each collector
|
||||
- Total execution duration to get the `fetch` objects from each collector
|
||||
- Breakdown per collector type with details on the execution duration for `fetch` and `isReady`
|
||||
|
||||
The overall durations show the overall health of the collection mechanism, while the breakdown objects help diagnose specific collectors and improve upon them.
|
||||
|
||||
## Why is this in telemetry and not in CI?
|
||||
Adding limits and checks in CI is a good idea for catching early issues. Collecting these metrics via telemetry will also help us identify bottlenecks against real-world use cases from Kibanas in the wild.
|
||||
|
||||
## What does the usage collector stats look like?
|
||||
|
||||
The collector can be found under `stack_stats.kibana.plugins.usage_collector_stats` and looks like this:
|
||||
|
||||
```json
|
||||
"usage_collector_stats": {
|
||||
"not_ready": {
|
||||
"count": 1,
|
||||
"names": [
|
||||
"cloud_provider"
|
||||
]
|
||||
},
|
||||
"not_ready_timeout": {
|
||||
"count": 0,
|
||||
"names": []
|
||||
},
|
||||
"succeeded": {
|
||||
"count": 54,
|
||||
"names": [
|
||||
"task_manager",
|
||||
"ui_counters",
|
||||
"usage_counters",
|
||||
"kibana_stats",
|
||||
"kibana",
|
||||
...
|
||||
]
|
||||
},
|
||||
"failed": {
|
||||
"count": 0,
|
||||
"names": []
|
||||
},
|
||||
"total_is_ready_duration": 0.07500024700000003,
|
||||
"total_fetch_duration": 0.35939233100000006,
|
||||
"total_duration": 0.4343925780000001,
|
||||
"is_ready_duration_breakdown": {
|
||||
{ "name": "task_manager", "duration": 0.001828041 },
|
||||
{ "name": "ui_counters", "duration": 0.001790625 },
|
||||
{ "name": "usage_counters", "duration": 0.001778125 },
|
||||
{ "name": "kibana_stats", "duration": 0.001764709 },
|
||||
{ "name": "kibana", "duration": 0.001748917 },
|
||||
...
|
||||
},
|
||||
"fetch_duration_breakdown": {
|
||||
{ "name": "task_manager", "duration": 0.011157708 },
|
||||
{ "name": "ui_counters", "duration": 0.011002625 },
|
||||
{ "name": "usage_counters", "duration": 0.009945833 },
|
||||
{ "name": "kibana_stats", "duration": 0.009424458 },
|
||||
{ "name": "kibana", "duration": 0.009406416 },
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { usageCollectorsStatsCollector } from './usage_collector_stats_collector';
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { MakeSchemaFrom } from '../types';
|
||||
import type { CollectorsStats } from './usage_collector_stats_collector';
|
||||
|
||||
export const collectorsStatsSchema: MakeSchemaFrom<CollectorsStats> = {
|
||||
total_duration: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'The total execution duration to grab usage stats for all collectors in milliseconds',
|
||||
},
|
||||
},
|
||||
total_is_ready_duration: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'The total execution duration of the isReady function for all collectors in milliseconds',
|
||||
},
|
||||
},
|
||||
total_fetch_duration: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'The total execution duration of the fetch function for all ready collectors in milliseconds',
|
||||
},
|
||||
},
|
||||
is_ready_duration_breakdown: {
|
||||
type: 'array',
|
||||
items: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The name of the collector',
|
||||
},
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'The execution duration of the isReady function for the collector in milliseconds',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fetch_duration_breakdown: {
|
||||
type: 'array',
|
||||
items: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The name of the collector',
|
||||
},
|
||||
},
|
||||
duration: {
|
||||
type: 'long',
|
||||
_meta: {
|
||||
description:
|
||||
'The execution duration of the fetch function for the collector in milliseconds',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
not_ready: {
|
||||
count: {
|
||||
type: 'short',
|
||||
_meta: {
|
||||
description: 'The number of collectors that returned false from the isReady function',
|
||||
},
|
||||
},
|
||||
names: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description:
|
||||
'The name of the of collectors that returned false from the isReady function',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
not_ready_timeout: {
|
||||
count: {
|
||||
type: 'short',
|
||||
_meta: {
|
||||
description: 'The number of collectors that timedout during the isReady function',
|
||||
},
|
||||
},
|
||||
names: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The name of collectors that timedout during the isReady function',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
succeeded: {
|
||||
count: {
|
||||
type: 'short',
|
||||
_meta: {
|
||||
description: 'The number of collectors that returned true from the fetch function',
|
||||
},
|
||||
},
|
||||
names: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The name of the of collectors that returned true from the fetch function',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failed: {
|
||||
count: {
|
||||
type: 'short',
|
||||
_meta: {
|
||||
description: 'The number of collectors that threw an error from the fetch function',
|
||||
},
|
||||
},
|
||||
names: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The name of the of collectors that threw an error from the fetch function',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
usageCollectorsStatsCollector,
|
||||
CollectorsStatsCollectorParams,
|
||||
} from './usage_collector_stats_collector';
|
||||
import { UsageCollector } from '../usage_collector';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { createCollectorFetchContextMock } from '../../mocks';
|
||||
|
||||
describe('usageCollectorsStatsCollector', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const mockFetchContext = createCollectorFetchContextMock();
|
||||
const mockMakeUsageCollector = jest.fn().mockImplementation((args) => {
|
||||
return new UsageCollector(logger, args);
|
||||
});
|
||||
const mockCollectorSet = { makeUsageCollector: mockMakeUsageCollector };
|
||||
|
||||
const createCollectorStats = (
|
||||
params?: Partial<CollectorsStatsCollectorParams>
|
||||
): CollectorsStatsCollectorParams => ({
|
||||
fetchExecutionDurationByType: [],
|
||||
isReadyExecutionDurationByType: [],
|
||||
nonReadyCollectorTypes: [],
|
||||
timedOutCollectorsTypes: [],
|
||||
...params,
|
||||
});
|
||||
|
||||
it('calls makeUsageCollector to create a collector', () => {
|
||||
const collectorStats = createCollectorStats();
|
||||
const collector = usageCollectorsStatsCollector(mockCollectorSet, collectorStats);
|
||||
expect(mockMakeUsageCollector).toBeCalledTimes(1);
|
||||
expect(collector.type).toMatchInlineSnapshot(`"usage_collector_stats"`);
|
||||
expect(typeof collector.fetch).toBe('function');
|
||||
expect(collector).toBeInstanceOf(UsageCollector);
|
||||
});
|
||||
|
||||
it('returns collector stats totals and breakdowns from fetch', async () => {
|
||||
const collectorStats = createCollectorStats({
|
||||
fetchExecutionDurationByType: [
|
||||
{ duration: 1.2, status: 'success', type: 'SUCCESS_COLLECTOR' },
|
||||
{ duration: 8, status: 'success', type: 'SUCCESS_COLLECTOR_2' },
|
||||
{ duration: 2.2, status: 'failed', type: 'FAILED_COLLECTOR' },
|
||||
],
|
||||
isReadyExecutionDurationByType: [
|
||||
{ duration: 10.2, type: 'SUCCESS_COLLECTOR' },
|
||||
{ duration: 4.2, type: 'SUCCESS_COLLECTOR_2' },
|
||||
{ duration: 12, type: 'FAILED_COLLECTOR' },
|
||||
],
|
||||
nonReadyCollectorTypes: ['NON_READY_COLLECTOR'],
|
||||
timedOutCollectorsTypes: ['TIMED_OUT_READY_COLLECTOR'],
|
||||
});
|
||||
const collector = usageCollectorsStatsCollector(mockCollectorSet, collectorStats);
|
||||
const result = await collector.fetch(mockFetchContext);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failed": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"FAILED_COLLECTOR",
|
||||
],
|
||||
},
|
||||
"fetch_duration_breakdown": Array [
|
||||
Object {
|
||||
"duration": 1.2,
|
||||
"name": "SUCCESS_COLLECTOR",
|
||||
},
|
||||
Object {
|
||||
"duration": 8,
|
||||
"name": "SUCCESS_COLLECTOR_2",
|
||||
},
|
||||
Object {
|
||||
"duration": 2.2,
|
||||
"name": "FAILED_COLLECTOR",
|
||||
},
|
||||
],
|
||||
"is_ready_duration_breakdown": Array [
|
||||
Object {
|
||||
"duration": 10.2,
|
||||
"name": "SUCCESS_COLLECTOR",
|
||||
},
|
||||
Object {
|
||||
"duration": 4.2,
|
||||
"name": "SUCCESS_COLLECTOR_2",
|
||||
},
|
||||
Object {
|
||||
"duration": 12,
|
||||
"name": "FAILED_COLLECTOR",
|
||||
},
|
||||
],
|
||||
"not_ready": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"NON_READY_COLLECTOR",
|
||||
],
|
||||
},
|
||||
"not_ready_timeout": Object {
|
||||
"count": 1,
|
||||
"names": Array [
|
||||
"TIMED_OUT_READY_COLLECTOR",
|
||||
],
|
||||
},
|
||||
"succeeded": Object {
|
||||
"count": 2,
|
||||
"names": Array [
|
||||
"SUCCESS_COLLECTOR",
|
||||
"SUCCESS_COLLECTOR_2",
|
||||
],
|
||||
},
|
||||
"total_duration": 37.8,
|
||||
"total_fetch_duration": 11.399999999999999,
|
||||
"total_is_ready_duration": 26.4,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { sumBy } from 'lodash';
|
||||
import { collectorsStatsSchema } from './schema';
|
||||
import type { CollectorSet } from '../collector_set';
|
||||
|
||||
export interface CollectorsStats {
|
||||
not_ready: { count: number; names: string[] };
|
||||
not_ready_timeout: { count: number; names: string[] };
|
||||
succeeded: { count: number; names: string[] };
|
||||
failed: { count: number; names: string[] };
|
||||
|
||||
total_duration: number;
|
||||
total_is_ready_duration: number;
|
||||
total_fetch_duration: number;
|
||||
is_ready_duration_breakdown: Array<{ name: string; duration: number }>;
|
||||
fetch_duration_breakdown: Array<{ name: string; duration: number }>;
|
||||
}
|
||||
|
||||
export interface CollectorsStatsCollectorParams {
|
||||
nonReadyCollectorTypes: string[];
|
||||
timedOutCollectorsTypes: string[];
|
||||
isReadyExecutionDurationByType: Array<{ duration: number; type: string }>;
|
||||
fetchExecutionDurationByType: Array<{
|
||||
duration: number;
|
||||
type: string;
|
||||
status: 'failed' | 'success';
|
||||
}>;
|
||||
}
|
||||
|
||||
export const usageCollectorsStatsCollector = (
|
||||
usageCollection: Pick<CollectorSet, 'makeUsageCollector'>,
|
||||
{
|
||||
nonReadyCollectorTypes,
|
||||
timedOutCollectorsTypes,
|
||||
isReadyExecutionDurationByType,
|
||||
fetchExecutionDurationByType,
|
||||
}: CollectorsStatsCollectorParams
|
||||
) => {
|
||||
return usageCollection.makeUsageCollector<CollectorsStats>({
|
||||
type: 'usage_collector_stats',
|
||||
isReady: () => true,
|
||||
schema: collectorsStatsSchema,
|
||||
fetch: () => {
|
||||
const totalIsReadyDuration = sumBy(isReadyExecutionDurationByType, 'duration');
|
||||
const totalFetchDuration = sumBy(fetchExecutionDurationByType, 'duration');
|
||||
|
||||
const succeededCollectorTypes = fetchExecutionDurationByType
|
||||
.filter(({ status }) => status === 'success')
|
||||
.map(({ type }) => type);
|
||||
const failedCollectorTypes = fetchExecutionDurationByType
|
||||
.filter(({ status }) => status === 'failed')
|
||||
.map(({ type }) => type);
|
||||
|
||||
const collectorsStats: CollectorsStats = {
|
||||
// isReady and fetch stats
|
||||
not_ready: { count: nonReadyCollectorTypes.length, names: nonReadyCollectorTypes },
|
||||
not_ready_timeout: {
|
||||
count: timedOutCollectorsTypes.length,
|
||||
names: timedOutCollectorsTypes,
|
||||
},
|
||||
succeeded: { count: succeededCollectorTypes.length, names: succeededCollectorTypes },
|
||||
failed: { count: failedCollectorTypes.length, names: failedCollectorTypes },
|
||||
|
||||
// total durations
|
||||
total_is_ready_duration: totalIsReadyDuration,
|
||||
total_fetch_duration: totalFetchDuration,
|
||||
total_duration: totalIsReadyDuration + totalFetchDuration,
|
||||
|
||||
// durations breakdown
|
||||
is_ready_duration_breakdown: isReadyExecutionDurationByType.map(
|
||||
({ type: name, duration }) => ({ name, duration })
|
||||
),
|
||||
fetch_duration_breakdown: fetchExecutionDurationByType.map(({ type: name, duration }) => ({
|
||||
name,
|
||||
duration,
|
||||
})),
|
||||
};
|
||||
|
||||
return collectorsStats;
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { PerformanceObserver, performance } from 'perf_hooks';
|
||||
|
||||
export const createPerformanceObsHook = () => {
|
||||
const marks: Record<string, number> = {};
|
||||
const obs = new PerformanceObserver((items) => {
|
||||
for (const { duration, name } of items.getEntries()) {
|
||||
marks[name] = duration;
|
||||
}
|
||||
|
||||
performance.clearMarks();
|
||||
});
|
||||
|
||||
obs.observe({ entryTypes: ['function'] });
|
||||
|
||||
// teardown function returns the marked measurements.
|
||||
// returning the data after teardown ensures that we proprely teardown
|
||||
// the observer.
|
||||
return () => {
|
||||
obs.disconnect();
|
||||
return marks;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper around performance.timerify which defined the name of the returned
|
||||
* wrapped function to help identify observed function types inside the `PerformanceObserver`.
|
||||
*
|
||||
* @param name name of the function used to track the performance of the function execution
|
||||
* @param fn the function to be wrapped by the performance.timerify method.
|
||||
* @returns
|
||||
*/
|
||||
export const perfTimerify = <T extends (...params: unknown[]) => unknown>(name: string, fn: T) => {
|
||||
return performance.timerify(Object.defineProperty(fn, 'name', { value: name }));
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue