Skip to content

Commit 3884a3c

Browse files
[Usage Collection] Ensure no type duplicates (#70946)
Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co>
1 parent 06bc389 commit 3884a3c

File tree

3 files changed

+43
-18
lines changed

3 files changed

+43
-18
lines changed

src/plugins/usage_collection/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ All you need to provide is a `type` for organizing your fields, `schema` field t
5959

6060
// create usage collector
6161
const myCollector = usageCollection.makeUsageCollector<Usage>({
62-
type: MY_USAGE_TYPE,
62+
type: 'MY_USAGE_TYPE',
6363
schema: {
6464
my_objects: {
6565
total: 'long',
@@ -84,7 +84,11 @@ All you need to provide is a `type` for organizing your fields, `schema` field t
8484
}
8585
```
8686

87-
Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection.
87+
Some background:
88+
89+
- `MY_USAGE_TYPE` can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector.
90+
- The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection.
91+
In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest`, where the request headers are expected to have read privilege on the entire `.kibana' index.
8892

8993
Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS, or use other clients like a custom SavedObjects client. In that case it's up to the plugin to initialize those clients like the example below:
9094

src/plugins/usage_collection/server/collector/collector_set.test.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('CollectorSet', () => {
4141
loggerSpies.warn.mockRestore();
4242
});
4343

44-
const mockCallCluster = () => Promise.resolve({ passTest: 1000 });
44+
const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 });
4545

4646
it('should throw an error if non-Collector type of object is registered', () => {
4747
const collectors = new CollectorSet({ logger });
@@ -58,6 +58,23 @@ describe('CollectorSet', () => {
5858
);
5959
});
6060

61+
it('should throw when 2 collectors with the same type are registered', () => {
62+
const collectorSet = new CollectorSet({ logger });
63+
collectorSet.registerCollector(
64+
new Collector(logger, { type: 'test_duplicated', fetch: () => 1, isReady: () => true })
65+
);
66+
expect(() =>
67+
collectorSet.registerCollector(
68+
// Even for Collector vs. UsageCollector
69+
new UsageCollector(logger, {
70+
type: 'test_duplicated',
71+
fetch: () => 2,
72+
isReady: () => false,
73+
})
74+
)
75+
).toThrowError(`Usage collector's type "test_duplicated" is duplicated.`);
76+
});
77+
6178
it('should log debug status of fetching from the collector', async () => {
6279
const collectors = new CollectorSet({ logger });
6380
collectors.registerCollector(
@@ -68,7 +85,7 @@ describe('CollectorSet', () => {
6885
})
6986
);
7087

71-
const result = await collectors.bulkFetch(mockCallCluster as any);
88+
const result = await collectors.bulkFetch(mockCallCluster);
7289
expect(loggerSpies.debug).toHaveBeenCalledTimes(1);
7390
expect(loggerSpies.debug).toHaveBeenCalledWith(
7491
'Fetching data from MY_TEST_COLLECTOR collector'
@@ -93,7 +110,7 @@ describe('CollectorSet', () => {
93110

94111
let result;
95112
try {
96-
result = await collectors.bulkFetch(mockCallCluster as any);
113+
result = await collectors.bulkFetch(mockCallCluster);
97114
} catch (err) {
98115
// Do nothing
99116
}
@@ -111,7 +128,7 @@ describe('CollectorSet', () => {
111128
})
112129
);
113130

114-
const result = await collectors.bulkFetch(mockCallCluster as any);
131+
const result = await collectors.bulkFetch(mockCallCluster);
115132
expect(result).toStrictEqual([
116133
{
117134
type: 'MY_TEST_COLLECTOR',
@@ -129,7 +146,7 @@ describe('CollectorSet', () => {
129146
} as any)
130147
);
131148

132-
const result = await collectors.bulkFetch(mockCallCluster as any);
149+
const result = await collectors.bulkFetch(mockCallCluster);
133150
expect(result).toStrictEqual([
134151
{
135152
type: 'MY_TEST_COLLECTOR',
@@ -152,7 +169,7 @@ describe('CollectorSet', () => {
152169
})
153170
);
154171

155-
const result = await collectors.bulkFetch(mockCallCluster as any);
172+
const result = await collectors.bulkFetch(mockCallCluster);
156173
expect(result).toStrictEqual([
157174
{
158175
type: 'MY_TEST_COLLECTOR',

src/plugins/usage_collection/server/collector/collector_set.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ export class CollectorSet {
3232
private _waitingForAllCollectorsTimestamp?: number;
3333
private readonly logger: Logger;
3434
private readonly maximumWaitTimeForAllCollectorsInS: number;
35-
private collectors: Array<Collector<any, any>> = [];
35+
private readonly collectors: Map<string, Collector<any, any>>;
3636
constructor({ logger, maximumWaitTimeForAllCollectorsInS, collectors = [] }: CollectorSetConfig) {
3737
this.logger = logger;
38-
this.collectors = collectors;
38+
this.collectors = new Map(collectors.map((collector) => [collector.type, collector]));
3939
this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60;
4040
}
4141

@@ -55,7 +55,11 @@ export class CollectorSet {
5555
throw new Error('CollectorSet can only have Collector instances registered');
5656
}
5757

58-
this.collectors.push(collector);
58+
if (this.collectors.get(collector.type)) {
59+
throw new Error(`Usage collector's type "${collector.type}" is duplicated.`);
60+
}
61+
62+
this.collectors.set(collector.type, collector);
5963

6064
if (collector.init) {
6165
this.logger.debug(`Initializing ${collector.type} collector`);
@@ -64,7 +68,7 @@ export class CollectorSet {
6468
};
6569

6670
public getCollectorByType = (type: string) => {
67-
return this.collectors.find((c) => c.type === type);
71+
return [...this.collectors.values()].find((c) => c.type === type);
6872
};
6973

7074
public isUsageCollector = (x: UsageCollector | any): x is UsageCollector => {
@@ -81,7 +85,7 @@ export class CollectorSet {
8185

8286
const collectorTypesNotReady: string[] = [];
8387
let allReady = true;
84-
for (const collector of collectorSet.collectors) {
88+
for (const collector of collectorSet.collectors.values()) {
8589
if (!(await collector.isReady())) {
8690
allReady = false;
8791
collectorTypesNotReady.push(collector.type);
@@ -113,10 +117,10 @@ export class CollectorSet {
113117

114118
public bulkFetch = async (
115119
callCluster: LegacyAPICaller,
116-
collectors: Array<Collector<any, any>> = this.collectors
120+
collectors: Map<string, Collector<any, any>> = this.collectors
117121
) => {
118122
const responses = [];
119-
for (const collector of collectors) {
123+
for (const collector of collectors.values()) {
120124
this.logger.debug(`Fetching data from ${collector.type} collector`);
121125
try {
122126
responses.push({
@@ -136,7 +140,7 @@ export class CollectorSet {
136140
* @return {new CollectorSet}
137141
*/
138142
public getFilteredCollectorSet = (filter: (col: Collector) => boolean) => {
139-
const filtered = this.collectors.filter(filter);
143+
const filtered = [...this.collectors.values()].filter(filter);
140144
return this.makeCollectorSetFromArray(filtered);
141145
};
142146

@@ -188,12 +192,12 @@ export class CollectorSet {
188192

189193
// TODO: remove
190194
public map = (mapFn: any) => {
191-
return this.collectors.map(mapFn);
195+
return [...this.collectors.values()].map(mapFn);
192196
};
193197

194198
// TODO: remove
195199
public some = (someFn: any) => {
196-
return this.collectors.some(someFn);
200+
return [...this.collectors.values()].some(someFn);
197201
};
198202

199203
private makeCollectorSetFromArray = (collectors: Collector[]) => {

0 commit comments

Comments
 (0)