[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:
Ahmad Bamieh 2022-05-16 17:48:41 +03:00 committed by GitHub
parent f7c6b77317
commit 7e7f862a79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 937 additions and 165 deletions

View file

@ -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": {

View file

@ -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"
}
}
}
}
}
}
}
}

View 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",
},
]
`;

View file

@ -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 () => {

View file

@ -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) },
];
};
/*

View file

@ -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 },
...
}
}
```

View file

@ -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';

View file

@ -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',
},
},
},
},
};

View file

@ -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,
}
`);
});
});

View file

@ -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;
},
});
};

View file

@ -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 }));
};