Skip to content

Commit e3923b5

Browse files
committed
[Usage Collection] Report nodes feature usage (#70108)
* Adds nodes feature usage stats merged into cluster_stats.nodes when usage collection is local
1 parent 9b00a2a commit e3923b5

7 files changed

Lines changed: 299 additions & 11 deletions

File tree

src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919

2020
import expect from '@kbn/expect';
2121
import sinon from 'sinon';
22+
import { merge, omit } from 'lodash';
2223

24+
import { TIMEOUT } from '../constants';
2325
import { mockGetClusterInfo } from './get_cluster_info';
2426
import { mockGetClusterStats } from './get_cluster_stats';
2527

26-
import { omit } from 'lodash';
2728
import { getLocalStats, handleLocalStats } from '../get_local_stats';
2829

2930
const mockUsageCollection = (kibanaUsage = {}) => ({
@@ -51,10 +52,26 @@ const getMockServer = (getCluster = sinon.stub()) => ({
5152
elasticsearch: { getCluster },
5253
},
5354
});
55+
function mockGetNodesUsage(callCluster, nodesUsage, req) {
56+
callCluster
57+
.withArgs(
58+
req,
59+
{
60+
method: 'GET',
61+
path: '/_nodes/usage',
62+
query: {
63+
timeout: TIMEOUT,
64+
},
65+
},
66+
'transport.request'
67+
)
68+
.returns(nodesUsage);
69+
}
5470

55-
function mockGetLocalStats(callCluster, clusterInfo, clusterStats, req) {
71+
function mockGetLocalStats(callCluster, clusterInfo, clusterStats, nodesUsage, req) {
5672
mockGetClusterInfo(callCluster, clusterInfo, req);
5773
mockGetClusterStats(callCluster, clusterStats, req);
74+
mockGetNodesUsage(callCluster, nodesUsage, req);
5875
}
5976

6077
describe('get_local_stats', () => {
@@ -68,13 +85,36 @@ describe('get_local_stats', () => {
6885
number: version,
6986
},
7087
};
88+
const nodesUsage = [
89+
{
90+
node_id: 'some_node_id',
91+
timestamp: 1588617023177,
92+
since: 1588616945163,
93+
rest_actions: {
94+
nodes_usage_action: 1,
95+
create_index_action: 1,
96+
document_get_action: 1,
97+
search_action: 19,
98+
nodes_info_action: 36,
99+
},
100+
aggregations: {
101+
terms: {
102+
bytes: 2,
103+
},
104+
scripted_metric: {
105+
other: 7,
106+
},
107+
},
108+
},
109+
];
71110
const clusterStats = {
72111
_nodes: { failed: 123 },
73112
cluster_name: 'real-cool',
74113
indices: { totally: 456 },
75114
nodes: { yup: 'abc' },
76115
random: 123,
77116
};
117+
78118
const kibana = {
79119
kibana: {
80120
great: 'googlymoogly',
@@ -97,12 +137,16 @@ describe('get_local_stats', () => {
97137
snow: { chances: 0 },
98138
};
99139

140+
const clusterStatsWithNodesUsage = {
141+
...clusterStats,
142+
nodes: merge(clusterStats.nodes, { usage: nodesUsage }),
143+
};
100144
const combinedStatsResult = {
101145
collection: 'local',
102146
cluster_uuid: clusterUuid,
103147
cluster_name: clusterName,
104148
version,
105-
cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'),
149+
cluster_stats: omit(clusterStatsWithNodesUsage, '_nodes', 'cluster_name'),
106150
stack_stats: {
107151
kibana: {
108152
great: 'googlymoogly',
@@ -135,7 +179,7 @@ describe('get_local_stats', () => {
135179

136180
describe('handleLocalStats', () => {
137181
it('returns expected object without xpack and kibana data', () => {
138-
const result = handleLocalStats(clusterInfo, clusterStats, void 0, context);
182+
const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context);
139183
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
140184
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
141185
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
@@ -146,7 +190,7 @@ describe('get_local_stats', () => {
146190
});
147191

148192
it('returns expected object with xpack', () => {
149-
const result = handleLocalStats(clusterInfo, clusterStats, void 0, context);
193+
const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context);
150194
const { stack_stats: stack, ...cluster } = result;
151195
expect(cluster.collection).to.be(combinedStatsResult.collection);
152196
expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid);
@@ -167,7 +211,8 @@ describe('get_local_stats', () => {
167211
mockGetLocalStats(
168212
callClusterUsageFailed,
169213
Promise.resolve(clusterInfo),
170-
Promise.resolve(clusterStats)
214+
Promise.resolve(clusterStats),
215+
Promise.resolve(nodesUsage)
171216
);
172217
const result = await getLocalStats([], {
173218
server: getMockServer(),
@@ -177,6 +222,7 @@ describe('get_local_stats', () => {
177222
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
178223
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
179224
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
225+
expect(result.cluster_stats.nodes).to.eql(combinedStatsResult.cluster_stats.nodes);
180226
expect(result.version).to.be('2.3.4');
181227
expect(result.collection).to.be('local');
182228

@@ -188,7 +234,12 @@ describe('get_local_stats', () => {
188234
it('returns expected object with xpack and kibana data', async () => {
189235
const callCluster = sinon.stub();
190236
const usageCollection = mockUsageCollection(kibana);
191-
mockGetLocalStats(callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats));
237+
mockGetLocalStats(
238+
callCluster,
239+
Promise.resolve(clusterInfo),
240+
Promise.resolve(clusterStats),
241+
Promise.resolve(nodesUsage)
242+
);
192243

193244
const result = await getLocalStats([], {
194245
server: getMockServer(callCluster),

src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { getClusterInfo, ESClusterInfo } from './get_cluster_info';
2525
import { getClusterStats } from './get_cluster_stats';
2626
import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana';
27+
import { getNodesUsage } from './get_nodes_usage';
2728

2829
/**
2930
* Handle the separate local calls by combining them into a single object response that looks like the
@@ -67,12 +68,21 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async (
6768

6869
return await Promise.all(
6970
clustersDetails.map(async (clustersDetail) => {
70-
const [clusterInfo, clusterStats, kibana] = await Promise.all([
71+
const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([
7172
getClusterInfo(callCluster), // cluster info
7273
getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_)
74+
getNodesUsage(callCluster), // nodes_usage info
7375
getKibana(usageCollection, callCluster),
7476
]);
75-
return handleLocalStats(clusterInfo, clusterStats, kibana, context);
77+
return handleLocalStats(
78+
clusterInfo,
79+
{
80+
...clusterStats,
81+
nodes: { ...clusterStats.nodes, usage: nodesUsage },
82+
},
83+
kibana,
84+
context
85+
);
7686
})
7787
);
7888
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { getNodesUsage } from './get_nodes_usage';
21+
import { TIMEOUT } from './constants';
22+
23+
const mockedNodesFetchResponse = {
24+
cluster_name: 'test cluster',
25+
nodes: {
26+
some_node_id: {
27+
timestamp: 1588617023177,
28+
since: 1588616945163,
29+
rest_actions: {
30+
nodes_usage_action: 1,
31+
create_index_action: 1,
32+
document_get_action: 1,
33+
search_action: 19,
34+
nodes_info_action: 36,
35+
},
36+
aggregations: {
37+
terms: {
38+
bytes: 2,
39+
},
40+
scripted_metric: {
41+
other: 7,
42+
},
43+
},
44+
},
45+
},
46+
};
47+
describe('get_nodes_usage', () => {
48+
it('calls fetchNodesUsage', async () => {
49+
const callCluster = jest.fn();
50+
callCluster.mockResolvedValueOnce(mockedNodesFetchResponse);
51+
await getNodesUsage(callCluster);
52+
expect(callCluster).toHaveBeenCalledWith('transport.request', {
53+
path: '/_nodes/usage',
54+
method: 'GET',
55+
query: {
56+
timeout: TIMEOUT,
57+
},
58+
});
59+
});
60+
it('returns a modified array of node usage data', async () => {
61+
const callCluster = jest.fn();
62+
callCluster.mockResolvedValueOnce(mockedNodesFetchResponse);
63+
const result = await getNodesUsage(callCluster);
64+
expect(result.nodes).toEqual([
65+
{
66+
aggregations: { scripted_metric: { other: 7 }, terms: { bytes: 2 } },
67+
node_id: 'some_node_id',
68+
rest_actions: {
69+
create_index_action: 1,
70+
document_get_action: 1,
71+
nodes_info_action: 36,
72+
nodes_usage_action: 1,
73+
search_action: 19,
74+
},
75+
since: 1588616945163,
76+
timestamp: 1588617023177,
77+
},
78+
]);
79+
});
80+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { LegacyAPICaller } from 'kibana/server';
20+
import { TIMEOUT } from './constants';
21+
22+
export interface NodeAggregation {
23+
[key: string]: number;
24+
}
25+
26+
// we set aggregations as an optional type because it was only added in v7.8.0
27+
export interface NodeObj {
28+
node_id?: string;
29+
timestamp: number;
30+
since: number;
31+
rest_actions: {
32+
[key: string]: number;
33+
};
34+
aggregations?: {
35+
[key: string]: NodeAggregation;
36+
};
37+
}
38+
39+
export interface NodesFeatureUsageResponse {
40+
cluster_name: string;
41+
nodes: {
42+
[key: string]: NodeObj;
43+
};
44+
}
45+
46+
export type NodesUsageGetter = (
47+
callCluster: LegacyAPICaller
48+
) => Promise<{ nodes: NodeObj[] | Array<{}> }>;
49+
/**
50+
* Get the nodes usage data from the connected cluster.
51+
*
52+
* This is the equivalent to GET /_nodes/usage?timeout=30s.
53+
*
54+
* The Nodes usage API was introduced in v6.0.0
55+
*/
56+
export async function fetchNodesUsage(
57+
callCluster: LegacyAPICaller
58+
): Promise<NodesFeatureUsageResponse> {
59+
const response = await callCluster('transport.request', {
60+
method: 'GET',
61+
path: '/_nodes/usage',
62+
query: {
63+
timeout: TIMEOUT,
64+
},
65+
});
66+
return response;
67+
}
68+
69+
/**
70+
* Get the nodes usage from the connected cluster
71+
* @param callCluster APICaller
72+
* @returns Object containing array of modified usage information with the node_id nested within the data for that node.
73+
*/
74+
export const getNodesUsage: NodesUsageGetter = async (callCluster) => {
75+
const result = await fetchNodesUsage(callCluster);
76+
const transformedNodes = Object.entries(result?.nodes || {}).map(([key, value]) => ({
77+
...(value as NodeObj),
78+
node_id: key,
79+
}));
80+
return { nodes: transformedNodes };
81+
};

test/api_integration/apis/telemetry/telemetry_local.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export default function ({ getService }) {
113113
'cluster_stats.nodes.plugins',
114114
'cluster_stats.nodes.process',
115115
'cluster_stats.nodes.versions',
116+
'cluster_stats.nodes.usage',
116117
'cluster_stats.status',
117118
'cluster_stats.timestamp',
118119
'cluster_uuid',

0 commit comments

Comments
 (0)