Skip to content

Commit

Permalink
Separate the license retrieval from the stats
Browse files Browse the repository at this point in the history
  • Loading branch information
afharo committed Feb 21, 2020
1 parent b573500 commit 0ce1cad
Show file tree
Hide file tree
Showing 14 changed files with 342 additions and 116 deletions.
52 changes: 45 additions & 7 deletions src/legacy/core_plugins/telemetry/server/collection_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { encryptTelemetry } from './collectors';
import { CallCluster } from '../../elasticsearch';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server';
import { ESLicense } from './telemetry_collection/get_local_license';

export type EncryptedStatsGetterConfig = { unencrypted: false } & {
server: any;
Expand All @@ -45,22 +46,38 @@ export interface StatsCollectionConfig {
end: string | number;
}

export interface BasicStatsPayload {
timestamp: string;
cluster_uuid: string;
cluster_name: string;
version: string;
cluster_stats: object;
collection?: string;
stack_stats: object;
}

export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig;
export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise<ClusterDetails[]>;
export type StatsGetter<T = unknown> = (
export type StatsGetter<T extends BasicStatsPayload = BasicStatsPayload> = (
clustersDetails: ClusterDetails[],
config: StatsCollectionConfig
) => Promise<T[]>;
export type LicenseGetter = (
clustersDetails: ClusterDetails[],
config: StatsCollectionConfig
) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>;

interface CollectionConfig {
interface CollectionConfig<T extends BasicStatsPayload> {
title: string;
priority: number;
esCluster: string;
statsGetter: StatsGetter;
statsGetter: StatsGetter<T>;
clusterDetailsGetter: ClusterDetailsGetter;
licenseGetter: LicenseGetter;
}
interface Collection {
statsGetter: StatsGetter;
licenseGetter: LicenseGetter;
clusterDetailsGetter: ClusterDetailsGetter;
esCluster: string;
title: string;
Expand All @@ -70,8 +87,15 @@ export class TelemetryCollectionManager {
private usageGetterMethodPriority = -1;
private collections: Collection[] = [];

public setCollection = (collectionConfig: CollectionConfig) => {
const { title, priority, esCluster, statsGetter, clusterDetailsGetter } = collectionConfig;
public setCollection = <T extends BasicStatsPayload>(collectionConfig: CollectionConfig<T>) => {
const {
title,
priority,
esCluster,
statsGetter,
clusterDetailsGetter,
licenseGetter,
} = collectionConfig;

if (typeof priority !== 'number') {
throw new Error('priority must be set.');
Expand All @@ -88,10 +112,14 @@ export class TelemetryCollectionManager {
throw Error('esCluster name must be set for the getCluster method.');
}
if (!clusterDetailsGetter) {
throw Error('Cluser UUIds method is not set.');
throw Error('Cluster UUIds method is not set.');
}
if (!licenseGetter) {
throw Error('License getter method not set.');
}

this.collections.unshift({
licenseGetter,
statsGetter,
clusterDetailsGetter,
esCluster,
Expand Down Expand Up @@ -141,7 +169,17 @@ export class TelemetryCollectionManager {
return;
}

return await collection.statsGetter(clustersDetails, statsCollectionConfig);
const stats = await collection.statsGetter(clustersDetails, statsCollectionConfig);
const licenses = await collection.licenseGetter(clustersDetails, statsCollectionConfig);

return stats.map(stat => {
const license = licenses[stat.cluster_uuid];
return {
...(license ? { license } : {}),
...stat,
collectionSource: collection.title,
};
});
};

public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { LicenseGetter } from '../collection_manager';

// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html
export interface ESLicense {
status: string;
uid: string;
type: string;
issue_date: string;
issue_date_in_millis: number;
expiry_date: string;
expirty_date_in_millis: number;
max_nodes: number;
issued_to: string;
issuer: string;
start_date_in_millis: number;
}
let cachedLicense: ESLicense | undefined;

function fetchLicense(callCluster: CallCluster, local: boolean) {
return callCluster<{ license: ESLicense }>('transport.request', {
method: 'GET',
path: '/_license',
query: {
local,
// For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license.
accept_enterprise: 'true',
},
});
}

/**
* Get the cluster's license from the connected node.
*
* This is the equivalent of GET /_license?local=true .
*
* Like any X-Pack related API, X-Pack must installed for this to work.
*/
async function getLicenseFromLocalOrMaster(callCluster: CallCluster) {
// Fetching the local license is cheaper than getting it from the master and good enough
const { license } = await fetchLicense(callCluster, true).catch(async err => {
if (cachedLicense) {
try {
// Fallback to the master node's license info
const response = await fetchLicense(callCluster, false);
return response;
} catch (masterError) {
if (masterError.statusCode === 404) {
// If the master node does not have a license, we can assume there is no license
cachedLicense = undefined;
} else {
// Any other errors from the master node, throw and do not send any telemetry
throw err;
}
}
}
return { license: void 0 };
});

if (license) {
cachedLicense = license;
}
return license;
}

export const getLocalLicense: LicenseGetter = async (clustersDetails, { callCluster }) => {
const license = await getLicenseFromLocalOrMaster(callCluster);

// It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case.
return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {});
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import { telemetryCollectionManager } from '../collection_manager';
import { getLocalStats } from './get_local_stats';
import { getClusterUuids } from './get_cluster_stats';
import { getLocalLicense } from './get_local_license';

export function registerCollection() {
telemetryCollectionManager.setCollection({
Expand All @@ -47,5 +48,6 @@ export function registerCollection() {
priority: 0,
statsGetter: getLocalStats,
clusterDetailsGetter: getClusterUuids,
licenseGetter: getLocalLicense,
});
}
5 changes: 4 additions & 1 deletion src/plugins/telemetry/public/services/telemetry_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ export class TelemetryService {
body: JSON.stringify({
unencrypted,
timeRange: {
min: now.subtract(20, 'minutes').toISOString(),
min: now
.clone() // Need to clone it to avoid mutation (and max being the same value)
.subtract(20, 'minutes')
.toISOString(),
max: now.toISOString(),
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import sinon from 'sinon';
import { addStackStats, getAllStats, handleAllStats } from './get_all_stats';
import { getStackStats, getAllStats, handleAllStats } from './get_all_stats';
import { ESClusterStats } from './get_es_stats';
import { KibanaStats } from './get_kibana_stats';
import { ClustersHighLevelStats } from './get_high_level_stats';
Expand Down Expand Up @@ -233,9 +233,8 @@ describe('get_all_stats', () => {
});
});

describe('addStackStats', () => {
describe('getStackStats', () => {
it('searches for clusters', () => {
const cluster = { cluster_uuid: 'a' };
const stats = {
a: {
count: 2,
Expand All @@ -250,9 +249,7 @@ describe('get_all_stats', () => {
},
};

addStackStats(cluster as ESClusterStats, stats, 'xyz');

expect((cluster as any).stack_stats.xyz).toStrictEqual(stats.a);
expect(getStackStats('a', stats, 'xyz')).toStrictEqual({ xyz: stats.a });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,38 +61,31 @@ export function handleAllStats(
}
) {
return clusters.map(cluster => {
// if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats
addStackStats(cluster, kibana, KIBANA_SYSTEM_ID);
addStackStats(cluster, logstash, LOGSTASH_SYSTEM_ID);
addStackStats(cluster, beats, BEATS_SYSTEM_ID);
mergeXPackStats(cluster, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph
const stats = {
...cluster,
stack_stats: {
...cluster.stack_stats,
// if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats
...getStackStats(cluster.cluster_uuid, kibana, KIBANA_SYSTEM_ID),
...getStackStats(cluster.cluster_uuid, logstash, LOGSTASH_SYSTEM_ID),
...getStackStats(cluster.cluster_uuid, beats, BEATS_SYSTEM_ID),
},
};

return cluster;
mergeXPackStats(stats, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph

return stats;
});
}

/**
* Add product data to the {@code cluster}, only if it exists for the current {@code cluster}.
*
* @param {Object} cluster The current Elasticsearch cluster stats
* @param {Object} allProductStats Product stats, keyed by Cluster UUID
* @param {String} product The product name being added (e.g., 'kibana' or 'logstash')
*/
export function addStackStats<T extends { [clusterUuid: string]: K }, K>(
cluster: ESClusterStats & { stack_stats?: { [product: string]: K } },
export function getStackStats<T extends { [clusterUuid: string]: K }, K>(
clusterUuid: string,
allProductStats: T,
product: string
) {
const productStats = allProductStats[cluster.cluster_uuid];

const productStats = allProductStats[clusterUuid];
// Don't add it if they're not using (or configured to report stats) this product for this cluster
if (productStats) {
if (!cluster.stack_stats) {
cluster.stack_stats = {};
}

cluster.stack_stats[product] = productStats;
}
return productStats ? { [product]: productStats } : {};
}

export function mergeXPackStats<T extends { [clusterUuid: string]: unknown }>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ export function fetchElasticsearchStats(
'hits.hits._source.timestamp',
'hits.hits._source.cluster_name',
'hits.hits._source.version',
'hits.hits._source.license.status', // license data only includes necessary fields to drive UI
'hits.hits._source.license.type',
'hits.hits._source.license.issue_date',
'hits.hits._source.license.expiry_date',
'hits.hits._source.license.expiry_date_in_millis',
'hits.hits._source.cluster_stats',
'hits.hits._source.stack_stats',
],
Expand All @@ -79,7 +74,11 @@ export function fetchElasticsearchStats(

export interface ESClusterStats {
cluster_uuid: string;
type: 'cluster_stats';
cluster_name: string;
timestamp: string;
version: string;
cluster_stats: object;
stack_stats?: object;
}

/**
Expand Down
Loading

0 comments on commit 0ce1cad

Please sign in to comment.