Skip to content

Commit 0ce1cad

Browse files
committed
Separate the license retrieval from the stats
1 parent b573500 commit 0ce1cad

File tree

14 files changed

+342
-116
lines changed

14 files changed

+342
-116
lines changed

src/legacy/core_plugins/telemetry/server/collection_manager.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import { encryptTelemetry } from './collectors';
2121
import { CallCluster } from '../../elasticsearch';
2222
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server';
23+
import { ESLicense } from './telemetry_collection/get_local_license';
2324

2425
export type EncryptedStatsGetterConfig = { unencrypted: false } & {
2526
server: any;
@@ -45,22 +46,38 @@ export interface StatsCollectionConfig {
4546
end: string | number;
4647
}
4748

49+
export interface BasicStatsPayload {
50+
timestamp: string;
51+
cluster_uuid: string;
52+
cluster_name: string;
53+
version: string;
54+
cluster_stats: object;
55+
collection?: string;
56+
stack_stats: object;
57+
}
58+
4859
export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig;
4960
export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise<ClusterDetails[]>;
50-
export type StatsGetter<T = unknown> = (
61+
export type StatsGetter<T extends BasicStatsPayload = BasicStatsPayload> = (
5162
clustersDetails: ClusterDetails[],
5263
config: StatsCollectionConfig
5364
) => Promise<T[]>;
65+
export type LicenseGetter = (
66+
clustersDetails: ClusterDetails[],
67+
config: StatsCollectionConfig
68+
) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>;
5469

55-
interface CollectionConfig {
70+
interface CollectionConfig<T extends BasicStatsPayload> {
5671
title: string;
5772
priority: number;
5873
esCluster: string;
59-
statsGetter: StatsGetter;
74+
statsGetter: StatsGetter<T>;
6075
clusterDetailsGetter: ClusterDetailsGetter;
76+
licenseGetter: LicenseGetter;
6177
}
6278
interface Collection {
6379
statsGetter: StatsGetter;
80+
licenseGetter: LicenseGetter;
6481
clusterDetailsGetter: ClusterDetailsGetter;
6582
esCluster: string;
6683
title: string;
@@ -70,8 +87,15 @@ export class TelemetryCollectionManager {
7087
private usageGetterMethodPriority = -1;
7188
private collections: Collection[] = [];
7289

73-
public setCollection = (collectionConfig: CollectionConfig) => {
74-
const { title, priority, esCluster, statsGetter, clusterDetailsGetter } = collectionConfig;
90+
public setCollection = <T extends BasicStatsPayload>(collectionConfig: CollectionConfig<T>) => {
91+
const {
92+
title,
93+
priority,
94+
esCluster,
95+
statsGetter,
96+
clusterDetailsGetter,
97+
licenseGetter,
98+
} = collectionConfig;
7599

76100
if (typeof priority !== 'number') {
77101
throw new Error('priority must be set.');
@@ -88,10 +112,14 @@ export class TelemetryCollectionManager {
88112
throw Error('esCluster name must be set for the getCluster method.');
89113
}
90114
if (!clusterDetailsGetter) {
91-
throw Error('Cluser UUIds method is not set.');
115+
throw Error('Cluster UUIds method is not set.');
116+
}
117+
if (!licenseGetter) {
118+
throw Error('License getter method not set.');
92119
}
93120

94121
this.collections.unshift({
122+
licenseGetter,
95123
statsGetter,
96124
clusterDetailsGetter,
97125
esCluster,
@@ -141,7 +169,17 @@ export class TelemetryCollectionManager {
141169
return;
142170
}
143171

144-
return await collection.statsGetter(clustersDetails, statsCollectionConfig);
172+
const stats = await collection.statsGetter(clustersDetails, statsCollectionConfig);
173+
const licenses = await collection.licenseGetter(clustersDetails, statsCollectionConfig);
174+
175+
return stats.map(stat => {
176+
const license = licenses[stat.cluster_uuid];
177+
return {
178+
...(license ? { license } : {}),
179+
...stat,
180+
collectionSource: collection.title,
181+
};
182+
});
145183
};
146184

147185
public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
21+
import { LicenseGetter } from '../collection_manager';
22+
23+
// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html
24+
export interface ESLicense {
25+
status: string;
26+
uid: string;
27+
type: string;
28+
issue_date: string;
29+
issue_date_in_millis: number;
30+
expiry_date: string;
31+
expirty_date_in_millis: number;
32+
max_nodes: number;
33+
issued_to: string;
34+
issuer: string;
35+
start_date_in_millis: number;
36+
}
37+
let cachedLicense: ESLicense | undefined;
38+
39+
function fetchLicense(callCluster: CallCluster, local: boolean) {
40+
return callCluster<{ license: ESLicense }>('transport.request', {
41+
method: 'GET',
42+
path: '/_license',
43+
query: {
44+
local,
45+
// For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license.
46+
accept_enterprise: 'true',
47+
},
48+
});
49+
}
50+
51+
/**
52+
* Get the cluster's license from the connected node.
53+
*
54+
* This is the equivalent of GET /_license?local=true .
55+
*
56+
* Like any X-Pack related API, X-Pack must installed for this to work.
57+
*/
58+
async function getLicenseFromLocalOrMaster(callCluster: CallCluster) {
59+
// Fetching the local license is cheaper than getting it from the master and good enough
60+
const { license } = await fetchLicense(callCluster, true).catch(async err => {
61+
if (cachedLicense) {
62+
try {
63+
// Fallback to the master node's license info
64+
const response = await fetchLicense(callCluster, false);
65+
return response;
66+
} catch (masterError) {
67+
if (masterError.statusCode === 404) {
68+
// If the master node does not have a license, we can assume there is no license
69+
cachedLicense = undefined;
70+
} else {
71+
// Any other errors from the master node, throw and do not send any telemetry
72+
throw err;
73+
}
74+
}
75+
}
76+
return { license: void 0 };
77+
});
78+
79+
if (license) {
80+
cachedLicense = license;
81+
}
82+
return license;
83+
}
84+
85+
export const getLocalLicense: LicenseGetter = async (clustersDetails, { callCluster }) => {
86+
const license = await getLicenseFromLocalOrMaster(callCluster);
87+
88+
// It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case.
89+
return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {});
90+
};

src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import { telemetryCollectionManager } from '../collection_manager';
4040
import { getLocalStats } from './get_local_stats';
4141
import { getClusterUuids } from './get_cluster_stats';
42+
import { getLocalLicense } from './get_local_license';
4243

4344
export function registerCollection() {
4445
telemetryCollectionManager.setCollection({
@@ -47,5 +48,6 @@ export function registerCollection() {
4748
priority: 0,
4849
statsGetter: getLocalStats,
4950
clusterDetailsGetter: getClusterUuids,
51+
licenseGetter: getLocalLicense,
5052
});
5153
}

src/plugins/telemetry/public/services/telemetry_service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ export class TelemetryService {
9292
body: JSON.stringify({
9393
unencrypted,
9494
timeRange: {
95-
min: now.subtract(20, 'minutes').toISOString(),
95+
min: now
96+
.clone() // Need to clone it to avoid mutation (and max being the same value)
97+
.subtract(20, 'minutes')
98+
.toISOString(),
9699
max: now.toISOString(),
97100
},
98101
}),

x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import sinon from 'sinon';
8-
import { addStackStats, getAllStats, handleAllStats } from './get_all_stats';
8+
import { getStackStats, getAllStats, handleAllStats } from './get_all_stats';
99
import { ESClusterStats } from './get_es_stats';
1010
import { KibanaStats } from './get_kibana_stats';
1111
import { ClustersHighLevelStats } from './get_high_level_stats';
@@ -233,9 +233,8 @@ describe('get_all_stats', () => {
233233
});
234234
});
235235

236-
describe('addStackStats', () => {
236+
describe('getStackStats', () => {
237237
it('searches for clusters', () => {
238-
const cluster = { cluster_uuid: 'a' };
239238
const stats = {
240239
a: {
241240
count: 2,
@@ -250,9 +249,7 @@ describe('get_all_stats', () => {
250249
},
251250
};
252251

253-
addStackStats(cluster as ESClusterStats, stats, 'xyz');
254-
255-
expect((cluster as any).stack_stats.xyz).toStrictEqual(stats.a);
252+
expect(getStackStats('a', stats, 'xyz')).toStrictEqual({ xyz: stats.a });
256253
});
257254
});
258255
});

x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -61,38 +61,31 @@ export function handleAllStats(
6161
}
6262
) {
6363
return clusters.map(cluster => {
64-
// if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats
65-
addStackStats(cluster, kibana, KIBANA_SYSTEM_ID);
66-
addStackStats(cluster, logstash, LOGSTASH_SYSTEM_ID);
67-
addStackStats(cluster, beats, BEATS_SYSTEM_ID);
68-
mergeXPackStats(cluster, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph
64+
const stats = {
65+
...cluster,
66+
stack_stats: {
67+
...cluster.stack_stats,
68+
// if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats
69+
...getStackStats(cluster.cluster_uuid, kibana, KIBANA_SYSTEM_ID),
70+
...getStackStats(cluster.cluster_uuid, logstash, LOGSTASH_SYSTEM_ID),
71+
...getStackStats(cluster.cluster_uuid, beats, BEATS_SYSTEM_ID),
72+
},
73+
};
6974

70-
return cluster;
75+
mergeXPackStats(stats, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph
76+
77+
return stats;
7178
});
7279
}
7380

74-
/**
75-
* Add product data to the {@code cluster}, only if it exists for the current {@code cluster}.
76-
*
77-
* @param {Object} cluster The current Elasticsearch cluster stats
78-
* @param {Object} allProductStats Product stats, keyed by Cluster UUID
79-
* @param {String} product The product name being added (e.g., 'kibana' or 'logstash')
80-
*/
81-
export function addStackStats<T extends { [clusterUuid: string]: K }, K>(
82-
cluster: ESClusterStats & { stack_stats?: { [product: string]: K } },
81+
export function getStackStats<T extends { [clusterUuid: string]: K }, K>(
82+
clusterUuid: string,
8383
allProductStats: T,
8484
product: string
8585
) {
86-
const productStats = allProductStats[cluster.cluster_uuid];
87-
86+
const productStats = allProductStats[clusterUuid];
8887
// Don't add it if they're not using (or configured to report stats) this product for this cluster
89-
if (productStats) {
90-
if (!cluster.stack_stats) {
91-
cluster.stack_stats = {};
92-
}
93-
94-
cluster.stack_stats[product] = productStats;
95-
}
88+
return productStats ? { [product]: productStats } : {};
9689
}
9790

9891
export function mergeXPackStats<T extends { [clusterUuid: string]: unknown }>(

x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@ export function fetchElasticsearchStats(
4848
'hits.hits._source.timestamp',
4949
'hits.hits._source.cluster_name',
5050
'hits.hits._source.version',
51-
'hits.hits._source.license.status', // license data only includes necessary fields to drive UI
52-
'hits.hits._source.license.type',
53-
'hits.hits._source.license.issue_date',
54-
'hits.hits._source.license.expiry_date',
55-
'hits.hits._source.license.expiry_date_in_millis',
5651
'hits.hits._source.cluster_stats',
5752
'hits.hits._source.stack_stats',
5853
],
@@ -79,7 +74,11 @@ export function fetchElasticsearchStats(
7974

8075
export interface ESClusterStats {
8176
cluster_uuid: string;
82-
type: 'cluster_stats';
77+
cluster_name: string;
78+
timestamp: string;
79+
version: string;
80+
cluster_stats: object;
81+
stack_stats?: object;
8382
}
8483

8584
/**

0 commit comments

Comments
 (0)